diff --git a/README.md b/README.md index 0cf5f76..e3bd53e 100644 --- a/README.md +++ b/README.md @@ -1 +1,280 @@ -# technical-books-search +# Technical Books Search + +A full-stack web application for searching and managing technical books using Google Books API. + +![Go Version](https://img.shields.io/badge/go-1.25.3-blue.svg) +![React](https://img.shields.io/badge/react-19.1.1-blue.svg) + +## System Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Client Browser │ +│ React SPA (Vite + TypeScript) │ +└──────────────────────────┬──────────────────────────────────────┘ + │ HTTP/REST + │ +┌──────────────────────────▼──────────────────────────────────────┐ +│ Backend API (Go) │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Handler Layer (HTTP Controllers) │ │ +│ │ - SearchBooksHandler │ │ +│ │ - TsundokuHandler │ │ +│ │ - FavoritesHandler │ │ +│ └────────────────┬─────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────▼─────────────────────────────────────────┐ │ +│ │ Service Layer (Business Logic) │ │ +│ │ - books.Service │ │ +│ │ - tsundoku.Service │ │ +│ │ - favorites.Service │ │ +│ └───┬──────────────────────────────────────────┬───────────┘ │ +│ │ │ │ +│ ┌───▼─────────────────────┐ ┌────────────▼───────────┐ │ +│ │ Infrastructure Layer │ │ Repository Interface │ │ +│ │ - GoogleBooks Client │ │ - Favorites Repo │ │ +│ │ (External API) │ │ - Tsundoku Repo │ │ +│ └───┬─────────────────────┘ └────────────┬───────────┘ │ +└──────┼──────────────────────────────────────────┼──────────────┘ + │ │ + │ │ +┌──────▼──────────────────────┐ ┌────────────▼───────────┐ +│ Google Books API v1 │ │ File Storage (JSON) │ +│ (External Service) │ │ - favorites.json │ +└─────────────────────────────┘ │ - tsundoku.json │ + └────────────────────────┘ +``` + +## Features + +### Book Search +- Search technical books via Google Books API +- Technology tag filtering +- Pagination support +- Real-time search results + +### Favorites Management +- Add/remove favorite books +- Persistent storage +- Dedicated favorites view + +### Tsundoku (Reading List) +- Track reading progress with three statuses: Stacked, Currently Reading, Completed +- Add books to reading list +- Update reading status + +## API Endpoints + +### Books +- `GET /api/technical-books` - Search books with query parameters + - `q`: search keywords + - `genre`: additional search terms + - `startIndex`: pagination start (default: 0) + - `maxResults`: results per page (1-40, default: 20) + - `orderBy`: sort order (relevance/newest) + - `lang`: language filter (ja/en) + +### Tsundoku +- `GET /api/tsundoku` - Get all reading list items +- `POST /api/tsundoku` - Add book to reading list +- `PUT /api/tsundoku/:id/status` - Update book status +- `DELETE /api/tsundoku/:id` - Remove from reading list + +### Favorites +- `GET /api/favorites` - Get all favorites +- `POST /api/favorites` - Add to favorites +- `DELETE /api/favorites/:id` - Remove from favorites + +## Tech Stack + +### Backend +- **Language**: Go 1.25.3 +- **Router**: chi v5.2.3 +- **External API**: Google Books API v1 +- **Storage**: File-based JSON +- **Architecture**: Clean architecture with service layer pattern + +### Frontend +- **Framework**: React 19.1.1 with TypeScript +- **Build Tool**: Vite 7.1.7 +- **State Management**: React Hooks +- **HTTP Client**: Fetch API + +## Data Flow Diagram + +``` +┌────────────────┐ +│ User Action │ +└───────┬────────┘ + │ + ▼ +┌───────────────────────────────────┐ +│ React Component (SearchPage) │ +│ - Calls custom hook │ +└───────┬───────────────────────────┘ + │ + ▼ +┌───────────────────────────────────┐ +│ Custom Hook (useBooksSearch) │ +│ - Manages state │ +│ - Calls API client │ +└───────┬───────────────────────────┘ + │ + ▼ +┌───────────────────────────────────┐ +│ API Client (api.ts) │ +│ - Constructs HTTP request │ +└───────┬───────────────────────────┘ + │ HTTP/REST + ▼ +┌───────────────────────────────────┐ +│ Backend Handler │ +│ - Validates request │ +│ - Calls service layer │ +└───────┬───────────────────────────┘ + │ + ▼ +┌───────────────────────────────────┐ +│ Service Layer │ +│ - Business logic │ +│ - Calls repository/client │ +└───────┬───────────┬───────────────┘ + │ │ + ▼ ▼ +┌─────────────┐ ┌──────────────────┐ +│ Google │ │ File Repository │ +│ Books API │ │ (JSON Storage) │ +└─────────────┘ └──────────────────┘ +``` + +## API Integration Details + +### Google Books API Integration +- **Base URL**: `https://www.googleapis.com/books/v1/volumes` +- **Authentication**: API Key (optional, passed via query parameter) +- **Request Method**: GET +- **Response Format**: JSON + +### Backend API Client Pattern +``` +infra/googlebooks/client.go implements: +- HTTP client configuration +- Request construction with query parameters +- Response parsing and error handling +- Rate limiting considerations +``` + +### Frontend API Client Pattern +``` +src/api.ts implements: +- Typed API functions with TypeScript +- Error handling with custom ApiError class +- Response parsing utilities +- Base URL configuration (localhost:8080) +``` + +## Project Structure + +``` +back/ +├── cmd/api/main.go # Entry point, dependency injection +├── internal/ +│ ├── handler/ # HTTP handlers +│ ├── service/ # Business logic +│ └── infra/ # External integrations +│ ├── googlebooks/ # Google Books API client +│ └── {favorites,tsundoku}/filestore/ # JSON storage +└── data/ # Data files + +front/ +├── src/ +│ ├── components/ # UI components +│ ├── hooks/ # Custom React hooks +│ ├── pages/ # Page components +│ └── api.ts # API client +``` + +## Installation & Setup + +### Prerequisites +- Go 1.25.3 or higher +- Node.js 18.x or higher + +### Backend Setup + +```bash +cd back +go mod tidy + +# Optional: Set environment variables +# export BOOKS_API_KEY="your-api-key" + +go run cmd/api/main.go +``` + +Backend runs on `http://localhost:8080` + +### Frontend Setup + +```bash +cd front +npm install +npm run dev +``` + +Frontend runs on `http://localhost:5173` + +## Environment Variables + +### Backend +| Variable | Description | Default | +|----------|-------------|---------| +| `BOOKS_BASE_URL` | Google Books API URL | `https://www.googleapis.com/books/v1/volumes` | +| `BOOKS_API_KEY` | API key (optional) | - | +| `STORAGE_BACKEND` | Storage type | `file` | +| `TSUNDOKU_STORE_PATH` | Tsundoku data path | `data/tsundoku.json` | +| `FAVORITES_STORE_PATH` | Favorites data path | `data/favorites.json` | + +## Usage + +### Search Books +- Enter keywords or select technology tags +- Browse paginated results + +### Manage Favorites +- Click star icon on book cards to add/remove favorites +- View all favorites in dedicated page + +### Track Reading (Tsundoku) +- Add books to reading list +- Pick from top to start reading +- Update status: Stacked, Currently Reading, Completed + +## Building for Production + +### Backend +```bash +cd back +go build -o server cmd/api/main.go +./server +``` + +### Frontend +```bash +cd front +npm run build +``` + +Output in `front/dist/` + +## Architecture Pattern + +**Clean Architecture** with clear separation: +- Handler Layer: HTTP request/response handling +- Service Layer: Business logic +- Infrastructure Layer: External API and file storage +- Repository Pattern: Data access abstraction + +**Dependency Injection** in [back/cmd/api/main.go](back/cmd/api/main.go): +- Services depend on repository interfaces +- Infrastructure implementations injected at startup diff --git a/back/README.md b/back/README.md index c4d9344..d80fde7 100644 --- a/back/README.md +++ b/back/README.md @@ -3,16 +3,16 @@ back ├── .env.template ├── .gitignore ├── cmd -│   └── api -│   └── main.go #APIのエントリーポイント +│ └── api +│ └── main.go # API entry point ├── go.mod ├── internal -│   ├── handler #各ハンドラーの定義 -│   └── server #HTTPサーバー・ルーティングの定義 +│ ├── handler # Handler definitions +│ └── server # HTTP server & routing definitions └── README.md ``` -# 起動 +# Startup ```bash go run cmd/api/main.go ``` @@ -23,25 +23,25 @@ curl -i http://localhost:8080/health ``` # technical books search -まだ暫定です +Still provisional -## エンドポイント +## Endpoints - GET `/api/technical-books` -### クエリパラメータ(仮) -- `q`(任意): 検索キーワード -- `genre`(任意): 追加検索語(今は `q` とスペース連結して使用) -- `startIndex`(任意, default: 0) -- `maxResults`(任意, default: 20, 1〜40) -- `orderBy`(任意, default: `relevance`, 値: `relevance` | `newest`) -- `lang`(任意, default: なし, 例: `ja`/`en`、`all`で未指定扱い) +### Query Parameters (provisional) +- `q` (optional): Search keywords +- `genre` (optional): Additional search terms (currently concatenated with `q` using spaces) +- `startIndex` (optional, default: 0) +- `maxResults` (optional, default: 20, range: 1-40) +- `orderBy` (optional, default: `relevance`, values: `relevance` | `newest`) +- `lang` (optional, default: none, examples: `ja`/`en`, `all` for unspecified) -### リクエスト例 +### Request Example ```bash curl "http://localhost:8080/api/technical-books?genre=programming&q=golang&startIndex=0&maxResults=10" ``` -### レスポンス例 +### Response Example ```json { "TotalItems": 1000000, diff --git a/back/cmd/api/main.go b/back/cmd/api/main.go index 8229b33..7099687 100644 --- a/back/cmd/api/main.go +++ b/back/cmd/api/main.go @@ -6,29 +6,44 @@ import ( "os" "github.com/recursion-goapi-project/technical-books-search/back/internal/handler" + favoritesfs "github.com/recursion-goapi-project/technical-books-search/back/internal/infra/favorites/filestore" "github.com/recursion-goapi-project/technical-books-search/back/internal/infra/googlebooks" tsundokofs "github.com/recursion-goapi-project/technical-books-search/back/internal/infra/tsundoku/filestore" "github.com/recursion-goapi-project/technical-books-search/back/internal/server" "github.com/recursion-goapi-project/technical-books-search/back/internal/service/books" + "github.com/recursion-goapi-project/technical-books-search/back/internal/service/favorites" "github.com/recursion-goapi-project/technical-books-search/back/internal/service/tsundoku" ) func main() { + // Initialize environment configuration apiKey := os.Getenv("BOOKS_API_KEY") baseURL := os.Getenv("BOOKS_BASE_URL") + if baseURL == "" { + baseURL = "https://www.googleapis.com/books/v1/volumes" + } + // Setup Google Books API client and service client := googlebooks.NewClient(baseURL, apiKey) bookService := books.NewService(client) searchHandler := handler.NewSearchBooksHandler(bookService) + // Setup Tsundoku (reading list) service tsundokuRepo := buildTsundokuRepository() tsundokuService := tsundoku.NewService(tsundokuRepo) tsundokuHandler := handler.NewTsundokuHandler(tsundokuService) - r := server.NewRouter(searchHandler, tsundokuHandler) - log.Println("Server is running on port 8080") - if err := http.ListenAndServe(":8080", r); err != nil { - log.Fatalf("server exited: %v", err) + // Setup Favorites service + favoritesRepo := buildFavoritesRepository() + favoritesService := favorites.NewService(favoritesRepo) + favoritesHandler := handler.NewFavoritesHandler(favoritesService) + + // Initialize HTTP router and start server + r := server.NewRouter(searchHandler, tsundokuHandler, favoritesHandler) + port := ":8080" + log.Printf("Server is starting on port %s", port) + if err := http.ListenAndServe(port, r); err != nil { + log.Fatalf("Server failed to start: %v", err) } } @@ -49,3 +64,21 @@ func buildTsundokuRepository() tsundoku.Repository { } return nil } + +func buildFavoritesRepository() favorites.Repository { + switch backend := os.Getenv("STORAGE_BACKEND"); backend { + case "", "file": + path := os.Getenv("FAVORITES_STORE_PATH") + if path == "" { + path = "data/favorites.json" + } + repo, err := favoritesfs.New(path) + if err != nil { + log.Fatalf("failed to initialize favorites file repository: %v", err) + } + return repo + default: + log.Fatalf("unsupported STORAGE_BACKEND: %s", backend) + } + return nil +} diff --git a/back/data/favorites.json b/back/data/favorites.json new file mode 100644 index 0000000..9a09f83 --- /dev/null +++ b/back/data/favorites.json @@ -0,0 +1,138 @@ +{ + "items": { + "0DidDwAAQBAJ": { + "ID": "0DidDwAAQBAJ", + "Book": { + "ID": "0DidDwAAQBAJ", + "Title": "React Native for Mobile Development", + "Authors": [ + "Akshat Paul", + "Abhishek Nalwaya" + ], + "PublishedDate": "2019-06-12", + "Description": "Develop native iOS and Android apps with ease using React Native. Learn by doing through an example-driven approach, and have a substantial running app at the end of each chapter. This second edition is fully updated to include ES7 (ECMAScript 7), the latest version of React Native (including Redux), and development on Android. You will start by setting up React Native and exploring the anatomy of React Native apps. You'll then move on to Redux data flow, how it differs from flux, and how you can include it in your React Native project to solve state management differently and efficiently. You will also learn how to boost your development by including popular packages developed by the React Native community that will help you write less; do more. Finally, you'll learn to how write test cases using Jest and submit your application to the App Store. React Native challenges the status quo of native iOS and Android development with revolutionary components, asynchronous execution, unique methods for touch handling, and much more. This book reveals the the path-breaking concepts of React.js and acquaints you with the React way of thinking so you can learn to create stunning user interfaces. What You'll Learn Build stunning iOS and Android applications Understand the Redux design pattern and use it in your project Interact with iOS and android device capabilities such as addressbook, camera, GPS and more with your apps Test and launch your application to the App Store Who This Book Is For Anyone with JavaScript experience who wants to build native mobile applications but dreads the thought of programming in Objective-C or Java. Developers who have experience with JavaScript but are new or not acquainted to React Native or ReactJS.", + "Categories": [ + "Computers" + ], + "PageCount": 243, + "Thumbnail": "http://books.google.com/books/content?id=0DidDwAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=0DidDwAAQBAJ\u0026source=gbs_api" + }, + "AddedAt": "2025-10-30T02:13:00.4075256Z" + }, + "6bfnEAAAQBAJ": { + "ID": "6bfnEAAAQBAJ", + "Book": { + "ID": "6bfnEAAAQBAJ", + "Title": "Mastering React Native", + "Authors": [ + "Cybellium" + ], + "PublishedDate": "", + "Description": "Elevate Your Mobile App Development with \"Mastering React Native\" In today's fast-paced world of mobile app development, creating high-quality cross-platform apps is essential for reaching a broad and engaged user base. React Native, a powerful and flexible framework developed by Facebook, empowers developers to build native mobile apps using their existing knowledge of JavaScript and React. \"Mastering React Native\" is your comprehensive guide to becoming a proficient app developer, equipping you with the knowledge, skills, and strategies to create stunning, performant, and cross-platform mobile applications. Your Path to React Native Excellence React Native is more than just a framework; it's a game-changer in the world of mobile app development. Whether you're new to React Native or an experienced developer looking to expand your skills, this book will empower you to master the art of cross-platform app development. What You Will Discover React Native Fundamentals: Gain a deep understanding of React Native, its architecture, and how it leverages JavaScript and React to build native apps. Cross-Platform Development: Dive into the world of cross-platform app development, enabling you to create apps that run seamlessly on both iOS and Android platforms. UI/UX Design: Learn to create captivating user interfaces using React Native's components, styles, and navigation. Native Integration: Explore how to integrate native device features, such as camera, geolocation, and sensors, into your apps. Data Management: Master data handling, storage, and synchronization, connecting your app to backends and databases. Testing and Debugging: Discover best practices for testing and debugging your React Native apps to ensure optimal performance and reliability. Deployment and Distribution: Learn how to package and distribute your apps to app stores and devices. Why \"Mastering React Native\" Is Essential Comprehensive Coverage: This book provides comprehensive coverage of React Native development, ensuring you have a well-rounded understanding of the framework's capabilities and best practices. Expert Guidance: Benefit from insights and advice from experienced React Native developers and industry experts who share their knowledge and proven strategies. Career Advancement: Cross-platform app development skills are highly sought after, and this book will help you unlock your full potential in this dynamic field. Stay Competitive: In a mobile-centric world, mastering React Native is essential for staying competitive and reaching a wide audience with your apps. Your Gateway to React Native Mastery \"Mastering React Native\" is your passport to excellence in cross-platform app development. Whether you aspire to be a mobile app developer, enhance your current app development skills, or broaden your app's reach across multiple platforms, this guide will equip you with the skills and knowledge to achieve your goals. Don't miss the opportunity to become a proficient React Native developer. Start your journey to React Native excellence today and join the ranks of developers who are shaping the future of cross-platform mobile app development. \"Mastering React Native\" is the ultimate resource for individuals seeking to excel in the world of cross-platform mobile app development. Whether you are new to React Native or looking to enhance your skills, this book will provide you with the knowledge and strategies to become a proficient cross-platform app developer. Don't wait; begin your journey to React Native mastery today! © 2023 Cybellium Ltd. All rights reserved. www.cybellium.com", + "Categories": [ + "Computers" + ], + "PageCount": 207, + "Thumbnail": "http://books.google.com/books/content?id=6bfnEAAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=6bfnEAAAQBAJ\u0026source=gbs_api" + }, + "AddedAt": "2025-10-30T02:12:57.9545063Z" + }, + "J8crEAAAQBAJ": { + "ID": "J8crEAAAQBAJ", + "Book": { + "ID": "J8crEAAAQBAJ", + "Title": "Vue.js 3 By Example", + "Authors": [ + "John Au-Yeung" + ], + "PublishedDate": "2021-04-26", + "Description": "Kick-start your Vue.js development career by learning the fundamentals of Vue 3 and its integration with modern web technologies such as Electron, GraphQL, Ionic, and Laravel Key FeaturesDownload complete source code for all Vue.js projects built throughout the bookDiscover steps to build production-ready Vue.js apps across web, mobile, and desktopBuild a compelling portfolio of web apps, including shopping cart system, booking app, slider puzzle game, real-time chat app, and moreBook Description With its huge ecosystem and wide adoption, Vue is one of the leading frameworks thanks to its ease of use when developing applications. However, it can get challenging for aspiring Vue.js developers to make sense of the ecosystem and build meaningful applications. This book will help you understand how you can leverage Vue effectively to develop impressive apps quickly using its latest version – Vue 3.0. The book takes an example-based approach to help you get to grips with the basics of Vue 3 and create a simple application by exploring features such as components and directives. You'll then enhance your app building skills by learning how to test the app with Jest and Vue Test Utils. As you advance, you'll understand how to write non-web apps with Vue 3, create cross-platform desktop apps with the Electron plugin, and build a multi-purpose mobile app with Vue and Ionic. You'll also be able to develop web apps with Vue 3 that interact well with GraphQL APIs. Finally, you'll build a chat app that performs real-time communication using Vue 3 and Laravel. By the end of this Vue.js book, you'll have developed the skills you need to build real-world apps using Vue 3 by working through a range of projects. What you will learnGet to grips with Vue architecture, components, props, directives, mixins, and other advanced featuresUnderstand the Vue 3 template system and use directivesUse third-party libraries such as Vue Router for routing and Vuex for state managementCreate GraphQL APIs to power your Vue 3 web appsBuild cross-platform Vue 3 apps with Electron and IonicMake your Vue 3 apps more captivating with PrimeVueBuild real-time communication apps with Vue 3 as the frontend and LaravelWho this book is for This book is for web developers who want to learn frontend web development with Vue 3 and use it to create professional applications. You'll also find this book useful if you're looking to create full-stack web apps with Vue.js 3.0 as the frontend. Knowledge of JavaScript programming is required to get the most out of this book.", + "Categories": [ + "Computers" + ], + "PageCount": 321, + "Thumbnail": "http://books.google.com/books/content?id=J8crEAAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=J8crEAAAQBAJ\u0026source=gbs_api" + }, + "AddedAt": "2025-10-30T01:04:56.0153457Z" + }, + "N-gWEQAAQBAJ": { + "ID": "N-gWEQAAQBAJ", + "Book": { + "ID": "N-gWEQAAQBAJ", + "Title": "Vue.js 3 for Beginners", + "Authors": [ + "Simone Cuomo" + ], + "PublishedDate": "2024-09-06", + "Description": "Gain proficiency in Vue.js 3 and its core libraries, including Pinia, Vue Router, and Vitest, by developing a social media web application with detailed, hands-on instructions Key Features Discover best practices for building scalable and performant Vue.js applications Learn the basics of component-based architecture Familiarize yourself with Vue.js core libraries and ecosystem Purchase of the print or Kindle book includes a free PDF eBook Book DescriptionDiscover why Vue.js is a must-learn JavaScript framework for aspiring developers. If you’re a beginner fascinated by Vue.js and its potential, then this book will show you how the progressive and versatile framework can help you build performant applications. Written by an accomplished software architect with over 12 years of experience, Vue.js 3 for Beginners provides a solid foundation in Vue.js and guides you at every step to create a robust social media application, component by component. Starting with a clean canvas using plain HTML and CSS, you’ll learn new topics to build your application incrementally. Beyond the core features, you’ll explore crucial parts of the Vue.js ecosystem, such as state management with Pinia, routing with Vue Router, and testing with Vitest, and Cypress. The structured GitHub repository ensures a smooth transition from one chapter to the next, offering valuable insights into advanced topics, techniques, and resources. This book is designed to serve as a practical reference guide, allowing you to quickly revisit specific topics when needed. By the end of the book, you’ll have built a strong understanding of Vue.js and be ready to build simple applications effortlessly.What you will learn Gain practical knowledge of the Vue.js framework Deepen your understanding of Pinia, Vue Router, validation libraries, and their integration with Vue.js applications Explore the core concepts of Vue.js, including components, directives, and data binding Create scalable, maintainable applications from scratch Build applications using the script setup and the Composition API Debug your applications with the Vue debugger tool Who this book is for Vue.js for Beginners is for aspiring web developers, students, hobbyists, or anyone who wants to learn Vue.js from scratch and is eager to foray into front-end development using this modern and popular framework. Basic knowledge of HTML, CSS, and JavaScript is required to fully grasp the content of this Vue.js book.", + "Categories": [ + "Computers" + ], + "PageCount": 302, + "Thumbnail": "http://books.google.com/books/content?id=N-gWEQAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=N-gWEQAAQBAJ\u0026source=gbs_api" + }, + "AddedAt": "2025-10-30T01:04:55.2284628Z" + }, + "NnBbEQAAQBAJ": { + "ID": "NnBbEQAAQBAJ", + "Book": { + "ID": "NnBbEQAAQBAJ", + "Title": "Learn React Hooks", + "Authors": [ + "Daniel Bugl" + ], + "PublishedDate": "2025-05-23", + "Description": "Grasp the core concepts of React Hooks and enhance your development workflow with best practices and advanced patterns derived from modern React 19 features Key Features Build custom Hooks to simplify complex logic and promote code reusability Transform class components into modern, hook-based function components, and write robust tests Build maintainable, production-ready UIs using React 19's declarative approach and ecosystem tools Purchase of the print or Kindle book includes a free PDF eBook Book DescriptionReact Hooks allow you to easily encapsulate, reuse, and refactor logic with the same level of simplicity that components bring to user interface organization. In this second edition, React expert and author of many popular React books, Daniel Bugl guides you through the full spectrum of React Hooks, and teaches you how to handle forms, routing, and data fetching in modern React development. This edition is fully updated to React 19, with a revamped structure, expanded real-world use cases, and coverage of all newly introduced Hooks. Starting with the fundamentals, you'll gain a deep understanding of how Hooks work under the hood and how to apply Hooks patterns efficiently. The chapters guide you through using State, Reducer, and Effect Hooks to manage application state, side effects, and complex logic for building your first Hook-based app. You'll utilize Suspense and Context APIs for streamlined data fetching and state management, master Form Actions for handling submissions, and implement routing with Hooks. You’ll also create custom Hooks for advanced functionality and write tests for reliability. Finally, you’ll learn how to refactor an existing app with class components into modern Hooks architecture. By the end of this React book, you'll be well-equipped to use React Hooks to modernize and optimize your React projects.What you will learn Master and apply new React 19 Hooks, such as useActionState, useFormStatus, useOptimistic, and others Use React Hooks and Context to manage state in your web applications Efficiently fetch, update, and cache data using TanStack Query and Suspense Manage user input and form submission using Form Actions with Hooks Discover how to implement routing in your apps using React Router and Hooks Create and test your own Hooks to encapsulate, reuse, and refactor logic in your apps Who this book is for This book is ideal for React developers looking to modernize their applications using React Hooks, Context, and Suspense. Beginners and experienced developers alike will gain a solid understanding of Hooks and their internal workings. If you're familiar with React and JavaScript, this book will help you upskill by teaching you best practices, performance optimization, and scalable application building. No prior experience with Hooks is required—this book covers everything from fundamentals to advanced patterns.", + "Categories": [ + "Computers" + ], + "PageCount": 373, + "Thumbnail": "http://books.google.com/books/content?id=NnBbEQAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=NnBbEQAAQBAJ\u0026source=gbs_api" + }, + "AddedAt": "2025-10-30T02:12:59.3668499Z" + }, + "TkB9EAAAQBAJ": { + "ID": "TkB9EAAAQBAJ", + "Book": { + "ID": "TkB9EAAAQBAJ", + "Title": "Getting started with Vue Native", + "Authors": [ + "Andrés Cruz" + ], + "PublishedDate": "", + "Description": "Vue Native is a framework to create native mobile applications for Android and IOS using JavaScript, specifically Vue; with this technology we can create good quality mobile applications. Another fundamental point is that Vue Native uses the React Native code for each of the components that we have to create our applications; therefore, Vue Native communicates directly with the React Native core to be able to display through components, images, texts, buttons, lists, and much more. The fundamental window that this platform has is that we can use the Vue and React Naive plugins to create our applications. Unfortunately this technology was obsolete at the time of writing this book; therefore, the book does not have all the chapters that were initially thought and it remains as a reference to start with this framework. However, even if it is obsolete or deprecated, it helps us to know everything that this tool offers us, we can use it to encourage us to develop in similar frameworks such as React Native, Galio, among others. As a recommendation, use Visual Studio Code as editor, since it is an excellent editor, with many customization options, extensions, intuitive, light and that you can develop in a lot of platforms, technologies, frameworks and programming languages; so overall Visual Studio Code will be a great companion for you; but, if you prefer other editors like Sublime Text, PHPStorm or similar, you can use it without any problem. Chapters This book has a total of 8 chapters, it is recommended that you read in the order in which they are arranged and as we explain the components of the framework, go directly to the practice, replicate, test and modify the codes that we show in this book. Chapter 1: It explains what is the necessary software, and its installation to develop in Vue Native. Chapter 2: We'll talk about Vue Native technology, create a project, and do some very basic testing. Chapter 3: We will give a detailed explanation of each of the most used components in Vue Native. Chapter 4: We will use a plugin to enable navigation between screens and see its practical use. Chapter 5: We will see the different ways to work with the styles in a project in Vue Native. Chapter 6: We will create a listing app consuming a Rest API on the Internet. Chapter 7: We will see the use of animations in Vue Native in a basic way along with tweens. Chapter 8: We will create a CRUD type application with forms, lists, several pages, validations, confirmation messages and redirections. Who is this book for This book is for anyone who wants to get started with mobile development and knows the basics of Vue. For those who want to know the framework and who know Vue, who want to know current frameworks similar to this one, but do not have the necessary knowledge to venture into these because they do not know the bases that support them. For those people who want to learn something new, learn about a framework that has very little documentation, who want to improve a skill in web development, who want to grow as a developer and who want to continue climbing their path with others frameworks superior to this; As long as you meet at least some of the above considerations, this book is for you.", + "Categories": [ + "Computers" + ], + "PageCount": 137, + "Thumbnail": "http://books.google.com/books/content?id=TkB9EAAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=TkB9EAAAQBAJ\u0026source=gbs_api" + }, + "AddedAt": "2025-10-30T01:04:53.628004Z" + }, + "Zu7xEAAAQBAJ": { + "ID": "Zu7xEAAAQBAJ", + "Book": { + "ID": "Zu7xEAAAQBAJ", + "Title": "Getting started with Vue 3", + "Authors": [ + "Andres Cruz Yoris" + ], + "PublishedDate": "", + "Description": "Vue is a versatile framework used in creating SPA-type websites; It is a modular, component-based technology where a component can be seen as a small piece of code and we can group components together to create more complex components. Vue is a small, simple and lightweight framework if we compare it with other frameworks like React or Angular, but its simplicity gives us windows like: Less steep learning curve than your competition. The framework is smaller than the competition (about 470 KB and 18 KB minified). It is a versatile framework which means that it can be used together with other solutions such as typescript. It is a reactive framework, which means that when your data model is updated the view is updated and vice versa. Vue is a progressive framework, which means that we can extend it through other officially supported plugins such as Vuex, Router, Testing, among others. This book is mostly practical, we will learn the basics of Vue, knowing its main features based on a small application that we will expand chapter after chapter. This book consists of 5 chapters, with which we will learn in detail the most important and basic features of Vue in version 3: Chapter 1: In this chapter we are going to learn about the basic features of Vue such as its main features, installation modes and project creation, we will make a hello world to present the main features of the web framework. Chapter 2: In this chapter we are going to learn about the 3 blocks of Vue, script block, template and style, in addition to creating small examples to present the main features of Vue. Chapter 3: In this chapter we are going to create our first CRUD type project using Vue and an existing CRUD type Rest Api; that is, a Rest Api with a limited scope using with Oruga UI as a component-based client-side web framework. Chapter 4: In this chapter we are going to create another CRUD type application in Vue using Naive UI instead of Oruga UI as a component-based web framework. Chapter 5: In this chapter we are going to create an application with Pinia and learn how to use this state manager and understand its components, which are the store, state, actions and getters. About the Book This guide is intended to take your first steps with Vue 3 using JavaScript; with this, we are going to propose two things: It is not a book that aims to know 100% of Vue in its version 3, or from zero to expert, since it would be too big an objective for the scope of this guide, otherwise, to know what it offers us and create the first web applications with Vue, know the use of components, hooks among other features of the framework. It is assumed that the reader has at least basic knowledge of JavaScript, HTML and CSS. This book has a practical approach, knowing the key aspects of the technology and moving into practice, gradually implementing small features of an application that has real scope. To follow this book you need to have a computer with Windows, Linux or MacOS. The book is currently in development. This book is currently in development and will have more chapters both at the end and in intermediate chapters; The book has two chapters taken from other books (Laravel and Django) that were adapted for this book.", + "Categories": [ + "Computers" + ], + "PageCount": 191, + "Thumbnail": "http://books.google.com/books/content?id=Zu7xEAAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=Zu7xEAAAQBAJ\u0026source=gbs_api" + }, + "AddedAt": "2025-10-30T01:04:54.5346284Z" + } + } +} diff --git a/back/data/tsundoku.json b/back/data/tsundoku.json new file mode 100644 index 0000000..2015dc2 --- /dev/null +++ b/back/data/tsundoku.json @@ -0,0 +1,224 @@ +{ + "items": { + "-PDPBvIPYBkC": { + "ID": "-PDPBvIPYBkC", + "Book": { + "ID": "-PDPBvIPYBkC", + "Title": "Classic Operating Systems", + "Authors": [ + "Per Brinch Hansen" + ], + "PublishedDate": "2001-01-10", + "Description": "An essential reader containing the 25 most important papers in the development of modern operating systems for computer science and software engineering. The papers illustrate the major breakthroughs in operating system technology from the 1950s to the 1990s. The editor provides an overview chapter and puts all development in perspective with chapter introductions and expository apparatus. Essential resource for graduates, professionals, and researchers in CS with an interest in operating system principles.", + "Categories": [ + "Computers" + ], + "PageCount": 626, + "Thumbnail": "http://books.google.com/books/content?id=-PDPBvIPYBkC\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "http://books.google.ca/books?id=-PDPBvIPYBkC\u0026dq=(%22operating+systems%22+OR+linux)\u0026hl=\u0026as_pt=BOOKS\u0026source=gbs_api" + }, + "Status": "done", + "AddedAt": "2025-10-30T01:30:40.0022187Z", + "UpdatedAt": "2025-10-30T02:51:23.9823855Z", + "StartedAt": "2025-10-30T02:11:26.794154Z", + "CompletedAt": "2025-10-30T02:51:23.9823855Z" + }, + "-jYe2k1p5tIC": { + "ID": "-jYe2k1p5tIC", + "Book": { + "ID": "-jYe2k1p5tIC", + "Title": "Linux System Administration", + "Authors": [ + "Tom Adelstein", + "Bill Lubanovic" + ], + "PublishedDate": "2007-03-27", + "Description": "This guide provides a solid background for Linux desktop users who want to move beyond the basics of Linux, and for experienced system administrators who are looking to gain more advanced skills.", + "Categories": [ + "Computers" + ], + "PageCount": 299, + "Thumbnail": "http://books.google.com/books/content?id=-jYe2k1p5tIC\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "http://books.google.ca/books?id=-jYe2k1p5tIC\u0026dq=(%22operating+systems%22+OR+linux)\u0026hl=\u0026as_pt=BOOKS\u0026source=gbs_api" + }, + "Status": "stacked", + "AddedAt": "2025-10-30T01:30:37.3648528Z", + "UpdatedAt": "2025-10-30T02:03:22.4834853Z" + }, + "0DidDwAAQBAJ": { + "ID": "0DidDwAAQBAJ", + "Book": { + "ID": "0DidDwAAQBAJ", + "Title": "React Native for Mobile Development", + "Authors": [ + "Akshat Paul", + "Abhishek Nalwaya" + ], + "PublishedDate": "2019-06-12", + "Description": "Develop native iOS and Android apps with ease using React Native. Learn by doing through an example-driven approach, and have a substantial running app at the end of each chapter. This second edition is fully updated to include ES7 (ECMAScript 7), the latest version of React Native (including Redux), and development on Android. You will start by setting up React Native and exploring the anatomy of React Native apps. You'll then move on to Redux data flow, how it differs from flux, and how you can include it in your React Native project to solve state management differently and efficiently. You will also learn how to boost your development by including popular packages developed by the React Native community that will help you write less; do more. Finally, you'll learn to how write test cases using Jest and submit your application to the App Store. React Native challenges the status quo of native iOS and Android development with revolutionary components, asynchronous execution, unique methods for touch handling, and much more. This book reveals the the path-breaking concepts of React.js and acquaints you with the React way of thinking so you can learn to create stunning user interfaces. What You'll Learn Build stunning iOS and Android applications Understand the Redux design pattern and use it in your project Interact with iOS and android device capabilities such as addressbook, camera, GPS and more with your apps Test and launch your application to the App Store Who This Book Is For Anyone with JavaScript experience who wants to build native mobile applications but dreads the thought of programming in Objective-C or Java. Developers who have experience with JavaScript but are new or not acquainted to React Native or ReactJS.", + "Categories": [ + "Computers" + ], + "PageCount": 243, + "Thumbnail": "http://books.google.com/books/content?id=0DidDwAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=0DidDwAAQBAJ\u0026source=gbs_api" + }, + "Status": "stacked", + "AddedAt": "2025-10-30T02:13:00.7224531Z", + "UpdatedAt": "2025-10-30T02:13:00.7224531Z" + }, + "6bfnEAAAQBAJ": { + "ID": "6bfnEAAAQBAJ", + "Book": { + "ID": "6bfnEAAAQBAJ", + "Title": "Mastering React Native", + "Authors": [ + "Cybellium" + ], + "PublishedDate": "", + "Description": "Elevate Your Mobile App Development with \"Mastering React Native\" In today's fast-paced world of mobile app development, creating high-quality cross-platform apps is essential for reaching a broad and engaged user base. React Native, a powerful and flexible framework developed by Facebook, empowers developers to build native mobile apps using their existing knowledge of JavaScript and React. \"Mastering React Native\" is your comprehensive guide to becoming a proficient app developer, equipping you with the knowledge, skills, and strategies to create stunning, performant, and cross-platform mobile applications. Your Path to React Native Excellence React Native is more than just a framework; it's a game-changer in the world of mobile app development. Whether you're new to React Native or an experienced developer looking to expand your skills, this book will empower you to master the art of cross-platform app development. What You Will Discover React Native Fundamentals: Gain a deep understanding of React Native, its architecture, and how it leverages JavaScript and React to build native apps. Cross-Platform Development: Dive into the world of cross-platform app development, enabling you to create apps that run seamlessly on both iOS and Android platforms. UI/UX Design: Learn to create captivating user interfaces using React Native's components, styles, and navigation. Native Integration: Explore how to integrate native device features, such as camera, geolocation, and sensors, into your apps. Data Management: Master data handling, storage, and synchronization, connecting your app to backends and databases. Testing and Debugging: Discover best practices for testing and debugging your React Native apps to ensure optimal performance and reliability. Deployment and Distribution: Learn how to package and distribute your apps to app stores and devices. Why \"Mastering React Native\" Is Essential Comprehensive Coverage: This book provides comprehensive coverage of React Native development, ensuring you have a well-rounded understanding of the framework's capabilities and best practices. Expert Guidance: Benefit from insights and advice from experienced React Native developers and industry experts who share their knowledge and proven strategies. Career Advancement: Cross-platform app development skills are highly sought after, and this book will help you unlock your full potential in this dynamic field. Stay Competitive: In a mobile-centric world, mastering React Native is essential for staying competitive and reaching a wide audience with your apps. Your Gateway to React Native Mastery \"Mastering React Native\" is your passport to excellence in cross-platform app development. Whether you aspire to be a mobile app developer, enhance your current app development skills, or broaden your app's reach across multiple platforms, this guide will equip you with the skills and knowledge to achieve your goals. Don't miss the opportunity to become a proficient React Native developer. Start your journey to React Native excellence today and join the ranks of developers who are shaping the future of cross-platform mobile app development. \"Mastering React Native\" is the ultimate resource for individuals seeking to excel in the world of cross-platform mobile app development. Whether you are new to React Native or looking to enhance your skills, this book will provide you with the knowledge and strategies to become a proficient cross-platform app developer. Don't wait; begin your journey to React Native mastery today! © 2023 Cybellium Ltd. All rights reserved. www.cybellium.com", + "Categories": [ + "Computers" + ], + "PageCount": 207, + "Thumbnail": "http://books.google.com/books/content?id=6bfnEAAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=6bfnEAAAQBAJ\u0026source=gbs_api" + }, + "Status": "stacked", + "AddedAt": "2025-10-30T02:12:58.3859711Z", + "UpdatedAt": "2025-10-30T02:12:58.3859711Z" + }, + "N-gWEQAAQBAJ": { + "ID": "N-gWEQAAQBAJ", + "Book": { + "ID": "N-gWEQAAQBAJ", + "Title": "Vue.js 3 for Beginners", + "Authors": [ + "Simone Cuomo" + ], + "PublishedDate": "2024-09-06", + "Description": "Gain proficiency in Vue.js 3 and its core libraries, including Pinia, Vue Router, and Vitest, by developing a social media web application with detailed, hands-on instructions Key Features Discover best practices for building scalable and performant Vue.js applications Learn the basics of component-based architecture Familiarize yourself with Vue.js core libraries and ecosystem Purchase of the print or Kindle book includes a free PDF eBook Book DescriptionDiscover why Vue.js is a must-learn JavaScript framework for aspiring developers. If you’re a beginner fascinated by Vue.js and its potential, then this book will show you how the progressive and versatile framework can help you build performant applications. Written by an accomplished software architect with over 12 years of experience, Vue.js 3 for Beginners provides a solid foundation in Vue.js and guides you at every step to create a robust social media application, component by component. Starting with a clean canvas using plain HTML and CSS, you’ll learn new topics to build your application incrementally. Beyond the core features, you’ll explore crucial parts of the Vue.js ecosystem, such as state management with Pinia, routing with Vue Router, and testing with Vitest, and Cypress. The structured GitHub repository ensures a smooth transition from one chapter to the next, offering valuable insights into advanced topics, techniques, and resources. This book is designed to serve as a practical reference guide, allowing you to quickly revisit specific topics when needed. By the end of the book, you’ll have built a strong understanding of Vue.js and be ready to build simple applications effortlessly.What you will learn Gain practical knowledge of the Vue.js framework Deepen your understanding of Pinia, Vue Router, validation libraries, and their integration with Vue.js applications Explore the core concepts of Vue.js, including components, directives, and data binding Create scalable, maintainable applications from scratch Build applications using the script setup and the Composition API Debug your applications with the Vue debugger tool Who this book is for Vue.js for Beginners is for aspiring web developers, students, hobbyists, or anyone who wants to learn Vue.js from scratch and is eager to foray into front-end development using this modern and popular framework. Basic knowledge of HTML, CSS, and JavaScript is required to fully grasp the content of this Vue.js book.", + "Categories": [ + "Computers" + ], + "PageCount": 302, + "Thumbnail": "http://books.google.com/books/content?id=N-gWEQAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=N-gWEQAAQBAJ\u0026source=gbs_api" + }, + "Status": "stacked", + "AddedAt": "2025-10-30T02:03:12.5881777Z", + "UpdatedAt": "2025-10-30T02:03:12.5881777Z" + }, + "NnBbEQAAQBAJ": { + "ID": "NnBbEQAAQBAJ", + "Book": { + "ID": "NnBbEQAAQBAJ", + "Title": "Learn React Hooks", + "Authors": [ + "Daniel Bugl" + ], + "PublishedDate": "2025-05-23", + "Description": "Grasp the core concepts of React Hooks and enhance your development workflow with best practices and advanced patterns derived from modern React 19 features Key Features Build custom Hooks to simplify complex logic and promote code reusability Transform class components into modern, hook-based function components, and write robust tests Build maintainable, production-ready UIs using React 19's declarative approach and ecosystem tools Purchase of the print or Kindle book includes a free PDF eBook Book DescriptionReact Hooks allow you to easily encapsulate, reuse, and refactor logic with the same level of simplicity that components bring to user interface organization. In this second edition, React expert and author of many popular React books, Daniel Bugl guides you through the full spectrum of React Hooks, and teaches you how to handle forms, routing, and data fetching in modern React development. This edition is fully updated to React 19, with a revamped structure, expanded real-world use cases, and coverage of all newly introduced Hooks. Starting with the fundamentals, you'll gain a deep understanding of how Hooks work under the hood and how to apply Hooks patterns efficiently. The chapters guide you through using State, Reducer, and Effect Hooks to manage application state, side effects, and complex logic for building your first Hook-based app. You'll utilize Suspense and Context APIs for streamlined data fetching and state management, master Form Actions for handling submissions, and implement routing with Hooks. You’ll also create custom Hooks for advanced functionality and write tests for reliability. Finally, you’ll learn how to refactor an existing app with class components into modern Hooks architecture. By the end of this React book, you'll be well-equipped to use React Hooks to modernize and optimize your React projects.What you will learn Master and apply new React 19 Hooks, such as useActionState, useFormStatus, useOptimistic, and others Use React Hooks and Context to manage state in your web applications Efficiently fetch, update, and cache data using TanStack Query and Suspense Manage user input and form submission using Form Actions with Hooks Discover how to implement routing in your apps using React Router and Hooks Create and test your own Hooks to encapsulate, reuse, and refactor logic in your apps Who this book is for This book is ideal for React developers looking to modernize their applications using React Hooks, Context, and Suspense. Beginners and experienced developers alike will gain a solid understanding of Hooks and their internal workings. If you're familiar with React and JavaScript, this book will help you upskill by teaching you best practices, performance optimization, and scalable application building. No prior experience with Hooks is required—this book covers everything from fundamentals to advanced patterns.", + "Categories": [ + "Computers" + ], + "PageCount": 373, + "Thumbnail": "http://books.google.com/books/content?id=NnBbEQAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=NnBbEQAAQBAJ\u0026source=gbs_api" + }, + "Status": "reading", + "AddedAt": "2025-10-30T02:12:59.6774055Z", + "UpdatedAt": "2025-10-30T02:51:24.9212529Z", + "StartedAt": "2025-10-30T02:51:24.9212529Z" + }, + "PPG8PvUyTOAC": { + "ID": "PPG8PvUyTOAC", + "Book": { + "ID": "PPG8PvUyTOAC", + "Title": "Linux For Dummies", + "Authors": [ + "Dee-Ann LeBlanc", + "Richard Blum" + ], + "PublishedDate": "2007-09-24", + "Description": "Focusing on Fedora Core 6, this accessible guide shows newcomers how to create a full-featured Linux desktop setup that's comparable to a Windows system Substantially revised and updated with new material on setting up a wireless home network, recycling an old Windows computer as a Linux home-networking server, running Linux on a laptop, editing digital photos, managing and playing audio and video, using open source productivity software, and more The DVD features the full Fedora Core installation and Fedora Core CD ISOs; there's also a coupon for readers who prefer to get Fedora Core on CD-ROM A companion Web site provides installation options and information on other popular Linux distributions, including SuSE, Mandriva, Linspire, Knoppix, and Ubuntu Note: CD-ROM/DVD and other supplementary materials are not included as part of eBook file.", + "Categories": [ + "Computers" + ], + "PageCount": 434, + "Thumbnail": "http://books.google.com/books/content?id=PPG8PvUyTOAC\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "http://books.google.ca/books?id=PPG8PvUyTOAC\u0026dq=(%22operating+systems%22+OR+linux)\u0026hl=\u0026as_pt=BOOKS\u0026source=gbs_api" + }, + "Status": "stacked", + "AddedAt": "2025-10-30T01:30:38.1248286Z", + "UpdatedAt": "2025-10-30T01:30:38.1248286Z" + }, + "TkB9EAAAQBAJ": { + "ID": "TkB9EAAAQBAJ", + "Book": { + "ID": "TkB9EAAAQBAJ", + "Title": "Getting started with Vue Native", + "Authors": [ + "Andrés Cruz" + ], + "PublishedDate": "", + "Description": "Vue Native is a framework to create native mobile applications for Android and IOS using JavaScript, specifically Vue; with this technology we can create good quality mobile applications. Another fundamental point is that Vue Native uses the React Native code for each of the components that we have to create our applications; therefore, Vue Native communicates directly with the React Native core to be able to display through components, images, texts, buttons, lists, and much more. The fundamental window that this platform has is that we can use the Vue and React Naive plugins to create our applications. Unfortunately this technology was obsolete at the time of writing this book; therefore, the book does not have all the chapters that were initially thought and it remains as a reference to start with this framework. However, even if it is obsolete or deprecated, it helps us to know everything that this tool offers us, we can use it to encourage us to develop in similar frameworks such as React Native, Galio, among others. As a recommendation, use Visual Studio Code as editor, since it is an excellent editor, with many customization options, extensions, intuitive, light and that you can develop in a lot of platforms, technologies, frameworks and programming languages; so overall Visual Studio Code will be a great companion for you; but, if you prefer other editors like Sublime Text, PHPStorm or similar, you can use it without any problem. Chapters This book has a total of 8 chapters, it is recommended that you read in the order in which they are arranged and as we explain the components of the framework, go directly to the practice, replicate, test and modify the codes that we show in this book. Chapter 1: It explains what is the necessary software, and its installation to develop in Vue Native. Chapter 2: We'll talk about Vue Native technology, create a project, and do some very basic testing. Chapter 3: We will give a detailed explanation of each of the most used components in Vue Native. Chapter 4: We will use a plugin to enable navigation between screens and see its practical use. Chapter 5: We will see the different ways to work with the styles in a project in Vue Native. Chapter 6: We will create a listing app consuming a Rest API on the Internet. Chapter 7: We will see the use of animations in Vue Native in a basic way along with tweens. Chapter 8: We will create a CRUD type application with forms, lists, several pages, validations, confirmation messages and redirections. Who is this book for This book is for anyone who wants to get started with mobile development and knows the basics of Vue. For those who want to know the framework and who know Vue, who want to know current frameworks similar to this one, but do not have the necessary knowledge to venture into these because they do not know the bases that support them. For those people who want to learn something new, learn about a framework that has very little documentation, who want to improve a skill in web development, who want to grow as a developer and who want to continue climbing their path with others frameworks superior to this; As long as you meet at least some of the above considerations, this book is for you.", + "Categories": [ + "Computers" + ], + "PageCount": 137, + "Thumbnail": "http://books.google.com/books/content?id=TkB9EAAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=TkB9EAAAQBAJ\u0026source=gbs_api" + }, + "Status": "done", + "AddedAt": "2025-10-30T01:04:58.4837823Z", + "UpdatedAt": "2025-10-30T02:03:14.7282018Z", + "StartedAt": "2025-10-30T01:30:52.4486228Z", + "CompletedAt": "2025-10-30T02:03:14.7282018Z" + }, + "Zu7xEAAAQBAJ": { + "ID": "Zu7xEAAAQBAJ", + "Book": { + "ID": "Zu7xEAAAQBAJ", + "Title": "Getting started with Vue 3", + "Authors": [ + "Andres Cruz Yoris" + ], + "PublishedDate": "", + "Description": "Vue is a versatile framework used in creating SPA-type websites; It is a modular, component-based technology where a component can be seen as a small piece of code and we can group components together to create more complex components. Vue is a small, simple and lightweight framework if we compare it with other frameworks like React or Angular, but its simplicity gives us windows like: Less steep learning curve than your competition. The framework is smaller than the competition (about 470 KB and 18 KB minified). It is a versatile framework which means that it can be used together with other solutions such as typescript. It is a reactive framework, which means that when your data model is updated the view is updated and vice versa. Vue is a progressive framework, which means that we can extend it through other officially supported plugins such as Vuex, Router, Testing, among others. This book is mostly practical, we will learn the basics of Vue, knowing its main features based on a small application that we will expand chapter after chapter. This book consists of 5 chapters, with which we will learn in detail the most important and basic features of Vue in version 3: Chapter 1: In this chapter we are going to learn about the basic features of Vue such as its main features, installation modes and project creation, we will make a hello world to present the main features of the web framework. Chapter 2: In this chapter we are going to learn about the 3 blocks of Vue, script block, template and style, in addition to creating small examples to present the main features of Vue. Chapter 3: In this chapter we are going to create our first CRUD type project using Vue and an existing CRUD type Rest Api; that is, a Rest Api with a limited scope using with Oruga UI as a component-based client-side web framework. Chapter 4: In this chapter we are going to create another CRUD type application in Vue using Naive UI instead of Oruga UI as a component-based web framework. Chapter 5: In this chapter we are going to create an application with Pinia and learn how to use this state manager and understand its components, which are the store, state, actions and getters. About the Book This guide is intended to take your first steps with Vue 3 using JavaScript; with this, we are going to propose two things: It is not a book that aims to know 100% of Vue in its version 3, or from zero to expert, since it would be too big an objective for the scope of this guide, otherwise, to know what it offers us and create the first web applications with Vue, know the use of components, hooks among other features of the framework. It is assumed that the reader has at least basic knowledge of JavaScript, HTML and CSS. This book has a practical approach, knowing the key aspects of the technology and moving into practice, gradually implementing small features of an application that has real scope. To follow this book you need to have a computer with Windows, Linux or MacOS. The book is currently in development. This book is currently in development and will have more chapters both at the end and in intermediate chapters; The book has two chapters taken from other books (Laravel and Django) that were adapted for this book.", + "Categories": [ + "Computers" + ], + "PageCount": 191, + "Thumbnail": "http://books.google.com/books/content?id=Zu7xEAAAQBAJ\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "https://play.google.com/store/books/details?id=Zu7xEAAAQBAJ\u0026source=gbs_api" + }, + "Status": "done", + "AddedAt": "2025-10-30T01:04:57.6657014Z", + "UpdatedAt": "2025-10-30T01:30:51.0762713Z", + "StartedAt": "2025-10-30T01:30:44.0648144Z", + "CompletedAt": "2025-10-30T01:30:51.0762713Z" + }, + "k_ocKY0iegsC": { + "ID": "k_ocKY0iegsC", + "Book": { + "ID": "k_ocKY0iegsC", + "Title": "Linux System Programming", + "Authors": [ + "Robert Love" + ], + "PublishedDate": "2007-09-18", + "Description": "This book is about writing software that makes the most effective use of the system you're running on -- code that interfaces directly with the kernel and core system libraries, including the shell, text editor, compiler, debugger, core utilities, and system daemons. The majority of both Unix and Linux code is still written at the system level, and Linux System Programming focuses on everything above the kernel, where applications such as Apache, bash, cp, vim, Emacs, gcc, gdb, glibc, ls, mv, and X exist. Written primarily for engineers looking to program (better) at the low level, this book is an ideal teaching tool for any programmer. Even with the trend toward high-level development, either through web software (such as PHP) or managed code (C#), someone still has to write the PHP interpreter and the C# virtual machine. Linux System Programming gives you an understanding of core internals that makes for better code, no matter where it appears in the stack. Debugging high-level code often requires you to understand the system calls and kernel behavior of your operating system, too. Key topics include: An overview of Linux, the kernel, the C library, and the C compiler Reading from and writing to files, along with other basic file I/O operations, including how the Linux kernel implements and manages file I/O Buffer size management, including the Standard I/O library Advanced I/O interfaces, memory mappings, and optimization techniques The family of system calls for basic process management Advanced process management, including real-time processes File and directories-creating, moving, copying, deleting, and managing them Memory management -- interfaces for allocating memory, managing the memory youhave, and optimizing your memory access Signals and their role on a Unix system, plus basic and advanced signal interfaces Time, sleeping, and clock management, starting with the basics and continuing through POSIX clocks and high resolution timers With Linux System Programming, you will be able to take an in-depth look at Linux from both a theoretical and an applied perspective as you cover a wide range of programming topics.", + "Categories": [ + "Computers" + ], + "PageCount": 391, + "Thumbnail": "http://books.google.com/books/content?id=k_ocKY0iegsC\u0026printsec=frontcover\u0026img=1\u0026zoom=1\u0026edge=curl\u0026source=gbs_api", + "InfoLink": "http://books.google.ca/books?id=k_ocKY0iegsC\u0026dq=(%22operating+systems%22+OR+linux)\u0026hl=\u0026as_pt=BOOKS\u0026source=gbs_api" + }, + "Status": "stacked", + "AddedAt": "2025-10-30T01:30:38.9482934Z", + "UpdatedAt": "2025-10-30T01:30:38.9482934Z" + } + } +} diff --git a/back/go.mod b/back/go.mod index cc948d4..706cbf5 100644 --- a/back/go.mod +++ b/back/go.mod @@ -2,4 +2,4 @@ module github.com/recursion-goapi-project/technical-books-search/back go 1.25.3 -require github.com/go-chi/chi/v5 v5.2.3 // indirect +require github.com/go-chi/chi/v5 v5.2.3 diff --git a/back/internal/handler/favorites.go b/back/internal/handler/favorites.go new file mode 100644 index 0000000..0ca8a8d --- /dev/null +++ b/back/internal/handler/favorites.go @@ -0,0 +1,90 @@ +package handler + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/go-chi/chi/v5" + + "github.com/recursion-goapi-project/technical-books-search/back/internal/service/books" + "github.com/recursion-goapi-project/technical-books-search/back/internal/service/favorites" +) + +// FavoritesHandler exposes HTTP handlers for favorites features. +type FavoritesHandler struct { + service *favorites.Service +} + +// NewFavoritesHandler creates a handler set bound to the service. +func NewFavoritesHandler(service *favorites.Service) *FavoritesHandler { + return &FavoritesHandler{service: service} +} + +// Register wires the handler to the provided router. +func (h *FavoritesHandler) Register(r chi.Router) { + r.Get("/", h.List) + r.Post("/", h.Add) + r.Delete("/{id}", h.Delete) +} + +type addFavoriteRequest struct { + Book books.Book `json:"Book"` +} + +// Add creates a new favorite item. +func (h *FavoritesHandler) Add(w http.ResponseWriter, r *http.Request) { + var req addFavoriteRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + item, err := h.service.Add(r.Context(), req.Book) + if err != nil { + switch { + case errors.Is(err, favorites.ErrInvalidInput): + http.Error(w, err.Error(), http.StatusBadRequest) + case errors.Is(err, favorites.ErrAlreadyExists): + http.Error(w, err.Error(), http.StatusConflict) + default: + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + return + } + writeJSON(w, http.StatusCreated, item) +} + +// List returns all favorite items. +func (h *FavoritesHandler) List(w http.ResponseWriter, r *http.Request) { + items, err := h.service.List(r.Context()) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) +} + +// Delete removes a favorite by book ID. +func (h *FavoritesHandler) Delete(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + if id == "" { + http.Error(w, "Book ID is required", http.StatusBadRequest) + return + } + + err := h.service.Delete(r.Context(), id) + if err != nil { + switch { + case errors.Is(err, favorites.ErrNotFound): + http.Error(w, err.Error(), http.StatusNotFound) + case errors.Is(err, favorites.ErrInvalidInput): + http.Error(w, err.Error(), http.StatusBadRequest) + default: + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/back/internal/infra/favorites/filestore/repository.go b/back/internal/infra/favorites/filestore/repository.go new file mode 100644 index 0000000..394cf3e --- /dev/null +++ b/back/internal/infra/favorites/filestore/repository.go @@ -0,0 +1,142 @@ +package filestore + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "sync" + + "github.com/recursion-goapi-project/technical-books-search/back/internal/service/favorites" +) + +// Repository persists favorite items on the local filesystem as JSON. +type Repository struct { + path string + mu sync.Mutex +} + +type store struct { + Items map[string]favorites.Item `json:"items"` +} + +// New creates a file-backed repository for favorites. +func New(path string) (*Repository, error) { + if path == "" { + return nil, fmt.Errorf("filestore path is required") + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return nil, err + } + if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { + if err := os.WriteFile(path, []byte(`{"items":{}}`), 0o644); err != nil { + return nil, err + } + } + return &Repository{path: path}, nil +} + +func (r *Repository) Get(_ context.Context, bookID string) (favorites.Item, error) { + r.mu.Lock() + defer r.mu.Unlock() + + st, err := r.load() + if err != nil { + return favorites.Item{}, err + } + item, ok := st.Items[bookID] + if !ok { + return favorites.Item{}, favorites.ErrNotFound + } + return item, nil +} + +func (r *Repository) Upsert(_ context.Context, item favorites.Item) error { + r.mu.Lock() + defer r.mu.Unlock() + + st, err := r.load() + if err != nil { + return err + } + if st.Items == nil { + st.Items = make(map[string]favorites.Item) + } + st.Items[item.ID] = item + return r.persist(st) +} + +func (r *Repository) Delete(_ context.Context, bookID string) error { + r.mu.Lock() + defer r.mu.Unlock() + + st, err := r.load() + if err != nil { + return err + } + delete(st.Items, bookID) + return r.persist(st) +} + +func (r *Repository) List(_ context.Context) ([]favorites.Item, error) { + r.mu.Lock() + defer r.mu.Unlock() + + st, err := r.load() + if err != nil { + return nil, err + } + + var items []favorites.Item + for _, it := range st.Items { + items = append(items, it) + } + sort.Slice(items, func(i, j int) bool { + if items[i].AddedAt.Equal(items[j].AddedAt) { + return items[i].ID < items[j].ID + } + return items[i].AddedAt.Before(items[j].AddedAt) + }) + return items, nil +} + +func (r *Repository) load() (store, error) { + bytes, err := os.ReadFile(r.path) + if err != nil { + return store{}, err + } + if len(bytes) == 0 { + return store{Items: make(map[string]favorites.Item)}, nil + } + var st store + if err := json.Unmarshal(bytes, &st); err != nil { + return store{}, err + } + if st.Items == nil { + st.Items = make(map[string]favorites.Item) + } + return st, nil +} + +func (r *Repository) persist(st store) error { + tmp, err := os.CreateTemp(filepath.Dir(r.path), "favorites-*.json") + if err != nil { + return err + } + enc := json.NewEncoder(tmp) + enc.SetIndent("", " ") + if err := enc.Encode(st); err != nil { + tmp.Close() + _ = os.Remove(tmp.Name()) + return err + } + if err := tmp.Close(); err != nil { + return err + } + return os.Rename(tmp.Name(), r.path) +} + +var _ favorites.Repository = (*Repository)(nil) diff --git a/back/internal/server/router.go b/back/internal/server/router.go index ddefd8f..1fcd0e0 100644 --- a/back/internal/server/router.go +++ b/back/internal/server/router.go @@ -4,21 +4,44 @@ import ( "net/http" "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "github.com/recursion-goapi-project/technical-books-search/back/internal/handler" ) -func NewRouter(searchBooksHandler http.HandlerFunc, tsundokuHandler *handler.TsundokuHandler) *chi.Mux { +// NewRouter creates and configures the main HTTP router with all endpoints and middleware. +func NewRouter(searchBooksHandler http.HandlerFunc, tsundokuHandler *handler.TsundokuHandler, favoritesHandler *handler.FavoritesHandler) *chi.Mux { r := chi.NewRouter() - // Health Check + // Apply middleware + r.Use(middleware.Recoverer) // Recover from panics + r.Use(corsMiddleware) // Enable CORS for frontend + + // Health check endpoint r.Get("/health", handler.HealthCheckHandler) - // Technical Books Search + // API routes r.Get("/api/technical-books", searchBooksHandler) - - // Tsundoku r.Route("/api/tsundoku", tsundokuHandler.Register) + r.Route("/api/favorites", favoritesHandler.Register) return r } + +// corsMiddleware enables Cross-Origin Resource Sharing for all routes. +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set CORS headers + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + // Handle preflight requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/back/internal/service/favorites/errors.go b/back/internal/service/favorites/errors.go new file mode 100644 index 0000000..8de0078 --- /dev/null +++ b/back/internal/service/favorites/errors.go @@ -0,0 +1,14 @@ +package favorites + +import "errors" + +var ( + // ErrNotFound is returned when a favorite item does not exist. + ErrNotFound = errors.New("favorite not found") + + // ErrAlreadyExists is returned when attempting to add a duplicate favorite. + ErrAlreadyExists = errors.New("favorite already exists") + + // ErrInvalidInput is returned when required fields are missing. + ErrInvalidInput = errors.New("invalid input") +) diff --git a/back/internal/service/favorites/port.go b/back/internal/service/favorites/port.go new file mode 100644 index 0000000..05a7597 --- /dev/null +++ b/back/internal/service/favorites/port.go @@ -0,0 +1,18 @@ +package favorites + +import "context" + +// Repository defines the data layer for favorite operations. +type Repository interface { + // Get retrieves a favorite by book ID. + Get(ctx context.Context, bookID string) (Item, error) + + // Upsert creates or updates a favorite item. + Upsert(ctx context.Context, item Item) error + + // Delete removes a favorite by book ID. + Delete(ctx context.Context, bookID string) error + + // List returns all favorite items. + List(ctx context.Context) ([]Item, error) +} diff --git a/back/internal/service/favorites/service.go b/back/internal/service/favorites/service.go new file mode 100644 index 0000000..3752afc --- /dev/null +++ b/back/internal/service/favorites/service.go @@ -0,0 +1,78 @@ +package favorites + +import ( + "context" + "errors" + "time" + + "github.com/recursion-goapi-project/technical-books-search/back/internal/service/books" +) + +// Service contains the application logic for favorites operations. +type Service struct { + repo Repository + now func() time.Time +} + +// NewService creates a new favorites service. +func NewService(repo Repository) *Service { + return &Service{ + repo: repo, + now: time.Now, + } +} + +// WithNow overrides the now function (primarily for testing). +func (s *Service) WithNow(fn func() time.Time) { + if fn != nil { + s.now = fn + } +} + +// Add creates a new favorite item. +func (s *Service) Add(ctx context.Context, book books.Book) (Item, error) { + if book.ID == "" { + return Item{}, ErrInvalidInput + } + + // Check if already exists + _, err := s.repo.Get(ctx, book.ID) + if err == nil { + return Item{}, ErrAlreadyExists + } + if !errors.Is(err, ErrNotFound) { + return Item{}, err + } + + now := s.now().UTC() + item := Item{ + ID: book.ID, + Book: book, + AddedAt: now, + } + + if err := s.repo.Upsert(ctx, item); err != nil { + return Item{}, err + } + return item, nil +} + +// List retrieves all favorite items. +func (s *Service) List(ctx context.Context) ([]Item, error) { + return s.repo.List(ctx) +} + +// Delete removes a favorite by book ID. +func (s *Service) Delete(ctx context.Context, bookID string) error { + if bookID == "" { + return ErrInvalidInput + } + + // Check if exists + _, err := s.repo.Get(ctx, bookID) + if err != nil { + return err + } + + return s.repo.Delete(ctx, bookID) +} diff --git a/back/internal/service/favorites/types.go b/back/internal/service/favorites/types.go new file mode 100644 index 0000000..c87d903 --- /dev/null +++ b/back/internal/service/favorites/types.go @@ -0,0 +1,14 @@ +package favorites + +import ( + "time" + + "github.com/recursion-goapi-project/technical-books-search/back/internal/service/books" +) + +// Item represents a favorite book entry. +type Item struct { + ID string `json:"ID"` + Book books.Book `json:"Book"` + AddedAt time.Time `json:"AddedAt"` +} diff --git a/back/internal/service/tsundoku/service.go b/back/internal/service/tsundoku/service.go index 46bc34b..0ec85c7 100644 --- a/back/internal/service/tsundoku/service.go +++ b/back/internal/service/tsundoku/service.go @@ -77,7 +77,7 @@ func (s *Service) Pickup(ctx context.Context) (Item, error) { if err != nil { return Item{}, err } - if len(readings) > 0 { + if len(readings) > 1 { return Item{}, ErrReadingInProgress } @@ -104,7 +104,7 @@ func (s *Service) StartReading(ctx context.Context, id string) (Item, error) { if err != nil { return Item{}, err } - if len(readings) > 0 { + if len(readings) > 1 { return Item{}, ErrReadingInProgress } diff --git a/back/server.exe b/back/server.exe new file mode 100644 index 0000000..7dcdd17 Binary files /dev/null and b/back/server.exe differ diff --git a/front/package-lock.json b/front/package-lock.json index 0b4ac54..5c1f7a3 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -55,6 +55,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1387,6 +1388,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1428,6 +1430,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1533,6 +1536,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -1754,6 +1758,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2416,6 +2421,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2477,6 +2483,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2721,6 +2728,7 @@ "integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/front/src/App.tsx b/front/src/App.tsx index 726f0b7..0acb9e7 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -2,15 +2,22 @@ import { useMemo, useState } from 'react'; import type { CSSProperties } from 'react'; import SearchPage from './pages/SearchPage'; import TsundokuPage from './pages/TsundokuPage'; +import FavoritesPage from './pages/FavoritesPage'; import { useTsundoku } from './hooks/useTsundoku'; -import type { Book, TsundokuItem } from './types'; +import { useFavorites } from './hooks/useFavorites'; +import type { Book, TsundokuItem, FavoriteItem } from './types'; -type View = 'search' | 'tsundoku'; +type View = 'search' | 'tsundoku' | 'favorites'; +/** + * Main application component that manages the navigation between different views + */ export default function App() { const [view, setView] = useState('search'); const tsundoku = useTsundoku(); + const favorites = useFavorites(); + // Create index maps for quick lookup of tsundoku and favorite items by ID const tsundokuIndex = useMemo>>(() => { const index: Record = {}; tsundoku.items.forEach((item) => { @@ -19,33 +26,55 @@ export default function App() { return index; }, [tsundoku.items]); + const favoritesIndex = useMemo(() => { + const index: Record = {}; + favorites.items.forEach((item) => { + index[item.ID] = item; + }); + return index; + }, [favorites.items]); + + // Handler functions for adding/removing items const handleAddTsundoku = (book: Book) => tsundoku.add(book); + const handleAddFavorite = (book: Book) => favorites.add(book); + const handleRemoveFavorite = (bookId: string) => favorites.remove(bookId); return ( -
+
-
-

