diff --git a/app/Actions/Activities/CreateActivityAction.php b/app/Actions/Activities/CreateActivityAction.php new file mode 100644 index 0000000..0fa8753 --- /dev/null +++ b/app/Actions/Activities/CreateActivityAction.php @@ -0,0 +1,26 @@ +user_id = $userId; + $activity->title = $data["title"]; + $activity->notes = $data["notes"] ?? ""; + $activity->duration_s = (int)$data["duration_s"]; + $activity->distance_m = (int)$data["distance_m"]; + $activity->activityType = $data["activityType"]; + + $activity->save(); + + return $activity; + } +} diff --git a/app/Actions/Activities/GetActivityPhotoAction.php b/app/Actions/Activities/GetActivityPhotoAction.php new file mode 100644 index 0000000..682fbdb --- /dev/null +++ b/app/Actions/Activities/GetActivityPhotoAction.php @@ -0,0 +1,21 @@ +exists($filename)) { + return Storage::disk("activityPhotos")->get($filename); + } + + return null; + } +} diff --git a/app/Actions/Activities/ListActivitiesAction.php b/app/Actions/Activities/ListActivitiesAction.php new file mode 100644 index 0000000..d0e74a1 --- /dev/null +++ b/app/Actions/Activities/ListActivitiesAction.php @@ -0,0 +1,19 @@ +where("user_id", $userId) + ->latest() + ->paginate(10); + } +} diff --git a/app/Actions/Avatars/GetDefaultAvatarAction.php b/app/Actions/Avatars/GetDefaultAvatarAction.php index 1b8f4a6..f776487 100644 --- a/app/Actions/Avatars/GetDefaultAvatarAction.php +++ b/app/Actions/Avatars/GetDefaultAvatarAction.php @@ -9,7 +9,7 @@ class GetDefaultAvatarAction { - public function execute(int $userId): string + public function execute(int $userId): ?string { $identicon = new Identicon(new SvgGenerator()); diff --git a/app/Enums/ActivityType.php b/app/Enums/ActivityType.php new file mode 100644 index 0000000..5c00a1b --- /dev/null +++ b/app/Enums/ActivityType.php @@ -0,0 +1,13 @@ +user(); + + $activities = $listActivitiesAction->execute($user->id); + + return ActivityResource::collection($activities); + } + + public function store(StoreActivityRequest $request, CreateActivityAction $createActivityAction): ActivityResource + { + $validated = $request->validated(); + $user = $request->user(); + $photo = $request->file("photo"); + + $activity = $createActivityAction->execute($user->id, $validated); + + $photo?->storeAs("", "activity_" . $activity->id . ".png", "activityPhotos"); + + return ActivityResource::make($activity); + } + + public function getPhoto(int $id, GetActivityPhotoAction $getActivityPhotoAction): Response + { + $photo = $getActivityPhotoAction->execute($id); + + if ($photo) { + return response($photo) + ->header("Content-Type", "image/png") + ->header("Cache-Control", "max-age=31536000, public"); + } + + return response()->noContent(); + } +} diff --git a/app/Http/Requests/StoreActivityRequest.php b/app/Http/Requests/StoreActivityRequest.php new file mode 100644 index 0000000..cea2e10 --- /dev/null +++ b/app/Http/Requests/StoreActivityRequest.php @@ -0,0 +1,38 @@ +check(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + "title" => ["required", "string", "max:255"], + "notes" => ["nullable", "string", "max:2048"], + "duration_s" => ["required", "integer", "min:1"], + "distance_m" => ["required", "integer", "min:1"], + "activityType" => ["required", new Enum(ActivityType::class)], + "photo" => ["required", "image", "mimes:png", "max:4096"], + ]; + } +} diff --git a/app/Http/Resources/ActivityResource.php b/app/Http/Resources/ActivityResource.php new file mode 100644 index 0000000..e5d1b66 --- /dev/null +++ b/app/Http/Resources/ActivityResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + "title" => $this->title, + "notes" => $this->notes, + "activity_type" => $this->activityType, + "duration_s" => $this->duration_s, + "distance_m" => $this->distance_m, + "photo" => $this->photo, + "created_at" => $this->created_at, + ]; + } +} diff --git a/app/Models/Activity.php b/app/Models/Activity.php new file mode 100644 index 0000000..9e85977 --- /dev/null +++ b/app/Models/Activity.php @@ -0,0 +1,51 @@ + "integer", + "distance_m" => "integer", + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + protected function photo(): Attribute + { + return Attribute::get(fn(): string => url("/api/activities/{$this->id}/photo")); + } +} diff --git a/config/filesystems.php b/config/filesystems.php index 04ffe80..05dc160 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -34,6 +34,12 @@ "url" => env("APP_URL") . "/storage/avatars", "visibility" => "public", ], + "activityPhotos" => [ + "driver" => "local", + "root" => storage_path("app/public/activityPhotos"), + "url" => env("APP_URL") . "/storage/activityPhotos", + "visibility" => "public", + ], ], "links" => [ public_path("storage") => storage_path("app/public"), diff --git a/database/migrations/2025_12_21_173736_create_activities_table.php b/database/migrations/2025_12_21_173736_create_activities_table.php new file mode 100644 index 0000000..91b0179 --- /dev/null +++ b/database/migrations/2025_12_21_173736_create_activities_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId("user_id")->constrained()->cascadeOnDelete(); + + $table->string("title"); + $table->text("notes"); + $table->integer("duration_s"); + $table->integer("distance_m"); + $table->enum("activityType", ["run", "ride", "walk", "other"]); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("activities"); + } +}; diff --git a/routes/api.php b/routes/api.php index a063c43..31cc5a7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -5,6 +5,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; +use Strava\Http\Controllers\ActivitiesController; use Strava\Http\Controllers\Auth\LoginController; use Strava\Http\Controllers\Auth\LogoutController; use Strava\Http\Controllers\Auth\PasswordController; @@ -17,6 +18,10 @@ Route::middleware(["auth:sanctum"])->group(function (): void { Route::post("/auth/logout", [LogoutController::class, "logout"])->name("logout"); + Route::post("/activities", [ActivitiesController::class, "store"])->name("activities.store"); + Route::get("/activities", [ActivitiesController::class, "index"])->name("activities.index"); + Route::get("/activities/{id}/photo", [ActivitiesController::class, "getPhoto"])->name("activities.store"); + Route::get("/profile", [ProfileController::class, "show"])->name("profile.show"); Route::patch("/profile", [ProfileController::class, "update"])->name("profile.update"); Route::post("/profile/avatar", [ProfileController::class, "changeAvatar"])->name("profile.avatar.update");