diff --git a/src/App.tsx b/src/App.tsx index 19d7e81..906b064 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,8 +10,9 @@ import { MovieDetailModal } from './components/MovieDetailModal'; import { StatisticsDashboard } from './components/StatisticsDashboard'; import { AchievementsGrid } from './components/AchievementsGrid'; import { BottomNav } from './components/BottomNav'; +import { Recommendations } from './components/Recommendations'; import { useToast } from './components/Toast'; -import { Search, Plus, Trash2, Heart, Eye, Shield, ListPlus } from 'lucide-react'; +import { Search, Plus, Trash2, Heart, Eye, Shield, ListPlus, Sparkles } from 'lucide-react'; import { SplashScreen } from '@capacitor/splash-screen'; import { shareMovie } from './lib/share'; @@ -121,11 +122,16 @@ function App({ conductor }: AppProps) { } const filteredItems = state.items.filter((movie) => { - if (state.filter === 'favorites') return movie.favorite; - if (state.filter === 'watched') return movie.watched; + if (state.filter === 'favorites' && !movie.favorite) return false; + if (state.filter === 'watched' && !movie.watched) return false; + if (state.tagFilter && !(movie.tags || []).includes(state.tagFilter)) return false; return true; }); + const allTags = Array.from( + new Set(state.items.flatMap((m) => m.tags || []).filter(Boolean)) + ).sort(); + return (
@@ -191,6 +197,12 @@ function App({ conductor }: AppProps) { ) : state.filter === 'statistics' ? ( + ) : state.filter === 'recommendations' ? ( + ) : ( /* Movie Grid */ <> @@ -201,6 +213,37 @@ function App({ conductor }: AppProps) { {state.customLists.find(l => l.id === state.activeListId)?.name}
)} + + {/* Tag filter chips: nur sichtbar, wenn Tags existieren */} + {allTags.length > 0 && ( +
+ Tags: + {state.tagFilter && ( + + )} + {allTags.slice(0, 12).map(tag => { + const active = state.tagFilter === tag; + return ( + + ); + })} +
+ )}
{filteredItems.map((movie) => (
{movie.source !== 'tmdb' && ( -
- { e.stopPropagation(); conductor.dispatch({ type: 'TOGGLE_FAVORITE', payload: movie.id }); }} - /> - { e.stopPropagation(); conductor.dispatch({ type: 'TOGGLE_WATCHED', payload: movie.id }); }} - /> -
+ <> + {!!movie.tags?.length && ( +
+ {movie.tags!.slice(0, 2).map(tag => ( + + #{tag} + + ))} +
+ )} +
+ { e.stopPropagation(); conductor.dispatch({ type: 'TOGGLE_FAVORITE', payload: movie.id }); }} + /> + { e.stopPropagation(); conductor.dispatch({ type: 'TOGGLE_WATCHED', payload: movie.id }); }} + /> + {typeof movie.userRating === 'number' && movie.userRating > 0 && ( + ★ {movie.userRating} + )} +
+ )}
@@ -301,6 +358,7 @@ function App({ conductor }: AppProps) { onShowWatched={() => conductor.dispatch({ type: 'SET_FILTER', payload: 'watched' })} onShowAchievements={() => conductor.dispatch({ type: 'SET_FILTER', payload: 'achievements' })} onShowStatistics={() => conductor.dispatch({ type: 'SET_FILTER', payload: 'statistics' })} + onShowRecommendations={() => conductor.dispatch({ type: 'SET_FILTER', payload: 'recommendations' })} /> {/* Profile Modal */} diff --git a/src/components/BottomNav.tsx b/src/components/BottomNav.tsx index f567d8f..27e9233 100644 --- a/src/components/BottomNav.tsx +++ b/src/components/BottomNav.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { Home, Heart, User, Eye, Zap, BarChart2 } from 'lucide-react'; +import { Home, Heart, User, Eye, Zap, BarChart2, Sparkles } from 'lucide-react'; interface BottomNavProps { currentFilter: string; @@ -10,6 +10,7 @@ interface BottomNavProps { onShowWatched: () => void; onShowAchievements: () => void; onShowStatistics: () => void; + onShowRecommendations?: () => void; } export function BottomNav({ @@ -20,7 +21,8 @@ export function BottomNav({ onShowProfile, onShowWatched, onShowAchievements, - onShowStatistics + onShowStatistics, + onShowRecommendations }: BottomNavProps) { const { t } = useTranslation(); @@ -85,6 +87,18 @@ export function BottomNav({ > + + {onShowRecommendations && ( + + )} ); } diff --git a/src/components/LoginScreen.tsx b/src/components/LoginScreen.tsx index 4529f2e..67acd0a 100644 --- a/src/components/LoginScreen.tsx +++ b/src/components/LoginScreen.tsx @@ -98,7 +98,7 @@ export function LoginScreen({ onLoginSuccess }: LoginScreenProps) { required value={email} onChange={(e) => setEmail(e.target.value)} - className="w-full bg-app-bg/40 border border-app-border rounded-xl py-3 pl-12 pr-4 text-app-text focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all placeholder-app-text-muted" + className="auth-input w-full bg-app-bg/40 border border-app-border rounded-xl py-3 pl-12 pr-4 text-app-text focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all placeholder-app-text-muted" placeholder="name@example.com" /> @@ -115,7 +115,7 @@ export function LoginScreen({ onLoginSuccess }: LoginScreenProps) { minLength={6} value={password} onChange={(e) => setPassword(e.target.value)} - className="w-full bg-app-bg/40 border border-app-border rounded-xl py-3 pl-12 pr-4 text-app-text focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50 transition-all placeholder-app-text-muted" + className="auth-input w-full bg-app-bg/40 border border-app-border rounded-xl py-3 pl-12 pr-4 text-app-text focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50 transition-all placeholder-app-text-muted" placeholder="••••••••" /> diff --git a/src/components/MovieDetailModal.tsx b/src/components/MovieDetailModal.tsx index b974bf7..2912e09 100644 --- a/src/components/MovieDetailModal.tsx +++ b/src/components/MovieDetailModal.tsx @@ -1,8 +1,8 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Movie, CastMember, WatchProvider } from '../types/domain'; import { MovieConductor } from '../core/conductor/MovieConductor'; -import { X, Play, Check, Plus, Share2, ListPlus } from 'lucide-react'; +import { X, Play, Check, Plus, Share2, ListPlus, Star, Tag, NotebookPen } from 'lucide-react'; import { ListCreationModal } from './ListCreationModal'; interface MovieDetailModalProps { @@ -42,13 +42,20 @@ export function MovieDetailModal({
+
+

+ {movie.title} +

+
+ -
+
- - - - +
+
+ + +
+
+ + +
+
+ + {/* Persönliche Sektion: Bewertung + Notizen + Tags. Nur sichtbar, sobald der Film + tatsächlich in der Library liegt — vor dem Hinzufügen gibt es nichts zu speichern. */} + {movie.source === 'database' && ( + + )} + conductor.dispatch({ type: 'SELECT_MOVIE', payload: id })} @@ -75,15 +99,15 @@ export function MovieDetailModal({ ); } -// ==================== SUB-COMPONENTS (vollständig, typisiert) ==================== +// ==================== SUB-COMPONENTS ==================== function HeroSection({ movie }: { movie: Movie }) { return ( -
+
{movie.trailerKey ? (