From 6009c2761a4fb613e4de43c35a1cee055f93313d Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 23 Nov 2025 21:18:14 +0100 Subject: [PATCH 01/25] add api for profiles --- app/Actions/Profile/UpdateProfileAction.php | 18 +++++++++ app/Enums/Gender.php | 11 +++++ .../Controllers/Profile/ProfileController.php | 33 +++++++++++++++ .../Profile/ProfilesController.php | 40 +++++++++++++++++++ app/Http/Requests/UpdateProfileRequest.php | 29 ++++++++++++++ app/Http/Resources/UserResource.php | 29 ++++++++++++++ app/Models/User.php | 13 ++++++ .../0001_01_01_000000_create_users_table.php | 6 +++ routes/api.php | 8 ++++ 9 files changed, 187 insertions(+) create mode 100644 app/Actions/Profile/UpdateProfileAction.php create mode 100644 app/Enums/Gender.php create mode 100644 app/Http/Controllers/Profile/ProfileController.php create mode 100644 app/Http/Controllers/Profile/ProfilesController.php create mode 100644 app/Http/Requests/UpdateProfileRequest.php create mode 100644 app/Http/Resources/UserResource.php diff --git a/app/Actions/Profile/UpdateProfileAction.php b/app/Actions/Profile/UpdateProfileAction.php new file mode 100644 index 0000000..a36265c --- /dev/null +++ b/app/Actions/Profile/UpdateProfileAction.php @@ -0,0 +1,18 @@ +fill($data); + $user->save(); + + return $user->fresh(); + } +} diff --git a/app/Enums/Gender.php b/app/Enums/Gender.php new file mode 100644 index 0000000..fd1dc4a --- /dev/null +++ b/app/Enums/Gender.php @@ -0,0 +1,11 @@ +validated(); + + $user = request()->user(); + $updated = $updateProfileAction->execute($user, $validated); + + return response()->json(UserResource::make($updated), Response::HTTP_OK); + } + + public function show(): JsonResponse + { + $user = request()->user(); + return response()->json(UserResource::make($user), Response::HTTP_OK); + } +} diff --git a/app/Http/Controllers/Profile/ProfilesController.php b/app/Http/Controllers/Profile/ProfilesController.php new file mode 100644 index 0000000..a3c2a83 --- /dev/null +++ b/app/Http/Controllers/Profile/ProfilesController.php @@ -0,0 +1,40 @@ +select([ + 'id', + 'name', + 'first_name', + 'last_name', + 'birth_date', + 'height', + 'weight', + 'gender', + ])->paginate(10); + + return UserResource::collection($users); + } + public function show(int $id): JsonResponse + { + $user = User::query()->find($id); + + if($user === null) { + return response()->json([], Response::HTTP_NOT_FOUND); + } + + return response()->json(UserResource::make($user), Response::HTTP_OK); + } +} diff --git a/app/Http/Requests/UpdateProfileRequest.php b/app/Http/Requests/UpdateProfileRequest.php new file mode 100644 index 0000000..6d098a9 --- /dev/null +++ b/app/Http/Requests/UpdateProfileRequest.php @@ -0,0 +1,29 @@ +check(); + } + + public function rules(): array + { + return [ + "first_name" => ["nullable", "string", "max:255"], + "last_name" => ["nullable", "string", "max:255"], + "birth_date" => ["nullable", "date", "before:today"], + "height" => ["nullable", "integer", "min:50", "max:300"], + "weight" => ["nullable", "numeric", "min:10", "max:300"], + "gender" => ["nullable", new Enum(Gender::class)], + ]; + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php new file mode 100644 index 0000000..d15e3d2 --- /dev/null +++ b/app/Http/Resources/UserResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + "id" => $this->id, + "email" => $this->email, + "name" => $this->name, + "first_name" => $this->first_name, + "last_name" => $this->last_name, + "birth_date" => $this->birth_date, + "height" => $this->height, + "weight" => $this->weight, + "created_at" => $this->created_at, + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 03125e6..001e7a2 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,12 +9,19 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; +use Strava\Enums\Gender; /** * @property int $id * @property string $name + * @property string|null $first_name + * @property string|null $last_name * @property string $email * @property string $password + * @property Carbon $birth_date + * @property int|null $height + * @property string|null $weight + * @property Gender $gender * @property Carbon $email_verified_at * @property Carbon $created_at * @property Carbon $updated_at @@ -29,6 +36,12 @@ class User extends Authenticatable "name", "email", "password", + "first_name", + "last_name", + "birth_date", + "height", + "weight", + "gender", ]; protected $hidden = [ "password", diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 7c9f45d..3e5c7b8 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -12,9 +12,15 @@ public function up(): void Schema::create("users", function (Blueprint $table): void { $table->id(); $table->string("name"); + $table->string("first_name")->nullable(); + $table->string("last_name")->nullable(); $table->string("email")->unique(); $table->timestamp("email_verified_at")->nullable(); $table->string("password"); + $table->date("birth_date")->nullable(); + $table->unsignedSmallInteger("height")->nullable(); + $table->decimal("weight")->nullable(); + $table->enum("gender", ["male", "female"])->default("male"); $table->rememberToken(); $table->timestamps(); }); diff --git a/routes/api.php b/routes/api.php index 55af1e1..97b2a53 100644 --- a/routes/api.php +++ b/routes/api.php @@ -9,12 +9,17 @@ use Strava\Http\Controllers\Auth\LogoutController; use Strava\Http\Controllers\Auth\PasswordController; use Strava\Http\Controllers\Auth\RegisterController; +use Strava\Http\Controllers\Profile\ProfileController; +use Strava\Http\Controllers\Profile\ProfilesController; Route::middleware("auth:sanctum")->get("/user", fn(Request $request): JsonResponse => new JsonResponse($request->user())); Route::middleware(["auth:sanctum"])->group(function (): void { Route::post("/auth/logout", [LogoutController::class, "logout"])->name("logout"); + Route::get("/profile", [ProfileController::class, "show"])->name("profile.show"); + Route::patch("/profile", [ProfileController::class, "update"])->name("profile.update"); + Route::post("/user/change-password", [PasswordController::class, "changePassword"])->name("change-password"); }); @@ -22,3 +27,6 @@ Route::post("/auth/register", [RegisterController::class, "register"])->name("register"); Route::post("/auth/forgot-password", [PasswordController::class, "sendResetEmail"])->name("forgot-password"); Route::post("/auth/reset-password", [PasswordController::class, "resetPassword"])->name("reset-password"); + +Route::get("/profiles", [ProfilesController::class, "index"])->name("profiles.index"); +Route::get("/profiles/{id}", [ProfilesController::class, "show"])->name("profiles.show"); From 152cc3be891977e68ebe539778091791cb06f377 Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 14 Dec 2025 22:53:32 +0100 Subject: [PATCH 02/25] Add avatars --- Taskfile.yml | 18 +- app/Actions/Avatars/ChangeAvatarAction.php | 17 ++ app/Actions/Avatars/DeleteAvatarAction.php | 23 ++ app/Actions/Avatars/GetAvatarAction.php | 21 ++ .../Avatars/GetDefaultAvatarAction.php | 18 ++ app/Helpers/IdenticonHelper.php | 39 +++ .../Controllers/Profile/ProfileController.php | 60 +++- .../Profile/ProfilesController.php | 22 +- app/Http/Requests/ChangeAvatarRequest.php | 31 ++ app/Http/Resources/UserResource.php | 3 + app/Models/User.php | 9 + composer.json | 4 +- composer.lock | 287 +++++++++++++++++- config/filesystems.php | 6 + database/factories/UserFactory.php | 3 +- routes/api.php | 3 + tests/Feature/ProfileTest.php | 194 ++++++++++++ 17 files changed, 734 insertions(+), 24 deletions(-) create mode 100644 app/Actions/Avatars/ChangeAvatarAction.php create mode 100644 app/Actions/Avatars/DeleteAvatarAction.php create mode 100644 app/Actions/Avatars/GetAvatarAction.php create mode 100644 app/Actions/Avatars/GetDefaultAvatarAction.php create mode 100644 app/Helpers/IdenticonHelper.php create mode 100644 app/Http/Requests/ChangeAvatarRequest.php create mode 100644 tests/Feature/ProfileTest.php diff --git a/Taskfile.yml b/Taskfile.yml index db677d0..c6433d8 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -31,6 +31,7 @@ tasks: - task: _set-app-key - cmd: "{{.DOCKER_EXEC_USER}} npm install" - cmd: "{{.DOCKER_EXEC_USER}} php artisan migrate" + - task: create-test-db frp:init: desc: "Add default FRP domains (api, app, mail)" @@ -110,9 +111,9 @@ tasks: - cmd: "{{.DOCKER_EXEC_USER}} php artisan {{.CLI_ARGS}}" test: - desc: "Run pest tests" + desc: "Run tests" cmds: - - cmd: "{{.DOCKER_EXEC_USER}} vendor/bin/pest {{.CLI_ARGS}}" + - cmd: "{{.DOCKER_EXEC_USER}} composer test" fix: desc: "Run fixers" @@ -135,3 +136,16 @@ tasks: echo "APP_KEY is not set. Creating:" docker compose exec {{.DOCKER_COMPOSE_APP_CONTAINER}} php artisan key:generate fi + + create-test-db: + desc: "Create Postgres test database inside db container (if not exists)" + vars: + DOCKER_COMPOSE_DATABASE_CONTAINER: database + DATABASE_USERNAME: '{{default .DB_USERNAME "MiniStravaAPI"}}' + TEST_DATABASE_NAME: '{{default .TEST_DB_DATABASE "MiniStravaAPI-test"}}' + cmds: + - | + docker compose exec {{.DOCKER_COMPOSE_DATABASE_CONTAINER}} \ + bash -lc 'createdb --username={{.DATABASE_USERNAME}} {{.TEST_DATABASE_NAME}} &> /dev/null \ + && echo "Created database for tests ({{.TEST_DATABASE_NAME}})." \ + || echo "Database for tests ({{.TEST_DATABASE_NAME}}) exists."' diff --git a/app/Actions/Avatars/ChangeAvatarAction.php b/app/Actions/Avatars/ChangeAvatarAction.php new file mode 100644 index 0000000..c4fe0e6 --- /dev/null +++ b/app/Actions/Avatars/ChangeAvatarAction.php @@ -0,0 +1,17 @@ +storeAs("", $userId . ".png", "avatars"); + + return true; + } +} diff --git a/app/Actions/Avatars/DeleteAvatarAction.php b/app/Actions/Avatars/DeleteAvatarAction.php new file mode 100644 index 0000000..8d478b9 --- /dev/null +++ b/app/Actions/Avatars/DeleteAvatarAction.php @@ -0,0 +1,23 @@ +exists($filename)) { + Storage::disk("avatars")->delete($filename); + + return true; + } + + return false; + } +} diff --git a/app/Actions/Avatars/GetAvatarAction.php b/app/Actions/Avatars/GetAvatarAction.php new file mode 100644 index 0000000..cf9fb13 --- /dev/null +++ b/app/Actions/Avatars/GetAvatarAction.php @@ -0,0 +1,21 @@ +exists($filename)) { + return Storage::disk("avatars")->get($filename); + } + + return null; + } +} diff --git a/app/Actions/Avatars/GetDefaultAvatarAction.php b/app/Actions/Avatars/GetDefaultAvatarAction.php new file mode 100644 index 0000000..f776487 --- /dev/null +++ b/app/Actions/Avatars/GetDefaultAvatarAction.php @@ -0,0 +1,18 @@ +getImageData((string)$userId, 300); + } +} diff --git a/app/Helpers/IdenticonHelper.php b/app/Helpers/IdenticonHelper.php new file mode 100644 index 0000000..b16c182 --- /dev/null +++ b/app/Helpers/IdenticonHelper.php @@ -0,0 +1,39 @@ +identicon = new Identicon(); + } + + public static function url(string|int $name): string + { + return url("/api/profiles/{$name}/avatar"); + } + + public function create(string|int $filename, string $data): string + { + $image = $this->identicon->getImageData($data, 300); + + return $this->save($filename, $image); + } + + private function save(string|int $name, string $imageData): string + { + $path = $name . ".png"; + + Storage::disk("avatars")->put($path, $imageData); + + return Storage::disk("avatars")->url($path); + } +} diff --git a/app/Http/Controllers/Profile/ProfileController.php b/app/Http/Controllers/Profile/ProfileController.php index e41a039..67bcd3d 100644 --- a/app/Http/Controllers/Profile/ProfileController.php +++ b/app/Http/Controllers/Profile/ProfileController.php @@ -4,30 +4,72 @@ namespace Strava\Http\Controllers\Profile; -use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Http\Response; +use Strava\Actions\Avatars\ChangeAvatarAction; +use Strava\Actions\Avatars\DeleteAvatarAction; +use Strava\Actions\Avatars\GetAvatarAction; +use Strava\Actions\Avatars\GetDefaultAvatarAction; use Strava\Actions\Profile\UpdateProfileAction; use Strava\Http\Controllers\Controller; +use Strava\Http\Requests\ChangeAvatarRequest; use Strava\Http\Requests\UpdateProfileRequest; use Strava\Http\Resources\UserResource; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Profiler\Profile; class ProfileController extends Controller { - public function update(UpdateProfileRequest $request, UpdateProfileAction $updateProfileAction): JsonResponse + public function update(UpdateProfileRequest $request, UpdateProfileAction $updateProfileAction): UserResource { $validated = $request->validated(); - $user = request()->user(); + $user = $request->user(); $updated = $updateProfileAction->execute($user, $validated); - return response()->json(UserResource::make($updated), Response::HTTP_OK); + return UserResource::make($updated); } - public function show(): JsonResponse + public function show(Request $request): UserResource { - $user = request()->user(); - return response()->json(UserResource::make($user), Response::HTTP_OK); + $user = $request->user(); + + return UserResource::make($user); + } + + public function changeAvatar(ChangeAvatarRequest $request, ChangeAvatarAction $changeAvatarAction): UserResource + { + $user = $request->user(); + + $changeAvatarAction->execute( + $request->file("avatar"), + $user->id, + ); + + return UserResource::make($user); + } + + public function getAvatar(int $userId, GetAvatarAction $getAvatarAction, GetDefaultAvatarAction $getDefaultAvatarAction): Response + { + $avatar = $getAvatarAction->execute($userId); + + if ($avatar) { + return response($avatar) + ->header("Content-Type", "image/png") + ->header("Cache-Control", "public, max-age=31536000"); + } + + $defaultAvatar = $getDefaultAvatarAction->execute($userId); + + return response($defaultAvatar) + ->header("Content-Type", "image/svg+xml") + ->header("Cache-Control", "public, max-age=86400"); + } + + public function deleteAvatar(Request $request, DeleteAvatarAction $deleteProfileAction): UserResource + { + $user = $request->user(); + + $deleteProfileAction->execute($user->id); + + return UserResource::make($user); } } diff --git a/app/Http/Controllers/Profile/ProfilesController.php b/app/Http/Controllers/Profile/ProfilesController.php index a3c2a83..5c6dd1c 100644 --- a/app/Http/Controllers/Profile/ProfilesController.php +++ b/app/Http/Controllers/Profile/ProfilesController.php @@ -1,9 +1,10 @@ select([ - 'id', - 'name', - 'first_name', - 'last_name', - 'birth_date', - 'height', - 'weight', - 'gender', + "id", + "name", + "first_name", + "last_name", + "birth_date", + "height", + "weight", + "gender", ])->paginate(10); return UserResource::collection($users); } + public function show(int $id): JsonResponse { $user = User::query()->find($id); - if($user === null) { + if ($user === null) { return response()->json([], Response::HTTP_NOT_FOUND); } diff --git a/app/Http/Requests/ChangeAvatarRequest.php b/app/Http/Requests/ChangeAvatarRequest.php new file mode 100644 index 0000000..009f84d --- /dev/null +++ b/app/Http/Requests/ChangeAvatarRequest.php @@ -0,0 +1,31 @@ +check(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + "avatar" => ["required", "image", "mimes:png", "max:2048"], // max 2MB + ]; + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index d15e3d2..1170fb4 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -1,5 +1,7 @@ $this->birth_date, "height" => $this->height, "weight" => $this->weight, + "avatar" => $this->avatar, "created_at" => $this->created_at, ]; } diff --git a/app/Models/User.php b/app/Models/User.php index 001e7a2..3d0e8c6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,11 +5,13 @@ namespace Strava\Models; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; use Strava\Enums\Gender; +use Strava\Helpers\IdenticonHelper; /** * @property int $id @@ -22,6 +24,7 @@ * @property int|null $height * @property string|null $weight * @property Gender $gender + * @property string $avatar * @property Carbon $email_verified_at * @property Carbon $created_at * @property Carbon $updated_at @@ -53,6 +56,12 @@ protected function casts(): array return [ "email_verified_at" => "datetime", "password" => "hashed", + "gender" => Gender::class, ]; } + + protected function avatar(): Attribute + { + return Attribute::get(fn(): string => IdenticonHelper::url($this->id)); + } } diff --git a/composer.json b/composer.json index 4396298..7e1e087 100644 --- a/composer.json +++ b/composer.json @@ -9,9 +9,11 @@ "ext-pdo": "*", "guzzlehttp/guzzle": "^7.9.3", "inertiajs/inertia-laravel": "^2.0.3", + "intervention/image-laravel": "^1.5", "laravel/framework": "^12.20.0", "laravel/sanctum": "^4.1.2", - "laravel/tinker": "^2.10.1" + "laravel/tinker": "^2.10.1", + "yzalis/identicon": "^2.0" }, "require-dev": { "blumilksoftware/codestyle": "^v5.0.0", diff --git a/composer.lock b/composer.lock index fc642e8..0e9624a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "752c7ea01b88d876022bd9e620bea0b8", + "content-hash": "2f1a23fce8047cced39633cffc64fbce", "packages": [ { "name": "brick/math", @@ -1123,6 +1123,234 @@ }, "time": "2025-09-28T21:21:36+00:00" }, + { + "name": "intervention/gif", + "version": "4.2.2", + "source": { + "type": "git", + "url": "https://github.com/Intervention/gif.git", + "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/gif/zipball/5999eac6a39aa760fb803bc809e8909ee67b451a", + "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Gif\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Native PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], + "support": { + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/4.2.2" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2025-03-29T07:46:21+00:00" + }, + { + "name": "intervention/image", + "version": "3.11.4", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "8c49eb21a6d2572532d1bc425964264f3e496846" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/8c49eb21a6d2572532d1bc425964264f3e496846", + "reference": "8c49eb21a6d2572532d1bc425964264f3e496846", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "intervention/gif": "^4.2", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "PHP image manipulation", + "homepage": "https://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/3.11.4" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2025-07-30T13:13:19+00:00" + }, + { + "name": "intervention/image-laravel", + "version": "1.5.6", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image-laravel.git", + "reference": "056029431400a5cc56036172787a578f334683c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image-laravel/zipball/056029431400a5cc56036172787a578f334683c4", + "reference": "056029431400a5cc56036172787a578f334683c4", + "shasum": "" + }, + "require": { + "illuminate/http": "^8|^9|^10|^11|^12", + "illuminate/routing": "^8|^9|^10|^11|^12", + "illuminate/support": "^8|^9|^10|^11|^12", + "intervention/image": "^3.11", + "php": "^8.1" + }, + "require-dev": { + "ext-fileinfo": "*", + "orchestra/testbench": "^8.18 || ^9.9", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Image": "Intervention\\Image\\Laravel\\Facades\\Image" + }, + "providers": [ + "Intervention\\Image\\Laravel\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Intervention\\Image\\Laravel\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Laravel Integration of Intervention Image", + "homepage": "https://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "laravel", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image-laravel/issues", + "source": "https://github.com/Intervention/image-laravel/tree/1.5.6" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2025-04-04T15:09:55+00:00" + }, { "name": "laravel/framework", "version": "v12.33.0", @@ -6157,6 +6385,63 @@ "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "yzalis/identicon", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/yzalis/Identicon.git", + "reference": "ff5ed090129cab9bfa2a322857d4a01d107aa0ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yzalis/Identicon/zipball/ff5ed090129cab9bfa2a322857d4a01d107aa0ae", + "reference": "ff5ed090129cab9bfa2a322857d4a01d107aa0ae", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "ext-imagick": "*", + "fzaninotto/faker": "^1.2.0", + "phpunit/phpunit": "^4.0 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Identicon\\": "src/Identicon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Laugueux", + "email": "benjamin@yzalis.com" + } + ], + "description": "Create awesome unique avatar.", + "homepage": "http://identicon-php.org", + "keywords": [ + "avatar", + "identicon", + "image" + ], + "support": { + "issues": "https://github.com/yzalis/Identicon/issues", + "source": "https://github.com/yzalis/Identicon/tree/master" + }, + "abandoned": true, + "time": "2019-10-14T09:30:57+00:00" } ], "packages-dev": [ diff --git a/config/filesystems.php b/config/filesystems.php index b8b1b78..04ffe80 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -28,6 +28,12 @@ "use_path_style_endpoint" => env("AWS_USE_PATH_STYLE_ENDPOINT", false), "throw" => false, ], + "avatars" => [ + "driver" => "local", + "root" => storage_path("app/public/avatars"), + "url" => env("APP_URL") . "/storage/avatars", + "visibility" => "public", + ], ], "links" => [ public_path("storage") => storage_path("app/public"), diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index f6931ba..38f044f 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -5,6 +5,7 @@ namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Strava\Models\User; @@ -19,7 +20,7 @@ public function definition(): array "name" => fake()->name(), "email" => fake()->unique()->safeEmail(), "email_verified_at" => now(), - "password" => "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", + "password" => Hash::make("password"), "remember_token" => Str::random(10), ]; } diff --git a/routes/api.php b/routes/api.php index 97b2a53..a063c43 100644 --- a/routes/api.php +++ b/routes/api.php @@ -19,6 +19,8 @@ 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"); + Route::delete("/profile/avatar", [ProfileController::class, "deleteAvatar"])->name("profile.avatar.delete"); Route::post("/user/change-password", [PasswordController::class, "changePassword"])->name("change-password"); }); @@ -30,3 +32,4 @@ Route::get("/profiles", [ProfilesController::class, "index"])->name("profiles.index"); Route::get("/profiles/{id}", [ProfilesController::class, "show"])->name("profiles.show"); +Route::get("/profiles/{id}/avatar", [ProfileController::class, "getAvatar"])->name("avatar.show"); diff --git a/tests/Feature/ProfileTest.php b/tests/Feature/ProfileTest.php new file mode 100644 index 0000000..8ae4fac --- /dev/null +++ b/tests/Feature/ProfileTest.php @@ -0,0 +1,194 @@ +create([ + "name" => "Kacper", + "first_name" => "Kacper", + "last_name" => "Nazwisko", + "email" => "kacper@example.com", + "birth_date" => "2000-01-02", + "height" => 180, + "weight" => "78.50", + ]); + + $res = $this->acting($user)->getJson("/api/profile"); + + $this->assertUserResource($res, $user); + } + + public function testUpdatesProfileAndReturnsUserResource(): void + { + $user = User::factory()->create([ + "name" => "Old Username", + "first_name" => "Old", + "last_name" => "Name", + "email" => "old@example.com", + "birth_date" => "1999-02-03", + "height" => 170, + "weight" => "70.00", + ]); + + $payload = [ + "first_name" => "New", + "last_name" => "Name", + "birth_date" => "2001-10-11", + "height" => 190, + "weight" => "82.00", + ]; + + $res = $this->acting($user)->patchJson("/api/profile", $payload); + + $this->assertDatabaseHas("users", [ + "id" => $user->id, + "first_name" => "New", + "last_name" => "Name", + "birth_date" => "2001-10-11", + "height" => 190, + "weight" => "82.00", + ]); + + $this->assertUserResource($res, $user); + } + + public function testChangesAvatarAndStoresPngOnAvatarsDisk(): void + { + Storage::fake("avatars"); + + $user = User::factory()->create([ + "birth_date" => "2000-01-02", + "height" => 180, + "weight" => "78.50", + ]); + + $file = UploadedFile::fake()->create("avatar.png", 10, "image/png"); + + $res = $this->acting($user)->postJson("/api/profile/avatar", [ + "avatar" => $file, + ]); + + $this->assertUserResource($res, $user); + Storage::disk("avatars")->assertExists($user->id . ".png"); + } + + public function testReturnsPngAvatarIfExistsWithLongCacheHeaders(): void + { + Storage::fake("avatars"); + + $user = User::factory()->create(); + Storage::disk("avatars")->put($user->id . ".png", "PNGDATA"); + + $res = $this->get("/api/profiles/" . $user->id . "/avatar"); + + $res->assertOk(); + $res->assertHeader("Content-Type", "image/png"); + $res->assertHeader("Cache-Control", "max-age=31536000, public"); + $this->assertSame("PNGDATA", $res->getContent()); + } + + public function testReturnsSvgIdenticonIfAvatarMissingWithShorterCacheHeaders(): void + { + Storage::fake("avatars"); + + $user = User::factory()->create(); + + $res = $this->get("/api/profiles/" . $user->id . "/avatar"); + + $res->assertOk(); + $res->assertHeader("Content-Type", "image/svg+xml"); + $res->assertHeader("Cache-Control", "max-age=86400, public"); + $this->assertStringContainsString("getContent()); + } + + public function testDeletesAvatarIfExists(): void + { + Storage::fake("avatars"); + + $user = User::factory()->create([ + "birth_date" => "2000-01-02", + "height" => 180, + "weight" => "78.50", + ]); + + Storage::disk("avatars")->put($user->id . ".png", "PNGDATA"); + + $res = $this->acting($user)->deleteJson("/api/profile/avatar"); + + $this->assertUserResource($res, $user); + Storage::disk("avatars")->assertMissing($user->id . ".png"); + } + + public function testReturnsOkEvenIfAvatarDoesNotExistOnDelete(): void + { + Storage::fake("avatars"); + + $user = User::factory()->create([ + "birth_date" => "2000-01-02", + "height" => 180, + "weight" => "78.50", + ]); + + $res = $this->acting($user)->deleteJson("/api/profile/avatar"); + + $this->assertUserResource($res, $user); + Storage::disk("avatars")->assertMissing($user->id . ".png"); + } + + private function acting(User $user): self + { + return $this->actingAs($user); + } + + private function assertUserResource($res, User $user): void + { + $user->refresh(); + + $res->assertOk(); + $res->assertJsonStructure([ + "data" => [ + "id", + "email", + "name", + "first_name", + "last_name", + "birth_date", + "height", + "weight", + "avatar", + "created_at", + ], + ]); + + $data = $res->json("data"); + + $this->assertSame($user->id, $data["id"]); + $this->assertSame($user->email, $data["email"]); + $this->assertSame($user->name, $data["name"]); + $this->assertSame($user->first_name, $data["first_name"]); + $this->assertSame($user->last_name, $data["last_name"]); + $this->assertSame($user->height, $data["height"]); + $this->assertSame($user->weight, $data["weight"]); + + $this->assertSame(IdenticonHelper::url($user->id), $data["avatar"]); + + $expectedBirth = (string)$user->birth_date; + $this->assertSame($expectedBirth, (string)$data["birth_date"]); + + $this->assertSame($user->created_at->toJSON(), $data["created_at"]); + } +} From 960fc7b5bcca8b33c0239c271c876945cbeae41b Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 15:14:23 +0100 Subject: [PATCH 03/25] code suggestions --- app/Http/Controllers/Profile/ProfileController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Profile/ProfileController.php b/app/Http/Controllers/Profile/ProfileController.php index 67bcd3d..92526b6 100644 --- a/app/Http/Controllers/Profile/ProfileController.php +++ b/app/Http/Controllers/Profile/ProfileController.php @@ -54,21 +54,21 @@ public function getAvatar(int $userId, GetAvatarAction $getAvatarAction, GetDefa if ($avatar) { return response($avatar) ->header("Content-Type", "image/png") - ->header("Cache-Control", "public, max-age=31536000"); + ->header("Cache-Control", "max-age=31536000, public"); } $defaultAvatar = $getDefaultAvatarAction->execute($userId); return response($defaultAvatar) ->header("Content-Type", "image/svg+xml") - ->header("Cache-Control", "public, max-age=86400"); + ->header("Cache-Control", "max-age=86400, public"); } - public function deleteAvatar(Request $request, DeleteAvatarAction $deleteProfileAction): UserResource + public function deleteAvatar(Request $request, DeleteAvatarAction $deleteAvatarAction): UserResource { $user = $request->user(); - $deleteProfileAction->execute($user->id); + $deleteAvatarAction->execute($user->id); return UserResource::make($user); } From 9ec0bd8349accd1f13875a12cb4ccf61f22410d5 Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 15:18:37 +0100 Subject: [PATCH 04/25] update .env.ci --- .env.ci | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.env.ci b/.env.ci index f63876c..474f7fb 100644 --- a/.env.ci +++ b/.env.ci @@ -14,3 +14,7 @@ QUEUE_CONNECTION=sync SESSION_DRIVER=array SESSION_LIFETIME=120 MAIL_MAILER=array + +DB_CONNECTION=sqlite +DB_DATABASE=:memory: +DB_FOREIGN_KEYS=true From 00251ac2dd0c37f982794a2bdcacb24b4d40daba Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 15:30:55 +0100 Subject: [PATCH 05/25] tests --- .github/workflows/test-and-lint-php.yml | 4 +-- phpunit.xml.ci | 37 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 phpunit.xml.ci diff --git a/.github/workflows/test-and-lint-php.yml b/.github/workflows/test-and-lint-php.yml index 0d1366e..ec236a3 100644 --- a/.github/workflows/test-and-lint-php.yml +++ b/.github/workflows/test-and-lint-php.yml @@ -8,7 +8,7 @@ on: - '**.php' - 'composer.json' - 'composer.lock' - - 'phpunit.xml' + - 'phpunit.xml.ci' - '.env.ci' jobs: @@ -65,4 +65,4 @@ jobs: - name: Execute tests run: | cp .env.ci .env - php artisan test --colors=always + php artisan test --configuration=phpunit.xml.ci --colors=always diff --git a/phpunit.xml.ci b/phpunit.xml.ci new file mode 100644 index 0000000..7319bac --- /dev/null +++ b/phpunit.xml.ci @@ -0,0 +1,37 @@ + + + + + ./tests/Unit + + + ./tests/Feature + + + + + ./app + + + + + + + + + + + + + + + + + + + + From b52fc3686c49d5ffcda60e3653edc79872d3986b Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 15:34:31 +0100 Subject: [PATCH 06/25] tests --- phpunit.xml.ci | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/phpunit.xml.ci b/phpunit.xml.ci index 7319bac..f0c9aa5 100644 --- a/phpunit.xml.ci +++ b/phpunit.xml.ci @@ -26,9 +26,9 @@ - - - + + + From e117983ddee1e8154b7aa416aaa8f095930f29f9 Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 15:39:36 +0100 Subject: [PATCH 07/25] tests --- .github/workflows/test-and-lint-php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-and-lint-php.yml b/.github/workflows/test-and-lint-php.yml index ec236a3..d0533d8 100644 --- a/.github/workflows/test-and-lint-php.yml +++ b/.github/workflows/test-and-lint-php.yml @@ -65,4 +65,4 @@ jobs: - name: Execute tests run: | cp .env.ci .env - php artisan test --configuration=phpunit.xml.ci --colors=always + php artisan test --colors=always From 5b43c1c198ddc72d6cf8e4ee2759ee593a86eb9d Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 15:40:41 +0100 Subject: [PATCH 08/25] tests --- .github/workflows/test-and-lint-php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-and-lint-php.yml b/.github/workflows/test-and-lint-php.yml index d0533d8..e6af08d 100644 --- a/.github/workflows/test-and-lint-php.yml +++ b/.github/workflows/test-and-lint-php.yml @@ -65,4 +65,4 @@ jobs: - name: Execute tests run: | cp .env.ci .env - php artisan test --colors=always + vendor/bin/phpunit -c phpunit.xml.ci --colors=always From 6f16baf5c4268a69a63ffc145a5a6b79c4389da0 Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 20:33:13 +0100 Subject: [PATCH 09/25] add activities --- .../Activities/CreateActivityAction.php | 27 +++++++++ .../Activities/GetActivityPhotoAction.php | 21 +++++++ .../Activities/ListActivitiesAction.php | 19 +++++++ app/Enums/ActivityType.php | 13 +++++ app/Http/Controllers/ActivitiesController.php | 49 +++++++++++++++++ app/Http/Requests/StoreActivityRequest.php | 35 ++++++++++++ app/Http/Resources/ActivityResource.php | 27 +++++++++ app/Models/Activity.php | 55 +++++++++++++++++++ config/filesystems.php | 6 ++ ...5_12_21_173736_create_activities_table.php | 35 ++++++++++++ routes/api.php | 5 ++ 11 files changed, 292 insertions(+) create mode 100644 app/Actions/Activities/CreateActivityAction.php create mode 100644 app/Actions/Activities/GetActivityPhotoAction.php create mode 100644 app/Actions/Activities/ListActivitiesAction.php create mode 100644 app/Enums/ActivityType.php create mode 100644 app/Http/Controllers/ActivitiesController.php create mode 100644 app/Http/Requests/StoreActivityRequest.php create mode 100644 app/Http/Resources/ActivityResource.php create mode 100644 app/Models/Activity.php create mode 100644 database/migrations/2025_12_21_173736_create_activities_table.php diff --git a/app/Actions/Activities/CreateActivityAction.php b/app/Actions/Activities/CreateActivityAction.php new file mode 100644 index 0000000..067b05d --- /dev/null +++ b/app/Actions/Activities/CreateActivityAction.php @@ -0,0 +1,27 @@ +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..fb3d6db --- /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..856d94f --- /dev/null +++ b/app/Actions/Activities/ListActivitiesAction.php @@ -0,0 +1,19 @@ +where('user_id', $userId) + ->latest() + ->paginate(10); + } +} 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(string $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..e2b27d3 --- /dev/null +++ b/app/Http/Requests/StoreActivityRequest.php @@ -0,0 +1,35 @@ +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..b625237 --- /dev/null +++ b/app/Http/Resources/ActivityResource.php @@ -0,0 +1,27 @@ + + */ + 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..89cb664 --- /dev/null +++ b/app/Models/Activity.php @@ -0,0 +1,55 @@ + '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..07a2011 --- /dev/null +++ b/database/migrations/2025_12_21_173736_create_activities_table.php @@ -0,0 +1,35 @@ +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(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('activities'); + } +}; diff --git a/routes/api.php b/routes/api.php index a063c43..f29b54c 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.store"); + 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"); From 50e0f7be7c814ad820a4cfcbadd1479004943ca3 Mon Sep 17 00:00:00 2001 From: Kacper Walenga <87613926+KacperWalenga@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:37:03 +0100 Subject: [PATCH 10/25] Update app/Actions/Profile/UpdateProfileAction.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Actions/Profile/UpdateProfileAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Actions/Profile/UpdateProfileAction.php b/app/Actions/Profile/UpdateProfileAction.php index a36265c..c58a8cb 100644 --- a/app/Actions/Profile/UpdateProfileAction.php +++ b/app/Actions/Profile/UpdateProfileAction.php @@ -8,7 +8,7 @@ class UpdateProfileAction { - public function execute(User $user, array $data): ?User + public function execute(User $user, array $data): User { $user->fill($data); $user->save(); From f4ffd46e1ac9ae8730d4439563ecaf21bbc7acfd Mon Sep 17 00:00:00 2001 From: Kacper Walenga <87613926+KacperWalenga@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:38:01 +0100 Subject: [PATCH 11/25] Update app/Models/User.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Models/User.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Models/User.php b/app/Models/User.php index 3d0e8c6..e99d192 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -55,6 +55,7 @@ protected function casts(): array { return [ "email_verified_at" => "datetime", + "birth_date" => "date", "password" => "hashed", "gender" => Gender::class, ]; From aa24de1d8ba19ce5846c285c3494cfeef0a2fafd Mon Sep 17 00:00:00 2001 From: Kacper Walenga <87613926+KacperWalenga@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:39:41 +0100 Subject: [PATCH 12/25] Update database/migrations/0001_01_01_000000_create_users_table.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- database/migrations/0001_01_01_000000_create_users_table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 3e5c7b8..1bf3abe 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -19,7 +19,7 @@ public function up(): void $table->string("password"); $table->date("birth_date")->nullable(); $table->unsignedSmallInteger("height")->nullable(); - $table->decimal("weight")->nullable(); + $table->decimal("weight", 5, 2)->nullable(); $table->enum("gender", ["male", "female"])->default("male"); $table->rememberToken(); $table->timestamps(); From 5ee7dea9637d1b962819b697145ca36c3bf47547 Mon Sep 17 00:00:00 2001 From: Kacper Walenga <87613926+KacperWalenga@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:40:25 +0100 Subject: [PATCH 13/25] Update app/Actions/Avatars/ChangeAvatarAction.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Actions/Avatars/ChangeAvatarAction.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Actions/Avatars/ChangeAvatarAction.php b/app/Actions/Avatars/ChangeAvatarAction.php index c4fe0e6..d9e171a 100644 --- a/app/Actions/Avatars/ChangeAvatarAction.php +++ b/app/Actions/Avatars/ChangeAvatarAction.php @@ -10,8 +10,8 @@ class ChangeAvatarAction { public function execute(UploadedFile $uploadedFile, int $userId): bool { - $uploadedFile->storeAs("", $userId . ".png", "avatars"); + $stored = $uploadedFile->storeAs("", $userId . ".png", "avatars"); - return true; + return $stored !== false; } } From 258dffd1eea82ba647d8d684d203dce6f39a478b Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 20:55:48 +0100 Subject: [PATCH 14/25] fix birth_date --- app/Http/Resources/UserResource.php | 2 +- tests/Feature/ProfileTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 1170fb4..9dbba26 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -22,7 +22,7 @@ public function toArray(Request $request): array "name" => $this->name, "first_name" => $this->first_name, "last_name" => $this->last_name, - "birth_date" => $this->birth_date, + "birth_date" => $this->birth_date?->format("Y-m-d"), "height" => $this->height, "weight" => $this->weight, "avatar" => $this->avatar, diff --git a/tests/Feature/ProfileTest.php b/tests/Feature/ProfileTest.php index 8ae4fac..4198c1a 100644 --- a/tests/Feature/ProfileTest.php +++ b/tests/Feature/ProfileTest.php @@ -186,7 +186,7 @@ private function assertUserResource($res, User $user): void $this->assertSame(IdenticonHelper::url($user->id), $data["avatar"]); - $expectedBirth = (string)$user->birth_date; + $expectedBirth = $user->birth_date?->toDateString(); $this->assertSame($expectedBirth, (string)$data["birth_date"]); $this->assertSame($user->created_at->toJSON(), $data["created_at"]); From 58e27a24b89a9e5e1cbc4127876edbff1cbca709 Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 20:57:17 +0100 Subject: [PATCH 15/25] fix index profiles --- app/Http/Controllers/Profile/ProfilesController.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/Http/Controllers/Profile/ProfilesController.php b/app/Http/Controllers/Profile/ProfilesController.php index 5c6dd1c..dedf69b 100644 --- a/app/Http/Controllers/Profile/ProfilesController.php +++ b/app/Http/Controllers/Profile/ProfilesController.php @@ -15,16 +15,7 @@ class ProfilesController extends Controller { public function index(): AnonymousResourceCollection { - $users = User::query()->select([ - "id", - "name", - "first_name", - "last_name", - "birth_date", - "height", - "weight", - "gender", - ])->paginate(10); + $users = User::query()->select()->paginate(10); return UserResource::collection($users); } From 1c14ed43683d7d85732ac0e04e841993b3bfe24e Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 20:33:13 +0100 Subject: [PATCH 16/25] add activities --- .../Activities/CreateActivityAction.php | 27 +++++++++ .../Activities/GetActivityPhotoAction.php | 21 +++++++ .../Activities/ListActivitiesAction.php | 19 +++++++ app/Enums/ActivityType.php | 13 +++++ app/Http/Controllers/ActivitiesController.php | 49 +++++++++++++++++ app/Http/Requests/StoreActivityRequest.php | 35 ++++++++++++ app/Http/Resources/ActivityResource.php | 27 +++++++++ app/Models/Activity.php | 55 +++++++++++++++++++ config/filesystems.php | 6 ++ ...5_12_21_173736_create_activities_table.php | 35 ++++++++++++ routes/api.php | 5 ++ 11 files changed, 292 insertions(+) create mode 100644 app/Actions/Activities/CreateActivityAction.php create mode 100644 app/Actions/Activities/GetActivityPhotoAction.php create mode 100644 app/Actions/Activities/ListActivitiesAction.php create mode 100644 app/Enums/ActivityType.php create mode 100644 app/Http/Controllers/ActivitiesController.php create mode 100644 app/Http/Requests/StoreActivityRequest.php create mode 100644 app/Http/Resources/ActivityResource.php create mode 100644 app/Models/Activity.php create mode 100644 database/migrations/2025_12_21_173736_create_activities_table.php diff --git a/app/Actions/Activities/CreateActivityAction.php b/app/Actions/Activities/CreateActivityAction.php new file mode 100644 index 0000000..067b05d --- /dev/null +++ b/app/Actions/Activities/CreateActivityAction.php @@ -0,0 +1,27 @@ +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..fb3d6db --- /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..856d94f --- /dev/null +++ b/app/Actions/Activities/ListActivitiesAction.php @@ -0,0 +1,19 @@ +where('user_id', $userId) + ->latest() + ->paginate(10); + } +} 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(string $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..e2b27d3 --- /dev/null +++ b/app/Http/Requests/StoreActivityRequest.php @@ -0,0 +1,35 @@ +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..b625237 --- /dev/null +++ b/app/Http/Resources/ActivityResource.php @@ -0,0 +1,27 @@ + + */ + 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..89cb664 --- /dev/null +++ b/app/Models/Activity.php @@ -0,0 +1,55 @@ + '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..07a2011 --- /dev/null +++ b/database/migrations/2025_12_21_173736_create_activities_table.php @@ -0,0 +1,35 @@ +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(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('activities'); + } +}; diff --git a/routes/api.php b/routes/api.php index a063c43..f29b54c 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.store"); + 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"); From df0ec9a6d2123ba3155b7ab8d93ad19a37da04bb Mon Sep 17 00:00:00 2001 From: Kacper Walenga <87613926+KacperWalenga@users.noreply.github.com> Date: Sun, 21 Dec 2025 21:00:59 +0100 Subject: [PATCH 17/25] Update app/Models/Activity.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Models/Activity.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 89cb664..ac627b9 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -7,7 +7,6 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Carbon; -use Strava\Helpers\IdenticonHelper; /** * Class Activity From ae5a9975a324700751f600eae437cbc1c2dde257 Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 21:01:30 +0100 Subject: [PATCH 18/25] fix route --- routes/api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/api.php b/routes/api.php index f29b54c..31cc5a7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -19,7 +19,7 @@ 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.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"); From ae9068edc59cdc3f9ef412dc4b89def8b766f47e Mon Sep 17 00:00:00 2001 From: Kacper Walenga <87613926+KacperWalenga@users.noreply.github.com> Date: Sun, 21 Dec 2025 21:01:44 +0100 Subject: [PATCH 19/25] Update app/Actions/Activities/GetActivityPhotoAction.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Actions/Activities/GetActivityPhotoAction.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Actions/Activities/GetActivityPhotoAction.php b/app/Actions/Activities/GetActivityPhotoAction.php index fb3d6db..682fbdb 100644 --- a/app/Actions/Activities/GetActivityPhotoAction.php +++ b/app/Actions/Activities/GetActivityPhotoAction.php @@ -8,9 +8,9 @@ class GetActivityPhotoAction { - public function execute(int $userId): ?string + public function execute(int $activityId): ?string { - $filename = "activity_" . $userId . ".png"; + $filename = "activity_" . $activityId . ".png"; if (Storage::disk("activityPhotos")->exists($filename)) { return Storage::disk("activityPhotos")->get($filename); From 8aed74499843a8b2afd87fd2630f4da18aef4838 Mon Sep 17 00:00:00 2001 From: Kacper Walenga <87613926+KacperWalenga@users.noreply.github.com> Date: Sun, 21 Dec 2025 21:02:33 +0100 Subject: [PATCH 20/25] Update app/Models/Activity.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Models/Activity.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Activity.php b/app/Models/Activity.php index ac627b9..cc48397 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -38,7 +38,7 @@ class Activity extends Model ]; protected $casts = [ - 'duration_seconds' => 'integer', + 'duration_s' => 'integer', 'distance_m' => 'integer', ]; From b96c5176edc968dc2bc6ef5f34ba73a1dc699473 Mon Sep 17 00:00:00 2001 From: Kacper Walenga <87613926+KacperWalenga@users.noreply.github.com> Date: Sun, 21 Dec 2025 21:02:50 +0100 Subject: [PATCH 21/25] Update app/Models/Activity.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Models/Activity.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Models/Activity.php b/app/Models/Activity.php index cc48397..64216ce 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -34,7 +34,6 @@ class Activity extends Model 'duration_s', 'distance_m', 'activityType', - 'photo_url', ]; protected $casts = [ From 4d598b798b7d55fce63fc2bb740ed3b93d7d52f4 Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 21:03:50 +0100 Subject: [PATCH 22/25] update Activity.php --- app/Models/Activity.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Models/Activity.php b/app/Models/Activity.php index ac627b9..bc3e6be 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -1,9 +1,10 @@ Date: Sun, 21 Dec 2025 21:05:08 +0100 Subject: [PATCH 23/25] update ActivitiesController.php --- app/Http/Controllers/ActivitiesController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/ActivitiesController.php b/app/Http/Controllers/ActivitiesController.php index ba059b2..532391a 100644 --- a/app/Http/Controllers/ActivitiesController.php +++ b/app/Http/Controllers/ActivitiesController.php @@ -1,5 +1,7 @@ execute($id); From 07751c488947576f6fe612c59ca26fb98f3d6fb8 Mon Sep 17 00:00:00 2001 From: Kacper Walenga <87613926+KacperWalenga@users.noreply.github.com> Date: Sun, 21 Dec 2025 21:05:44 +0100 Subject: [PATCH 24/25] Update app/Http/Requests/StoreActivityRequest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Http/Requests/StoreActivityRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Requests/StoreActivityRequest.php b/app/Http/Requests/StoreActivityRequest.php index e2b27d3..4875341 100644 --- a/app/Http/Requests/StoreActivityRequest.php +++ b/app/Http/Requests/StoreActivityRequest.php @@ -28,7 +28,7 @@ public function rules(): array 'notes' => ['nullable', 'string', 'max:2048'], 'duration_s' => ['required', 'integer', 'min:1'], 'distance_m' => ['required', 'integer', 'min:1'], - 'activityType' => ['required', new Enum(ActivityType::class)], + 'activityType' => ['required', new Enum(ActivityType::class)], "photo" => ["required", "image", "mimes:png", "max:4096"], ]; } From 60d3775a0b7da82dec70100cb38978266fc2b6f4 Mon Sep 17 00:00:00 2001 From: Kacper Walenga Date: Sun, 21 Dec 2025 21:06:50 +0100 Subject: [PATCH 25/25] task fix --- .../Activities/CreateActivityAction.php | 11 ++++----- .../Activities/ListActivitiesAction.php | 2 +- app/Http/Controllers/ActivitiesController.php | 2 +- app/Http/Requests/StoreActivityRequest.php | 15 +++++++----- app/Http/Resources/ActivityResource.php | 16 +++++++------ app/Models/Activity.php | 19 +++++++-------- ...5_12_21_173736_create_activities_table.php | 23 ++++++++----------- 7 files changed, 42 insertions(+), 46 deletions(-) diff --git a/app/Actions/Activities/CreateActivityAction.php b/app/Actions/Activities/CreateActivityAction.php index 067b05d..0fa8753 100644 --- a/app/Actions/Activities/CreateActivityAction.php +++ b/app/Actions/Activities/CreateActivityAction.php @@ -10,15 +10,14 @@ class CreateActivityAction { public function execute(int $userId, array $data): Activity { - $activity = new Activity(); $activity->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->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(); diff --git a/app/Actions/Activities/ListActivitiesAction.php b/app/Actions/Activities/ListActivitiesAction.php index 856d94f..d0e74a1 100644 --- a/app/Actions/Activities/ListActivitiesAction.php +++ b/app/Actions/Activities/ListActivitiesAction.php @@ -12,7 +12,7 @@ class ListActivitiesAction public function execute(int $userId): LengthAwarePaginator { return Activity::query() - ->where('user_id', $userId) + ->where("user_id", $userId) ->latest() ->paginate(10); } diff --git a/app/Http/Controllers/ActivitiesController.php b/app/Http/Controllers/ActivitiesController.php index 532391a..7f33374 100644 --- a/app/Http/Controllers/ActivitiesController.php +++ b/app/Http/Controllers/ActivitiesController.php @@ -27,7 +27,7 @@ public function store(StoreActivityRequest $request, CreateActivityAction $creat { $validated = $request->validated(); $user = $request->user(); - $photo = $request->file('photo'); + $photo = $request->file("photo"); $activity = $createActivityAction->execute($user->id, $validated); diff --git a/app/Http/Requests/StoreActivityRequest.php b/app/Http/Requests/StoreActivityRequest.php index e2b27d3..cea2e10 100644 --- a/app/Http/Requests/StoreActivityRequest.php +++ b/app/Http/Requests/StoreActivityRequest.php @@ -1,7 +1,10 @@ |string> + * @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)], + "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 index b625237..e5d1b66 100644 --- a/app/Http/Resources/ActivityResource.php +++ b/app/Http/Resources/ActivityResource.php @@ -1,5 +1,7 @@ $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, + "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 index 8faba86..9e85977 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -10,8 +10,6 @@ use Illuminate\Support\Carbon; /** - * Class Activity - * * @property int $id * @property int $user_id * @property string $title @@ -29,17 +27,16 @@ class Activity extends Model { protected $fillable = [ - 'user_id', - 'title', - 'notes', - 'duration_s', - 'distance_m', - 'activityType', + "user_id", + "title", + "notes", + "duration_s", + "distance_m", + "activityType", ]; - protected $casts = [ - 'duration_s' => 'integer', - 'distance_m' => 'integer', + "duration_s" => "integer", + "distance_m" => "integer", ]; public function user(): BelongsTo diff --git a/database/migrations/2025_12_21_173736_create_activities_table.php b/database/migrations/2025_12_21_173736_create_activities_table.php index 07a2011..91b0179 100644 --- a/database/migrations/2025_12_21_173736_create_activities_table.php +++ b/database/migrations/2025_12_21_173736_create_activities_table.php @@ -1,35 +1,30 @@ id(); $table->foreignId("user_id")->constrained()->cascadeOnDelete(); - $table->string('title'); - $table->text('notes'); - $table->integer('duration_s'); - $table->integer('distance_m'); + $table->string("title"); + $table->text("notes"); + $table->integer("duration_s"); + $table->integer("distance_m"); $table->enum("activityType", ["run", "ride", "walk", "other"]); $table->timestamps(); }); } - /** - * Reverse the migrations. - */ public function down(): void { - Schema::dropIfExists('activities'); + Schema::dropIfExists("activities"); } };