Skip to content
Draft
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
157 changes: 157 additions & 0 deletions frontend/src/components/TrailerSection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { trailerScenes } from '../data/trailerScenes'

const currentIndex = ref(0)
const isTransitioning = ref(false)
const isPaused = ref(false)
const isCollapsed = ref(false)
let timer: ReturnType<typeof setInterval> | undefined

const scene = computed(() => trailerScenes[currentIndex.value])
const totalScenes = trailerScenes.length

function nextScene() {
isTransitioning.value = true
setTimeout(() => {
currentIndex.value = (currentIndex.value + 1) % totalScenes
isTransitioning.value = false
}, 500)
}

function prevScene() {
isTransitioning.value = true
setTimeout(() => {
currentIndex.value = (currentIndex.value - 1 + totalScenes) % totalScenes
isTransitioning.value = false
}, 500)
}

function goToScene(index: number) {
if (index === currentIndex.value) return
isTransitioning.value = true
setTimeout(() => {
currentIndex.value = index
isTransitioning.value = false
}, 500)
}

function startAutoPlay() {
timer = setInterval(() => {
if (!isPaused.value) nextScene()
}, 6000)
}

function togglePause() {
isPaused.value = !isPaused.value
}

onMounted(() => startAutoPlay())
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>

<template>
<div data-testid="trailer-section" class="mb-8">
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold text-indigo-800">🎬 Game Trailer</h2>
<button
data-testid="trailer-collapse-toggle"
class="text-xs text-gray-400 hover:text-indigo-600 transition"
@click="isCollapsed = !isCollapsed"
>
{{ isCollapsed ? '▼ Show Trailer' : '▲ Hide Trailer' }}
</button>
</div>

<div v-if="!isCollapsed" data-testid="trailer-player" class="relative overflow-hidden rounded-2xl shadow-lg">
<!-- Background layer -->
<div
class="bg-gradient-to-br transition-all duration-700 ease-in-out"
:class="[
scene.bgGradient,
isTransitioning ? 'opacity-0 scale-105' : 'opacity-100 scale-100',
]"
>
<!-- Decorative sparkles -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute top-4 left-8 text-white/10 text-6xl animate-pulse">✦</div>
<div class="absolute top-12 right-12 text-white/10 text-4xl animate-pulse" style="animation-delay: 1s">✧</div>
<div class="absolute bottom-8 left-1/3 text-white/10 text-5xl animate-pulse" style="animation-delay: 2s">✦</div>
</div>

<!-- Content -->
<div class="relative px-6 py-8 md:px-10 md:py-12 min-h-[260px] flex flex-col justify-between">
<!-- Scene counter -->
<div class="flex items-center justify-between mb-4">
<span class="text-xs font-mono text-white/40 uppercase tracking-widest">
Scene {{ currentIndex + 1 }} / {{ totalScenes }}
</span>
<span class="text-xs font-mono text-white/30">
~{{ scene.durationSeconds }}s prompt
</span>
</div>

<!-- Title -->
<div class="mb-4">
<h3 class="text-2xl md:text-3xl font-bold text-white drop-shadow-lg">
{{ scene.title }}
</h3>
</div>

<!-- Video prompt -->
<div class="mb-4 bg-black/30 rounded-lg p-4 backdrop-blur-sm border border-white/10">
<p class="text-xs font-mono text-white/50 mb-1 uppercase tracking-wider">Text-to-Video Prompt</p>
<p class="text-sm text-white/80 leading-relaxed italic">
"{{ scene.prompt }}"
</p>
</div>

<!-- Punchline subtitle -->
<p data-testid="trailer-subtitle" class="text-base md:text-lg text-white/90 font-medium">
{{ scene.subtitle }}
</p>

<!-- Controls -->
<div class="flex items-center justify-between mt-6">
<div class="flex items-center gap-2">
<button
data-testid="trailer-prev"
class="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center transition"
@click="prevScene"
>
</button>
<button
data-testid="trailer-pause"
class="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center transition"
@click="togglePause"
>
{{ isPaused ? '▶' : '⏸' }}
</button>
<button
data-testid="trailer-next"
class="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center transition"
@click="nextScene"
>
</button>
</div>

