- 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).
133 lines
4.1 KiB
PHP
133 lines
4.1 KiB
PHP
<?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;
|
|
}
|
|
}
|
|
|