- Added public gallery API with token-expiry enforcement, branding payload, cursor pagination, and per-photo download stream (app/Http/Controllers/Api/EventPublicController.php:1, routes/api.php:16). 410 is returned when the package gallery duration has lapsed.
- Served the guest PWA at /g/{token} and introduced a mobile-friendly gallery page with lazy-loaded thumbnails, themed colors, lightbox, and download links plus new gallery data client (resources/js/guest/pages/PublicGalleryPage.tsx:1, resources/js/guest/services/galleryApi.ts:1, resources/js/guest/router.tsx:1). Added i18n strings for the public gallery experience (resources/js/guest/i18n/messages.ts:1).
- Ensured checkout step changes snap back to the progress bar on mobile via smooth scroll anchoring (resources/ js/pages/marketing/checkout/CheckoutWizard.tsx:1).
- Enabled tenant admins to export all approved event photos through a new download action that streams a ZIP archive, with translations and routing in place (app/Http/Controllers/Tenant/EventPhotoArchiveController.php:1, app/Filament/Resources/EventResource.php:1, routes/web.php:1, resources/lang/de/admin.php:1, resources/lang/en/admin.php:1).
This commit is contained in:
132
app/Http/Controllers/Tenant/EventPhotoArchiveController.php
Normal file
132
app/Http/Controllers/Tenant/EventPhotoArchiveController.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Tenant;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventMediaAsset;
|
||||
use App\Models\Photo;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use ZipArchive;
|
||||
|
||||
class EventPhotoArchiveController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, Event $event): StreamedResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (! $user || (int) $user->tenant_id !== (int) $event->tenant_id) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
$photos = Photo::query()
|
||||
->with('mediaAsset')
|
||||
->where('event_id', $event->id)
|
||||
->where('status', 'approved')
|
||||
->orderBy('created_at')
|
||||
->get();
|
||||
|
||||
if ($photos->isEmpty()) {
|
||||
abort(404, 'No approved photos available for this event.');
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$tempPath = tempnam(sys_get_temp_dir(), 'fotospiel-photos-');
|
||||
|
||||
if ($tempPath === false || $zip->open($tempPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
abort(500, 'Unable to generate archive.');
|
||||
}
|
||||
|
||||
foreach ($photos as $photo) {
|
||||
$filename = $this->buildFilename($event, $photo);
|
||||
|
||||
$asset = $photo->mediaAsset ?? EventMediaAsset::query()
|
||||
->where('photo_id', $photo->id)
|
||||
->where('variant', 'original')
|
||||
->first();
|
||||
|
||||
$added = false;
|
||||
|
||||
if ($asset && $asset->path) {
|
||||
$added = $this->addDiskFileToArchive($zip, $asset->disk ?? config('filesystems.default'), $asset->path, $filename);
|
||||
}
|
||||
|
||||
if (! $added && $photo->file_path) {
|
||||
$added = $this->addDiskFileToArchive($zip, config('filesystems.default'), $photo->file_path, $filename);
|
||||
}
|
||||
|
||||
if (! $added) {
|
||||
Log::warning('Skipping photo in archive build', [
|
||||
'event_id' => $event->id,
|
||||
'photo_id' => $photo->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
$downloadName = sprintf('fotospiel-event-%s-photos.zip', $event->slug ?? $event->id);
|
||||
|
||||
return response()->streamDownload(function () use ($tempPath) {
|
||||
$stream = fopen($tempPath, 'rb');
|
||||
|
||||
if (! $stream) {
|
||||
throw new FileNotFoundException('Archive could not be opened.');
|
||||
}
|
||||
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
unlink($tempPath);
|
||||
}, $downloadName, [
|
||||
'Content-Type' => 'application/zip',
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildFilename(Event $event, Photo $photo): string
|
||||
{
|
||||
$timestamp = $photo->created_at?->format('Ymd_His') ?? now()->format('Ymd_His');
|
||||
$extension = pathinfo($photo->file_path ?? '', PATHINFO_EXTENSION) ?: 'jpg';
|
||||
|
||||
return sprintf('%s-photo-%d.%s', $timestamp, $photo->id, $extension);
|
||||
}
|
||||
|
||||
private function addDiskFileToArchive(ZipArchive $zip, ?string $diskName, string $path, string $filename): bool
|
||||
{
|
||||
$disk = $diskName ? Storage::disk($diskName) : Storage::disk(config('filesystems.default'));
|
||||
|
||||
try {
|
||||
if (method_exists($disk, 'path')) {
|
||||
$absolute = $disk->path($path);
|
||||
|
||||
if (is_file($absolute)) {
|
||||
return $zip->addFile($absolute, $filename);
|
||||
}
|
||||
}
|
||||
|
||||
$stream = $disk->readStream($path);
|
||||
|
||||
if ($stream) {
|
||||
$contents = stream_get_contents($stream);
|
||||
fclose($stream);
|
||||
|
||||
if ($contents !== false) {
|
||||
return $zip->addFromString($filename, $contents);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Failed to add file to archive', [
|
||||
'disk' => $diskName,
|
||||
'path' => $path,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user