feat: implement responsive ReadingView with preference support
Implements Task 3 from Bible Reader 2025 plan: - Created lib/reading-preferences.ts with 4 presets (default, dyslexia, highContrast, minimal) - Implemented loadPreferences/savePreferences using localStorage - Added getCSSVariables for dynamic styling - Created ReadingView component with full mobile responsiveness - Touch interaction: tap top third shows header, bottom third toggles controls - Verse text is clickable with hover effects - Navigation controls (prev/next chapter, settings button) - Created test file for preferences 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
176
components/bible/reading-view.tsx
Normal file
176
components/bible/reading-view.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
'use client'
|
||||
import { useState, useEffect, CSSProperties } from 'react'
|
||||
import { Box, Typography, IconButton, Paper, useMediaQuery, useTheme } from '@mui/material'
|
||||
import { NavigateBefore, NavigateNext, Settings as SettingsIcon } from '@mui/icons-material'
|
||||
import { BibleChapter } from '@/types'
|
||||
import { getCSSVariables, loadPreferences } from '@/lib/reading-preferences'
|
||||
|
||||
interface ReadingViewProps {
|
||||
chapter: BibleChapter
|
||||
loading: boolean
|
||||
onPrevChapter: () => void
|
||||
onNextChapter: () => void
|
||||
onVerseClick: (verseId: string) => void
|
||||
onSettingsOpen: () => void
|
||||
hasPrevChapter: boolean
|
||||
hasNextChapter: boolean
|
||||
}
|
||||
|
||||
export function ReadingView({
|
||||
chapter,
|
||||
loading,
|
||||
onPrevChapter,
|
||||
onNextChapter,
|
||||
onVerseClick,
|
||||
onSettingsOpen,
|
||||
hasPrevChapter,
|
||||
hasNextChapter,
|
||||
}: ReadingViewProps) {
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down('md'))
|
||||
const [preferences, setPreferences] = useState(loadPreferences())
|
||||
const [showControls, setShowControls] = useState(!isMobile)
|
||||
|
||||
useEffect(() => {
|
||||
setPreferences(loadPreferences())
|
||||
}, [])
|
||||
|
||||
const cssVars = getCSSVariables(preferences)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||||
<Typography>Loading chapter...</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
...cssVars,
|
||||
backgroundColor: 'var(--bg-color)',
|
||||
color: 'var(--text-color)',
|
||||
minHeight: '100vh',
|
||||
transition: 'background-color 0.2s, color 0.2s',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative'
|
||||
} as CSSProperties}
|
||||
onClick={(e) => {
|
||||
if (isMobile) {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const y = e.clientY - rect.top
|
||||
if (y < rect.height * 0.3) {
|
||||
setShowControls(true)
|
||||
} else if (y > rect.height * 0.7) {
|
||||
setShowControls(!showControls)
|
||||
} else {
|
||||
setShowControls(false)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
{(showControls || !isMobile) && (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
backgroundColor: 'inherit',
|
||||
borderBottom: `1px solid var(--text-color)`,
|
||||
opacity: 0.7
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" fontWeight={600}>
|
||||
{chapter.bookName} {chapter.chapter}
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Main Text Area */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
p: 3,
|
||||
maxWidth: 700,
|
||||
mx: 'auto',
|
||||
width: '100%',
|
||||
margin: 'var(--margin-width)',
|
||||
lineHeight: 'var(--line-height)',
|
||||
fontSize: 'var(--font-size)',
|
||||
fontFamily: 'var(--font-family)',
|
||||
} as CSSProperties}
|
||||
>
|
||||
{chapter.verses.map((verse) => (
|
||||
<span
|
||||
key={verse.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onVerseClick(verse.id)
|
||||
}}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'rgba(255, 193, 7, 0.3)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent'
|
||||
}}
|
||||
>
|
||||
<sup style={{ fontSize: '0.8em', marginRight: '0.25em', fontWeight: 600, opacity: 0.6 }}>
|
||||
{verse.verseNum}
|
||||
</sup>
|
||||
{verse.text}{' '}
|
||||
</span>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Navigation Footer */}
|
||||
{(showControls || !isMobile) && (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
backgroundColor: 'inherit',
|
||||
borderTop: `1px solid var(--text-color)`,
|
||||
opacity: 0.7,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={onPrevChapter}
|
||||
disabled={!hasPrevChapter}
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
>
|
||||
<NavigateBefore />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant="body2">
|
||||
Chapter {chapter.chapter}
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
onClick={onSettingsOpen}
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
onClick={onNextChapter}
|
||||
disabled={!hasNextChapter}
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
>
|
||||
<NavigateNext />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user