Technical Books Search

+
+ 📚 +
+

+ Technical Books +

+

Discover & Organize Your Learning

+
-
@@ -96,15 +136,21 @@ const mainStyle: CSSProperties = { function navButtonStyle(active: boolean): CSSProperties { return { - padding: '6px 20px', - borderRadius: 999, - border: active ? '1px solid rgba(255,255,255,0.9)' : '1px solid rgba(148,163,184,0.35)', - background: active ? '#e2e8f0' : 'transparent', - color: active ? '#0f172a' : '#e2e8f0', + padding: '10px 20px', + borderRadius: 12, + border: 'none', + background: active + ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' + : 'transparent', + color: active ? '#fff' : '#4a5568', cursor: active ? 'default' : 'pointer', - fontSize: 15, + fontSize: 14, + fontWeight: active ? 600 : 500, display: 'flex', alignItems: 'center', justifyContent: 'center', + transition: 'all 0.3s ease', + boxShadow: active ? '0 4px 15px rgba(102, 126, 234, 0.4)' : 'none', + transform: active ? 'translateY(-2px)' : 'none', }; } diff --git a/front/src/api.ts b/front/src/api.ts index ee11813..6718816 100644 --- a/front/src/api.ts +++ b/front/src/api.ts @@ -1,5 +1,8 @@ -import type { Book, SearchResponse, TsundokuItem, TsundokuStatus } from './types'; +import type { Book, SearchResponse, TsundokuItem, TsundokuStatus, FavoriteItem } from './types'; +/** + * Parameters for searching books via Google Books API + */ export type SearchParams = { q?: string; page?: number; @@ -8,15 +11,22 @@ export type SearchParams = { lang?: string; }; +/** + * Custom error class for API-related errors + */ export class ApiError extends Error { status: number; constructor(status: number, message: string) { super(message); this.status = status; + this.name = 'ApiError'; } } +/** + * Helper function to parse API responses + */ async function parseResponse(res: Response): Promise { if (!res.ok) { const text = await res.text(); @@ -28,6 +38,9 @@ async function parseResponse(res: Response): Promise { return res.json() as Promise; } +/** + * Search for technical books using the Google Books API + */ export async function searchBooks(params: SearchParams): Promise { const usp = new URLSearchParams(); if (params.q) usp.set('q', params.q); @@ -42,6 +55,13 @@ export async function searchBooks(params: SearchParams): Promise return parseResponse(res); } +// ============================================================================ +// Tsundoku (Reading List) API +// ============================================================================ + +/** + * Fetch all tsundoku items, optionally filtered by status + */ export async function fetchTsundokuItems(status?: TsundokuStatus): Promise { const usp = new URLSearchParams(); if (status) usp.set('status', status); @@ -99,3 +119,37 @@ export async function restackTsundokuItem(id: string): Promise { }); return parseResponse(res); } + +// ============================================================================ +// Favorites API +// ============================================================================ + +/** + * Fetch all favorite items + */ +export async function fetchFavoriteItems(): Promise { + const res = await fetch('/api/favorites'); + return parseResponse(res); +} + +/** + * Add a book to favorites + */ +export async function addFavoriteItem(book: Book): Promise { + const res = await fetch('/api/favorites', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Book: book }), + }); + return parseResponse(res); +} + +/** + * Remove a book from favorites by its ID + */ +export async function removeFavoriteItem(bookId: string): Promise { + const res = await fetch(`/api/favorites/${encodeURIComponent(bookId)}`, { + method: 'DELETE', + }); + return parseResponse(res); +} diff --git a/front/src/components/Pagination.tsx b/front/src/components/Pagination.tsx index 5ab5b1e..50452db 100644 --- a/front/src/components/Pagination.tsx +++ b/front/src/components/Pagination.tsx @@ -8,10 +8,83 @@ type Props = { export default function Pagination({ page, hasNext, loading, onPrev, onNext }: Props) { return ( -
- - Page {page} - +
+ + + Page {page} + +
); } diff --git a/front/src/components/ResultCard.tsx b/front/src/components/ResultCard.tsx index aaecba7..da2e74e 100644 --- a/front/src/components/ResultCard.tsx +++ b/front/src/components/ResultCard.tsx @@ -1,10 +1,13 @@ import { useState, type MouseEvent, type CSSProperties } from 'react'; -import type { Book, TsundokuItem } from '../types'; +import type { Book, TsundokuItem, FavoriteItem } from '../types'; type Props = { item: Book; tsundokuItem?: TsundokuItem; + favoriteItem?: FavoriteItem; onAddTsundoku?: (book: Book) => Promise; + onAddFavorite?: (book: Book) => Promise; + onRemoveFavorite?: (bookId: string) => Promise; }; function statusLabel(status?: string) { @@ -20,10 +23,12 @@ function statusLabel(status?: string) { } } -export default function ResultCard({ item, tsundokuItem, onAddTsundoku }: Props) { +export default function ResultCard({ item, tsundokuItem, favoriteItem, onAddTsundoku, onAddFavorite, onRemoveFavorite }: Props) { const [adding, setAdding] = useState(false); + const [favoriting, setFavoriting] = useState(false); const status = tsundokuItem?.Status; const disabled = adding || (status !== undefined && status !== 'done'); + const isFavorite = !!favoriteItem; const buttonIcon = (() => { if (adding) return '…'; @@ -57,53 +62,164 @@ export default function ResultCard({ item, tsundokuItem, onAddTsundoku }: Props) } }; + const handleFavoriteToggle = async (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (isFavorite && onRemoveFavorite) { + try { + setFavoriting(true); + await onRemoveFavorite(item.ID); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'お気に入りから削除できませんでした'; + window.alert(message); + } finally { + setFavoriting(false); + } + } else if (!isFavorite && onAddFavorite) { + try { + setFavoriting(true); + await onAddFavorite(item); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'お気に入りに追加できませんでした'; + window.alert(message); + } finally { + setFavoriting(false); + } + } + }; + return (
{ + e.currentTarget.style.transform = 'translateY(-4px)'; + e.currentTarget.style.boxShadow = '0 12px 32px rgba(102, 126, 234, 0.2)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 4px 20px rgba(102, 126, 234, 0.08)'; }} > - {onAddTsundoku && ( - - )} +
+ {(onAddFavorite || onRemoveFavorite) && ( + + )} + {onAddTsundoku && ( + + )} +
-
+
{item.Thumbnail - ? {item.Title} - : No Image} + ? {item.Title} + : 📖}
-
{item.Title}
-
{Array.isArray(item.Authors) ? item.Authors.join(', ') : ''}
+
{item.Title}
+
{Array.isArray(item.Authors) ? item.Authors.join(', ') : ''}
{status && (
@@ -119,20 +235,18 @@ function statusBadgeStyle(status: string): CSSProperties { display: 'inline-flex', alignItems: 'center', gap: 4, - fontSize: 11, - padding: '4px 8px', + fontSize: 12, + padding: '6px 12px', borderRadius: 999, - background: '#e2e8f0', - color: '#0f172a', - fontWeight: 500, + fontWeight: 600, }; switch (status) { case 'stacked': - return { ...base, background: '#dbeafe', color: '#1d4ed8' }; + return { ...base, background: 'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)', color: '#1e40af' }; case 'reading': - return { ...base, background: '#ffedd5', color: '#c2410c' }; + return { ...base, background: 'linear-gradient(135deg, #fed7aa 0%, #fdba74 100%)', color: '#9a3412' }; case 'done': - return { ...base, background: '#dcfce7', color: '#15803d' }; + return { ...base, background: 'linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%)', color: '#065f46' }; default: return base; } diff --git a/front/src/components/ResultsGrid.tsx b/front/src/components/ResultsGrid.tsx index e8de8ad..f4fdd80 100644 --- a/front/src/components/ResultsGrid.tsx +++ b/front/src/components/ResultsGrid.tsx @@ -1,13 +1,16 @@ -import type { Book, TsundokuItem } from '../types'; +import type { Book, TsundokuItem, FavoriteItem } from '../types'; import ResultCard from './ResultCard'; type Props = { items?: Book[]; getTsundokuItem?: (id: string) => TsundokuItem | undefined; + getFavoriteItem?: (id: string) => FavoriteItem | undefined; onAddTsundoku?: (book: Book) => Promise; + onAddFavorite?: (book: Book) => Promise; + onRemoveFavorite?: (bookId: string) => Promise; }; -export default function ResultsGrid({ items, getTsundokuItem, onAddTsundoku }: Props) { +export default function ResultsGrid({ items, getTsundokuItem, getFavoriteItem, onAddTsundoku, onAddFavorite, onRemoveFavorite }: Props) { if (!items) return null; if (items.length === 0) return
結果がありません
; return ( @@ -21,7 +24,10 @@ export default function ResultsGrid({ items, getTsundokuItem, onAddTsundoku }: P key={b.ID} item={b} tsundokuItem={getTsundokuItem?.(b.ID)} + favoriteItem={getFavoriteItem?.(b.ID)} onAddTsundoku={onAddTsundoku} + onAddFavorite={onAddFavorite} + onRemoveFavorite={onRemoveFavorite} /> ))}
diff --git a/front/src/components/SearchBar.tsx b/front/src/components/SearchBar.tsx index 24a266d..d67ae4b 100644 --- a/front/src/components/SearchBar.tsx +++ b/front/src/components/SearchBar.tsx @@ -7,17 +7,63 @@ type Props = { export default function SearchBar({ q, onChangeQ, onSubmit, disabled }: Props) { return ( -
{ e.preventDefault(); onSubmit(); }} style={{ display: 'grid', gap: 8, marginBottom: 12 }}> - + { e.preventDefault(); onSubmit(); }} style={{ display: 'grid', gap: 12 }}> + onChangeQ(e.target.value)} - placeholder="キーワード (例: golang)" - style={{ padding: 8 }} + placeholder="Keywords (e.g., golang, react, machine learning...)" + style={{ + padding: '14px 18px', + borderRadius: 12, + border: '2px solid rgba(102, 126, 234, 0.2)', + fontSize: 15, + outline: 'none', + transition: 'all 0.3s ease', + }} + onFocus={(e) => { + e.currentTarget.style.borderColor = '#667eea'; + e.currentTarget.style.boxShadow = '0 0 0 4px rgba(102, 126, 234, 0.1)'; + }} + onBlur={(e) => { + e.currentTarget.style.borderColor = 'rgba(102, 126, 234, 0.2)'; + e.currentTarget.style.boxShadow = 'none'; + }} />
- +
); diff --git a/front/src/components/TechTags.tsx b/front/src/components/TechTags.tsx index ea8c4ba..7f40a06 100644 --- a/front/src/components/TechTags.tsx +++ b/front/src/components/TechTags.tsx @@ -7,25 +7,47 @@ type Props = { export default function TechTags({ selected, onToggle }: Props) { return ( -
- {TECH_TAGS.map((t) => { - const active = selected.includes(t.key); - return ( - - ); - })} +
+ +
+ {TECH_TAGS.map((t) => { + const active = selected.includes(t.key); + return ( + + ); + })} +
); } diff --git a/front/src/hooks/useFavorites.ts b/front/src/hooks/useFavorites.ts new file mode 100644 index 0000000..a5ac376 --- /dev/null +++ b/front/src/hooks/useFavorites.ts @@ -0,0 +1,116 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ApiError, addFavoriteItem, fetchFavoriteItems, removeFavoriteItem } from '../api'; +import type { Book, FavoriteItem } from '../types'; + +/** + * Sort favorite items by added date (oldest first) + */ +function sortItems(items: FavoriteItem[]): FavoriteItem[] { + return [...items].sort((a, b) => { + const aTime = new Date(a.AddedAt).getTime(); + const bTime = new Date(b.AddedAt).getTime(); + if (aTime === bTime) { + return a.ID.localeCompare(b.ID); + } + return aTime - bTime; + }); +} + +/** + * Custom hook for managing favorites state and operations + */ +export function useFavorites() { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + // Fetch all favorites from the API + const refresh = useCallback(async () => { + setLoading(true); + try { + const data = await fetchFavoriteItems(); + setItems(sortItems(data)); + setError(''); + } catch (e: unknown) { + const message = e instanceof Error ? e.message : 'Failed to fetch favorites'; + setError(message); + throw e; + } finally { + setLoading(false); + } + }, []); + + // Load favorites on mount + useEffect(() => { + refresh().catch(() => {}); + }, [refresh]); + + const upsertItem = useCallback((next: FavoriteItem) => { + setItems((prev) => { + const filtered = prev.filter((item) => item.ID !== next.ID); + return sortItems([...filtered, next]); + }); + }, []); + + const removeItem = useCallback((bookId: string) => { + setItems((prev) => prev.filter((item) => item.ID !== bookId)); + }, []); + + const add = useCallback(async (book: Book) => { + try { + const created = await addFavoriteItem(book); + upsertItem(created); + setError(''); + return created; + } catch (e: unknown) { + if (e instanceof ApiError && e.status === 409) { + const friendly = new Error('この本は既にお気に入りに追加されています'); + setError(friendly.message); + throw friendly; + } + const err = e instanceof Error ? e : new Error('お気に入りの追加に失敗しました'); + setError(err.message); + throw err; + } + }, [upsertItem]); + + const remove = useCallback(async (bookId: string) => { + try { + await removeFavoriteItem(bookId); + removeItem(bookId); + setError(''); + } catch (e: unknown) { + if (e instanceof ApiError && e.status === 404) { + const friendly = new Error('お気に入りが見つかりませんでした'); + setError(friendly.message); + throw friendly; + } + const err = e instanceof Error ? e : new Error('お気に入りの削除に失敗しました'); + setError(err.message); + throw err; + } + }, [removeItem]); + + const favoriteMap = useMemo(() => { + const acc = new Map(); + items.forEach((item) => { + acc.set(item.ID, item); + }); + return acc; + }, [items]); + + const isFavorite = useCallback( + (bookId: string) => favoriteMap.has(bookId), + [favoriteMap] + ); + + return { + items, + loading, + error, + refresh, + add, + remove, + isFavorite, + } as const; +} diff --git a/front/src/index.css b/front/src/index.css index e6f7689..4267473 100644 --- a/front/src/index.css +++ b/front/src/index.css @@ -1,7 +1,7 @@ :root { - color: #0f172a; - background-color: #f1f5f9; - font-family: "Inter", "Noto Sans JP", system-ui, sans-serif; + color: #1e293b; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + font-family: "Inter", "Noto Sans JP", -apple-system, BlinkMacSystemFont, system-ui, sans-serif; line-height: 1.6; font-weight: 400; text-rendering: optimizeLegibility; @@ -17,39 +17,50 @@ body { margin: 0; - background-color: #f1f5f9; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: inherit; + min-height: 100vh; } a { color: inherit; text-decoration: none; + transition: all 0.3s ease; +} + +a:hover { + opacity: 0.8; } button { - border-radius: 999px; - border: 1px solid rgba(15, 23, 42, 0.1); - padding: 0.45rem 1rem; + border-radius: 12px; + border: none; + padding: 0.6rem 1.2rem; font-size: 0.95rem; - font-weight: 500; + font-weight: 600; font-family: inherit; - background-color: #ffffff; + background: rgba(102, 126, 234, 0.1); + color: #667eea; cursor: pointer; - transition: background-color 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + transition: all 0.3s ease; +} + +button:hover:not(:disabled) { + background: rgba(102, 126, 234, 0.2); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); } -button:hover { - background-color: #eff6ff; - border-color: rgba(37, 99, 235, 0.4); - box-shadow: 0 4px 10px rgba(37, 99, 235, 0.15); +button:active:not(:disabled) { + transform: translateY(0); } button:disabled { - background-color: #f1f5f9; + background: #e2e8f0; color: #94a3b8; - border-color: rgba(148, 163, 184, 0.5); cursor: not-allowed; box-shadow: none; + transform: none; } code { diff --git a/front/src/pages/FavoritesPage.tsx b/front/src/pages/FavoritesPage.tsx new file mode 100644 index 0000000..27c89e9 --- /dev/null +++ b/front/src/pages/FavoritesPage.tsx @@ -0,0 +1,239 @@ +import type { CSSProperties } from 'react'; +import type { FavoriteItem } from '../types'; + +type Props = { + items: FavoriteItem[]; + loading: boolean; + error?: string; + onRefresh: () => Promise; + onRemove: (bookId: string) => Promise; +}; + +export default function FavoritesPage({ items, loading, error, onRefresh, onRemove }: Props) { + return ( +
+
+
+
+ +

Favorites List

+
+ +
+ {error &&
{error}
} + {loading &&
⏳ Loading favorites...
} +
+ + {items.length === 0 ? ( +
+
+
+ No favorite books yet.
+ Mark books with "⭐" from search results. +
+
+ ) : ( +
+ {items.map((item) => ( +
+ {item.Book.Thumbnail && ( + {item.Book.Title} + )} +
+
+

{item.Book.Title}

+
{item.Book.Authors?.join(', ')}
+ {item.Book.PublishedDate && ( +
📅 Published: {item.Book.PublishedDate}
+ )} + {item.Book.PageCount && ( +
📄 Pages: {item.Book.PageCount}
+ )} +
🕒 Added: {formatDate(item.AddedAt)}
+
+
+ {item.Book.InfoLink && ( + + View Details + + )} + +
+
+
+ ))} +
+ )} +
+ ); +} + +function formatDate(value: string) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString('ja-JP', { hour12: false }); +} + +function openAlert(err: unknown) { + const message = err instanceof Error ? err.message : String(err); + window.alert(message); +} + +const summaryCardStyle: CSSProperties = { + background: 'rgba(255, 255, 255, 0.98)', + borderRadius: 20, + padding: '32px', + boxShadow: '0 10px 40px rgba(102, 126, 234, 0.15)', + display: 'grid', + gap: 12, + border: '1px solid rgba(102, 126, 234, 0.1)', + backdropFilter: 'blur(10px)', +}; + +const emptyCardStyle: CSSProperties = { + background: 'rgba(255, 255, 255, 0.98)', + borderRadius: 20, + padding: '64px 28px', + boxShadow: '0 10px 40px rgba(102, 126, 234, 0.15)', + border: '2px dashed rgba(102, 126, 234, 0.2)', +}; + +const emptyStateStyle: CSSProperties = { + textAlign: 'center', + color: '#94a3b8', + fontSize: 16, + lineHeight: 1.8, +}; + +const titleStyle: CSSProperties = { + margin: 0, + fontSize: 24, + fontWeight: 700, + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', +}; + +const gridStyle: CSSProperties = { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', + gap: 24, +}; + +const cardStyle: CSSProperties = { + background: '#ffffff', + borderRadius: 16, + padding: '24px', + boxShadow: '0 8px 28px rgba(102, 126, 234, 0.12)', + display: 'flex', + flexDirection: 'column', + gap: 20, + border: '1px solid rgba(102, 126, 234, 0.1)', + transition: 'all 0.3s ease', + cursor: 'pointer', +}; + +const thumbnailStyle: CSSProperties = { + width: '100%', + height: 'auto', + maxHeight: 240, + objectFit: 'contain', + borderRadius: 12, + background: 'linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%)', + padding: 16, +}; + +const contentStyle: CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: 16, + flex: 1, +}; + +const bookTitleStyle: CSSProperties = { + margin: 0, + fontSize: 17, + fontWeight: 700, + color: '#1e293b', + lineHeight: 1.5, +}; + +const authorsStyle: CSSProperties = { + fontSize: 14, + color: '#64748b', + marginTop: 6, + fontWeight: 500, +}; + +const metaStyle: CSSProperties = { + fontSize: 13, + color: '#94a3b8', + marginTop: 6, +}; + +const actionRowStyle: CSSProperties = { + display: 'flex', + gap: 10, + marginTop: 'auto', + flexWrap: 'wrap', +}; + +const linkButtonStyle: CSSProperties = { + padding: '10px 18px', + borderRadius: 10, + border: 'none', + background: 'linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%)', + color: '#667eea', + fontSize: 13, + fontWeight: 600, + cursor: 'pointer', + textDecoration: 'none', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 0.3s ease', +}; + +const removeButtonStyle: CSSProperties = { + padding: '10px 18px', + borderRadius: 10, + border: 'none', + background: 'linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%)', + color: '#dc2626', + fontSize: 13, + fontWeight: 600, + cursor: 'pointer', + transition: 'all 0.3s ease', +}; diff --git a/front/src/pages/SearchPage.tsx b/front/src/pages/SearchPage.tsx index 22f9369..0ea04eb 100644 --- a/front/src/pages/SearchPage.tsx +++ b/front/src/pages/SearchPage.tsx @@ -5,14 +5,17 @@ import ResultsGrid from '../components/ResultsGrid'; import Pagination from '../components/Pagination'; import { useBooksSearch } from '../hooks/useBooksSearch'; import TechTags from '../components/TechTags'; -import type { Book, TsundokuItem } from '../types'; +import type { Book, TsundokuItem, FavoriteItem } from '../types'; type Props = { tsundokuItems: Partial>; + favoriteItems: Partial>; onAddTsundoku: (book: Book) => Promise; + onAddFavorite: (book: Book) => Promise; + onRemoveFavorite: (bookId: string) => Promise; }; -export default function SearchPage({ tsundokuItems, onAddTsundoku }: Props) { +export default function SearchPage({ tsundokuItems, favoriteItems, onAddTsundoku, onAddFavorite, onRemoveFavorite }: Props) { const { state: { q, page, tagKeys }, setQ, setPage, @@ -22,28 +25,31 @@ export default function SearchPage({ tsundokuItems, onAddTsundoku }: Props) { } = useBooksSearch({ page: 1 }); const tagLabel = useMemo( - () => (hasSearched ? (loading ? '読み込み中…' : error ? {error} : `${total}件`) : null), + () => (hasSearched ? (loading ? 'Loading...' : error ? {error} : `${total} results`) : null), [hasSearched, loading, error, total] ); const hasItems = hasSearched && items && items.length > 0; return ( -
+
-

検索条件

+
+ 🔍 +

Search Criteria

+

- タグを一つ選ぶと関連クエリが自動でセットされます。キーワードはスペース区切りで AND、引用符でフレーズ検索になります。 + Select a tag to automatically set related queries. Use spaces for AND searches, and quotes for phrase searches.

-
+
{ if (tagKeys.length === 1 && tagKeys[0] === key) { - setTagKeys([]); // 同じタグなら解除 + setTagKeys([]); // Deselect if same tag setPage(1); } else { - setTagKeys([key]); // 別タグを選んだら置き換え + setTagKeys([key]); // Replace with new tag setPage(1); } }} @@ -56,7 +62,15 @@ export default function SearchPage({ tsundokuItems, onAddTsundoku }: Props) { disabled={!canSearch} /> {tagLabel && ( -
+
{tagLabel}
)} @@ -64,10 +78,13 @@ export default function SearchPage({ tsundokuItems, onAddTsundoku }: Props) {
-
-

検索結果

+
+
+ 📖 +

Search Results

+

- 気になる本はカード右上の「+」で積読へ。積読済みの本はステータスが表示されます。 + Mark books with "⭐" to add to favorites, or "+" to add to reading list.

@@ -76,10 +93,13 @@ export default function SearchPage({ tsundokuItems, onAddTsundoku }: Props) { tsundokuItems[id]} + getFavoriteItem={(id) => favoriteItems[id]} onAddTsundoku={onAddTsundoku} + onAddFavorite={onAddFavorite} + onRemoveFavorite={onRemoveFavorite} /> -
+
- 上の検索ボックスでキーワードやタグを指定してみてください。 +
📚
+ Try entering keywords or selecting tags from the search box above.
)} {hasSearched && !hasItems && ( -
- 条件に合う書籍が見つかりませんでした。キーワードやタグを変えて再検索してみてください。 +
+ No books found matching your criteria. Try different keywords or tags.
)}
@@ -112,19 +144,23 @@ export default function SearchPage({ tsundokuItems, onAddTsundoku }: Props) { } const cardStyle: CSSProperties = { - background: '#ffffff', - borderRadius: 16, - padding: '24px 28px', - boxShadow: '0 12px 32px rgba(15, 23, 42, 0.08)', + background: 'rgba(255, 255, 255, 0.98)', + borderRadius: 20, + padding: '32px', + boxShadow: '0 10px 40px rgba(102, 126, 234, 0.15)', display: 'grid', gap: 16, + border: '1px solid rgba(102, 126, 234, 0.1)', + backdropFilter: 'blur(10px)', }; const sectionTitleStyle: CSSProperties = { margin: 0, - fontSize: 20, - fontWeight: 600, - color: '#0f172a', + fontSize: 24, + fontWeight: 700, + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', }; const sectionHintStyle: CSSProperties = { diff --git a/front/src/pages/TsundokuPage.tsx b/front/src/pages/TsundokuPage.tsx index 26c1098..a86bc83 100644 --- a/front/src/pages/TsundokuPage.tsx +++ b/front/src/pages/TsundokuPage.tsx @@ -17,73 +17,75 @@ export default function TsundokuPage({ items, loading, error, onRefresh, onPicku const reading = items.filter((item) => item.Status === 'reading'); const done = items.filter((item) => item.Status === 'done'); - const primaryError = error && !/積読の一覧が空です/.test(error); - const pickupDisabled = loading || stacked.length === 0 || reading.length > 0; + const primaryError = error && !/The reading list is empty/.test(error); + const pickupDisabled = loading || stacked.length === 0 || reading.length > 1; const columns: ColumnConfig[] = [ { key: 'stacked', - title: '積読中', + title: 'Stacked', description: stacked.length > 0 - ? `次に読む予定は先頭の「${stacked[0].Book.Title}」。再読で戻した本は末尾に並びます。` - : 'まだ積んでいません。検索結果のカード右上から追加してください。', + ? `Next up: "${stacked[0]?.Book.Title}". Books returned for re-reading are placed at the end.` + : 'No books in stack yet. Add them from search results using the top-right button.', items: stacked, renderActions: (item) => (
- 追加日: {formatDate(item.AddedAt)} + Added: {formatDate(item.AddedAt)}
), }, { key: 'reading', - title: '読書中', - description: reading.length > 0 - ? '読んでいる本に集中しましょう。読み終えたら読了、まだなら積読に戻せます。' - : '「先頭から取り出す」で読書中の本を一冊だけ選べます。', + title: 'Currently Reading', + description: reading.length > 1 + ? 'Focus on your books. Mark as done when finished, or return to stack.' + : reading.length === 1 + ? 'You can read up to 2 books at a time. Click "Pick from top" to select another.' + : 'Click "Pick from top" to select up to 2 books to read.', items: reading, renderActions: (item) => (
- 開始日: {formatDate(item.StartedAt)} + Started: {formatDate(item.StartedAt)}
), }, { key: 'done', - title: '読了済み', + title: 'Completed', description: done.length > 0 - ? '再読したくなったら「積読に戻す」で末尾へ積み直せます。' - : '読了履歴はまだありません。', + ? 'Want to re-read? Use "Return to Stack" to add it back to the end of the stack.' + : 'No completed books yet.', items: done, renderActions: (item) => (
- 読了日: {formatDate(item.CompletedAt)} + Completed: {formatDate(item.CompletedAt)}
), @@ -91,32 +93,71 @@ export default function TsundokuPage({ items, loading, error, onRefresh, onPicku ]; return ( -
+
-

積読ダッシュボード

-
-
+
- - 先頭以外を読みたいときは下のリストから選択できます。 + + To read a specific book, select from the list below. {reading.length > 0 && ( - 読書中の本があります。まず読了にするか積読へ戻してください。 + + ⚠️ You have a book in progress. Please mark it as done or return to stack first. + )}
{primaryError &&
{error}
} - {loading &&
最新の積読情報を取得しています...
} + {loading &&
Fetching latest reading list...
}
@@ -127,7 +168,7 @@ export default function TsundokuPage({ items, loading, error, onRefresh, onPicku

{col.description}

{col.items.length === 0 ? ( -
該当する本はまだありません。
+
No books found in this category yet.
) : (
    {col.items.map((item) => ( @@ -135,7 +176,7 @@ export default function TsundokuPage({ items, loading, error, onRefresh, onPicku
    {item.Book.Title}
    {item.Book.Authors?.join(', ')}
    - {item.Note &&
    メモ: {item.Note}
    } + {item.Note &&
    Note: {item.Note}
    }
    {col.renderActions && (
    @@ -176,50 +217,66 @@ function openAlert(err: unknown) { } const summaryCardStyle: CSSProperties = { - background: '#ffffff', - borderRadius: 18, - padding: '24px 28px', - boxShadow: '0 14px 40px rgba(15, 23, 42, 0.12)', + background: 'rgba(255, 255, 255, 0.98)', + borderRadius: 20, + padding: '32px', + boxShadow: '0 10px 40px rgba(102, 126, 234, 0.15)', display: 'grid', - gap: 12, + gap: 16, + border: '1px solid rgba(102, 126, 234, 0.1)', + backdropFilter: 'blur(10px)', +}; + +const sectionTitleStyle: CSSProperties = { + margin: 0, + fontSize: 24, + fontWeight: 700, + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', }; const columnGridStyle: CSSProperties = { display: 'grid', - gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', - gap: 20, + gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', + gap: 24, }; const columnCardStyle: CSSProperties = { background: '#ffffff', borderRadius: 16, - padding: '20px 22px', - boxShadow: '0 10px 28px rgba(15, 23, 42, 0.08)', + padding: '24px', + boxShadow: '0 8px 28px rgba(102, 126, 234, 0.12)', display: 'grid', - gap: 16, - minHeight: 220, + gap: 20, + minHeight: 240, + border: '1px solid rgba(102, 126, 234, 0.1)', }; const columnTitleStyle: CSSProperties = { margin: 0, - fontSize: 18, - fontWeight: 600, - color: '#0f172a', + fontSize: 20, + fontWeight: 700, + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', }; const columnHintStyle: CSSProperties = { - margin: '6px 0 0', - fontSize: 13, + margin: '8px 0 0', + fontSize: 14, color: '#64748b', + lineHeight: 1.6, }; const emptyStateStyle: CSSProperties = { - background: '#f8fafc', + background: 'linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%)', borderRadius: 12, - padding: '24px 16px', + padding: '32px 16px', textAlign: 'center', color: '#94a3b8', - fontSize: 13, + fontSize: 14, + border: '2px dashed rgba(102, 126, 234, 0.2)', }; const itemListStyle: CSSProperties = { diff --git a/front/src/tags.ts b/front/src/tags.ts index 99468e6..32c1bb8 100644 --- a/front/src/tags.ts +++ b/front/src/tags.ts @@ -1,10 +1,10 @@ export type TechTag = { key: string; label: string; - queries: string[]; // OR束でqに入れる語 + queries: string[]; // OR bundle for q parameter }; -// キーワードはクォートして使う想定。linux に変更済み。 +// Keywords are intended to be used with quotes. Changed to linux. export const TECH_TAGS: TechTag[] = [ { key: 'computer-science', label: 'Computer Science', queries: ['Computer Science'] }, { key: 'network', label: 'Network', queries: ['computer networks', 'network protocols'] }, @@ -18,7 +18,7 @@ export const TECH_TAGS: TechTag[] = [ { key: 'design-pattern', label: 'Design Pattern', queries: ['design patterns', 'software patterns'] }, { key: 'git', label: 'Git/GitHub', queries: ['Git', 'GitHub'] }, { key: 'discrete-math', label: 'Discrete Mathematics', queries: ['discrete mathematics'] }, - { key: 'html-css', label: 'HTMLとCSS', queries: ['HTML', 'CSS'] }, + { key: 'html-css', label: 'HTML & CSS', queries: ['HTML', 'CSS'] }, { key: 'javascript', label: 'JavaScript', queries: ['JavaScript'] }, { key: 'vue', label: 'Vue', queries: ['Vue', 'Vue.js', 'VueJS'] }, { key: 'django', label: 'Django', queries: ['Django'] }, @@ -28,7 +28,7 @@ export const TECH_TAGS: TechTag[] = [ { key: 'rails', label: 'Ruby on Rails', queries: ['Ruby on Rails', 'Rails'] }, { key: 'unity', label: 'Unity', queries: ['Unity'] }, { key: 'swiftui', label: 'Swift UI', queries: ['SwiftUI', 'Swift UI'] }, - { key: 'uikit', label: 'UIKit(iOS)', queries: ['UIKit', 'iOS'] }, + { key: 'uikit', label: 'UIKit (iOS)', queries: ['UIKit', 'iOS'] }, ]; export function getQueriesFor(keys: string[]): string[] { diff --git a/front/src/types.ts b/front/src/types.ts index 26e137a..03c4228 100644 --- a/front/src/types.ts +++ b/front/src/types.ts @@ -28,3 +28,9 @@ export type TsundokuItem = { StartedAt?: string | null; CompletedAt?: string | null; }; + +export type FavoriteItem = { + ID: string; + Book: Book; + AddedAt: string; +};