diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..016e708 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,338 @@ +--- +name: "Modern React Project Template" +description: "A comprehensive development guide for modern frontend projects based on React 18 + TypeScript + Vite, including complete development standards and best practices" +category: "Frontend Framework" +author: "Agents.md Collection" +authorUrl: "https://github.com/gakeez/agents_md_collection" +tags: ["react", "typescript", "vite", "frontend", "spa"] +lastUpdated: "2024-12-19" +--- + +# Modern React Project Development Guide + +## Project Overview + +This is a modern frontend project template based on React 18, TypeScript, and Vite. It's suitable for building high-performance Single Page Applications (SPA) with integrated modern development toolchain and best practices. + +## Tech Stack + +- **Frontend Framework**: React 18 + TypeScript +- **Build Tool**: Vite +- **State Management**: Zustand / Redux Toolkit +- **Routing**: React Router v6 +- **UI Components**: Ant Design / Material-UI +- **Styling**: Tailwind CSS / Styled-components +- **Testing Framework**: Vitest + React Testing Library +- **Code Quality**: ESLint + Prettier + Husky + +## Project Structure + +``` +react-project/ +├── public/ # Static assets +│ ├── favicon.ico +│ └── index.html +├── src/ +│ ├── components/ # Reusable components +│ │ ├── common/ # Common components +│ │ └── ui/ # UI components +│ ├── pages/ # Page components +│ ├── hooks/ # Custom Hooks +│ ├── store/ # State management +│ ├── services/ # API services +│ ├── utils/ # Utility functions +│ ├── types/ # TypeScript type definitions +│ ├── styles/ # Global styles +│ ├── constants/ # Constants +│ ├── App.tsx +│ └── main.tsx +├── tests/ # Test files +├── docs/ # Project documentation +├── .env.example # Environment variables example +├── package.json +├── tsconfig.json +├── vite.config.ts +└── README.md +``` + +## Development Guidelines + +### Component Development Standards + +1. **Function Components First**: Use function components and Hooks +2. **TypeScript Types**: Define interfaces for all props +3. **Component Naming**: Use PascalCase, file name matches component name +4. **Single Responsibility**: Each component handles only one functionality + +```tsx +// Example: Button Component +interface ButtonProps { + variant: 'primary' | 'secondary' | 'danger'; + size?: 'small' | 'medium' | 'large'; + disabled?: boolean; + onClick?: () => void; + children: React.ReactNode; +} + +export const Button: React.FC = ({ + variant, + size = 'medium', + disabled = false, + onClick, + children +}) => { + return ( + + ); +}; +``` + +### State Management Standards + +Using Zustand for state management: + +```tsx +// store/userStore.ts +import { create } from 'zustand'; + +interface User { + id: string; + name: string; + email: string; +} + +interface UserState { + user: User | null; + isLoading: boolean; + setUser: (user: User) => void; + clearUser: () => void; + setLoading: (loading: boolean) => void; +} + +export const useUserStore = create((set) => ({ + user: null, + isLoading: false, + setUser: (user) => set({ user }), + clearUser: () => set({ user: null }), + setLoading: (isLoading) => set({ isLoading }), +})); +``` + +### API Service Standards + +```tsx +// services/api.ts +import axios from 'axios'; + +const api = axios.create({ + baseURL: import.meta.env.VITE_API_URL, + timeout: 10000, +}); + +// Request interceptor +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// Response interceptor +api.interceptors.response.use( + (response) => response.data, + (error) => { + console.error('API Error:', error); + return Promise.reject(error); + } +); + +export default api; +``` + +## Environment Setup + +### Development Requirements +- Node.js >= 18.0.0 +- npm >= 8.0.0 or yarn >= 1.22.0 + +### Installation Steps +```bash +# 1. Create project +npm create vite@latest my-react-app -- --template react-ts + +# 2. Navigate to project directory +cd my-react-app + +# 3. Install dependencies +npm install + +# 4. Install additional dependencies +npm install zustand react-router-dom axios +npm install -D @types/node + +# 5. Start development server +npm run dev +``` + +### Environment Variables Configuration +```env +# .env.local +VITE_API_URL=http://localhost:3001/api +VITE_APP_TITLE=My React App +VITE_ENABLE_MOCK=false +``` + +## Routing Configuration + +```tsx +// App.tsx +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { HomePage } from './pages/HomePage'; +import { AboutPage } from './pages/AboutPage'; +import { NotFoundPage } from './pages/NotFoundPage'; + +function App() { + return ( + + + } /> + } /> + } /> + + + ); +} + +export default App; +``` + +## Testing Strategy + +### Unit Testing Example +```tsx +// tests/components/Button.test.tsx +import { render, screen, fireEvent } from '@testing-library/react'; +import { Button } from '../src/components/Button'; + +describe('Button Component', () => { + test('renders button with text', () => { + render(); + expect(screen.getByText('Click me')).toBeInTheDocument(); + }); + + test('calls onClick when clicked', () => { + const handleClick = vi.fn(); + render( + + ); + + fireEvent.click(screen.getByText('Click me')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); +}); +``` + +## Performance Optimization + +### Code Splitting +```tsx +import { lazy, Suspense } from 'react'; + +const LazyComponent = lazy(() => import('./LazyComponent')); + +function App() { + return ( + Loading...}> + + + ); +} +``` + +### Memory Optimization +```tsx +import { memo, useMemo, useCallback } from 'react'; + +const ExpensiveComponent = memo(({ data, onUpdate }) => { + const processedData = useMemo(() => { + return data.map(item => ({ ...item, processed: true })); + }, [data]); + + const handleUpdate = useCallback((id) => { + onUpdate(id); + }, [onUpdate]); + + return ( +
+ {processedData.map(item => ( +
handleUpdate(item.id)}> + {item.name} +
+ ))} +
+ ); +}); +``` + +## Deployment Configuration + +### Build Production Version +```bash +npm run build +``` + +### Vite Configuration Optimization +```ts +// vite.config.ts +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + rollupOptions: { + output: { + manualChunks: { + vendor: ['react', 'react-dom'], + router: ['react-router-dom'], + }, + }, + }, + }, + server: { + port: 3000, + open: true, + }, +}); +``` + +## Common Issues + +### Issue 1: Vite Development Server Slow Startup +**Solution**: +- Check dependency pre-build cache +- Use `npm run dev -- --force` to force rebuild +- Optimize optimizeDeps configuration in vite.config.ts + +### Issue 2: TypeScript Type Errors +**Solution**: +- Ensure correct type definition packages are installed +- Check tsconfig.json configuration +- Use `npm run type-check` for type checking + +## Reference Resources + +- [React Official Documentation](https://react.dev/) +- [Vite Official Documentation](https://vitejs.dev/) +- [TypeScript Official Documentation](https://www.typescriptlang.org/) +- [React Router Documentation](https://reactrouter.com/) +- [Zustand Documentation](https://github.com/pmndrs/zustand) \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 9c81ac5..975a964 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import { useState, useMemo, useEffect } from 'react'; import { LayoutGroup } from 'motion/react'; import { BrowserRouter, Routes, Route, useNavigate, useSearchParams, useParams, useLocation } from 'react-router-dom'; -import Header from './components/Header'; +import Sidebar from './components/Sidebar'; import BrowseView from './components/BrowseView'; import DashboardView from './components/DashboardView'; import DetailView from './components/DetailView'; @@ -338,17 +338,13 @@ function AppContent() { }; return ( -
-
+ -
+
-
- {/* Footer */} -
-
-
-
- kyoo + {/* Footer */} +
+
+
+
+ kyoo +
+ +

