Skip to content

Commit acbd4cc

Browse files
authored
Add reading progress and recently opened books (#19)
1 parent 694b70a commit acbd4cc

4 files changed

Lines changed: 100 additions & 2 deletions

File tree

frontend/src/components/system/BookRow.jsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ import { LuChevronRight, LuFileText, LuHeart, LuPencil } from 'react-icons/lu'
33
import { mediaUrl } from '../../api'
44
import { CATEGORY_ICONS } from '../../constants'
55
import { useFavorites } from '../../context/FavoritesContext'
6+
import { getBookPrefs } from '../../hooks/useBookPrefs'
67

78
export default function BookRow({ book, onOpen, onEdit, editing }) {
89
const [hovered, setHovered] = useState(false)
910
const { isFavorite, toggleFavorite } = useFavorites()
1011
const CatIcon = CATEGORY_ICONS[book.category] || LuFileText
1112

13+
const lastPage = getBookPrefs(book.id).page || 0
14+
const progress = book.page_count > 0 && lastPage > 1
15+
? Math.min(lastPage / book.page_count, 1)
16+
: 0
17+
1218
return (
1319
<div
1420
onClick={onOpen}
@@ -22,9 +28,16 @@ export default function BookRow({ book, onOpen, onEdit, editing }) {
2228
display: 'flex', alignItems: 'center', gap: 16, padding: '12px 16px',
2329
background: hovered ? 'var(--bg-card-hover)' : 'var(--bg-card)',
2430
border: '1px solid var(--border)', borderRadius: 8, cursor: 'pointer',
25-
transition: 'background 0.15s',
31+
transition: 'background 0.15s', position: 'relative', overflow: 'hidden',
2632
}}
2733
>
34+
{/* Reading progress bar at bottom of card */}
35+
{progress > 0 && (
36+
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'var(--bg-deep)' }}>
37+
<div style={{ width: `${progress * 100}%`, height: '100%', background: 'var(--gold-dim)', transition: 'width 0.3s' }} />
38+
</div>
39+
)}
40+
2841
<div style={{
2942
width: 36, height: 48, borderRadius: 4, overflow: 'hidden', flexShrink: 0,
3043
background: 'var(--bg-deep)', display: 'flex', alignItems: 'center', justifyContent: 'center',
@@ -41,6 +54,11 @@ export default function BookRow({ book, onOpen, onEdit, editing }) {
4154
</div>
4255
<div style={{ fontSize: 13, color: 'var(--text-muted)', display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 4, alignItems: 'center' }}>
4356
{book.page_count > 0 && <span>{book.page_count} pages</span>}
57+
{progress > 0 && (
58+
<span style={{ color: 'var(--gold-dim)' }}>
59+
p. {lastPage}
60+
</span>
61+
)}
4462
{book.year && <span>{book.year}</span>}
4563
{book.publisher && <span>{book.publisher}</span>}
4664
{book.is_explicit && (
@@ -58,6 +76,11 @@ export default function BookRow({ book, onOpen, onEdit, editing }) {
5876
index failed
5977
</span>
6078
)}
79+
{(book.tags || []).map(tag => (
80+
<span key={tag} style={{ fontSize: 11, padding: '1px 7px', borderRadius: 8, background: 'rgba(201,168,76,0.12)', border: '1px solid var(--gold-dim)', color: 'var(--gold)' }}>
81+
{tag.charAt(0).toUpperCase() + tag.slice(1)}
82+
</span>
83+
))}
6184
</div>
6285
</div>
6386

frontend/src/hooks/useBookPrefs.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,23 @@ export function getBookPrefs(bookId) {
2222
export function saveBookPrefs(bookId, updates) {
2323
write(bookId, updates)
2424
}
25+
26+
const RECENT_KEY = 'grimoire:recently-opened'
27+
const RECENT_MAX = 10
28+
29+
export function saveRecentBook(book) {
30+
try {
31+
const list = getRecentBooks().filter(b => b.id !== book.id)
32+
list.unshift({ id: book.id, title: book.title, has_thumbnail: book.has_thumbnail, page_count: book.page_count, openedAt: Date.now() })
33+
localStorage.setItem(RECENT_KEY, JSON.stringify(list.slice(0, RECENT_MAX)))
34+
} catch {}
35+
}
36+
37+
export function getRecentBooks() {
38+
try {
39+
const raw = localStorage.getItem(RECENT_KEY)
40+
return raw ? JSON.parse(raw) : []
41+
} catch {
42+
return []
43+
}
44+
}

frontend/src/views/LibraryView.jsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Spinner from '../components/Spinner'
55
import Tag from '../components/Tag'
66
import FavoriteButton from '../components/FavoriteButton'
77
import { getUserPrefs } from '../hooks/useUserPrefs'
8+
import { getRecentBooks, getBookPrefs } from '../hooks/useBookPrefs'
89

910
function SystemCard({ system, onClick, compact }) {
1011
const [hovered, setHovered] = useState(false)
@@ -145,9 +146,62 @@ export default function LibraryView() {
145146

146147
const compact = cardSize === 'compact'
147148
const minCard = compact ? '130px' : '220px'
149+
const recentBooks = getRecentBooks()
148150

149151
return (
150152
<div className="fade-in" style={{ padding: '32px 40px', maxWidth: 1400, width: '100%', margin: '0 auto', boxSizing: 'border-box' }}>
153+
154+
{/* Recently Opened */}
155+
{recentBooks.length > 0 && (
156+
<div style={{ marginBottom: 40 }}>
157+
<h3 style={{ fontSize: 16, color: 'var(--text-dim)', fontWeight: 500, marginBottom: 12, letterSpacing: '0.05em', textTransform: 'uppercase' }}>
158+
Recently Opened
159+
</h3>
160+
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
161+
{recentBooks.map(book => {
162+
const lastPage = getBookPrefs(book.id).page || 1
163+
const progress = book.page_count > 0 ? Math.min(lastPage / book.page_count, 1) : 0
164+
return (
165+
<div
166+
key={book.id}
167+
onClick={() => navigate(`/library/book/${book.id}?page=${lastPage}`)}
168+
style={{
169+
display: 'flex', alignItems: 'center', gap: 10,
170+
background: 'var(--bg-card)', border: '1px solid var(--border)',
171+
borderRadius: 8, padding: '8px 12px', cursor: 'pointer',
172+
maxWidth: 260, position: 'relative', overflow: 'hidden',
173+
}}
174+
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-card-hover)'}
175+
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-card)'}
176+
>
177+
{progress > 0 && (
178+
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'var(--bg-deep)' }}>
179+
<div style={{ width: `${progress * 100}%`, height: '100%', background: 'var(--gold-dim)' }} />
180+
</div>
181+
)}
182+
<div style={{ width: 28, height: 36, borderRadius: 3, overflow: 'hidden', flexShrink: 0, background: 'var(--bg-deep)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
183+
{book.has_thumbnail
184+
? <img src={mediaUrl(`/books/${book.id}/thumbnail`)} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
185+
: <span style={{ fontSize: 14, color: 'var(--text-muted)' }}>📄</span>
186+
}
187+
</div>
188+
<div style={{ minWidth: 0 }}>
189+
<div style={{ fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 180 }}>
190+
{book.title}
191+
</div>
192+
{progress > 0 && (
193+
<div style={{ fontSize: 11, color: 'var(--gold-dim)', marginTop: 2 }}>
194+
p. {lastPage}{book.page_count > 0 ? ` / ${book.page_count}` : ''}
195+
</div>
196+
)}
197+
</div>
198+
</div>
199+
)
200+
})}
201+
</div>
202+
</div>
203+
)}
204+
151205
<div style={{ marginBottom: 32 }}>
152206
<h2 style={{ fontSize: 28, marginBottom: 8 }}>Your Collection</h2>
153207
<p style={{ color: 'var(--text-dim)', fontSize: 17, fontFamily: 'Alegreya, serif', fontStyle: 'italic' }}>

frontend/src/views/ReaderView.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from 'react-icons/lu'
77
import api, { mediaUrl } from '../api'
88
import Spinner from '../components/Spinner'
9-
import { getBookPrefs, saveBookPrefs } from '../hooks/useBookPrefs'
9+
import { getBookPrefs, saveBookPrefs, saveRecentBook } from '../hooks/useBookPrefs'
1010
import { getUserPrefs } from '../hooks/useUserPrefs'
1111
import useReaderGestures from '../hooks/useReaderGestures'
1212
import TocSidebar from '../components/reader/TocSidebar'
@@ -87,6 +87,7 @@ export default function ReaderView() {
8787
api.get(`/books/${bookId}`).then(b => {
8888
setBook(b)
8989
setTotalPages(b.page_count || 0)
90+
saveRecentBook(b)
9091
})
9192
}, [bookId])
9293

0 commit comments

Comments
 (0)