where('slug', $slug)->first([ 'id', 'slug', 'name', 'default_locale', 'created_at', 'updated_at' ]); if (! $event) { return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 404); } return response()->json([ 'id' => $event->id, 'slug' => $event->slug, 'name' => $event->name, 'default_locale' => $event->default_locale, 'created_at' => $event->created_at, 'updated_at' => $event->updated_at, ])->header('Cache-Control', 'no-store'); } public function stats(string $slug) { $event = DB::table('events')->where('slug', $slug)->first(['id']); if (! $event) { return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 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 photos(Request $request, string $slug) { $event = DB::table('events')->where('slug', $slug)->first(['id']); if (! $event) { return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 404); } $eventId = $event->id; $since = $request->query('since'); $query = DB::table('photos') ->select(['id', 'file_path', 'thumbnail_path', 'likes_count', 'emotion_id', 'task_id', 'created_at']) ->where('event_id', $eventId) ->orderByDesc('created_at') ->limit(60); if ($since) { $query->where('created_at', '>', $since); } $rows = $query->get()->map(function ($r) { $r->file_path = $this->toPublicUrl((string)($r->file_path ?? '')); $r->thumbnail_path = $this->toPublicUrl((string)($r->thumbnail_path ?? '')); 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, $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') ->select(['id', 'event_id', 'file_path', 'thumbnail_path', 'likes_count', 'emotion_id', 'task_id', 'created_at']) ->where('id', $id) ->first(); if (! $row) { return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found']], 404); } $row->file_path = $this->toPublicUrl((string)($row->file_path ?? '')); $row->thumbnail_path = $this->toPublicUrl((string)($row->thumbnail_path ?? '')); 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')->where('id', $id)->first(['id', 'event_id']); if (! $photo) { return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found']], 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)->first(['id']); if (! $event) { return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 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'], '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, 'emotion_id' => $validated['emotion_id'] ?? null, 'task_id' => $validated['task_id'] ?? null, 'guest_name' => $validated['guest_name'] ?? $deviceId, 'file_path' => $url, 'thumbnail_path' => $thumbUrl, 'likes_count' => 0, 'is_featured' => 0, 'metadata' => null, 'created_at' => now(), 'updated_at' => now(), ]); return response()->json([ 'id' => $id, 'file_path' => $url, 'thumbnail_path' => $thumbUrl, ], 201); } }