From 3004f712f3babca763fc5fbbe53b0957ef5ef6ba Mon Sep 17 00:00:00 2001 From: Yuriy Panov Date: Mon, 2 Feb 2026 17:12:25 +0600 Subject: [PATCH] initial --- books/.gitignore | 24 +++++ books/App.tsx | 132 +++++++++++++++++++++++++ books/README.md | 20 ++++ books/components/BottomNav.tsx | 41 ++++++++ books/components/Layout.tsx | 25 +++++ books/constants.tsx | 70 ++++++++++++++ books/index.html | 92 ++++++++++++++++++ books/index.tsx | 16 ++++ books/metadata.json | 8 ++ books/package.json | 22 +++++ books/screens/AddBook.tsx | 136 ++++++++++++++++++++++++++ books/screens/BookDetails.tsx | 105 ++++++++++++++++++++ books/screens/Categories.tsx | 52 ++++++++++ books/screens/Library.tsx | 108 +++++++++++++++++++++ books/screens/Scanner.tsx | 165 ++++++++++++++++++++++++++++++++ books/services/geminiService.ts | 55 +++++++++++ books/tsconfig.json | 29 ++++++ books/types.ts | 34 +++++++ books/vite.config.ts | 23 +++++ 19 files changed, 1157 insertions(+) create mode 100644 books/.gitignore create mode 100644 books/App.tsx create mode 100644 books/README.md create mode 100644 books/components/BottomNav.tsx create mode 100644 books/components/Layout.tsx create mode 100644 books/constants.tsx create mode 100644 books/index.html create mode 100644 books/index.tsx create mode 100644 books/metadata.json create mode 100644 books/package.json create mode 100644 books/screens/AddBook.tsx create mode 100644 books/screens/BookDetails.tsx create mode 100644 books/screens/Categories.tsx create mode 100644 books/screens/Library.tsx create mode 100644 books/screens/Scanner.tsx create mode 100644 books/services/geminiService.ts create mode 100644 books/tsconfig.json create mode 100644 books/types.ts create mode 100644 books/vite.config.ts diff --git a/books/.gitignore b/books/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/books/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/books/App.tsx b/books/App.tsx new file mode 100644 index 0000000..a09f5b9 --- /dev/null +++ b/books/App.tsx @@ -0,0 +1,132 @@ + +import React, { useState } from 'react'; +import { AppScreen, Book } from './types'; +import { INITIAL_BOOKS } from './constants'; +import { Layout } from './components/Layout'; +import { Library } from './screens/Library'; +import { Categories } from './screens/Categories'; +import { BookDetails } from './screens/BookDetails'; +import { AddBook } from './screens/AddBook'; +import { Scanner } from './screens/Scanner'; + +const App: React.FC = () => { + const [currentScreen, setCurrentScreen] = useState(AppScreen.LIBRARY); + const [books, setBooks] = useState(INITIAL_BOOKS); + const [selectedBook, setSelectedBook] = useState(null); + const [prefilledData, setPrefilledData] = useState | null>(null); + + const handleBookClick = (book: Book) => { + setSelectedBook(book); + setCurrentScreen(AppScreen.DETAILS); + }; + + const handleAddClick = () => { + setPrefilledData(null); + setCurrentScreen(AppScreen.ADD_BOOK); + }; + + const handleSaveBook = (bookData: Partial) => { + if (selectedBook) { + // Edit existing + setBooks(prev => prev.map(b => b.id === selectedBook.id ? { ...b, ...bookData } as Book : b)); + } else { + // Add new + const newBook: Book = { + id: Math.random().toString(36).substr(2, 9), + title: bookData.title || 'Unknown', + author: bookData.author || 'Unknown', + genre: bookData.genre || 'Unknown', + annotation: bookData.annotation || '', + coverUrl: bookData.coverUrl || 'https://picsum.photos/seed/new/400/600', + status: 'want_to_read', + ...bookData + } as Book; + setBooks(prev => [...prev, newBook]); + } + setCurrentScreen(AppScreen.LIBRARY); + setSelectedBook(null); + setPrefilledData(null); + }; + + const handleDeleteBook = (id: string) => { + setBooks(prev => prev.filter(b => b.id !== id)); + setCurrentScreen(AppScreen.LIBRARY); + }; + + const handleDetected = (data: Partial) => { + setPrefilledData(data); + setCurrentScreen(AppScreen.ADD_BOOK); + }; + + const renderScreen = () => { + switch (currentScreen) { + case AppScreen.LIBRARY: + return ( + + ); + case AppScreen.CATEGORIES: + return ; + case AppScreen.DETAILS: + return selectedBook ? ( + setCurrentScreen(AppScreen.LIBRARY)} + onDelete={handleDeleteBook} + onEdit={(b) => { + setSelectedBook(b); + setCurrentScreen(AppScreen.ADD_BOOK); + }} + /> + ) : null; + case AppScreen.ADD_BOOK: + return ( + { + setCurrentScreen(selectedBook ? AppScreen.DETAILS : AppScreen.LIBRARY); + setPrefilledData(null); + }} + onScanClick={() => setCurrentScreen(AppScreen.SCANNER)} + /> + ); + case AppScreen.SCANNER: + return ( + setCurrentScreen(AppScreen.ADD_BOOK)} + onDetected={handleDetected} + /> + ); + case AppScreen.WISHLIST: + return ( +
+

Вишлист

+

Здесь будут книги, которые вы хотите прочитать.

+
+ ); + case AppScreen.SETTINGS: + return ( +
+

Настройки

+

Персонализируйте ваше приложение.

+
+ ); + default: + return ; + } + }; + + const hideNav = [AppScreen.SCANNER, AppScreen.DETAILS, AppScreen.ADD_BOOK].includes(currentScreen); + + return ( + + {renderScreen()} + + ); +}; + +export default App; diff --git a/books/README.md b/books/README.md new file mode 100644 index 0000000..7e7a93a --- /dev/null +++ b/books/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/temp/1 + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/books/components/BottomNav.tsx b/books/components/BottomNav.tsx new file mode 100644 index 0000000..6ae6f30 --- /dev/null +++ b/books/components/BottomNav.tsx @@ -0,0 +1,41 @@ + +import React from 'react'; +import { AppScreen } from '../types'; + +interface BottomNavProps { + currentScreen: AppScreen; + setScreen: (screen: AppScreen) => void; +} + +export const BottomNav: React.FC = ({ currentScreen, setScreen }) => { + const navItems = [ + { id: AppScreen.LIBRARY, label: 'Полка', icon: 'library_books' }, + { id: AppScreen.CATEGORIES, label: 'Категории', icon: 'category' }, + { id: AppScreen.WISHLIST, label: 'Вишлист', icon: 'bookmark' }, + { id: AppScreen.SETTINGS, label: 'Настройки', icon: 'settings' }, + ]; + + return ( + + ); +}; diff --git a/books/components/Layout.tsx b/books/components/Layout.tsx new file mode 100644 index 0000000..69548b6 --- /dev/null +++ b/books/components/Layout.tsx @@ -0,0 +1,25 @@ + +import React from 'react'; +import { AppScreen } from '../types'; +import { BottomNav } from './BottomNav'; + +interface LayoutProps { + children: React.ReactNode; + currentScreen: AppScreen; + setScreen: (screen: AppScreen) => void; + hideNav?: boolean; +} + +export const Layout: React.FC = ({ children, currentScreen, setScreen, hideNav }) => { + return ( +
+
+ {children} +
+ + {!hideNav && ( + + )} +
+ ); +}; diff --git a/books/constants.tsx b/books/constants.tsx new file mode 100644 index 0000000..3c2d8b7 --- /dev/null +++ b/books/constants.tsx @@ -0,0 +1,70 @@ + +import { Category, Book } from './types'; + +export const CATEGORIES: Category[] = [ + { id: 'fiction', name: 'Фантастика', count: 24, icon: 'rocket_launch', colorClass: 'bg-indigo-500/20 text-indigo-300' }, + { id: 'fantasy', name: 'Фэнтези', count: 18, icon: 'auto_fix_high', colorClass: 'bg-purple-500/20 text-purple-300' }, + { id: 'nonfiction', name: 'Научпоп', count: 7, icon: 'psychology', colorClass: 'bg-teal-500/20 text-teal-300' }, + { id: 'business', name: 'Бизнес', count: 3, icon: 'business_center', colorClass: 'bg-blue-500/20 text-blue-300' }, + { id: 'education', name: 'Учебная', count: 0, icon: 'school', colorClass: 'bg-orange-500/20 text-orange-300' }, + { id: 'classics', name: 'Классика', count: 15, icon: 'history_edu', colorClass: 'bg-amber-500/20 text-amber-300' }, +]; + +export const INITIAL_BOOKS: Book[] = [ + { + id: '1', + title: 'Великий Гэтсби', + author: 'Ф. Скотт Фицджеральд', + genre: 'Classic', + annotation: 'История о несбывшейся любви и трагедии американской мечты на фоне бурных двадцатых годов.', + coverUrl: 'https://picsum.photos/seed/gatsby/400/600', + pages: 208, + language: 'English', + publishedYear: 1925, + rating: 4.8, + status: 'reading', + progress: 45, + isFavorite: true + }, + { + id: '2', + title: '1984', + author: 'Джордж Оруэлл', + genre: 'Dystopian', + annotation: 'Антиутопия о тоталитарном государстве, где мысли контролируются, а правда переменчива.', + coverUrl: 'https://picsum.photos/seed/1984/400/600', + pages: 328, + language: 'English', + publishedYear: 1949, + rating: 4.9, + status: 'want_to_read', + isFavorite: true + }, + { + id: '3', + title: 'Дюна', + author: 'Фрэнк Герберт', + genre: 'Sci-Fi', + annotation: 'Эпическая сага о борьбе за власть над самой важной планетой во Вселенной.', + coverUrl: 'https://picsum.photos/seed/dune/400/600', + pages: 896, + language: 'English', + publishedYear: 1965, + rating: 4.7, + status: 'reading', + progress: 12 + }, + { + id: '4', + title: 'Хоббит', + author: 'Дж. Р. Р. Толкин', + genre: 'Fantasy', + annotation: 'Путешествие Бильбо Бэггинса туда и обратно в поисках сокровищ гномов.', + coverUrl: 'https://picsum.photos/seed/hobbit/400/600', + pages: 310, + language: 'English', + publishedYear: 1937, + rating: 4.9, + status: 'done' + } +]; diff --git a/books/index.html b/books/index.html new file mode 100644 index 0000000..855697a --- /dev/null +++ b/books/index.html @@ -0,0 +1,92 @@ + + + + + + + Книжная полка + + + + + + + + + + + +
+ + + diff --git a/books/index.tsx b/books/index.tsx new file mode 100644 index 0000000..aaa0c6e --- /dev/null +++ b/books/index.tsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); diff --git a/books/metadata.json b/books/metadata.json new file mode 100644 index 0000000..02b94b3 --- /dev/null +++ b/books/metadata.json @@ -0,0 +1,8 @@ + +{ + "name": "Книжная полка (Bookshelf)", + "description": "A sophisticated book management application featuring OCR-based book scanning, library management, and genre organization, with a sleek dark-themed aesthetic.", + "requestFramePermissions": [ + "camera" + ] +} diff --git a/books/package.json b/books/package.json new file mode 100644 index 0000000..81ff530 --- /dev/null +++ b/books/package.json @@ -0,0 +1,22 @@ +{ + "name": "книжная-полка-(bookshelf)", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4", + "@google/genai": "^1.39.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/books/screens/AddBook.tsx b/books/screens/AddBook.tsx new file mode 100644 index 0000000..efcfa23 --- /dev/null +++ b/books/screens/AddBook.tsx @@ -0,0 +1,136 @@ + +import React, { useState, useEffect } from 'react'; +import { Book, AppScreen } from '../types'; + +interface AddBookProps { + initialData?: Partial; + onSave: (book: Partial) => void; + onCancel: () => void; + onScanClick: () => void; +} + +export const AddBook: React.FC = ({ initialData, onSave, onCancel, onScanClick }) => { + const [formData, setFormData] = useState>({ + title: '', + author: '', + genre: '', + annotation: '', + status: 'want_to_read', + pages: 0, + language: 'Russian', + publishedYear: new Date().getFullYear(), + rating: 5, + coverUrl: 'https://picsum.photos/seed/newbook/400/600', + ...initialData + }); + + const handleChange = (e: React.ChangeEvent) => { + const { id, value } = e.target; + setFormData(prev => ({ ...prev, [id]: value })); + }; + + return ( +
+
+ +

Добавить книгу

+
+ +
+
+
+ {formData.coverUrl && !formData.coverUrl.includes('picsum') ? ( + Preview + ) : ( +
+
+ add_a_photo +
+

Загрузить или отсканировать

+
+ )} +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ expand_more +
+
+
+ +
+ +