<!-- Scene dots -->
<div class="flex gap-1.5">
<button
v-for="(s, i) in trailerScenes"
:key="s.id"
class="w-2 h-2 rounded-full transition-all"
:class="i === currentIndex ? 'bg-white scale-125' : 'bg-white/30 hover:bg-white/50'"
@click="goToScene(i)"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
65 changes: 65 additions & 0 deletions frontend/src/data/trailerScenes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
export const trailerScenes = [
{
id: 1,
prompt:
'Cinematic sweeping aerial shot of a grand magical wizard school perched on misty mountain cliffs at golden hour. Towering spires glow with arcane energy, enchanted books fly between towers, and a faint aurora ripples across the sky. Epic orchestral music swells. Camera slowly pushes toward the main gate.',
title: '🏰 A World of Magic Awaits',
subtitle: '...rendered entirely in plain text, because we blew the graphics budget on fonts.',
durationSeconds: 18,
bgGradient: 'from-indigo-900 via-purple-900 to-indigo-950',
emoji: '🏰',
},
{
id: 2,
prompt:
'Intense close-up of two powerful wizards facing off in a dark stone arena. Lightning crackles between their wands. One casts a massive fireball that illuminates the entire coliseum. Sparks and magical particles fill the air. Dramatic slow-motion as spells collide in a blinding explosion of light.',
title: '⚔️ Epic Wizard Battles',
subtitle: "You click 'Attack'. The enemy loses 3 HP. Riveting.",
durationSeconds: 16,
bgGradient: 'from-red-900 via-orange-900 to-red-950',
emoji: '⚔️',
},
{
id: 3,
prompt:
'High-speed first-person view of a wizard on a racing broomstick weaving through enchanted forest obstacles at breakneck speed. Neon magical trail streams behind. Other racers jostle for position as they barrel through glowing checkpoints. Wind whips robes dramatically. Camera spins during a barrel roll.',
title: '🧹 Heart-Pounding Broom Racing',
subtitle: 'Press arrow keys. Dodge a tree. Wow. Such adrenaline. Much speed.',
durationSeconds: 17,
bgGradient: 'from-amber-900 via-yellow-900 to-amber-950',
emoji: '🧹',
},
{
id: 4,
prompt:
'A vast underground goblin bank vault door slowly opens to reveal mountains of glittering gold coins, enchanted gemstones, and ancient artifacts. A dragon sleeps atop the hoard. Magical security runes pulse on the walls. Camera dollies through the treasure as coins cascade and shimmer.',
title: '🏦 Manage Your Fortune',
subtitle: "It's a number. In a database. You can make it go up. Or down. Thrilling finance.",
durationSeconds: 15,
bgGradient: 'from-yellow-900 via-amber-800 to-yellow-950',
emoji: '🏦',
},
{
id: 5,
prompt:
'A fellowship of diverse wizards stands united on a cliff edge at dawn, cloaks billowing in the wind, overlooking an endless magical battlefield. They raise their wands together creating a massive combined spell that shoots into the sky, forming a shield of pure light. Emotional cinematic moment.',
title: '🤝 Join a Fellowship',
subtitle: 'Group chat, but with wizard hats. You will still be ghosted.',
durationSeconds: 18,
bgGradient: 'from-green-900 via-emerald-900 to-green-950',
emoji: '🤝',
},
{
id: 6,
prompt:
'Fast-paced montage: wizard casting spells, broomstick racing through clouds, gold coins raining from the sky, epic battle explosions, a leaderboard with names climbing ranks. Everything freezes. Camera zooms out to reveal it is all just white text on a dark terminal screen. A cursor blinks. Title card fades in.',
title: '🎮 Wizard RPG',
subtitle:
"Honestly, it's a text-based RPG. But you read this far, so clearly you have nothing better to do. Welcome home.",
durationSeconds: 20,
bgGradient: 'from-purple-900 via-indigo-900 to-slate-950',
emoji: '🎮',
},
] as const

export type TrailerScene = (typeof trailerScenes)[number]
3 changes: 3 additions & 0 deletions frontend/src/views/DashboardView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { onMounted } from 'vue'
import { usePlayerStore } from '../stores/player'
import { useBankStore } from '../stores/bank'
import { useAuthStore } from '../stores/auth'
import TrailerSection from '../components/TrailerSection.vue'

const auth = useAuthStore()
const playerStore = usePlayerStore()
Expand All @@ -17,6 +18,8 @@ onMounted(async () => {
<div data-testid="dashboard-view">
<h1 class="text-3xl font-bold text-indigo-900 mb-6">🏰 Dashboard</h1>

<TrailerSection />

<div v-if="playerStore.loading" class="text-center py-12 text-gray-500">Loading…</div>

<div v-else-if="playerStore.profile" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
Expand Down