fixed like action, better dark mode, bottom navigation working, added taskcollection
This commit is contained in:
@@ -13,6 +13,14 @@ 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;
|
||||
@@ -46,13 +54,35 @@ class EventPublicController extends BaseController
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 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' => $event->name,
|
||||
'name' => $localizedName,
|
||||
'default_locale' => $event->default_locale,
|
||||
'created_at' => $event->created_at,
|
||||
'updated_at' => $event->updated_at,
|
||||
'type' => $eventTypeData,
|
||||
])->header('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
@@ -95,6 +125,139 @@ class EventPublicController extends BaseController
|
||||
->header('ETag', $etag);
|
||||
}
|
||||
|
||||
public function emotions(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);
|
||||
}
|
||||
|
||||
$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)->first(['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 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)->first(['id']);
|
||||
@@ -103,20 +266,46 @@ class EventPublicController extends BaseController
|
||||
}
|
||||
$eventId = $event->id;
|
||||
|
||||
$deviceId = (string) $request->header('X-Device-Id', 'anon');
|
||||
$filter = $request->query('filter');
|
||||
|
||||
$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')
|
||||
->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.created_at',
|
||||
'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('created_at', '>', $since);
|
||||
}
|
||||
$locale = request()->query('locale', 'de');
|
||||
|
||||
$rows = $query->get()->map(function ($r) {
|
||||
$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');
|
||||
@@ -124,7 +313,7 @@ class EventPublicController extends BaseController
|
||||
'data' => $rows,
|
||||
'latest_photo_at' => $latestPhotoAt,
|
||||
];
|
||||
$etag = sha1(json_encode([$since, $latestPhotoAt]));
|
||||
$etag = sha1(json_encode([$since, $filter, $deviceId, $latestPhotoAt]));
|
||||
$reqEtag = request()->headers->get('If-None-Match');
|
||||
if ($reqEtag && $reqEtag === $etag) {
|
||||
return response('', 304);
|
||||
@@ -138,14 +327,32 @@ class EventPublicController extends BaseController
|
||||
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)
|
||||
->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)
|
||||
->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 ?? ''));
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
@@ -211,6 +418,7 @@ class EventPublicController extends BaseController
|
||||
$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'],
|
||||
]);
|
||||
@@ -227,12 +435,14 @@ class EventPublicController extends BaseController
|
||||
|
||||
$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,
|
||||
|
||||
// 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(),
|
||||
@@ -245,4 +455,46 @@ class EventPublicController extends BaseController
|
||||
'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)
|
||||
}
|
||||
}
|
||||
|
||||
18
app/Http/Middleware/VerifyCsrfToken.php
Normal file
18
app/Http/Middleware/VerifyCsrfToken.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
'api/v1/photos/*/like',
|
||||
'api/v1/events/*/upload',
|
||||
];
|
||||
}
|
||||
67
app/Models/TaskCollection.php
Normal file
67
app/Models/TaskCollection.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class TaskCollection extends Model
|
||||
{
|
||||
protected $table = 'task_collections';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'name' => 'array',
|
||||
'description' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tasks in this collection
|
||||
*/
|
||||
public function tasks(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
Task::class,
|
||||
'task_collection_task',
|
||||
'task_collection_id',
|
||||
'task_id'
|
||||
)->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Events that use this collection
|
||||
*/
|
||||
public function events(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
Event::class,
|
||||
'event_task_collection',
|
||||
'task_collection_id',
|
||||
'event_id'
|
||||
)->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the localized name for the current locale
|
||||
*/
|
||||
public function getLocalizedNameAttribute(): string
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
return $this->name[$locale] ?? $this->name['de'] ?? 'Unnamed Collection';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the localized description for the current locale
|
||||
*/
|
||||
public function getLocalizedDescriptionAttribute(): ?string
|
||||
{
|
||||
if (!$this->description) return null;
|
||||
|
||||
$locale = app()->getLocale();
|
||||
return $this->description[$locale] ?? $this->description['de'] ?? null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user