+ © 2026 Kyoo Media Discovery. All rights reserved. +

- -

- © 2026 Kyoo Media Discovery. All rights reserved. -

-
-
+
+
); } diff --git a/src/components/DetailView.tsx b/src/components/DetailView.tsx index 449bfe5..5a01f9a 100644 --- a/src/components/DetailView.tsx +++ b/src/components/DetailView.tsx @@ -10,7 +10,10 @@ import { ChevronRight, Search, ListFilter, - ChevronDown + ChevronDown, + Calendar, + Clock, + Eye } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -28,6 +31,24 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) { const [castLimit, setCastLimit] = useState(6); const [showAllCast, setShowAllCast] = useState(false); const [expandedSeasons, setExpandedSeasons] = useState>(new Set()); + const [progress, setProgress] = useState(70.8); + + const hasEpisodes = media.episodes && media.episodes.length > 0; + const hasTracks = media.tracks && media.tracks.length > 0; + const hasCast = media.staff && media.staff.length > 0; + const tabs = [ + 'Overview', + ...(hasCast ? ['Cast'] : []), + 'Actions', + 'History', + ...(hasEpisodes ? ['Seasons'] : []), + ...(hasTracks ? ['Tracks'] : []), + 'Reviews', + 'Suggestions', + 'Watch On' + ]; + + const [activeTab, setActiveTab] = useState(tabs[0]); // Group episodes by season const episodesBySeason = useMemo(() => { @@ -68,31 +89,32 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) { const displayedCast = showAllCast ? media.staff : (media.staff?.slice(0, castLimit) || []); const hasMoreCast = (media.staff?.length || 0) > castLimit; + return (
{/* Banner */}
- {media.title}
- -
{/* Content */} -
-
- {/* Left Column: Poster + Metadata */} -
+
+
+ {/* Left Column: Cover Image */} +
- - {/* Compact metadata under poster */} -
- {media.studios && media.studios.length > 0 && ( -

- Studios: - {media.studios.join(', ')} -

- )} - {media.developers && media.developers.length > 0 && ( -
- Developers: - {media.developers.map(dev => ( - - {dev} - - ))} -
- )} - {media.platforms && media.platforms.length > 0 && ( -
- Platforms: - {media.platforms.map(platform => ( - - {platform} - - ))} -
- )} - {media.categories && media.categories.length > 0 && ( -
- Categories: - {media.categories.map(category => ( - - {category} - - ))} -
- )} - {media.completionStatus && ( -

- Completion: - {media.completionStatus} -

- )} - {media.source && ( -

- Source: - {media.source} -

- )} - {media.playCount !== undefined && media.playCount !== null && ( -

- Play Count: - {media.playCount} -

- )} - {media.playtime !== undefined && media.playtime !== null && media.playtime > 0 && ( -

- Playtime: - {media.playtime}h -

- )} - {media.lastActivity && ( -

- Last Activity: - {media.lastActivity} -

- )} -
- Links: - - -
-
{/* Right Column: Info */} -
-
-
-

- {media.title} ({media.year}) -

-
-
- - - -
-
- - {media.rating} / 10 -
-
-
- -
-

Genres

-
- {media.genres?.map(genre => ( - - • {genre} - - ))} -
-
-
- -
- - {/* Tags */} -
- {media.tags?.map(tag => ( - - {tag} +
+ {/* Header with tags */} +
+

+ {media.title} +

+ {media.status && ( + + {media.status.toUpperCase()} - ))} + )} + {media.completionStatus && ( + {media.completionStatus.toUpperCase()} + )}
-
-
- {/* Staff Section - Only show if staff data exists */} - {media.staff && media.staff.length > 0 && ( -
-
-

Cast & Crew

-
- - {showAllCast ? media.staff.length : displayedCast.length} / {media.staff.length} - - {hasMoreCast && ( - - )} + {/* Show Details */} +
+
+ + {media.year} +
+
+ {media.status ? media.status.charAt(0).toUpperCase() + media.status.slice(1) : 'Unknown'} +
+
+ + {media.playtime ? `${media.playtime}h` : '12h 30m'}
-
- {displayedCast.map(person => ( + + {/* Progress Bar */} +
+
+ Progress + {progress}% +
+
onPersonClick(person)} + className="h-full bg-gradient-to-r from-[#6d28d9] to-[#8b5cf6] transition-all duration-500" + style={{ width: `${progress}%` }} + /> +
+
+ + {/* Navigation Tabs */} +
+ {tabs.map(tab => ( +
+ {tab} + ))}
-
- )} - {/* Episodes Section - Only show if episodes data exists */} - {media.episodes && media.episodes.length > 0 && ( + {/* Genre Tags */} + {activeTab === 'Overview' && ( +
+ {media.genres?.map(genre => ( + + {genre} + + ))} +
+ )} + + {/* Description */} + {activeTab === 'Overview' && ( +
+ )} + + {/* Acting Section - Horizontal Scrollable */} + {media.staff && media.staff.length > 0 && activeTab === 'Cast' && ( +
+

Acting

+
+ {displayedCast.map(person => ( +
onPersonClick(person)} + > +
+ {person.name} +
+

{person.name}

+

{person.characterName || person.role}

+
+ ))} + {hasMoreCast && ( + + )} +
+
+ )} + {/* Episodes Section - Only show if episodes data exists and Seasons tab is active */} + {media.episodes && media.episodes.length > 0 && activeTab === 'Seasons' && (
@@ -365,8 +332,8 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
)} - {/* Tracks Section - Only show if tracks data exists (Music) */} - {media.tracks && media.tracks.length > 0 && ( + {/* Tracks Section - Only show if tracks data exists and Tracks tab is active */} + {media.tracks && media.tracks.length > 0 && activeTab === 'Tracks' && (
@@ -388,37 +355,24 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
-
-
- {media.tracks - .sort((a, b) => a.track_number - b.track_number) - .map((track, index) => ( -
-
-
- {track.track_number} -
-
-

- {track.title} -

-

{track.artist}

-
- {track.duration && ( - - {track.duration}s - - )} - -
-
- ))} -
+
+ {media.tracks.map(track => ( +
+ {track.track_number} +
+

+ {track.title} +

+

{track.artist}

+
+ {track.duration ? `${track.duration}m` : '-'} +
+ ))}
)} +
+
); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..8653076 --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,217 @@ +import { useState } from 'react'; +import { NavLink, useLocation } from 'react-router-dom'; +import { + LayoutDashboard, + BookOpen, + Film, + Tv, + Gamepad2, + Users, + Tag, + Music as MusicIcon, + Monitor, + Eye, + Dumbbell, + Calendar, + FolderKanban, + Settings, + Sun, + LogOut, + ChevronDown, + ChevronRight, + Menu, + X +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useTheme } from '@/contexts/ThemeContext'; +import { MediaCategory } from '@/types'; + +interface SidebarProps { + enabledCategories: MediaCategory[]; + onToggleCategory: (category: MediaCategory) => void; +} + +export default function Sidebar({ enabledCategories, onToggleCategory }: SidebarProps) { + const [isMediaExpanded, setIsMediaExpanded] = useState(true); + const [isMobileOpen, setIsMobileOpen] = useState(false); + const { theme, setTheme } = useTheme(); + const location = useLocation(); + + const categoryPaths: Record = { + 'Anime': 'anime', + 'Movies': 'movies', + 'TV Series': 'tv-series', + 'Music': 'music', + 'Books': 'books', + 'Games': 'games', + 'Consoles': 'consoles', + 'Adult': 'adult' + }; + + const categoryIcons: Record = { + 'Audio Book': , + 'Book': , + 'Movie': , + 'Music': , + 'Show': , + 'Video Game': , + 'Consoles': , + 'Adult': , + 'Groups': , + 'People': , + 'Genres': + }; + + const navItems = [ + { icon: , label: 'Dashboard', path: '/' }, + { + icon: , + label: 'Media', + hasSubmenu: true, + submenu: [ + ...(enabledCategories.includes('Anime') ? [{ label: 'Anime', path: '/anime' }] : []), + ...(enabledCategories.includes('Books') ? [{ label: 'Book', path: '/books' }] : []), + ...(enabledCategories.includes('Movies') ? [{ label: 'Movie', path: '/movies' }] : []), + ...(enabledCategories.includes('Music') ? [{ label: 'Music', path: '/music' }] : []), + ...(enabledCategories.includes('TV Series') ? [{ label: 'Show', path: '/tv-series' }] : []), + ...(enabledCategories.includes('Games') ? [{ label: 'Video Game', path: '/games' }] : []), + ...(enabledCategories.includes('Consoles') ? [{ label: 'Consoles', path: '/consoles' }] : []), + ...(enabledCategories.includes('Adult') ? [{ label: 'Adult', path: '/adult' }] : []), + { label: 'People', path: '/cast' }, + { label: 'Genres', path: '/browse' } + ].filter(Boolean) + }, + //{ icon: , label: 'Fitness', path: '/fitness' }, + //{ icon: , label: 'Calendar', path: '/calendar' }, + //{ icon: , label: 'Collections', path: '/collections' }, + { icon: , label: 'Settings', path: '/settings' } + ]; + + const toggleTheme = () => { + setTheme(theme === 'dark' ? 'light' : 'dark'); + }; + + const handleLogout = () => { + console.log('Logout clicked'); + }; + + return ( + <> + {/* Mobile menu button */} + + + {/* Overlay for mobile */} + {isMobileOpen && ( +
setIsMobileOpen(false)} + /> + )} + + {/* Sidebar */} + + + ); +}