Files
ai-stylegallery/resources/js/Pages/GalleryAccess.vue
soeren f5da8ed877 - Galerien sind nun eine Entität - es kann mehrere geben
- Neues Sparkbooth-Upload-Feature: Endpoint /api/sparkbooth/upload (Token-basiert pro Galerie), Controller Api/SparkboothUploadController, Migration 2026_01_21_000001_add_upload_fields_to_galleries_table.php mit Upload-Flags/Token/Expiry;
    Galerie-Modell und Factory/Seeder entsprechend erweitert.
  - Filament: Neue Setup-Seite SparkboothSetup (mit View) zur schnellen Galerie- und Token-Erstellung inkl. QR/Endpoint/Snippet;
    Galerie-Link-Views nutzen jetzt simple-qrcode (Composer-Dependency hinzugefügt) und bieten PNG-Download.
  - Galerie-Tabelle: Slug/Pfad-Spalten entfernt, Action „Link-Details“ mit Modal; Created-at-Spalte hinzugefügt.
  - Zugriffshärtung: Galerie-IDs in API (ImageController, Download/Print) geprüft; GalleryAccess/Middleware + Gallery-Modell/Slug-UUID
    eingeführt; GalleryAccess-Inertia-Seite.
  - UI/UX: LoadingSpinner/StyledImageDisplay verbessert, Delete-Confirm, Übersetzungen ergänzt.
2025-12-04 07:52:50 +01:00

138 lines
5.1 KiB
Vue

<template>
<Head :title="__('api.gallery.access_title')" />
<div class="min-h-screen bg-gradient-to-br from-slate-100 via-white to-slate-200 text-slate-900 transition-colors duration-500 dark:from-slate-950 dark:via-slate-900 dark:to-slate-950 dark:text-slate-100">
<div class="flex min-h-screen items-center justify-center px-4 py-12">
<div class="w-full max-w-xl space-y-8 rounded-3xl border border-slate-200/70 bg-white/80 p-8 shadow-2xl backdrop-blur dark:border-white/10 dark:bg-white/5">
<div class="space-y-3">
<p class="text-xs uppercase tracking-[0.35em] text-slate-500 dark:text-slate-400">
Style Gallery
</p>
<h1 class="text-3xl font-semibold text-slate-900 dark:text-white">
{{ __('api.gallery.access_title') }}
</h1>
<p v-if="expired" class="text-sm text-rose-600 dark:text-rose-300">
{{ __('api.gallery.expired') }}
</p>
<p v-else class="text-sm text-slate-600 dark:text-slate-300">
{{ props.flashMessage || __('api.gallery.password_required') }}
</p>
</div>
<div class="flex flex-wrap gap-2 text-xs text-slate-600 dark:text-slate-300">
<span v-if="formattedExpiresAt" class="inline-flex items-center gap-2 rounded-full bg-slate-100 px-3 py-1 dark:bg-white/10">
<span class="h-2 w-2 rounded-full bg-amber-400"></span>
{{ __('api.gallery.expires_at_hint', { datetime: formattedExpiresAt }) }}
</span>
<span v-if="props.accessDurationMinutes" class="inline-flex items-center gap-2 rounded-full bg-slate-100 px-3 py-1 dark:bg-white/10">
<span class="h-2 w-2 rounded-full bg-emerald-400"></span>
{{ __('api.gallery.duration_hint', { minutes: props.accessDurationMinutes }) }}
</span>
</div>
<form v-if="requiresPassword && !expired" @submit.prevent="submit" class="space-y-4">
<div class="space-y-2">
<label for="password" class="text-sm font-medium text-slate-800 dark:text-slate-200">
{{ __('api.gallery.password_label') }}
</label>
<input
id="password"
v-model="form.password"
type="password"
class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-base text-slate-900 shadow-sm transition focus:border-emerald-400 focus:outline-none focus:ring-2 focus:ring-emerald-400/40 dark:border-white/10 dark:bg-white/5 dark:text-white"
:aria-invalid="Boolean(form.errors.password)"
/>
<p v-if="form.errors.password" class="text-sm text-rose-500">
{{ form.errors.password }}
</p>
</div>
<button
type="submit"
class="flex w-full items-center justify-center rounded-2xl bg-emerald-500 px-4 py-3 text-base font-semibold text-white shadow-lg shadow-emerald-500/30 transition hover:bg-emerald-600 focus:outline-none focus:ring-2 focus:ring-emerald-400 focus:ring-offset-2 focus:ring-offset-white disabled:opacity-60 dark:focus:ring-offset-slate-900"
:disabled="form.processing"
>
{{ form.processing ? '...' : __('api.gallery.submit_password') }}
</button>
</form>
<div v-else-if="!expired" class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-emerald-800 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-100">
{{ __('api.gallery.password_required') }}
</div>
<div v-if="expired" class="space-y-3 rounded-2xl border border-rose-200 bg-rose-50 p-4 text-rose-700 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-100">
<p>{{ __('api.gallery.expired') }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { Head, useForm } from '@inertiajs/vue3';
import { computed } from 'vue';
const props = defineProps({
requiresPassword: {
type: Boolean,
default: false,
},
gallery: {
type: Object,
default: null,
},
expiresAt: {
type: [String, Date, null],
default: null,
},
accessDurationMinutes: {
type: [Number, null],
default: null,
},
expired: {
type: Boolean,
default: false,
},
flashMessage: {
type: String,
default: null,
},
});
const requiresPassword = computed(() => Boolean(props.requiresPassword));
const form = useForm({
password: '',
});
const formattedExpiresAt = computed(() => {
if (!props.expiresAt) {
return null;
}
const date = new Date(props.expiresAt);
if (Number.isNaN(date.getTime())) {
return null;
}
return new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date);
});
const submit = () => {
const hasGallery = Boolean(props.gallery?.slug);
const routeName = hasGallery ? 'gallery.access.store' : 'gallery.access.default.store';
const params = hasGallery ? { gallery: props.gallery.slug } : {};
form.post(route(routeName, params), {
preserveScroll: true,
onError: () => {
form.password = '';
},
});
};
</script>