Skip to content
Open
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
20 changes: 20 additions & 0 deletions docs/ITIL_V5_Evolution_2026-07-01.md
Original file line number Diff line number Diff line change
@@ -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.
33 changes: 32 additions & 1 deletion src/components/MovieDetailModal.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -296,6 +301,8 @@ const ListMenu = React.memo(
onCreateNewList: () => void;
}) => {
const { t } = useTranslation();
const [showTrailerMode, setShowTrailerMode] = useState(false);


return (
<div className="relative group">
Expand Down Expand Up @@ -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 (
<section>
Expand All @@ -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(
() =>
Expand Down Expand Up @@ -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 (
<section>
Expand Down Expand Up @@ -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 || [];
Expand Down Expand Up @@ -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 (
<section>
Expand Down Expand Up @@ -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<number | null>(movie.userRating ?? null);
const { ratings, loading: ratingsLoading } = useExternalRatings(
movie.tmdbId,
Expand Down Expand Up @@ -715,7 +734,19 @@ const PersonalSection = React.memo(
? t('common.saving', 'Speichere…')
: t('common.autoSaveHint', 'Wird beim Verlassen des Felds automatisch gespeichert.')}
</div>
</div>

<AnimatePresence>
{showTrailerMode && movie.trailerKey && (
<TrailerOverlay
title={movie.title}
overview={movie.overview}
trailerKey={movie.trailerKey}
isVisible={showTrailerMode}
onClose={() => setShowTrailerMode(false)}
/>
)}
</AnimatePresence>
</div>

{/* Tags - kollapsibel für mobile Usability */}
<div>
Expand Down
82 changes: 82 additions & 0 deletions src/components/TrailerOverlay.tsx
Original file line number Diff line number Diff line change
@@ -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<TrailerOverlayProps> = ({
title,
overview,
trailerKey,
onClose,
isVisible
}) => {
if (!isVisible) return null;

return (
<motion.div
initial={{ opacity: 0, backdropFilter: "blur(0px)" }}
animate={{ opacity: 1, backdropFilter: "blur(12px)" }}
exit={{ opacity: 0, backdropFilter: "blur(0px)" }}
className="fixed inset-0 z-[100] flex flex-col justify-end bg-black/60 p-6 md:p-12"
>
<button
onClick={onClose}
className="absolute top-6 right-6 w-12 h-12 flex items-center justify-center rounded-full bg-white/10 border border-white/20 text-white hover:bg-white/20 transition-all active:scale-90"
aria-label="Schließen"
>
<X size={24} />
</button>

<div className="absolute inset-0 -z-10">
<iframe
className="w-full h-full"
src={`https://www.youtube.com/embed/${trailerKey}?autoplay=1&mute=0&controls=0&showinfo=0&rel=0&modestbranding=1`}
title={title}
allow="autoplay; encrypted-media"
allowFullScreen
></iframe>
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/40 to-transparent" />
</div>

<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 100 }}
className="max-w-2xl bg-[var(--bg-card)] border border-[var(--border-color)] backdrop-blur-xl p-8 rounded-3xl shadow-2xl"
>
<div className="flex items-center gap-3 mb-4">
<span className="px-3 py-1 bg-red-600 text-[10px] font-bold text-white uppercase tracking-widest rounded-full">
Cinema Mode
</span>
<span className="text-[var(--text-muted)] text-sm font-medium italic">
Powered by CineLog AI
</span>
</div>

<h1 className="text-4xl md:text-5xl font-black text-[var(--text-main)] mb-4 tracking-tight">
{title}
</h1>

<p className="text-lg text-[var(--text-muted)] leading-relaxed mb-8 line-clamp-3 md:line-clamp-none">
{overview}
</p>

<div className="flex flex-wrap gap-4">
<button
onClick={onClose}
className="px-8 py-4 bg-[var(--accent-glow)] text-white font-bold rounded-2xl hover:scale-105 transition-transform active:scale-95 shadow-lg"
>
Video fortsetzen
</button>
</div>
</motion.div>
</motion.div>
);
};
21 changes: 21 additions & 0 deletions supabase/migrations/20260701_vibe_engine.sql
Original file line number Diff line number Diff line change
@@ -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;