171 lines
6.6 KiB
Vue
171 lines
6.6 KiB
Vue
<script setup lang="ts">
|
|
import AppLayout from '@/layouts/AppLayout.vue'
|
|
import { Head, usePage } from '@inertiajs/vue3'
|
|
import { onMounted, ref, computed } from 'vue'
|
|
|
|
const page = usePage<{ props: { movieId: number } }>()
|
|
const movieId = computed(() => (page.props as any).movieId as number)
|
|
|
|
type Rel = { id: number; name: string }
|
|
type Movie = {
|
|
id: number
|
|
title: string
|
|
original_title: string | null
|
|
description: string | null
|
|
poster_url: string | null
|
|
backdrop_url: string | null
|
|
rating: string | null
|
|
release_date: string | null
|
|
year: number | null
|
|
runtime: number | null
|
|
genres: Rel[]
|
|
actors: Rel[]
|
|
directors: Rel[]
|
|
studios: Rel[]
|
|
countries: Rel[]
|
|
languages: Rel[]
|
|
}
|
|
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const m = ref<Movie | null>(null)
|
|
const deleting = ref(false)
|
|
const showToast = ref(false)
|
|
const toastText = ref('')
|
|
|
|
async function fetchMovie() {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const res = await fetch(`/api/movies/${movieId.value}`, {
|
|
headers: { Accept: 'application/json' },
|
|
credentials: 'same-origin',
|
|
})
|
|
if (!res.ok) throw new Error(`Failed: ${res.status}`)
|
|
m.value = await res.json()
|
|
} catch (e: any) {
|
|
error.value = e?.message ?? 'Unexpected error'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(fetchMovie)
|
|
|
|
async function onDelete() {
|
|
if (!m.value) return
|
|
if (!confirm(`Delete '${m.value.title}'? This cannot be undone.`)) return
|
|
deleting.value = true
|
|
try {
|
|
const res = await fetch(`/admin/movies/${m.value.id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? '',
|
|
},
|
|
credentials: 'same-origin',
|
|
})
|
|
if (!res.ok) throw new Error(`Delete failed: ${res.status}`)
|
|
// Redirect to admin list if available; else back to public list
|
|
window.location.href = '/admin/movies'
|
|
} catch (e: any) {
|
|
toastText.value = e?.message ?? 'Delete failed'
|
|
showToast.value = true
|
|
setTimeout(() => (showToast.value = false), 2500)
|
|
} finally {
|
|
deleting.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Head :title="m?.title ?? 'Movie'" />
|
|
<AppLayout :breadcrumbs="[{ title: 'Movies', href: '/movies' }, { title: m?.title ?? '…', href: `/movies/${movieId}` }]">
|
|
<div class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4">
|
|
<div v-if="loading" class="text-sm text-muted-foreground">Loading…</div>
|
|
<div v-else-if="error" class="rounded-md border border-red-300 bg-red-50 p-3 text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200">{{ error }}</div>
|
|
<div v-else-if="m" class="grid grid-cols-1 gap-4 md:grid-cols-[160px,1fr]">
|
|
<div class="order-first">
|
|
<div class="h-[240px] w-[160px] overflow-hidden rounded bg-muted">
|
|
<img v-if="m.poster_url" :src="m.poster_url" alt="Poster" class="h-full w-full object-cover" />
|
|
</div>
|
|
</div>
|
|
<div class="flex min-w-0 flex-col">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<h1 class="text-xl font-semibold">{{ m.title }}</h1>
|
|
<div class="shrink-0 space-x-2">
|
|
<a :href="`/admin/movies/${m.id}/edit`" class="rounded-md border px-3 py-1.5 text-xs hover:bg-muted">Edit</a>
|
|
<button @click="onDelete" :disabled="deleting" class="rounded-md border px-3 py-1.5 text-xs hover:bg-muted disabled:opacity-60">
|
|
<span v-if="deleting">Deleting…</span>
|
|
<span v-else>Delete</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="mt-1 text-sm text-muted-foreground">
|
|
<span v-if="m.year" class="mr-2">{{ m.year }}</span>
|
|
<span v-if="m.rating" class="mr-2 rounded border px-1 py-0.5">{{ m.rating }}</span>
|
|
<span v-if="m.runtime" class="mr-2">{{ m.runtime }} min</span>
|
|
<span v-if="m.release_date" class="mr-2">Released: {{ m.release_date }}</span>
|
|
</div>
|
|
|
|
<div v-if="m.genres?.length" class="mt-2 text-sm">
|
|
<strong>Genres:</strong>
|
|
<span class="space-x-1">
|
|
<template v-for="(g, i) in m.genres" :key="g.id">
|
|
<a :href="`/genres/${g.id}`" class="underline-offset-2 hover:underline">{{ g.name }}</a><span v-if="i < m.genres.length - 1">,</span>
|
|
</template>
|
|
</span>
|
|
</div>
|
|
<div v-if="m.directors?.length" class="mt-1 text-sm">
|
|
<strong>Directors:</strong>
|
|
<span class="space-x-1">
|
|
<template v-for="(d, i) in m.directors" :key="(d as any).id">
|
|
<a :href="`/directors/${(d as any).id}`" class="underline-offset-2 hover:underline">{{ (d as any).name }}</a><span v-if="i < m.directors.length - 1">,</span>
|
|
</template>
|
|
</span>
|
|
</div>
|
|
<div v-if="m.actors?.length" class="mt-1 text-sm">
|
|
<strong>Actors:</strong>
|
|
<span class="space-x-1">
|
|
<template v-for="(a, i) in m.actors" :key="(a as any).id">
|
|
<a :href="`/actors/${(a as any).id}`" class="underline-offset-2 hover:underline">{{ (a as any).name }}</a><span v-if="i < m.actors.length - 1">,</span>
|
|
</template>
|
|
</span>
|
|
</div>
|
|
<div v-if="m.studios?.length" class="mt-1 text-sm">
|
|
<strong>Studios:</strong>
|
|
<span class="space-x-1">
|
|
<template v-for="(s, i) in m.studios" :key="(s as any).id">
|
|
<a :href="`/studios/${(s as any).id}`" class="underline-offset-2 hover:underline">{{ (s as any).name }}</a><span v-if="i < m.studios.length - 1">,</span>
|
|
</template>
|
|
</span>
|
|
</div>
|
|
<div v-if="m.countries?.length" class="mt-1 text-sm">
|
|
<strong>Countries:</strong> {{ m.countries.map(g => g.name).join(', ') }}
|
|
</div>
|
|
<div v-if="m.languages?.length" class="mt-1 text-sm">
|
|
<strong>Languages:</strong> {{ m.languages.map(g => g.name).join(', ') }}
|
|
</div>
|
|
|
|
<p class="mt-3 text-sm leading-6">{{ m.description }}</p>
|
|
</div>
|
|
</div>
|
|
<transition name="fade">
|
|
<div
|
|
v-if="showToast"
|
|
class="fixed bottom-4 left-1/2 z-50 -translate-x-1/2 rounded bg-foreground px-3 py-2 text-sm text-background shadow-lg"
|
|
>
|
|
{{ toastText }}
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</AppLayout>
|
|
|
|
</template>
|
|
|
|
<style scoped>
|
|
.fade-enter-active, .fade-leave-active { transition: opacity .2s }
|
|
.fade-enter-from, .fade-leave-to { opacity: 0 }
|
|
</style>
|