From 42f3808343efb386479073395c74cfe94c2b28ad Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Tue, 16 Jun 2026 18:52:25 +0330 Subject: [PATCH] feat: add breath colors --- .../Controllers/BreathingColorController.php | 68 +++++++++++++++++++ .../BreathingExerciseController.php | 18 +++-- app/Models/BreathingColor.php | 16 +++++ app/Models/BreathingTemplate.php | 7 +- ...dd_colors_to_breathing_templates_table.php | 23 +++++++ ...5_140000_create_breathing_colors_table.php | 25 +++++++ ...00_use_breathing_color_id_on_templates.php | 28 ++++++++ database/seeders/BreathingColorSeeder.php | 25 +++++++ routes/api.php | 4 ++ 9 files changed, 206 insertions(+), 8 deletions(-) create mode 100644 app/Http/Controllers/BreathingColorController.php create mode 100644 app/Models/BreathingColor.php create mode 100644 database/migrations/2026_06_15_130000_add_colors_to_breathing_templates_table.php create mode 100644 database/migrations/2026_06_15_140000_create_breathing_colors_table.php create mode 100644 database/migrations/2026_06_15_150000_use_breathing_color_id_on_templates.php create mode 100644 database/seeders/BreathingColorSeeder.php diff --git a/app/Http/Controllers/BreathingColorController.php b/app/Http/Controllers/BreathingColorController.php new file mode 100644 index 0000000..c0d66dd --- /dev/null +++ b/app/Http/Controllers/BreathingColorController.php @@ -0,0 +1,68 @@ +boolean('include_inactive')) { + $query->where('is_active', true); + } + + return response()->json($query->orderBy('order')->get()); + } + + public function show($id) + { + return response()->json(BreathingColor::findOrFail($id)); + } + + public function store(Request $request) + { + $data = $this->validateData($request, true); + + $color = BreathingColor::create($data); + + return response()->json([ + 'message' => 'Color created successfully', + 'color' => $color, + ], 201); + } + + public function update(Request $request, $id) + { + $color = BreathingColor::findOrFail($id); + + $color->update($this->validateData($request, false)); + + return response()->json([ + 'message' => 'Color updated successfully', + 'color' => $color, + ]); + } + + public function destroy($id) + { + BreathingColor::findOrFail($id)->delete(); + + return response()->json(['message' => 'Color deleted successfully']); + } + + private function validateData(Request $request, bool $creating): array + { + return $request->validate([ + 'name' => 'nullable|string|max:255', + 'colors' => ($creating ? 'required' : 'sometimes') . '|array|min:1', + 'colors.*' => ['string', 'regex:/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/'], + 'order' => 'nullable|integer', + 'is_active' => 'nullable|boolean', + ]); + } +} diff --git a/app/Http/Controllers/BreathingExerciseController.php b/app/Http/Controllers/BreathingExerciseController.php index 9792fd4..1050935 100644 --- a/app/Http/Controllers/BreathingExerciseController.php +++ b/app/Http/Controllers/BreathingExerciseController.php @@ -19,7 +19,9 @@ public function createTemplate(Request $request) 'duration' => 'sometimes|integer|min:0', 'description' => 'sometimes|string|nullable', 'image_id' => 'nullable|exists:images,id', - 'source' => 'nullable|string' + 'source' => 'nullable|string', + // The chosen color from the /breathing-colors palette. + 'breathing_color_id' => 'nullable|exists:breathing_colors,id', ]); $template = BreathingTemplate::create([ @@ -31,10 +33,11 @@ public function createTemplate(Request $request) 'duration'=> $data['duration'] ?? 60, 'description' => $data['description'] ?? null, 'image_id' => $data['image_id'] ?? null, - 'source' => 'nullable|string' + 'source' => $data['source'] ?? null, + 'breathing_color_id' => $data['breathing_color_id'] ?? null, ]); - return response()->json(['message' => 'Template created', 'template' => $template->load('image')]); + return response()->json(['message' => 'Template created', 'template' => $template->load('image', 'breathingColor')]); } public function updateTemplate(Request $request, $id) @@ -51,12 +54,13 @@ public function updateTemplate(Request $request, $id) 'duration' => 'sometimes|integer|min:0', 'description' => 'sometimes|string|nullable', 'image_id' => 'nullable|exists:images,id', - 'source' => 'nullable|string' + 'source' => 'nullable|string', + 'breathing_color_id' => 'nullable|exists:breathing_colors,id', ]); $template->update($data); - return response()->json(['message' => 'Template updated', 'template' => $template->load('image')]); + return response()->json(['message' => 'Template updated', 'template' => $template->load('image', 'breathingColor')]); } public function deleteTemplate($id) { @@ -75,7 +79,7 @@ public function getTemplates() $templates = BreathingTemplate::whereNull('user_id') ->orWhere('user_id', $userId) - ->with('image') // eager load image + ->with(['image', 'breathingColor']) // eager load image + color ->get() ->map(function ($template) { if ($template->image) { @@ -92,7 +96,7 @@ public function getTemplates() public function getUserTemplates() { $templates = BreathingTemplate::where('user_id', auth()->id()) - ->with('image') // eager load image + ->with(['image', 'breathingColor']) // eager load image + color ->get() ->map(function ($template) { if ($template->image) { diff --git a/app/Models/BreathingColor.php b/app/Models/BreathingColor.php new file mode 100644 index 0000000..3086187 --- /dev/null +++ b/app/Models/BreathingColor.php @@ -0,0 +1,16 @@ + 'array', + 'is_active' => 'boolean', + 'order' => 'integer', + ]; +} diff --git a/app/Models/BreathingTemplate.php b/app/Models/BreathingTemplate.php index 271afd2..1c9c040 100644 --- a/app/Models/BreathingTemplate.php +++ b/app/Models/BreathingTemplate.php @@ -8,12 +8,17 @@ class BreathingTemplate extends Model { use HasSaves; - protected $fillable = ['user_id', 'name', 'inhale', 'exhale', 'breath_hold' , 'duration' , 'description','image_id' , 'source']; + protected $fillable = ['user_id', 'name', 'inhale', 'exhale', 'breath_hold' , 'duration' , 'description','image_id' , 'source', 'breathing_color_id']; public function user() { return $this->belongsTo(User::class); } + + public function breathingColor() + { + return $this->belongsTo(BreathingColor::class); + } protected $appends = ['image_url', 'is_saved','saved_count']; public function getImageUrlAttribute() diff --git a/database/migrations/2026_06_15_130000_add_colors_to_breathing_templates_table.php b/database/migrations/2026_06_15_130000_add_colors_to_breathing_templates_table.php new file mode 100644 index 0000000..ff7a276 --- /dev/null +++ b/database/migrations/2026_06_15_130000_add_colors_to_breathing_templates_table.php @@ -0,0 +1,23 @@ +json('colors')->nullable()->after('image_id'); + }); + } + + public function down(): void + { + Schema::table('breathing_templates', function (Blueprint $table) { + $table->dropColumn('colors'); + }); + } +}; diff --git a/database/migrations/2026_06_15_140000_create_breathing_colors_table.php b/database/migrations/2026_06_15_140000_create_breathing_colors_table.php new file mode 100644 index 0000000..7fd33fb --- /dev/null +++ b/database/migrations/2026_06_15_140000_create_breathing_colors_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('name')->nullable(); + $table->json('colors'); // one hex (solid) or several (gradient) + $table->integer('order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('breathing_colors'); + } +}; diff --git a/database/migrations/2026_06_15_150000_use_breathing_color_id_on_templates.php b/database/migrations/2026_06_15_150000_use_breathing_color_id_on_templates.php new file mode 100644 index 0000000..4caddea --- /dev/null +++ b/database/migrations/2026_06_15_150000_use_breathing_color_id_on_templates.php @@ -0,0 +1,28 @@ +dropColumn('colors'); + } + $table->foreignId('breathing_color_id')->nullable()->after('image_id') + ->constrained('breathing_colors')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('breathing_templates', function (Blueprint $table) { + $table->dropForeign(['breathing_color_id']); + $table->dropColumn('breathing_color_id'); + $table->json('colors')->nullable(); + }); + } +}; diff --git a/database/seeders/BreathingColorSeeder.php b/database/seeders/BreathingColorSeeder.php new file mode 100644 index 0000000..c08e457 --- /dev/null +++ b/database/seeders/BreathingColorSeeder.php @@ -0,0 +1,25 @@ + 'آبی', 'order' => 1, 'colors' => ['#3ABDEB', '#553AEB']], + ['name' => 'صورتی', 'order' => 2, 'colors' => ['#CE4299', '#8628FF']], + ['name' => 'سبز', 'order' => 3, 'colors' => ['#5CC65C', '#00A489']], + ['name' => 'نارنجی', 'order' => 4, 'colors' => ['#E2990A', '#CC4B1C']], + ['name' => 'بنفش', 'order' => 5, 'colors' => ['#6829DD', '#1A50CC']], + ['name' => 'تک‌رنگ', 'order' => 6, 'colors' => ['#5360FC']], + ]; + + foreach ($colors as $c) { + BreathingColor::firstOrCreate(['name' => $c['name']], $c); + } + } +} diff --git a/routes/api.php b/routes/api.php index 1314dae..7ed365d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Route; use Laravel\Socialite\Facades\Socialite; use App\Http\Controllers\BreathingExerciseController; +use App\Http\Controllers\BreathingColorController; use App\Http\Controllers\PackageNameController; use App\Http\Controllers\ProductController; use App\Http\Controllers\BreathingTemplate; @@ -109,6 +110,9 @@ Route::get('user-templates', [BreathingExerciseController::class, 'getUserTemplates']); Route::get('breathing-sessions', [BreathingExerciseController::class, 'getSessions']); + // Breathing color palette (app reads it to pick a template color; admin manages it). + Route::apiResource('breathing-colors', BreathingColorController::class); + /// worry box feature Route::prefix('worries')->controller(WorryController::class)->group(function () { Route::post('/', 'store'); // Create worry + note