reworked the guest pwa, modernized start and gallery page. added share link functionality.

This commit is contained in:
Codex Agent
2025-11-10 22:25:25 +01:00
parent 1e8810ca51
commit 1cec116933
22 changed files with 1208 additions and 476 deletions

View File

@@ -6,6 +6,7 @@ use App\Models\Event;
use App\Models\EventJoinToken;
use App\Models\EventMediaAsset;
use App\Models\Photo;
use App\Models\PhotoShareLink;
use App\Services\Analytics\JoinTokenAnalyticsRecorder;
use App\Services\EventJoinTokenService;
use App\Services\Packages\PackageLimitEvaluator;
@@ -585,6 +586,18 @@ class EventPublicController extends BaseController
);
}
private function makeShareAssetUrl(PhotoShareLink $shareLink, string $variant): string
{
return URL::temporarySignedRoute(
'api.v1.photo-shares.asset',
now()->addSeconds(self::SIGNED_URL_TTL_SECONDS),
[
'slug' => $shareLink->slug,
'variant' => $variant,
]
);
}
public function gallery(Request $request, string $token)
{
$locale = $request->query('locale', app()->getLocale());
@@ -677,6 +690,170 @@ class EventPublicController extends BaseController
]);
}
public function createShareLink(Request $request, string $token, Photo $photo)
{
$resolved = $this->resolvePublishedEvent($request, $token, ['id']);
if ($resolved instanceof JsonResponse) {
return $resolved;
}
/** @var array{0: object{id:int}} $resolved */
[$eventRecord] = $resolved;
if ((int) $photo->event_id !== (int) $eventRecord->id) {
return ApiError::response(
'photo_not_shareable',
'Photo Not Shareable',
'The selected photo cannot be shared at this time.',
Response::HTTP_NOT_FOUND,
[
'photo_id' => $photo->id,
'event_id' => $eventRecord->id,
]
);
}
$deviceId = trim((string) ($request->header('X-Device-Id') ?? $request->input('device_id', '')));
$ttlHours = max(1, (int) config('share-links.ttl_hours', 48));
$existing = PhotoShareLink::query()
->where('photo_id', $photo->id)
->when($deviceId !== '', fn ($query) => $query->where('created_by_device_id', $deviceId))
->where('expires_at', '>', now())
->latest('id')
->first();
if ($existing && ! $existing->isExpired()) {
$shareLink = $existing;
} else {
$shareLink = PhotoShareLink::create([
'photo_id' => $photo->id,
'slug' => PhotoShareLink::generateSlug(),
'expires_at' => now()->addHours($ttlHours),
'created_by_device_id' => $deviceId ?: null,
'created_ip' => $request->ip(),
]);
}
return response()->json([
'slug' => $shareLink->slug,
'expires_at' => $shareLink->expires_at?->toIso8601String(),
'url' => url("/share/{$shareLink->slug}"),
])->header('Cache-Control', 'no-store');
}
public function shareLink(Request $request, string $slug)
{
$shareLink = PhotoShareLink::with(['photo.event', 'photo.emotion', 'photo.task'])
->where('slug', $slug)
->first();
if (! $shareLink || $shareLink->isExpired()) {
return ApiError::response(
'share_link_expired',
'Link Expired',
'This shared photo link is no longer available.',
Response::HTTP_GONE,
['slug' => $slug]
);
}
$shareLink->forceFill(['last_accessed_at' => now()])->save();
$photo = $shareLink->photo;
$event = $photo->event;
if (! $event || $photo->status !== 'approved') {
return ApiError::response(
'photo_not_shareable',
'Photo Not Shareable',
'The shared photo is no longer available.',
Response::HTTP_NOT_FOUND,
['slug' => $slug]
);
}
$taskTitle = null;
if ($photo->task) {
$taskTitle = $this->translateLocalized($photo->task->title, app()->getLocale(), '');
if ($taskTitle === '') {
$taskTitle = null;
}
}
$photoResource = [
'id' => $photo->id,
'title' => $taskTitle,
'emotion' => $photo->emotion ? [
'name' => $photo->emotion->name,
'emoji' => $photo->emotion->emoji,
] : null,
'likes_count' => $photo->likes()->count(),
'image_urls' => [
'thumbnail' => $this->makeShareAssetUrl($shareLink, 'thumbnail'),
'full' => $this->makeShareAssetUrl($shareLink, 'full'),
],
];
return response()->json([
'slug' => $shareLink->slug,
'expires_at' => $shareLink->expires_at?->toIso8601String(),
'photo' => $photoResource,
'event' => $event ? [
'id' => $event->id,
'name' => $event->name,
'city' => $event->city,
] : null,
])->header('Cache-Control', 'no-store');
}
public function shareLinkAsset(Request $request, string $slug, string $variant)
{
if (! in_array($variant, ['thumbnail', 'full'], true)) {
return ApiError::response(
'invalid_variant',
'Invalid Variant',
'The requested asset variant is not supported.',
Response::HTTP_BAD_REQUEST
);
}
$shareLink = PhotoShareLink::with(['photo.mediaAsset', 'photo.event.tenant'])
->where('slug', $slug)
->first();
if (! $shareLink || $shareLink->isExpired()) {
return ApiError::response(
'share_link_expired',
'Link Expired',
'This shared photo link is no longer available.',
Response::HTTP_GONE,
['slug' => $slug]
);
}
$photo = $shareLink->photo;
$event = $photo->event;
if (! $event || $photo->status !== 'approved') {
return ApiError::response(
'photo_not_shareable',
'Photo Not Shareable',
'The shared photo is no longer available.',
Response::HTTP_NOT_FOUND,
['slug' => $slug]
);
}
$variantPreference = $variant === 'thumbnail'
? ['thumbnail', 'original']
: ['original'];
return $this->streamGalleryPhoto($event, $photo, $variantPreference, 'inline');
}
public function galleryPhotoAsset(Request $request, string $token, int $photo, string $variant)
{
$resolved = $this->resolveGalleryEvent($request, $token);