diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7a4be79..cfefb00 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "lucide-react": "^0.539.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-router-dom": "^7.8.2", "tailwind-merge": "^3.3.1" }, "devDependencies": { @@ -26,6 +27,7 @@ "@types/node": "^24.3.1", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", + "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^5.0.0", "autoprefixer": "^10.4.21", "eslint": "^9.33.0", @@ -2154,6 +2156,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2191,6 +2200,29 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.39.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", @@ -2903,6 +2935,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4866,6 +4907,44 @@ } } }, + "node_modules/react-router": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz", + "integrity": "sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.2.tgz", + "integrity": "sha512-Z4VM5mKDipal2jQ385H6UBhiiEDlnJPx6jyWsTYoZQdl5TrjxEV2a9yl3Fi60NBJxYzOTGTTHXPi0pdizvTwow==", + "license": "MIT", + "dependencies": { + "react-router": "7.8.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -5028,6 +5107,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index b77594f..de7d886 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "lucide-react": "^0.539.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-router-dom": "^7.8.2", "tailwind-merge": "^3.3.1" }, "devDependencies": { @@ -28,6 +29,7 @@ "@types/node": "^24.3.1", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", + "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^5.0.0", "autoprefixer": "^10.4.21", "eslint": "^9.33.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 41dfa23..4ee5ee9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,54 +1,15 @@ -import { useState } from 'react'; -import Layout from '@/components/Layout'; -import BooksList from '@/components/BooksList'; -import BookDetailModal from '@/components/BookDetailModal'; -import { Button } from '@/components/ui/button'; -import type { Book } from '@/types'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import HomePage from '@/pages/HomePage'; +import BookDetailPage from '@/pages/BookDetailPage'; function App() { - const [selectedBookId, setSelectedBookId] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - - const handleBookClick = (book: Book) => { - setSelectedBookId(book.id); - setIsModalOpen(true); - }; - - const handleSearch = (query: string) => { - // This will be handled by the BooksList component - console.log('Search query:', query); - }; - return ( - - {/* Hero Section */} -
-

Книги по Python

-

- Откройте для себя лучшие книги по программированию на Python для любого уровня навыков. - От новичков до экспертов - найдите свою следующую отличную книгу. -

- -
- - {/* Books List */} - - - {/* Book Detail Modal */} - -
+ + + } /> + } /> + + ); } diff --git a/frontend/src/components/BookCard.tsx b/frontend/src/components/BookCard.tsx index 551109f..6c11ea6 100644 --- a/frontend/src/components/BookCard.tsx +++ b/frontend/src/components/BookCard.tsx @@ -16,10 +16,10 @@ export default function BookCard({ book, onClick }: BookCardProps) { return ( - +
-
+

{book.title}

-
+
- + {book.author.map(a => `${a.first_name} ${a.last_name}`).join(', ')}
- + {book.publisher.name}
- + {new Date(book.published_at).getFullYear()}
{book.total_pages && (
- + {book.total_pages} стр.
)} diff --git a/frontend/src/components/BookDetailModal.tsx b/frontend/src/components/BookDetailModal.tsx index 806eb81..7493eb4 100644 --- a/frontend/src/components/BookDetailModal.tsx +++ b/frontend/src/components/BookDetailModal.tsx @@ -56,7 +56,7 @@ export default function BookDetailModal({ bookId, open, onOpenChange }: BookDeta return ( - + {loading ? (
@@ -67,9 +67,9 @@ export default function BookDetailModal({ bookId, open, onOpenChange }: BookDeta {book.title} -
+
{/* Book Cover */} -
+
{/* Book Info */} -
+
diff --git a/frontend/src/components/LazyBookCard.tsx b/frontend/src/components/LazyBookCard.tsx index cadaaba..be6722d 100644 --- a/frontend/src/components/LazyBookCard.tsx +++ b/frontend/src/components/LazyBookCard.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import BookCard from './BookCard'; import { useIntersectionObserver } from '@/hooks/useIntersectionObserver'; import { cn } from '@/lib/utils'; @@ -25,13 +24,13 @@ export default function LazyBookCard({ const defaultPlaceholder = (
); return ( -
+
{hasIntersected ? ( ) : ( diff --git a/frontend/src/hooks/useInfiniteScroll.ts b/frontend/src/hooks/useInfiniteScroll.ts index e70f882..2e2b751 100644 --- a/frontend/src/hooks/useInfiniteScroll.ts +++ b/frontend/src/hooks/useInfiniteScroll.ts @@ -1,5 +1,4 @@ -import { useCallback } from 'react'; -import { useIntersectionObserver } from './useIntersectionObserver'; +import { useEffect, useRef } from 'react'; interface UseInfiniteScrollOptions { hasNextPage: boolean; @@ -14,22 +13,32 @@ export function useInfiniteScroll({ onLoadMore, rootMargin = '100px', }: UseInfiniteScrollOptions) { - const { elementRef, isIntersecting } = useIntersectionObserver({ - rootMargin, - threshold: 0.1, - }); + const sentinelRef = useRef(null); - // Trigger load more when the element comes into view - const handleIntersection = useCallback(() => { - if (isIntersecting && hasNextPage && !isLoading) { - onLoadMore(); - } - }, [isIntersecting, hasNextPage, isLoading, onLoadMore]); + useEffect(() => { + const element = sentinelRef.current; + if (!element) return; - // Call the handler when intersection changes - handleIntersection(); + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && hasNextPage && !isLoading) { + onLoadMore(); + } + }, + { + rootMargin, + threshold: 0.1, + } + ); + + observer.observe(element); + + return () => { + observer.disconnect(); + }; + }, [hasNextPage, isLoading, onLoadMore, rootMargin]); return { - sentinelRef: elementRef, + sentinelRef, }; } \ No newline at end of file diff --git a/frontend/src/hooks/useIntersectionObserver.ts b/frontend/src/hooks/useIntersectionObserver.ts index 613f237..6a9eeab 100644 --- a/frontend/src/hooks/useIntersectionObserver.ts +++ b/frontend/src/hooks/useIntersectionObserver.ts @@ -23,7 +23,9 @@ export function useIntersectionObserver( useEffect(() => { const element = elementRef.current; - if (!element) return; + if (!element) { + return; + } const observer = new IntersectionObserver( ([entry]) => { diff --git a/frontend/src/pages/BookDetailPage.tsx b/frontend/src/pages/BookDetailPage.tsx new file mode 100644 index 0000000..4f9e5df --- /dev/null +++ b/frontend/src/pages/BookDetailPage.tsx @@ -0,0 +1,227 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { ArrowLeft, Calendar, User, Building, FileText, Barcode, ExternalLink, Heart, MessageCircle, Loader2 } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import BookImage from '@/components/BookImage'; +import Layout from '@/components/Layout'; +import { booksApi } from '@/lib/api'; +import type { Book } from '@/types'; + +export default function BookDetailPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [book, setBook] = useState(null); + const [loading, setLoading] = useState(false); + const [commentText, setCommentText] = useState(''); + + useEffect(() => { + if (id) { + loadBookDetails(); + } + }, [id]); + + const loadBookDetails = async () => { + if (!id) return; + + setLoading(true); + try { + const bookData = await booksApi.getBook(parseInt(id)); + setBook(bookData); + } catch (error) { + console.error('Error loading book details:', error); + } finally { + setLoading(false); + } + }; + + const handleSubmitComment = (e: React.FormEvent) => { + e.preventDefault(); + // TODO: Implement comment submission + console.log('Comment submitted:', commentText); + setCommentText(''); + }; + + const handleSearch = (query: string) => { + // Navigate back to home with search + navigate(`/?search=${encodeURIComponent(query)}`); + }; + + if (loading) { + return ( + +
+ +
+
+ ); + } + + if (!book) { + return ( + +
+

Книга не найдена

+ +
+
+ ); + } + + return ( + + {/* Back Button */} +
+ +
+ +
+ {/* Book Cover */} +
+
+ +
+
+ + {/* Book Info */} +
+
+
+

{book.title}

+ +
+ + + {book.author.map(a => `${a.first_name} ${a.last_name}`).join(', ')} + + + + {book.publisher.name} + + + + {new Date(book.published_at).getFullYear()} + + {book.total_pages && ( + + + {book.total_pages} стр. + + )} + {book.isbn_code && ( + + + {book.isbn_code} + + )} +
+ + {/* Tags */} + {book.tags && book.tags.length > 0 && ( +
+ {book.tags.map((tag) => ( + + {tag.name} + + ))} +
+ )} + + {/* Action Buttons */} +
+ + +
+
+ + {/* Description */} + {book.description && ( +
+

Описание

+

{book.description}

+
+ )} +
+
+
+ + {/* Comments Section */} +
+
+ +

Комментарии

+
+ + {/* Add Comment Form */} + + + Добавить комментарий + + +
+