diff --git a/docs/ITIL_V5_Evolution_2026-07-01.md b/docs/ITIL_V5_Evolution_2026-07-01.md new file mode 100644 index 0000000..a9075fc --- /dev/null +++ b/docs/ITIL_V5_Evolution_2026-07-01.md @@ -0,0 +1,20 @@ +# ITIL V5 Service Evolution - Milestone: Trailer Cinema Mode + +## 1. Service Design & Strategy +**Service Name:** CineLog Trailer Immersive Experience +**Objective:** Provide an immersive, high-fidelity movie trailer viewing experience within the PWA. +**Value Proposition:** Emotional connection through cinematic presentation. + +## 2. Configuration Item (CI) Update +- New Component: `TrailerOverlay.tsx` +- Modified: `MovieDetailModal.tsx` +- Theme Integration: Semantic variables used for Dark/Light mode support. + +## 3. Risk & Rollback +**Risk:** UX interference with existing navigation. +**Rollback:** git checkout main; delete feat/trailer-cinema-mode branch. + +## 4. Milestone: Supabase Vibe Engine v3.0 (Data Layer) +**Service Component:** Persistence Layer +**Change:** New table `movie_vibes` for storing AI-generated mood profiles. +**Validation:** SQL Migration verification via Supabase API. diff --git a/src/components/MovieDetailModal.tsx b/src/components/MovieDetailModal.tsx index 32d8d9b..284b497 100644 --- a/src/components/MovieDetailModal.tsx +++ b/src/components/MovieDetailModal.tsx @@ -1,3 +1,4 @@ +import { TrailerOverlay } from './TrailerOverlay'; import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Movie, CastMember, WatchProvider } from '../types/domain'; @@ -45,6 +46,8 @@ export const MovieDetailModal = React.memo( onShowToast, }: MovieDetailModalProps) => { const { t } = useTranslation(); + const [showTrailerMode, setShowTrailerMode] = useState(false); + const isInLibrary = useMemo( () => libraryItems.some((m) => m.tmdbId === Number(movie.id) || m.id === movie.id), @@ -202,6 +205,8 @@ const ActionButtons = React.memo( onShowToast: (message: string, type: 'success' | 'error' | 'info') => void; }) => { const { t } = useTranslation(); + const [showTrailerMode, setShowTrailerMode] = useState(false); + const [showListCreation, setShowListCreation] = useState(false); const handleAddToList = useCallback( @@ -296,6 +301,8 @@ const ListMenu = React.memo( onCreateNewList: () => void; }) => { const { t } = useTranslation(); + const [showTrailerMode, setShowTrailerMode] = useState(false); + return (
@@ -341,6 +348,8 @@ ListMenu.displayName = 'ListMenu'; const PlotSection = React.memo(({ movie }: { movie: Movie }) => { const { t } = useTranslation(); + const [showTrailerMode, setShowTrailerMode] = useState(false); + if (!movie.overview) return null; return (
@@ -356,6 +365,8 @@ PlotSection.displayName = 'PlotSection'; const MetadataGrid = React.memo(({ movie }: { movie: Movie }) => { const { t } = useTranslation(); + const [showTrailerMode, setShowTrailerMode] = useState(false); + const items = useMemo( () => @@ -385,6 +396,8 @@ MetadataGrid.displayName = 'MetadataGrid'; const CastSection = React.memo(({ cast }: { cast?: CastMember[] }) => { const { t } = useTranslation(); + const [showTrailerMode, setShowTrailerMode] = useState(false); + if (!cast?.length) return null; return (
@@ -420,6 +433,8 @@ CastSection.displayName = 'CastSection'; const WatchProvidersSection = React.memo( ({ watchProviders }: { watchProviders?: Movie['watchProviders'] }) => { const { t } = useTranslation(); + const [showTrailerMode, setShowTrailerMode] = useState(false); + const flat = watchProviders?.flatrate || []; const rent = watchProviders?.rent || []; const buy = watchProviders?.buy || []; @@ -469,6 +484,8 @@ const RecommendationsSection = React.memo( onSelectMovie: (id: string) => void; }) => { const { t } = useTranslation(); + const [showTrailerMode, setShowTrailerMode] = useState(false); + if (!recommendations?.length) return null; return (
@@ -516,6 +533,8 @@ const PersonalSection = React.memo( onShowToast: (message: string, type: 'success' | 'error' | 'info') => void; }) => { const { t } = useTranslation(); + const [showTrailerMode, setShowTrailerMode] = useState(false); + const [rating, setRating] = useState(movie.userRating ?? null); const { ratings, loading: ratingsLoading } = useExternalRatings( movie.tmdbId, @@ -715,7 +734,19 @@ const PersonalSection = React.memo( ? t('common.saving', 'Speichere…') : t('common.autoSaveHint', 'Wird beim Verlassen des Felds automatisch gespeichert.')}
- + + + {showTrailerMode && movie.trailerKey && ( + setShowTrailerMode(false)} + /> + )} + + {/* Tags - kollapsibel für mobile Usability */}
diff --git a/src/components/TrailerOverlay.tsx b/src/components/TrailerOverlay.tsx new file mode 100644 index 0000000..12f4608 --- /dev/null +++ b/src/components/TrailerOverlay.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { X } from 'lucide-react'; + +interface TrailerOverlayProps { + title: string; + overview: string | null; + trailerKey: string; + onClose: () => void; + isVisible: boolean; +} + +export const TrailerOverlay: React.FC = ({ + title, + overview, + trailerKey, + onClose, + isVisible +}) => { + if (!isVisible) return null; + + return ( + + + +
+ +
+
+ + +
+ + Cinema Mode + + + Powered by CineLog AI + +
+ +

+ {title} +

+ +

+ {overview} +

+ +
+ +
+
+ + ); +}; diff --git a/supabase/migrations/20260701_vibe_engine.sql b/supabase/migrations/20260701_vibe_engine.sql new file mode 100644 index 0000000..64a0419 --- /dev/null +++ b/supabase/migrations/20260701_vibe_engine.sql @@ -0,0 +1,21 @@ +-- ITIL V5 Milestone: Vibe Engine v3.0 Base Schema +-- Service: Movie Recommendation Engine + +create table if not exists public.movie_vibes ( + id uuid default uuid_generate_v4() primary key, + movie_id uuid not null, -- references movies(id) - assuming id is uuid + vibe_name text not null, + intensity float check (intensity >= 0 and intensity <= 1), + ai_metadata jsonb, + created_at timestamp with time zone default now() +); + +-- Indices for performance +create index if not exists idx_movie_vibes_name on public.movie_vibes(vibe_name); +create index if not exists idx_movie_vibes_movie_id on public.movie_vibes(movie_id); + +-- Simple view for vibe exploration +create or replace view public.vibe_explorer as +select v.vibe_name, count(*) as frequency, avg(v.intensity) as avg_intensity +from public.movie_vibes v +group by v.vibe_name;