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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# .github/workflows/ci.yml
name: CI (Web + Lint + Test)
name: CI (Web + Typecheck + Test)

on:
push:
Expand All @@ -9,10 +9,16 @@ on:
jobs:
test:
runs-on: ubuntu-latest
env:
VITE_TMDB_API_KEY: test-key
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'npm' }
- run: npm ci
- run: npm run build
- run: npm run lint || true # falls du lint hast
- name: TypeScript check
run: npx tsc --noEmit
- name: Unit tests
run: npx vitest run
- name: Build
run: npm run build
9 changes: 4 additions & 5 deletions src/components/MovieDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,13 @@ function ActionButtons({
</button>

{/* Add to List */}
<ListMenu
customLists={customLists}
<ListMenu
customLists={customLists}
onAddToList={(listId: string) => {
conductor.dispatch({ type: 'ADD_TO_LIST', payload: { listId, movie } });
onShowToast('Zum Liste hinzugefügt', 'success');
}}
}}
onCreateNewList={() => setShowListCreation(true)}
conductor={conductor}
/>

{showListCreation && <ListCreationModal conductor={conductor} onClose={() => setShowListCreation(false)} />}
Expand Down Expand Up @@ -231,7 +230,7 @@ function RecommendationsSection({ recommendations, onSelectMovie }: { recommenda
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
{recommendations?.slice(0, 5).map(rec => (
<div key={rec.id} onClick={() => onSelectMovie(rec.id)} className="cursor-pointer">
<img src={rec.posterPath} alt={rec.title} className="rounded-lg" />
<img src={rec.posterPath ?? ''} alt={rec.title} className="rounded-lg" />
</div>
))}
</div>
Expand Down
70 changes: 48 additions & 22 deletions src/core/conductor/MovieConductor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { MovieServiceAdapter, UserIntent, WatchlistState, Movie, Achievement, Mo
type Listener = (state: WatchlistState) => void;

const INITIAL_ACHIEVEMENTS: Achievement[] = [
{ id: 'first-blood', title: 'First Blood', description: 'Add your first movie to the collection.', iconName: 'Popcorn', unlocked: false },
{ id: 'collector-novice', title: 'Collector Novice', description: 'Collect 5 movies.', iconName: 'Library', unlocked: false },
{ id: 'genre-guru', title: 'Genre Guru', description: 'Collect 10 movies to become a guru.', iconName: 'Library', unlocked: false }
{ id: 'first-blood', title: 'First Blood', description: 'Add your first movie to the collection.', iconName: 'Popcorn', unlocked: false, threshold: 1 },
{ id: 'collector-novice', title: 'Collector Novice', description: 'Collect 5 movies.', iconName: 'Library', unlocked: false, threshold: 5 },
{ id: 'genre-guru', title: 'Genre Guru', description: 'Collect 10 movies to become a guru.', iconName: 'Library', unlocked: false, threshold: 10 }
];

const INITIAL_STATISTICS: MovieStatistics = {
Expand All @@ -15,6 +15,7 @@ const INITIAL_STATISTICS: MovieStatistics = {
export class MovieConductor {
private adapter: MovieServiceAdapter;
private listeners: Listener[] = [];
private loadInFlight: Promise<void> | null = null;
private state: WatchlistState = {
items: [],
customLists: [],
Expand Down Expand Up @@ -108,22 +109,28 @@ export class MovieConductor {

// ==================== ORIGINAL HANDLER (rekonstruiert & funktionsfähig) ====================
private async handleLoadMovies(): Promise<void> {
if (this.loadInFlight) return this.loadInFlight;
this.updateState({ status: 'loading' });
try {
const [movies, lists] = await Promise.all([
this.adapter.getTrending(),
this.adapter.getLists()
]);
this.updateState({
items: movies,
customLists: lists,
status: 'idle',
statistics: this.calculateStatistics(movies),
achievements: this.checkAchievements(movies)
});
} catch (error) {
this.updateState({ status: 'error', error: error instanceof Error ? error.message : 'Load failed' });
}
this.loadInFlight = (async () => {
try {
const [movies, lists] = await Promise.all([
this.adapter.getTrending(),
this.adapter.getLists()
]);
this.updateState({
items: movies,
customLists: lists,
status: 'idle',
statistics: this.calculateStatistics(movies),
achievements: this.checkAchievements(movies)
});
} catch (error) {
this.updateState({ status: 'error', error: error instanceof Error ? error.message : 'Load failed' });
} finally {
this.loadInFlight = null;
}
})();
return this.loadInFlight;
}

private async handleSearch(query: string): Promise<void> {
Expand All @@ -138,8 +145,18 @@ export class MovieConductor {

private async handleAddMovie(movie: Movie): Promise<void> {
try {
const alreadyExists = await this.adapter.exists({ title: movie.title, tmdbId: movie.tmdbId });
if (alreadyExists) {
this.updateState({ error: `Movie "${movie.title}" already exists!` });
return;
}
const added = await this.adapter.add(movie);
this.updateState({ items: [added, ...this.state.items] });
const items = [added, ...this.state.items];
this.updateState({
items,
statistics: this.calculateStatistics(items),
achievements: this.checkAchievements(items)
});
} catch (error) {
this.updateState({ error: error instanceof Error ? error.message : 'Add failed' });
}
Expand Down Expand Up @@ -180,8 +197,16 @@ export class MovieConductor {

private async handleSelectMovie(id: string): Promise<void> {
try {
const details = await this.adapter.getById(id) || this.state.items.find(m => m.id === id);
this.updateState({ selectedMovie: details || null });
let details: Movie | null = null;
try {
details = await this.adapter.getMovieDetails(id);
} catch {
details = null;
}
if (!details) {
details = (await this.adapter.getById(id)) || this.state.items.find(m => m.id === id) || null;
}
this.updateState({ selectedMovie: details });
} catch (error) {
this.updateState({ error: error instanceof Error ? error.message : 'Select failed' });
}
Expand All @@ -199,7 +224,8 @@ export class MovieConductor {
}

private checkAchievements(items: Movie[]): Achievement[] {
return INITIAL_ACHIEVEMENTS.map(a => ({ ...a, unlocked: items.length >= parseInt(a.id.split('-')[1] || '1') }));
const count = items.length;
return INITIAL_ACHIEVEMENTS.map(a => ({ ...a, unlocked: count >= a.threshold }));
}

private updateState(updates: Partial<WatchlistState>): void {
Expand Down
1 change: 1 addition & 0 deletions src/types/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface Achievement {
description: string;
iconName: 'Popcorn' | 'Library';
unlocked: boolean;
threshold: number;
}

export interface MovieStatistics {
Expand Down
Loading