722 lines
29 KiB
PHP
722 lines
29 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Routing\Controller as BaseController;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use App\Support\ImageHelper;
|
|
|
|
class EventPublicController extends BaseController
|
|
{
|
|
private function getLocalized($value, $locale, $default = '') {
|
|
if (is_string($value) && json_decode($value) !== null) {
|
|
$data = json_decode($value, true);
|
|
return $data[$locale] ?? $data['de'] ?? $default;
|
|
}
|
|
return $value ?: $default;
|
|
}
|
|
|
|
private function toPublicUrl(?string $path): ?string
|
|
{
|
|
if (! $path) return null;
|
|
// Already absolute URL
|
|
if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) return $path;
|
|
// Already a public storage URL
|
|
if (str_starts_with($path, '/storage/')) return $path;
|
|
if (str_starts_with($path, 'storage/')) return '/' . $path;
|
|
|
|
// Common relative paths stored in DB (e.g. 'photos/...', 'thumbnails/...', 'events/...')
|
|
if (str_starts_with($path, 'photos/') || str_starts_with($path, 'thumbnails/') || str_starts_with($path, 'events/')) {
|
|
return Storage::url($path);
|
|
}
|
|
|
|
// Absolute server paths pointing into storage/app/public (Linux/Windows)
|
|
$normalized = str_replace('\\', '/', $path);
|
|
$needle = '/storage/app/public/';
|
|
if (str_contains($normalized, $needle)) {
|
|
$rel = substr($normalized, strpos($normalized, $needle) + strlen($needle));
|
|
return '/storage/' . ltrim($rel, '/');
|
|
}
|
|
|
|
return $path; // fallback as-is
|
|
}
|
|
public function event(string $slug)
|
|
{
|
|
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first([
|
|
'id', 'slug', 'name', 'default_locale', 'created_at', 'updated_at'
|
|
]);
|
|
if (! $event) {
|
|
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
|
}
|
|
|
|
$locale = request()->query('locale', 'de');
|
|
$nameData = json_decode($event->name, true);
|
|
$localizedName = $nameData[$locale] ?? $nameData['de'] ?? $event->name;
|
|
|
|
// Get event type for icon
|
|
$eventType = DB::table('events')
|
|
->join('event_types', 'events.event_type_id', '=', 'event_types.id')
|
|
->where('events.id', $event->id)
|
|
->first(['event_types.slug as type_slug', 'event_types.name as type_name']);
|
|
|
|
$locale = request()->query('locale', 'de');
|
|
$eventTypeData = $eventType ? [
|
|
'slug' => $eventType->type_slug,
|
|
'name' => $this->getLocalized($eventType->type_name, $locale, 'Event'),
|
|
'icon' => $eventType->type_slug === 'wedding' ? '❤️' : '👥'
|
|
] : [
|
|
'slug' => 'general',
|
|
'name' => $this->getLocalized('Event', $locale, 'Event'),
|
|
'icon' => '👥'
|
|
];
|
|
|
|
return response()->json([
|
|
'id' => $event->id,
|
|
'slug' => $event->slug,
|
|
'name' => $localizedName,
|
|
'default_locale' => $event->default_locale,
|
|
'created_at' => $event->created_at,
|
|
'updated_at' => $event->updated_at,
|
|
'type' => $eventTypeData,
|
|
])->header('Cache-Control', 'no-store');
|
|
}
|
|
|
|
public function stats(string $slug)
|
|
{
|
|
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
|
if (! $event) {
|
|
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
|
}
|
|
|
|
$eventId = $event->id;
|
|
|
|
// Approximate online guests as distinct recent uploaders in last 10 minutes.
|
|
$tenMinutesAgo = CarbonImmutable::now()->subMinutes(10);
|
|
$onlineGuests = DB::table('photos')
|
|
->where('event_id', $eventId)
|
|
->where('created_at', '>=', $tenMinutesAgo)
|
|
->distinct('guest_name')
|
|
->count('guest_name');
|
|
|
|
// Tasks solved as number of photos linked to a task (proxy metric).
|
|
$tasksSolved = DB::table('photos')->where('event_id', $eventId)->whereNotNull('task_id')->count();
|
|
|
|
$latestPhotoAt = DB::table('photos')->where('event_id', $eventId)->max('created_at');
|
|
|
|
$payload = [
|
|
'online_guests' => $onlineGuests,
|
|
'tasks_solved' => $tasksSolved,
|
|
'latest_photo_at' => $latestPhotoAt,
|
|
];
|
|
|
|
$etag = sha1(json_encode($payload));
|
|
$reqEtag = request()->headers->get('If-None-Match');
|
|
if ($reqEtag && $reqEtag === $etag) {
|
|
return response('', 304);
|
|
}
|
|
|
|
return response()->json($payload)
|
|
->header('Cache-Control', 'no-store')
|
|
->header('ETag', $etag);
|
|
}
|
|
|
|
public function emotions(string $slug)
|
|
{
|
|
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
|
if (! $event) {
|
|
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
|
}
|
|
|
|
$rows = DB::table('emotions')
|
|
->join('emotion_event_type', 'emotions.id', '=', 'emotion_event_type.emotion_id')
|
|
->join('event_types', 'emotion_event_type.event_type_id', '=', 'event_types.id')
|
|
->join('events', 'events.event_type_id', '=', 'event_types.id')
|
|
->where('events.id', $event->id)
|
|
->select([
|
|
'emotions.id',
|
|
'emotions.name',
|
|
'emotions.icon as emoji',
|
|
'emotions.description'
|
|
])
|
|
->orderBy('emotions.sort_order')
|
|
->get();
|
|
|
|
$payload = $rows->map(function ($r) {
|
|
$locale = request()->query('locale', 'de');
|
|
$nameData = json_decode($r->name, true);
|
|
$name = $nameData[$locale] ?? $nameData['de'] ?? $r->name;
|
|
|
|
$descriptionData = json_decode($r->description, true);
|
|
$description = $descriptionData ? ($descriptionData[$locale] ?? $descriptionData['de'] ?? '') : '';
|
|
|
|
return [
|
|
'id' => (int) $r->id,
|
|
'slug' => 'emotion-' . $r->id, // Generate slug from ID
|
|
'name' => $name,
|
|
'emoji' => $r->emoji,
|
|
'description' => $description,
|
|
];
|
|
});
|
|
|
|
$etag = sha1($payload->toJson());
|
|
$reqEtag = request()->headers->get('If-None-Match');
|
|
if ($reqEtag && $reqEtag === $etag) {
|
|
return response('', 304);
|
|
}
|
|
|
|
return response()->json($payload)
|
|
->header('Cache-Control', 'public, max-age=300')
|
|
->header('ETag', $etag);
|
|
}
|
|
|
|
public function tasks(string $slug, Request $request)
|
|
{
|
|
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
|
if (! $event) {
|
|
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
|
}
|
|
|
|
$query = DB::table('tasks')
|
|
->join('task_collection_task', 'tasks.id', '=', 'task_collection_task.task_id')
|
|
->join('event_task_collection', 'task_collection_task.task_collection_id', '=', 'event_task_collection.task_collection_id')
|
|
->where('event_task_collection.event_id', $event->id)
|
|
->select([
|
|
'tasks.id',
|
|
'tasks.title',
|
|
'tasks.description',
|
|
'tasks.example_text as instructions',
|
|
'tasks.emotion_id',
|
|
'tasks.sort_order'
|
|
])
|
|
->orderBy('event_task_collection.sort_order')
|
|
->orderBy('tasks.sort_order')
|
|
->limit(20);
|
|
|
|
$rows = $query->get();
|
|
|
|
$payload = $rows->map(function ($r) {
|
|
// Handle JSON fields for multilingual content
|
|
$getLocalized = function ($field, $default = '') use ($r) {
|
|
$locale = request()->query('locale', 'de');
|
|
$value = $r->$field;
|
|
if (is_string($value) && json_decode($value) !== null) {
|
|
$data = json_decode($value, true);
|
|
return $data[$locale] ?? $data['de'] ?? $default;
|
|
}
|
|
return $value ?: $default;
|
|
};
|
|
|
|
// Get emotion info if emotion_id exists
|
|
$emotion = null;
|
|
if ($r->emotion_id) {
|
|
$emotionRow = DB::table('emotions')->where('id', $r->emotion_id)->first(['name']);
|
|
if ($emotionRow && isset($emotionRow->name)) {
|
|
$locale = request()->query('locale', 'de');
|
|
$value = $emotionRow->name;
|
|
if (is_string($value) && json_decode($value) !== null) {
|
|
$data = json_decode($value, true);
|
|
$emotionName = $data[$locale] ?? $data['de'] ?? 'Unbekannte Emotion';
|
|
} else {
|
|
$emotionName = $value ?: 'Unbekannte Emotion';
|
|
}
|
|
$emotion = [
|
|
'slug' => 'emotion-' . $r->emotion_id, // Generate slug from ID
|
|
'name' => $emotionName,
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'id' => (int) $r->id,
|
|
'title' => $getLocalized('title', 'Unbenannte Aufgabe'),
|
|
'description' => $getLocalized('description', ''),
|
|
'instructions' => $getLocalized('instructions', ''),
|
|
'duration' => 3, // Default 3 minutes (no duration field in DB)
|
|
'is_completed' => false, // Default false (no is_completed field in DB)
|
|
'emotion' => $emotion,
|
|
];
|
|
});
|
|
|
|
// If no tasks found, return empty array instead of 404
|
|
if ($payload->isEmpty()) {
|
|
$payload = collect([]);
|
|
}
|
|
|
|
$etag = sha1($payload->toJson());
|
|
$reqEtag = request()->headers->get('If-None-Match');
|
|
if ($reqEtag && $reqEtag === $etag) {
|
|
return response('', 304);
|
|
}
|
|
|
|
return response()->json($payload)
|
|
->header('Cache-Control', 'public, max-age=300')
|
|
->header('ETag', $etag);
|
|
}
|
|
|
|
public function photos(Request $request, string $slug)
|
|
{
|
|
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
|
if (! $event) {
|
|
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
|
}
|
|
$eventId = $event->id;
|
|
|
|
$deviceId = (string) $request->header('X-Device-Id', 'anon');
|
|
$filter = $request->query('filter');
|
|
|
|
$since = $request->query('since');
|
|
$query = DB::table('photos')
|
|
->leftJoin('tasks', 'photos.task_id', '=', 'tasks.id')
|
|
->select([
|
|
'photos.id',
|
|
'photos.file_path',
|
|
'photos.thumbnail_path',
|
|
'photos.likes_count',
|
|
'photos.emotion_id',
|
|
'photos.task_id',
|
|
'photos.guest_name',
|
|
'tasks.title as task_title'
|
|
])
|
|
->where('photos.event_id', $eventId)
|
|
->orderByDesc('photos.created_at')
|
|
->limit(60);
|
|
|
|
// MyPhotos filter
|
|
if ($filter === 'myphotos' && $deviceId !== 'anon') {
|
|
$query->where('guest_name', $deviceId);
|
|
}
|
|
|
|
if ($since) {
|
|
$query->where('photos.created_at', '>', $since);
|
|
}
|
|
$locale = request()->query('locale', 'de');
|
|
|
|
$rows = $query->get()->map(function ($r) use ($locale) {
|
|
$r->file_path = $this->toPublicUrl((string)($r->file_path ?? ''));
|
|
$r->thumbnail_path = $this->toPublicUrl((string)($r->thumbnail_path ?? ''));
|
|
|
|
// Localize task title if present
|
|
if ($r->task_title) {
|
|
$r->task_title = $this->getLocalized($r->task_title, $locale, 'Unbenannte Aufgabe');
|
|
}
|
|
|
|
return $r;
|
|
});
|
|
$latestPhotoAt = DB::table('photos')->where('event_id', $eventId)->max('created_at');
|
|
$payload = [
|
|
'data' => $rows,
|
|
'latest_photo_at' => $latestPhotoAt,
|
|
];
|
|
$etag = sha1(json_encode([$since, $filter, $deviceId, $latestPhotoAt]));
|
|
$reqEtag = request()->headers->get('If-None-Match');
|
|
if ($reqEtag && $reqEtag === $etag) {
|
|
return response('', 304);
|
|
}
|
|
return response()->json($payload)
|
|
->header('Cache-Control', 'no-store')
|
|
->header('ETag', $etag)
|
|
->header('Last-Modified', (string)$latestPhotoAt);
|
|
}
|
|
|
|
public function photo(int $id)
|
|
{
|
|
$row = DB::table('photos')
|
|
->join('events', 'photos.event_id', '=', 'events.id')
|
|
->leftJoin('tasks', 'photos.task_id', '=', 'tasks.id')
|
|
->select([
|
|
'photos.id',
|
|
'photos.event_id',
|
|
'photos.file_path',
|
|
'photos.thumbnail_path',
|
|
'photos.likes_count',
|
|
'photos.emotion_id',
|
|
'photos.task_id',
|
|
'photos.created_at',
|
|
'tasks.title as task_title'
|
|
])
|
|
->where('photos.id', $id)
|
|
->where('events.status', 'published')
|
|
->first();
|
|
if (! $row) {
|
|
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found or event not public']], 404);
|
|
}
|
|
$row->file_path = $this->toPublicUrl((string)($row->file_path ?? ''));
|
|
$row->thumbnail_path = $this->toPublicUrl((string)($row->thumbnail_path ?? ''));
|
|
|
|
// Localize task title if present
|
|
$locale = request()->query('locale', 'de');
|
|
if ($row->task_title) {
|
|
$row->task_title = $this->getLocalized($row->task_title, $locale, 'Unbenannte Aufgabe');
|
|
}
|
|
|
|
return response()->json($row)->header('Cache-Control', 'no-store');
|
|
}
|
|
|
|
public function like(Request $request, int $id)
|
|
{
|
|
$deviceId = (string) $request->header('X-Device-Id', 'anon');
|
|
$deviceId = substr(preg_replace('/[^a-zA-Z0-9_-]/', '', $deviceId), 0, 64);
|
|
if ($deviceId === '') {
|
|
$deviceId = 'anon';
|
|
}
|
|
|
|
$photo = DB::table('photos')
|
|
->join('events', 'photos.event_id', '=', 'events.id')
|
|
->where('photos.id', $id)
|
|
->where('events.status', 'published')
|
|
->first(['photos.id', 'photos.event_id']);
|
|
if (! $photo) {
|
|
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found or event not public']], 404);
|
|
}
|
|
|
|
// Idempotent like per device
|
|
$exists = DB::table('photo_likes')->where('photo_id', $id)->where('guest_name', $deviceId)->exists();
|
|
if ($exists) {
|
|
$count = (int) DB::table('photos')->where('id', $id)->value('likes_count');
|
|
return response()->json(['liked' => true, 'likes_count' => $count]);
|
|
}
|
|
|
|
DB::beginTransaction();
|
|
try {
|
|
DB::table('photo_likes')->insert([
|
|
'photo_id' => $id,
|
|
'guest_name' => $deviceId,
|
|
'ip_address' => 'device',
|
|
'created_at' => now(),
|
|
]);
|
|
DB::table('photos')->where('id', $id)->update([
|
|
'likes_count' => DB::raw('likes_count + 1'),
|
|
'updated_at' => now(),
|
|
]);
|
|
DB::commit();
|
|
} catch (\Throwable $e) {
|
|
DB::rollBack();
|
|
Log::warning('like failed', ['error' => $e->getMessage()]);
|
|
}
|
|
|
|
$count = (int) DB::table('photos')->where('id', $id)->value('likes_count');
|
|
|
|
return response()->json(['liked' => true, 'likes_count' => $count]);
|
|
}
|
|
|
|
public function upload(Request $request, string $slug)
|
|
{
|
|
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
|
if (! $event) {
|
|
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
|
}
|
|
|
|
$deviceId = (string) $request->header('X-Device-Id', 'anon');
|
|
$deviceId = substr(preg_replace('/[^a-zA-Z0-9_-]/', '', $deviceId), 0, 64) ?: 'anon';
|
|
|
|
// Per-device cap per event (MVP: 50)
|
|
$deviceCount = DB::table('photos')->where('event_id', $event->id)->where('guest_name', $deviceId)->count();
|
|
if ($deviceCount >= 50) {
|
|
return response()->json(['error' => ['code' => 'limit_reached', 'message' => 'Upload-Limit erreicht']], 429);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'photo' => ['required', 'image', 'max:6144'], // 6 MB
|
|
'emotion_id' => ['nullable', 'integer'],
|
|
'emotion_slug' => ['nullable', 'string'],
|
|
'task_id' => ['nullable', 'integer'],
|
|
'guest_name' => ['nullable', 'string', 'max:255'],
|
|
]);
|
|
|
|
$file = $validated['photo'];
|
|
$path = Storage::disk('public')->putFile("events/{$event->id}/photos", $file);
|
|
$url = Storage::url($path);
|
|
|
|
// Generate thumbnail (JPEG) under photos/thumbs
|
|
$baseName = pathinfo($path, PATHINFO_FILENAME);
|
|
$thumbRel = "events/{$event->id}/photos/thumbs/{$baseName}_thumb.jpg";
|
|
$thumbPath = ImageHelper::makeThumbnailOnDisk('public', $path, $thumbRel, 640, 82);
|
|
$thumbUrl = $thumbPath ? Storage::url($thumbPath) : $url;
|
|
|
|
$id = DB::table('photos')->insertGetId([
|
|
'event_id' => $event->id,
|
|
'task_id' => $validated['task_id'] ?? null,
|
|
'guest_name' => $validated['guest_name'] ?? $deviceId,
|
|
'file_path' => $url,
|
|
'thumbnail_path' => $thumbUrl,
|
|
'likes_count' => 0,
|
|
|
|
// Handle emotion_id: prefer explicit ID, fallback to slug lookup, then default
|
|
'emotion_id' => $this->resolveEmotionId($validated, $event->id),
|
|
'is_featured' => 0,
|
|
'metadata' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'id' => $id,
|
|
'file_path' => $url,
|
|
'thumbnail_path' => $thumbUrl,
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* Resolve emotion_id from validation data, supporting both direct ID and slug lookup
|
|
*/
|
|
private function resolveEmotionId(array $validated, int $eventId): int
|
|
{
|
|
// 1. Use explicit emotion_id if provided
|
|
if (isset($validated['emotion_id']) && $validated['emotion_id'] !== null) {
|
|
return (int) $validated['emotion_id'];
|
|
}
|
|
|
|
// 2. Resolve from emotion_slug if provided (format: 'emotion-{id}')
|
|
if (isset($validated['emotion_slug']) && $validated['emotion_slug']) {
|
|
$slug = $validated['emotion_slug'];
|
|
if (str_starts_with($slug, 'emotion-')) {
|
|
$id = (int) substr($slug, 8); // Remove 'emotion-' prefix
|
|
if ($id > 0) {
|
|
// Verify emotion exists and is available for this event
|
|
$exists = DB::table('emotions')
|
|
->join('emotion_event_type', 'emotions.id', '=', 'emotion_event_type.emotion_id')
|
|
->join('events', 'emotion_event_type.event_type_id', '=', 'events.event_type_id')
|
|
->where('emotions.id', $id)
|
|
->where('events.id', $eventId)
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
return $id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Fallback: Get first available emotion for this event type
|
|
$defaultEmotion = DB::table('emotions')
|
|
->join('emotion_event_type', 'emotions.id', '=', 'emotion_event_type.emotion_id')
|
|
->join('events', 'emotion_event_type.event_type_id', '=', 'events.event_type_id')
|
|
->where('events.id', $eventId)
|
|
->orderBy('emotions.sort_order')
|
|
->value('emotions.id');
|
|
|
|
return $defaultEmotion ?: 1; // Ultimate fallback to emotion ID 1 (assuming "Happy" exists)
|
|
}
|
|
public function achievements(Request $request, string $slug)
|
|
{
|
|
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
|
if (! $event) {
|
|
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
|
}
|
|
|
|
$eventId = $event->id;
|
|
$locale = $request->query('locale', 'de');
|
|
|
|
$totalPhotos = (int) DB::table('photos')->where('event_id', $eventId)->count();
|
|
$uniqueGuests = (int) DB::table('photos')->where('event_id', $eventId)->distinct('guest_name')->count('guest_name');
|
|
$tasksSolved = (int) DB::table('photos')->where('event_id', $eventId)->whereNotNull('task_id')->count();
|
|
$likesTotal = (int) DB::table('photos')->where('event_id', $eventId)->sum('likes_count');
|
|
|
|
$summary = [
|
|
'total_photos' => $totalPhotos,
|
|
'unique_guests' => $uniqueGuests,
|
|
'tasks_solved' => $tasksSolved,
|
|
'likes_total' => $likesTotal,
|
|
];
|
|
|
|
$guestNameParam = trim((string) $request->query('guest_name', ''));
|
|
$deviceIdHeader = (string) $request->headers->get('X-Device-Id', '');
|
|
$deviceId = substr(preg_replace('/[^A-Za-z0-9 _\-]/', '', $deviceIdHeader), 0, 120);
|
|
$candidate = $guestNameParam !== '' ? $guestNameParam : $deviceId;
|
|
$guestIdentifier = $candidate !== '' ? substr(preg_replace('/[^A-Za-z0-9 _\-]/', '', $candidate), 0, 120) : null;
|
|
|
|
$personal = null;
|
|
if ($guestIdentifier) {
|
|
$personalPhotos = (int) DB::table('photos')
|
|
->where('event_id', $eventId)
|
|
->where('guest_name', $guestIdentifier)
|
|
->count();
|
|
|
|
$personalTasks = (int) DB::table('photos')
|
|
->where('event_id', $eventId)
|
|
->where('guest_name', $guestIdentifier)
|
|
->whereNotNull('task_id')
|
|
->distinct('task_id')
|
|
->count('task_id');
|
|
|
|
$personalLikes = (int) DB::table('photos')
|
|
->where('event_id', $eventId)
|
|
->where('guest_name', $guestIdentifier)
|
|
->sum('likes_count');
|
|
|
|
$badgeBlueprints = [
|
|
['id' => 'first_photo', 'title' => 'Erstes Foto', 'description' => 'Lade dein erstes Foto hoch.', 'target' => 1, 'metric' => 'photos'],
|
|
['id' => 'five_photos', 'title' => 'Fotoflair', 'description' => 'Fuenf Fotos hochgeladen.', 'target' => 5, 'metric' => 'photos'],
|
|
['id' => 'first_task', 'title' => 'Aufgabenstarter', 'description' => 'Erste Aufgabe erfuellt.', 'target' => 1, 'metric' => 'tasks'],
|
|
['id' => 'task_master', 'title' => 'Aufgabenmeister', 'description' => 'Fuenf Aufgaben erfuellt.', 'target' => 5, 'metric' => 'tasks'],
|
|
['id' => 'crowd_pleaser', 'title' => 'Publikumsliebling', 'description' => 'Sammle zwanzig Likes.', 'target' => 20, 'metric' => 'likes'],
|
|
];
|
|
|
|
$personalBadges = [];
|
|
foreach ($badgeBlueprints as $badge) {
|
|
$value = 0;
|
|
if ($badge['metric'] === 'photos') {
|
|
$value = $personalPhotos;
|
|
} elseif ($badge['metric'] === 'tasks') {
|
|
$value = $personalTasks;
|
|
} else {
|
|
$value = $personalLikes;
|
|
}
|
|
|
|
$personalBadges[] = [
|
|
'id' => $badge['id'],
|
|
'title' => $badge['title'],
|
|
'description' => $badge['description'],
|
|
'earned' => $value >= $badge['target'],
|
|
'progress' => $value,
|
|
'target' => $badge['target'],
|
|
];
|
|
}
|
|
|
|
$personal = [
|
|
'guest_name' => $guestIdentifier,
|
|
'photos' => $personalPhotos,
|
|
'tasks' => $personalTasks,
|
|
'likes' => $personalLikes,
|
|
'badges' => $personalBadges,
|
|
];
|
|
}
|
|
|
|
$topUploads = DB::table('photos')
|
|
->select('guest_name', DB::raw('COUNT(*) as total'), DB::raw('SUM(likes_count) as likes'))
|
|
->where('event_id', $eventId)
|
|
->groupBy('guest_name')
|
|
->orderByDesc('total')
|
|
->limit(5)
|
|
->get()
|
|
->map(fn ($row) => [
|
|
'guest' => $row->guest_name,
|
|
'photos' => (int) $row->total,
|
|
'likes' => (int) $row->likes,
|
|
])
|
|
->values();
|
|
|
|
$topLikes = DB::table('photos')
|
|
->select('guest_name', DB::raw('SUM(likes_count) as likes'), DB::raw('COUNT(*) as total'))
|
|
->where('event_id', $eventId)
|
|
->groupBy('guest_name')
|
|
->orderByDesc('likes')
|
|
->limit(5)
|
|
->get()
|
|
->map(fn ($row) => [
|
|
'guest' => $row->guest_name,
|
|
'likes' => (int) $row->likes,
|
|
'photos' => (int) $row->total,
|
|
])
|
|
->values();
|
|
|
|
$topPhotoRow = DB::table('photos')
|
|
->leftJoin('tasks', 'photos.task_id', '=', 'tasks.id')
|
|
->where('photos.event_id', $eventId)
|
|
->orderByDesc('photos.likes_count')
|
|
->orderByDesc('photos.created_at')
|
|
->select([
|
|
'photos.id',
|
|
'photos.guest_name',
|
|
'photos.likes_count',
|
|
'photos.file_path',
|
|
'photos.thumbnail_path',
|
|
'photos.created_at',
|
|
'tasks.title as task_title',
|
|
])
|
|
->first();
|
|
|
|
$topPhoto = $topPhotoRow ? [
|
|
'photo_id' => (int) $topPhotoRow->id,
|
|
'guest' => $topPhotoRow->guest_name,
|
|
'likes' => (int) $topPhotoRow->likes_count,
|
|
'task' => $topPhotoRow->task_title,
|
|
'created_at' => $topPhotoRow->created_at,
|
|
'thumbnail' => $this->toPublicUrl($topPhotoRow->thumbnail_path ?: $topPhotoRow->file_path),
|
|
] : null;
|
|
|
|
$trendingEmotionRow = DB::table('photos')
|
|
->join('emotions', 'photos.emotion_id', '=', 'emotions.id')
|
|
->where('photos.event_id', $eventId)
|
|
->select('emotions.id', 'emotions.name', DB::raw('COUNT(*) as total'))
|
|
->groupBy('emotions.id', 'emotions.name')
|
|
->orderByDesc('total')
|
|
->first();
|
|
|
|
$trendingEmotion = $trendingEmotionRow ? [
|
|
'emotion_id' => (int) $trendingEmotionRow->id,
|
|
'name' => $this->getLocalized($trendingEmotionRow->name, $locale, $trendingEmotionRow->name),
|
|
'count' => (int) $trendingEmotionRow->total,
|
|
] : null;
|
|
|
|
$timeline = DB::table('photos')
|
|
->where('event_id', $eventId)
|
|
->selectRaw('DATE(created_at) as day, COUNT(*) as photos, COUNT(DISTINCT guest_name) as guests')
|
|
->groupBy('day')
|
|
->orderBy('day')
|
|
->limit(14)
|
|
->get()
|
|
->map(fn ($row) => [
|
|
'date' => $row->day,
|
|
'photos' => (int) $row->photos,
|
|
'guests' => (int) $row->guests,
|
|
])
|
|
->values();
|
|
|
|
$feed = DB::table('photos')
|
|
->leftJoin('tasks', 'photos.task_id', '=', 'tasks.id')
|
|
->where('photos.event_id', $eventId)
|
|
->orderByDesc('photos.created_at')
|
|
->limit(12)
|
|
->get([
|
|
'photos.id',
|
|
'photos.guest_name',
|
|
'photos.thumbnail_path',
|
|
'photos.file_path',
|
|
'photos.likes_count',
|
|
'photos.created_at',
|
|
'tasks.title as task_title',
|
|
])
|
|
->map(fn ($row) => [
|
|
'photo_id' => (int) $row->id,
|
|
'guest' => $row->guest_name,
|
|
'task' => $row->task_title,
|
|
'likes' => (int) $row->likes_count,
|
|
'created_at' => $row->created_at,
|
|
'thumbnail' => $this->toPublicUrl($row->thumbnail_path ?: $row->file_path),
|
|
])
|
|
->values();
|
|
|
|
$payload = [
|
|
'summary' => $summary,
|
|
'personal' => $personal,
|
|
'leaderboards' => [
|
|
'uploads' => $topUploads,
|
|
'likes' => $topLikes,
|
|
],
|
|
'highlights' => [
|
|
'top_photo' => $topPhoto,
|
|
'trending_emotion' => $trendingEmotion,
|
|
'timeline' => $timeline,
|
|
],
|
|
'feed' => $feed,
|
|
];
|
|
|
|
$etag = sha1(json_encode($payload));
|
|
$ifNoneMatch = $request->headers->get('If-None-Match');
|
|
if ($ifNoneMatch && $ifNoneMatch === $etag) {
|
|
return response('', 304);
|
|
}
|
|
|
|
return response()->json($payload)
|
|
->header('Cache-Control', 'no-store')
|
|
->header('ETag', $etag);
|
|
}
|
|
}
|
|
|