PIMS/resources/js/pages/movies/Show.vue

171 lines
6.6 KiB
Vue
Raw Normal View History

2025-12-07 03:49:26 +00:00
<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>