Compare commits
1 Commits
v2.5.44
...
v3-cleanup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19aeb90403 |
@@ -211,6 +211,20 @@ async function listOne<T>(collection: string, filter: string, sort = ''): Promis
|
|||||||
const BOOKS_CACHE_KEY = 'books:all';
|
const BOOKS_CACHE_KEY = 'books:all';
|
||||||
const BOOKS_CACHE_TTL = 5 * 60; // 5 minutes
|
const BOOKS_CACHE_TTL = 5 * 60; // 5 minutes
|
||||||
|
|
||||||
|
const RATINGS_CACHE_KEY = 'book_ratings:all';
|
||||||
|
const RATINGS_CACHE_TTL = 5 * 60; // 5 minutes
|
||||||
|
|
||||||
|
const HOME_STATS_CACHE_KEY = 'home:stats';
|
||||||
|
const HOME_STATS_CACHE_TTL = 10 * 60; // 10 minutes — counts don't need to be exact
|
||||||
|
|
||||||
|
async function getAllRatings(): Promise<BookRating[]> {
|
||||||
|
const cached = await cache.get<BookRating[]>(RATINGS_CACHE_KEY);
|
||||||
|
if (cached) return cached;
|
||||||
|
const ratings = await listAll<BookRating>('book_ratings', '').catch(() => [] as BookRating[]);
|
||||||
|
await cache.set(RATINGS_CACHE_KEY, ratings, RATINGS_CACHE_TTL);
|
||||||
|
return ratings;
|
||||||
|
}
|
||||||
|
|
||||||
export async function listBooks(): Promise<Book[]> {
|
export async function listBooks(): Promise<Book[]> {
|
||||||
const cached = await cache.get<Book[]>(BOOKS_CACHE_KEY);
|
const cached = await cache.get<Book[]>(BOOKS_CACHE_KEY);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
@@ -282,7 +296,11 @@ export async function getBooksBySlugs(slugs: Iterable<string>): Promise<Book[]>
|
|||||||
|
|
||||||
/** Invalidate the books cache (call after a book is created/updated/deleted). */
|
/** Invalidate the books cache (call after a book is created/updated/deleted). */
|
||||||
export async function invalidateBooksCache(): Promise<void> {
|
export async function invalidateBooksCache(): Promise<void> {
|
||||||
await cache.invalidate(BOOKS_CACHE_KEY);
|
await Promise.all([
|
||||||
|
cache.invalidate(BOOKS_CACHE_KEY),
|
||||||
|
cache.invalidate(HOME_STATS_CACHE_KEY),
|
||||||
|
cache.invalidatePattern('books:recent:*')
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBook(slug: string): Promise<Book | null> {
|
export async function getBook(slug: string): Promise<Book | null> {
|
||||||
@@ -290,7 +308,12 @@ export async function getBook(slug: string): Promise<Book | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function recentlyAddedBooks(limit = 6): Promise<Book[]> {
|
export async function recentlyAddedBooks(limit = 6): Promise<Book[]> {
|
||||||
return listN<Book>('books', limit, '', '-meta_updated');
|
const key = `books:recent:${limit}`;
|
||||||
|
const cached = await cache.get<Book[]>(key);
|
||||||
|
if (cached) return cached;
|
||||||
|
const books = await listN<Book>('books', limit, '', '-meta_updated');
|
||||||
|
await cache.set(key, books, 5 * 60); // 5 minutes
|
||||||
|
return books;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HomeStats {
|
export interface HomeStats {
|
||||||
@@ -299,11 +322,19 @@ export interface HomeStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getHomeStats(): Promise<HomeStats> {
|
export async function getHomeStats(): Promise<HomeStats> {
|
||||||
|
const cached = await cache.get<HomeStats>(HOME_STATS_CACHE_KEY);
|
||||||
|
if (cached) return cached;
|
||||||
const [totalBooks, totalChapters] = await Promise.all([
|
const [totalBooks, totalChapters] = await Promise.all([
|
||||||
countCollection('books'),
|
countCollection('books'),
|
||||||
countCollection('chapters_idx')
|
countCollection('chapters_idx')
|
||||||
]);
|
]);
|
||||||
return { totalBooks, totalChapters };
|
const stats = { totalBooks, totalChapters };
|
||||||
|
await cache.set(HOME_STATS_CACHE_KEY, stats, HOME_STATS_CACHE_TTL);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invalidateHomeStatsCache(): Promise<void> {
|
||||||
|
await cache.invalidate(HOME_STATS_CACHE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Chapter index ────────────────────────────────────────────────────────────
|
// ─── Chapter index ────────────────────────────────────────────────────────────
|
||||||
@@ -1849,6 +1880,7 @@ export async function setBookRating(
|
|||||||
} else {
|
} else {
|
||||||
await pbPost('/api/collections/book_ratings/records', payload);
|
await pbPost('/api/collections/book_ratings/records', payload);
|
||||||
}
|
}
|
||||||
|
await cache.invalidate(RATINGS_CACHE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Shelves ───────────────────────────────────────────────────────────────────
|
// ─── Shelves ───────────────────────────────────────────────────────────────────
|
||||||
@@ -1918,7 +1950,7 @@ export async function getBooksForDiscovery(
|
|||||||
// Fetch avg ratings for candidates, weight top-rated books to surface earlier.
|
// Fetch avg ratings for candidates, weight top-rated books to surface earlier.
|
||||||
// Fetch in one shot for all candidate slugs. Low-rated / unrated books still
|
// Fetch in one shot for all candidate slugs. Low-rated / unrated books still
|
||||||
// appear — they're just pushed further back via a stable sort before shuffle.
|
// appear — they're just pushed further back via a stable sort before shuffle.
|
||||||
const ratingRows = await listAll<BookRating>('book_ratings', '').catch(() => [] as BookRating[]);
|
const ratingRows = await getAllRatings();
|
||||||
const ratingMap = new Map<string, { sum: number; count: number }>();
|
const ratingMap = new Map<string, { sum: number; count: number }>();
|
||||||
for (const r of ratingRows) {
|
for (const r of ratingRows) {
|
||||||
const cur = ratingMap.get(r.slug) ?? { sum: 0, count: 0 };
|
const cur = ratingMap.get(r.slug) ?? { sum: 0, count: 0 };
|
||||||
|
|||||||
@@ -130,7 +130,43 @@
|
|||||||
|
|
||||||
let cardEl = $state<HTMLDivElement | null>(null);
|
let cardEl = $state<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
// ── Card entry animation (prevents pop-to-full-size after swipe) ─────────────
|
||||||
|
let cardEntering = $state(false);
|
||||||
|
let entryTransition = $state(false);
|
||||||
|
let entryCleanup: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
function startEntryAnimation() {
|
||||||
|
if (entryCleanup) clearTimeout(entryCleanup);
|
||||||
|
cardEntering = true;
|
||||||
|
entryTransition = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
cardEntering = false;
|
||||||
|
entryCleanup = setTimeout(() => { entryTransition = false; }, 400);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEntryAnimation() {
|
||||||
|
if (entryCleanup) { clearTimeout(entryCleanup); entryCleanup = null; }
|
||||||
|
cardEntering = false;
|
||||||
|
entryTransition = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTransform = $derived(
|
||||||
|
cardEntering
|
||||||
|
? 'scale(0.95) translateY(13px)'
|
||||||
|
: `translateX(${offsetX}px) translateY(${offsetY}px) rotate(${rotation}deg)`
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeTransition = $derived(
|
||||||
|
isDragging
|
||||||
|
? 'none'
|
||||||
|
: (transitioning || entryTransition)
|
||||||
|
? 'transform 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275)'
|
||||||
|
: 'none'
|
||||||
|
);
|
||||||
|
|
||||||
function onPointerDown(e: PointerEvent) {
|
function onPointerDown(e: PointerEvent) {
|
||||||
|
cancelEntryAnimation();
|
||||||
if (animating || !currentBook) return;
|
if (animating || !currentBook) return;
|
||||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||||
startX = e.clientX;
|
startX = e.clientX;
|
||||||
@@ -216,6 +252,8 @@
|
|||||||
|
|
||||||
if (action === 'read_now') {
|
if (action === 'read_now') {
|
||||||
goto(`/books/${book.slug}`);
|
goto(`/books/${book.slug}`);
|
||||||
|
} else {
|
||||||
|
startEntryAnimation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,8 +531,8 @@
|
|||||||
bind:this={cardEl}
|
bind:this={cardEl}
|
||||||
class="absolute inset-0 rounded-2xl overflow-hidden shadow-2xl cursor-grab active:cursor-grabbing z-10"
|
class="absolute inset-0 rounded-2xl overflow-hidden shadow-2xl cursor-grab active:cursor-grabbing z-10"
|
||||||
style="
|
style="
|
||||||
transform: translateX({offsetX}px) translateY({offsetY}px) rotate({rotation}deg);
|
transform: {activeTransform};
|
||||||
transition: {(transitioning && !isDragging) ? 'transform 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275)' : 'none'};
|
transition: {activeTransition};
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
"
|
"
|
||||||
onpointerdown={onPointerDown}
|
onpointerdown={onPointerDown}
|
||||||
|
|||||||
Reference in New Issue
Block a user