From 205fee083141127846616701e42c460114a266ea Mon Sep 17 00:00:00 2001 From: SiteAgent Bot Date: Tue, 27 Jan 2026 15:58:12 +0800 Subject: [PATCH] manual save(2026-01-27 15:58) --- index.html | 8 +- src/App.tsx | 2 + src/components/Bookshelf.tsx | 270 +++++++++++++++++++++++++++++++++++ src/components/Footer.tsx | 19 ++- src/components/Header.tsx | 63 ++++++-- src/data/books.ts | 121 ++++++++++++++++ src/index.css | 73 ++++++++++ src/pages/BookDetail.tsx | 124 ++++++++++++++++ src/pages/Home.tsx | 114 ++------------- 9 files changed, 677 insertions(+), 117 deletions(-) create mode 100644 src/components/Bookshelf.tsx create mode 100644 src/data/books.ts create mode 100644 src/pages/BookDetail.tsx diff --git a/index.html b/index.html index 9958f76..bc56b4d 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,14 @@ - + - react-template + + + + + 书架
diff --git a/src/App.tsx b/src/App.tsx index 257538e..4687678 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,12 +3,14 @@ import { Home } from './pages/Home' import { PostDetail } from './pages/PostDetail' import { CategoriesPage } from './pages/Categories' import { CategoryDetail } from './pages/CategoryDetail' +import { BookDetail } from './pages/BookDetail' function App() { return ( } /> + } /> } /> } /> } /> diff --git a/src/components/Bookshelf.tsx b/src/components/Bookshelf.tsx new file mode 100644 index 0000000..56f952f --- /dev/null +++ b/src/components/Bookshelf.tsx @@ -0,0 +1,270 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { Link } from 'react-router-dom' +import type { Book } from '../data/books' + +type Props = { + books: Book[] +} + +type Point = { x: number; y: number } + +const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n)) + +const usePrefersReducedMotion = () => { + const [reduced, setReduced] = useState(false) + useEffect(() => { + const mq = window.matchMedia('(prefers-reduced-motion: reduce)') + const onChange = () => setReduced(mq.matches) + onChange() + mq.addEventListener?.('change', onChange) + return () => mq.removeEventListener?.('change', onChange) + }, []) + return reduced +} + +const BookSpine: React.FC<{ + book: Book + index: number + selected: boolean + onSelect: (event?: React.PointerEvent | React.MouseEvent) => void +}> = ({ book, index, selected, onSelect }) => { + const tilt = (index % 2 === 0 ? 1 : -1) * 0.9 + const height = 148 + (index % 4) * 14 + const width = 46 + (index % 3) * 6 + + return ( +
onSelect(e)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') onSelect() + }} + className={ + 'relative select-none rounded-lg cursor-pointer outline-none transition-transform duration-300 ' + + 'focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent focus-visible:ring-white/70' + } + style={{ + width, + height, + transform: selected + ? 'translateY(-10px) rotateZ(0deg)' + : `translateY(0px) rotateZ(${tilt}deg)`, + }} + > +
+ +
+ +
+ +
+
+ {book.title} +
+
+ {book.author} +
+
+ +
+
+ ) +} + +export const Bookshelf: React.FC = ({ books }) => { + const reducedMotion = usePrefersReducedMotion() + const [selectedSlug, setSelectedSlug] = useState(books[0]?.slug ?? null) + const [spark, setSpark] = useState<{ at: Point; key: number } | null>(null) + const sparkKeyRef = useRef(0) + const shelfRef = useRef(null) + const scrollerRef = useRef(null) + + const selectedBook = useMemo(() => { + if (!selectedSlug) return undefined + return books.find((b) => b.slug === selectedSlug) + }, [books, selectedSlug]) + + useEffect(() => { + // Ensure the shelf starts from the left edge (some environments preserve scroll positions). + if (scrollerRef.current) scrollerRef.current.scrollLeft = 0 + }, []) + + const onSelect = (book: Book, event?: React.PointerEvent | React.MouseEvent) => { + setSelectedSlug(book.slug) + const rect = shelfRef.current?.getBoundingClientRect() + if (!rect) return + const ev = event as (React.PointerEvent | React.MouseEvent | undefined) + const clientX = ev?.clientX ?? rect.left + rect.width * 0.32 + const clientY = ev?.clientY ?? rect.top + rect.height * 0.42 + const x = clamp(clientX - rect.left, 10, rect.width - 10) + const y = clamp(clientY - rect.top, 10, rect.height - 10) + sparkKeyRef.current += 1 + setSpark({ at: { x, y }, key: sparkKeyRef.current }) + } + + return ( +
+
+
+ ) +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index fffde69..17051a5 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -2,12 +2,19 @@ import React from 'react' export const Footer: React.FC = () => { return ( -