130 lines
5.2 KiB
Vue
130 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
import AppLayout from '@/layouts/AppLayout.vue'
|
|
import { dashboard } from '@/routes'
|
|
import { type BreadcrumbItem } from '@/types'
|
|
import { Head } from '@inertiajs/vue3'
|
|
import PlaceholderPattern from '../components/PlaceholderPattern.vue'
|
|
import { onMounted, ref } from 'vue'
|
|
|
|
type Movie = {
|
|
id: number
|
|
title: string
|
|
year: number | null
|
|
rating: string | null
|
|
description: string | null
|
|
poster_url: string | null
|
|
genres?: { id: number; name: string }[]
|
|
}
|
|
|
|
type Page = {
|
|
data: Movie[]
|
|
total: number
|
|
current_page: number
|
|
last_page: number
|
|
}
|
|
|
|
const breadcrumbs: BreadcrumbItem[] = [
|
|
{
|
|
title: 'Dashboard',
|
|
href: dashboard().url,
|
|
},
|
|
]
|
|
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const latest = ref<Movie[]>([])
|
|
|
|
function snippet(text: string | null, wordLimit = 50) {
|
|
if (!text) return ''
|
|
const words = text.split(/\s+/)
|
|
if (words.length <= wordLimit) return text
|
|
return words.slice(0, wordLimit).join(' ') + '…'
|
|
}
|
|
|
|
async function fetchLatest() {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const res = await fetch('/api/movies?sort=newest&per_page=3', {
|
|
headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
|
credentials: 'same-origin',
|
|
})
|
|
if (!res.ok) throw new Error(`Failed: ${res.status}`)
|
|
const data: Page = await res.json()
|
|
latest.value = data.data
|
|
} catch (e: any) {
|
|
error.value = e?.message ?? 'Unexpected error'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(fetchLatest)
|
|
</script>
|
|
|
|
<template>
|
|
<Head title="Dashboard" />
|
|
|
|
<AppLayout :breadcrumbs="breadcrumbs">
|
|
<div class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4">
|
|
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
|
<!-- Last Movies Added widget -->
|
|
<div class="relative overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border">
|
|
<div class="p-4">
|
|
<div class="mb-3 flex items-center justify-between gap-2">
|
|
<h2 class="text-sm font-semibold">Last Movies Added</h2>
|
|
<a href="/movies" class="text-xs text-primary underline-offset-2 hover:underline">View all</a>
|
|
</div>
|
|
<div v-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>
|
|
<div v-if="loading" class="space-y-3">
|
|
<div v-for="n in 3" :key="n" class="flex gap-3">
|
|
<div class="h-20 w-14 shrink-0 rounded bg-muted animate-pulse"></div>
|
|
<div class="flex min-w-0 flex-1 flex-col gap-2">
|
|
<div class="h-4 w-1/2 rounded bg-muted animate-pulse"></div>
|
|
<div class="h-3 w-2/3 rounded bg-muted animate-pulse"></div>
|
|
<div class="h-3 w-1/3 rounded bg-muted animate-pulse"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ul v-else class="divide-y divide-muted-foreground/20" :aria-busy="loading ? 'true' : 'false'">
|
|
<li v-for="m in latest" :key="m.id" class="py-2">
|
|
<a :href="`/movies/${m.id}`" class="flex gap-3 rounded px-2 py-2 hover:bg-muted/40 focus:bg-muted/40 focus:outline-none">
|
|
<div class="h-20 w-14 shrink-0 overflow-hidden rounded bg-muted">
|
|
<img v-if="m.poster_url" :src="m.poster_url" :alt="`Poster for ${m.title}`" loading="lazy" class="h-full w-full object-cover" />
|
|
</div>
|
|
<div class="flex min-w-0 flex-1 flex-col">
|
|
<div class="flex items-center justify-between gap-2">
|
|
<div class="truncate font-medium">{{ m.title }}</div>
|
|
<div class="text-xs text-muted-foreground">{{ m.year ?? '' }}</div>
|
|
</div>
|
|
<div class="mt-1 text-xs text-muted-foreground">
|
|
<span v-if="m.rating" class="mr-2 rounded border px-1 py-0.5">{{ m.rating }}</span>
|
|
<span v-if="m.genres?.length">{{ m.genres.map(g => g.name).join(', ') }}</span>
|
|
</div>
|
|
|
|
</div>
|
|
</a>
|
|
</li>
|
|
<li v-if="!latest.length" class="px-2 py-2 text-sm text-muted-foreground">No movies yet — add some from Admin → Movies.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border">
|
|
<PlaceholderPattern />
|
|
</div>
|
|
<div class="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border">
|
|
<PlaceholderPattern />
|
|
</div>
|
|
</div>
|
|
<div class="relative min-h-[100vh] flex-1 rounded-xl border border-sidebar-border/70 md:min-h-min dark:border-sidebar-border">
|
|
<PlaceholderPattern />
|
|
</div>
|
|
</div>
|
|
</AppLayout>
|
|
</template>
|