Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
"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": {
"@eslint/js": "^9.33.0",
"@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",
Expand Down
57 changes: 9 additions & 48 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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<number | null>(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 (
<Layout onSearch={handleSearch}>
{/* Hero Section */}
<div className="bg-gradient-to-r from-primary/10 to-primary/20 rounded-lg p-8 mb-8 text-center">
<h1 className="text-4xl md:text-5xl font-bold mb-4">Книги по Python</h1>
<p className="text-lg text-muted-foreground mb-6 max-w-2xl mx-auto">
Откройте для себя лучшие книги по программированию на Python для любого уровня навыков.
От новичков до экспертов - найдите свою следующую отличную книгу.
</p>
<Button
size="lg"
onClick={() => {
const searchInput = document.querySelector('input[type="text"]') as HTMLInputElement;
searchInput?.focus();
}}
>
Начать исследование
</Button>
</div>

{/* Books List */}
<BooksList onBookClick={handleBookClick} />

{/* Book Detail Modal */}
<BookDetailModal
bookId={selectedBookId}
open={isModalOpen}
onOpenChange={setIsModalOpen}
/>
</Layout>
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/book/:id" element={<BookDetailPage />} />
</Routes>
</Router>
);
}

Expand Down
16 changes: 8 additions & 8 deletions frontend/src/components/BookCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ export default function BookCard({ book, onClick }: BookCardProps) {

return (
<Card
className="cursor-pointer transition-all hover:shadow-lg hover:-translate-y-1 overflow-hidden py-0"
className="cursor-pointer transition-all hover:shadow-lg hover:-translate-y-1 overflow-hidden py-0 h-full flex flex-col"
onClick={handleClick}
>
<CardContent className="p-0">
<CardContent className="p-0 flex flex-col h-full">
<div className="relative">
<BookImage
src={book.cover_image}
Expand All @@ -41,30 +41,30 @@ export default function BookCard({ book, onClick }: BookCardProps) {
</div>
</div>

<div className="p-4">
<div className="p-4 flex flex-col flex-1">
<h3 className="font-semibold text-lg mb-2 line-clamp-2">{book.title}</h3>

<div className="space-y-2 text-sm text-muted-foreground">
<div className="space-y-2 text-sm text-muted-foreground flex-1">
<div className="flex items-center">
<User className="h-4 w-4 mr-2" />
<User className="h-4 w-4 mr-2 flex-shrink-0" />
<span className="line-clamp-1">
{book.author.map(a => `${a.first_name} ${a.last_name}`).join(', ')}
</span>
</div>

<div className="flex items-center">
<Building className="h-4 w-4 mr-2" />
<Building className="h-4 w-4 mr-2 flex-shrink-0" />
<span className="line-clamp-1">{book.publisher.name}</span>
</div>

<div className="flex items-center">
<Calendar className="h-4 w-4 mr-2" />
<Calendar className="h-4 w-4 mr-2 flex-shrink-0" />
<span>{new Date(book.published_at).getFullYear()}</span>
</div>

{book.total_pages && (
<div className="flex items-center">
<FileText className="h-4 w-4 mr-2" />
<FileText className="h-4 w-4 mr-2 flex-shrink-0" />
<span>{book.total_pages} стр.</span>
</div>
)}
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/BookDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function BookDetailModal({ bookId, open, onOpenChange }: BookDeta

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogContent className="max-w-7xl w-[95vw] max-h-[90vh] overflow-y-auto">
{loading ? (
<div className="flex justify-center items-center py-12">
<Loader2 className="h-8 w-8 animate-spin" />
Expand All @@ -67,9 +67,9 @@ export default function BookDetailModal({ bookId, open, onOpenChange }: BookDeta
<DialogTitle>{book.title}</DialogTitle>
</DialogHeader>

<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-6">
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6 mt-6">
{/* Book Cover */}
<div className="md:col-span-1">
<div className="lg:col-span-2">
<div className="sticky top-4">
<BookImage
src={book.cover_image}
Expand All @@ -81,7 +81,7 @@ export default function BookDetailModal({ bookId, open, onOpenChange }: BookDeta
</div>

{/* Book Info */}
<div className="md:col-span-2">
<div className="lg:col-span-3">
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="flex items-center">
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/components/LazyBookCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import BookCard from './BookCard';
import { useIntersectionObserver } from '@/hooks/useIntersectionObserver';
import { cn } from '@/lib/utils';
Expand All @@ -25,13 +24,13 @@ export default function LazyBookCard({

const defaultPlaceholder = (
<div className={cn(
"animate-pulse bg-gray-200 rounded-lg aspect-[3/4] w-full",
"animate-pulse bg-gray-200 rounded-lg w-full h-full min-h-[400px]",
className
)} />
);

return (
<div ref={elementRef} className="w-full">
<div ref={elementRef} className="w-full h-full">
{hasIntersected ? (
<BookCard book={book} onClick={onClick} />
) : (
Expand Down
39 changes: 24 additions & 15 deletions frontend/src/hooks/useInfiniteScroll.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useCallback } from 'react';
import { useIntersectionObserver } from './useIntersectionObserver';
import { useEffect, useRef } from 'react';

interface UseInfiniteScrollOptions {
hasNextPage: boolean;
Expand All @@ -14,22 +13,32 @@ export function useInfiniteScroll({
onLoadMore,
rootMargin = '100px',
}: UseInfiniteScrollOptions) {
const { elementRef, isIntersecting } = useIntersectionObserver({
rootMargin,
threshold: 0.1,
});
const sentinelRef = useRef<HTMLDivElement>(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,
};
}
4 changes: 3 additions & 1 deletion frontend/src/hooks/useIntersectionObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export function useIntersectionObserver(

useEffect(() => {
const element = elementRef.current;
if (!element) return;
if (!element) {
return;
}

const observer = new IntersectionObserver(
([entry]) => {
Expand Down
Loading