- 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.
138 lines
5.1 KiB
Vue
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>
|