From 07148700a515bf2d935fbf60f04b3bc1191f6f6c Mon Sep 17 00:00:00 2001 From: DoarMihai Date: Sat, 4 Oct 2025 19:40:13 +0300 Subject: [PATCH] loads of progress --- .../Controllers/ExerciseAliasController.php | 40 ++++++++ app/Http/Controllers/ExerciseController.php | 10 +- app/Models/Exercise.php | 22 +++++ app/Models/ExerciseAlias.php | 28 ++++++ ...4_200000_create_exercise_aliases_table.php | 26 +++++ lang/en/app.php | 98 ++++++++++++++++++- lang/ro/app.php | 98 ++++++++++++++++++- resources/views/exercises/index.blade.php | 4 +- resources/views/exercises/show.blade.php | 48 +++++---- resources/views/home/index.blade.php | 82 ++++++++++++++++ resources/views/layouts/navigation.blade.php | 6 ++ resources/views/workouts/session.blade.php | 2 +- resources/views/workouts/show.blade.php | 4 +- routes/web.php | 3 + 14 files changed, 443 insertions(+), 28 deletions(-) create mode 100644 app/Http/Controllers/ExerciseAliasController.php create mode 100644 app/Models/ExerciseAlias.php create mode 100644 database/migrations/2025_10_04_200000_create_exercise_aliases_table.php diff --git a/app/Http/Controllers/ExerciseAliasController.php b/app/Http/Controllers/ExerciseAliasController.php new file mode 100644 index 0000000..8e62511 --- /dev/null +++ b/app/Http/Controllers/ExerciseAliasController.php @@ -0,0 +1,40 @@ +validate([ + 'name' => ['nullable', 'string', 'max:255'], + ]); + + $name = trim((string) ($data['name'] ?? '')); + $userId = Auth::id(); + + if ($name === '') { + // Clear alias + ExerciseAlias::where('user_id', $userId)->where('exercise_id', $exercise->id)->delete(); + return back()->with('status', __('app.exercises.rename_cleared')); + } + + ExerciseAlias::updateOrCreate( + ['user_id' => $userId, 'exercise_id' => $exercise->id], + ['name' => $name] + ); + + return back()->with('status', __('app.exercises.renamed_success')); + } +} diff --git a/app/Http/Controllers/ExerciseController.php b/app/Http/Controllers/ExerciseController.php index e7a600b..dd77ff6 100644 --- a/app/Http/Controllers/ExerciseController.php +++ b/app/Http/Controllers/ExerciseController.php @@ -7,6 +7,7 @@ use App\Models\Equipment; use App\Models\Exercise; use App\Models\Muscle; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; class ExerciseController extends Controller { @@ -20,7 +21,8 @@ class ExerciseController extends Controller if ($perPage < 6 || $perPage > 60) { $perPage = 12; } $query = Exercise::query() - ->with(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'secondaryMuscles:id,name,slug', 'equipments:id,name,slug']) + ->with(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'secondaryMuscles:id,name,slug', 'equipments:id,name,slug'] + + (Auth::check() ? ['aliases' => function($rel){ $rel->where('user_id', Auth::id()); }] : [])) ->when($q !== '', function ($qBuilder) use ($q) { $qBuilder->where(function ($sub) use ($q) { $sub->where('name', 'like', "%{$q}%") @@ -68,7 +70,8 @@ class ExerciseController extends Controller public function show(Exercise $exercise) { - $exercise->load(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'secondaryMuscles:id,name,slug', 'equipments:id,name,slug']); + $exercise->load(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'secondaryMuscles:id,name,slug', 'equipments:id,name,slug'] + + (Auth::check() ? ['aliases' => function($rel){ $rel->where('user_id', Auth::id()); }] : [])); // Find similar exercises by shared target muscles, body parts, or equipment $targetIds = $exercise->targetMuscles->pluck('id')->all(); @@ -76,7 +79,8 @@ class ExerciseController extends Controller $equipmentIds = $exercise->equipments->pluck('id')->all(); $similarQuery = Exercise::query() - ->with(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'equipments:id,name,slug']) + ->with(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'equipments:id,name,slug'] + + (Auth::check() ? ['aliases' => function($rel){ $rel->where('user_id', Auth::id()); }] : [])) ->where('id', '!=', $exercise->id) ->where(function ($q) use ($targetIds, $bodyPartIds, $equipmentIds) { if (!empty($targetIds)) { diff --git a/app/Models/Exercise.php b/app/Models/Exercise.php index 084262b..5758fa1 100644 --- a/app/Models/Exercise.php +++ b/app/Models/Exercise.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Auth; class Exercise extends Model { @@ -44,4 +45,25 @@ class Exercise extends Model { return $this->belongsToMany(User::class, 'exercise_favorites')->withTimestamps(); } + + public function aliases() + { + return $this->hasMany(ExerciseAlias::class); + } + + public function getDisplayNameAttribute(): string + { + $name = (string) $this->attributes['name'] ?? ''; + $userId = Auth::id(); + if (!$userId) { + return $name; + } + // Use already loaded relation if present to avoid querying N+1 + if ($this->relationLoaded('aliases')) { + $alias = $this->aliases->firstWhere('user_id', $userId); + return $alias?->name ?: $name; + } + $alias = $this->aliases()->where('user_id', $userId)->first(); + return $alias?->name ?: $name; + } } diff --git a/app/Models/ExerciseAlias.php b/app/Models/ExerciseAlias.php new file mode 100644 index 0000000..7436d21 --- /dev/null +++ b/app/Models/ExerciseAlias.php @@ -0,0 +1,28 @@ +belongsTo(User::class); + } + + public function exercise(): BelongsTo + { + return $this->belongsTo(Exercise::class); + } +} diff --git a/database/migrations/2025_10_04_200000_create_exercise_aliases_table.php b/database/migrations/2025_10_04_200000_create_exercise_aliases_table.php new file mode 100644 index 0000000..debbd40 --- /dev/null +++ b/database/migrations/2025_10_04_200000_create_exercise_aliases_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('exercise_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->timestamps(); + + $table->unique(['user_id', 'exercise_id']); + $table->index(['exercise_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('exercise_aliases'); + } +}; diff --git a/lang/en/app.php b/lang/en/app.php index 4aa1d0a..5cb4c9e 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -6,7 +6,8 @@ return [ 'yoga' => 'Yoga', 'workouts' => 'Workouts', 'blog' => 'Blog', - 'build_workout' => 'Build a Workout', + 'build_workout' => 'Build a Workout', + 'dashboard' => 'Dashboard', 'profile' => 'Profile', 'logout' => 'Log Out', 'login' => 'Log in', @@ -36,6 +37,95 @@ return [ 'favorites' => 'Favorites', 'favorites_desc' => 'A balanced routine targeting all major muscle groups. Suitable for most levels and easy to adapt with available equipment.', 'try_routine' => 'Try This Routine', + 'features' => [ + 'title' => 'Everything you need to train smarter', + 'subtitle' => 'Powerful tools to plan workouts, learn movements, and track your progress — all in one place.', + 'items' => [ + 'tracking' => [ + 'title' => 'Session tracking', + 'desc' => 'Check off exercises as you go and mark sessions complete to visualize consistency.', + ], + 'library' => [ + 'title' => 'Rich exercise library', + 'desc' => 'Browse 1,000+ movements with target muscles, equipment, and clear instructions.', + ], + 'plans' => [ + 'title' => 'Custom workouts', + 'desc' => 'Build routines that match your goals and available equipment.', + ], + 'yoga' => [ + 'title' => 'Yoga poses', + 'desc' => 'Search and learn yoga poses by category and benefits with images.', + ], + 'progress' => [ + 'title' => 'Progress dashboard', + 'desc' => 'See completion rate, recent sessions, and 30‑day activity at a glance.', + ], + 'community' => [ + 'title' => 'Personalized', + 'desc' => 'Rename exercises for yourself and favorite the ones you use the most.', + ], + ], + ], + 'pricing' => [ + 'title' => 'Simple pricing', + 'subtitle' => 'Start free. Upgrade anytime for more features.', + 'cta' => 'Get started', + 'plans' => [ + 'free' => [ + 'name' => 'Free', + 'price' => '$0', + 'period' => '/ forever', + 'desc' => 'Everything you need to begin.', + 'features' => [ + 'feature1' => 'Exercise search and filters', + 'feature2' => 'Build and manage workouts', + 'feature3' => 'Yoga poses browser', + 'feature4' => 'Basic dashboard stats', + ], + ], + 'pro' => [ + 'name' => 'Pro', + 'price' => '$9', + 'period' => '/ month', + 'desc' => 'Deeper insights and personalization.', + 'features' => [ + 'feature1' => 'Advanced progress analytics', + 'feature2' => 'Unlimited favorites and aliases', + 'feature3' => 'Priority data updates', + 'feature4' => 'Early access features', + 'feature5' => 'Email support', + ], + ], + 'ultimate' => [ + 'name' => 'Ultimate', + 'price' => '$19', + 'period' => '/ month', + 'desc' => 'For power users and enthusiasts.', + 'features' => [ + 'feature1' => 'Everything in Pro', + 'feature2' => 'Customization extras', + 'feature3' => 'Priority support', + ], + ], + ], + ], + 'faq' => [ + 'title' => 'Frequently asked questions', + 'subtitle' => 'If you can’t find what you’re looking for, reach out and we’ll help.', + 'q1' => 'Is FitAtlas free to use?', + 'a1' => 'Yes. You can browse exercises, create workouts, and track sessions for free.', + 'q2' => 'What is the Dashboard?', + 'a2' => 'Your personal hub with stats like completion rate, recent sessions, and activity over the last 30 days.', + 'q3' => 'Do you support yoga?', + 'a3' => 'Yes. Browse yoga categories and poses, filter by benefits, and open detailed pages.', + 'q4' => 'Can I rename exercises?', + 'a4' => 'Yes. You can set custom names visible only to you on each exercise page.', + 'q5' => 'Will there be a Pro plan?', + 'a5' => 'Pricing is indicative. You can start free and upgrade later if you need more features.', + 'q6' => 'What languages are supported?', + 'a6' => 'English and Romanian. Switch with the language control in the header.', + ], 'footer' => [ 'about' => 'About', 'privacy' => 'Privacy', @@ -101,6 +191,12 @@ return [ 'none_found' => 'No exercises found. Try adjusting your filters.', 'similar' => 'Similar exercises', 'similar_none' => 'No similar exercises found.', + 'rename' => 'Custom name', + 'rename_placeholder' => 'Your custom name for this exercise', + 'rename_save' => 'Save', + 'rename_reset' => 'Reset', + 'renamed_success' => 'Your custom name has been saved.', + 'rename_cleared' => 'Custom name cleared.', ], 'sessions' => [ diff --git a/lang/ro/app.php b/lang/ro/app.php index f5aa41b..106b9d5 100644 --- a/lang/ro/app.php +++ b/lang/ro/app.php @@ -6,7 +6,8 @@ return [ 'yoga' => 'Yoga', 'workouts' => 'Antrenamente', 'blog' => 'Blog', - 'build_workout' => 'Creează un antrenament', + 'build_workout' => 'Creează un antrenament', + 'dashboard' => 'Tabloul de bord', 'profile' => 'Profil', 'logout' => 'Deconectare', 'login' => 'Autentificare', @@ -36,6 +37,95 @@ return [ 'favorites' => 'Favorite', 'favorites_desc' => 'O rutină echilibrată care vizează toate grupele musculare principale. Potrivită pentru majoritatea nivelurilor și ușor de adaptat în funcție de echipamentul disponibil.', 'try_routine' => 'Încearcă această rutină', + 'features' => [ + 'title' => 'Tot ce ai nevoie pentru a te antrena mai inteligent', + 'subtitle' => 'Instrumente puternice pentru a planifica antrenamente, a învăța mișcări și a-ți urmări progresul — într-un singur loc.', + 'items' => [ + 'tracking' => [ + 'title' => 'Urmărirea sesiunilor', + 'desc' => 'Bifează exercițiile pe parcurs și marchează sesiunile ca finalizate pentru a vedea consistența.', + ], + 'library' => [ + 'title' => 'Bibliotecă bogată de exerciții', + 'desc' => 'Răsfoiește peste 1.000 de mișcări cu mușchi țintă, echipament și instrucțiuni clare.', + ], + 'plans' => [ + 'title' => 'Antrenamente personalizate', + 'desc' => 'Construiește rutine potrivite obiectivelor și echipamentului disponibil.', + ], + 'yoga' => [ + 'title' => 'Poziții de yoga', + 'desc' => 'Caută și învață poziții de yoga după categorie și beneficii, cu imagini.', + ], + 'progress' => [ + 'title' => 'Tablou de bord al progresului', + 'desc' => 'Vezi rata de finalizare, sesiuni recente și activitatea din ultimele 30 de zile dintr-o privire.', + ], + 'community' => [ + 'title' => 'Personalizare', + 'desc' => 'Redenumește exercițiile pentru tine și adaugă la favorite pe cele folosite cel mai des.', + ], + ], + ], + 'pricing' => [ + 'title' => 'Prețuri simple', + 'subtitle' => 'Începe gratuit. Fă upgrade oricând pentru mai multe funcții.', + 'cta' => 'Începe acum', + 'plans' => [ + 'free' => [ + 'name' => 'Gratuit', + 'price' => '0 RON', + 'period' => '/ pentru totdeauna', + 'desc' => 'Tot ce îți trebuie pentru a începe.', + 'features' => [ + 'feature1' => 'Căutare și filtre pentru exerciții', + 'feature2' => 'Construiește și gestionează antrenamente', + 'feature3' => 'Catalog de poziții yoga', + 'feature4' => 'Statistici de bază în tablou', + ], + ], + 'pro' => [ + 'name' => 'Pro', + 'price' => '9 $', + 'period' => '/ lună', + 'desc' => 'Analize și personalizare aprofundate.', + 'features' => [ + 'feature1' => 'Analitice avansate de progres', + 'feature2' => 'Favorite și aliasuri nelimitate', + 'feature3' => 'Actualizări de date prioritare', + 'feature4' => 'Acces anticipat la funcții', + 'feature5' => 'Suport prin email', + ], + ], + 'ultimate' => [ + 'name' => 'Ultimate', + 'price' => '19 $', + 'period' => '/ lună', + 'desc' => 'Pentru utilizatori avansați și pasionați.', + 'features' => [ + 'feature1' => 'Tot din Pro', + 'feature2' => 'Extra opțiuni de personalizare', + 'feature3' => 'Suport prioritar', + ], + ], + ], + ], + 'faq' => [ + 'title' => 'Întrebări frecvente', + 'subtitle' => 'Dacă nu găsești răspunsul, scrie-ne și te ajutăm.', + 'q1' => 'FitAtlas este gratuit?', + 'a1' => 'Da. Poți răsfoi exerciții, crea antrenamente și urmări sesiuni gratuit.', + 'q2' => 'Ce este Tabloul de bord?', + 'a2' => 'Centrul tău personal cu statistici precum rata de finalizare, sesiuni recente și activitate pe 30 de zile.', + 'q3' => 'Aveți suport pentru yoga?', + 'a3' => 'Da. Răsfoiește categorii și poziții de yoga, filtrează după beneficii și vezi pagini detaliate.', + 'q4' => 'Pot redenumi exercițiile?', + 'a4' => 'Da. Poți seta nume personalizate vizibile doar pentru tine pe pagina fiecărui exercițiu.', + 'q5' => 'Va exista un plan Pro?', + 'a5' => 'Prețurile sunt orientative. Poți începe gratuit și face upgrade ulterior dacă ai nevoie.', + 'q6' => 'Ce limbi sunt disponibile?', + 'a6' => 'Engleză și Română. Schimbă limba din antet.', + ], 'footer' => [ 'about' => 'Despre', 'privacy' => 'Confidențialitate', @@ -101,6 +191,12 @@ return [ 'none_found' => 'Nu s-au găsit exerciții. Încearcă să ajustezi filtrele.', 'similar' => 'Exerciții similare', 'similar_none' => 'Nu s-au găsit exerciții similare.', + 'rename' => 'Nume personalizat', + 'rename_placeholder' => 'Numele tău pentru acest exercițiu', + 'rename_save' => 'Salvează', + 'rename_reset' => 'Resetează', + 'renamed_success' => 'Numele personalizat a fost salvat.', + 'rename_cleared' => 'Numele personalizat a fost eliminat.', ], 'sessions' => [ diff --git a/resources/views/exercises/index.blade.php b/resources/views/exercises/index.blade.php index 644ee5f..4121e42 100644 --- a/resources/views/exercises/index.blade.php +++ b/resources/views/exercises/index.blade.php @@ -90,9 +90,9 @@ @foreach($exercises as $exercise)
- {{ $exercise->name }} + {{ $exercise->display_name }}
-
{{ $exercise->name }}
+
{{ $exercise->display_name }}
diff --git a/resources/views/exercises/show.blade.php b/resources/views/exercises/show.blade.php index 775d161..e66685a 100644 --- a/resources/views/exercises/show.blade.php +++ b/resources/views/exercises/show.blade.php @@ -1,7 +1,7 @@
- {{ $exercise->name }} + {{ $exercise->display_name }}
@@ -9,22 +9,34 @@ ← Back
-

{{ $exercise->name }}

+

{{ $exercise->display_name }}

@auth - @php $isFav = auth()->user()->hasFavoritedExercise($exercise); @endphp -
- @csrf - -
+
+ @php $isFav = auth()->user()->hasFavoritedExercise($exercise); @endphp +
+ @csrf + +
+ + @php $alias = optional($exercise->aliases->first())->name ?? ''; @endphp + +
@endauth
+ +
+
+
+

{{ __('app.home.features.title') }}

+

{{ __('app.home.features.subtitle') }}

+
+
+ @php $icons = [ + 'tracking' => 'M4 12h16M4 6h16M4 18h16', + 'library' => 'M4 5h16v14H4z M8 9h8M8 13h8', + 'plans' => 'M6 6h12v12H6z M9 9h6v6H9z', + 'yoga' => 'M12 2v6M12 16v6M4 12h6M14 12h6M7 7l3 3M14 14l3 3M17 7l-3 3M10 14l-3 3', + 'progress' => 'M4 20h16M6 16h2v4H6zm5-6h2v10h-2zm5-4h2v14h-2z', + 'community' => 'M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5zm7 9a7 7 0 0 0-14 0', + ]; @endphp + @foreach(['tracking','library','plans','yoga','progress','community'] as $key) +
+
+ + + +
+

{{ __('app.home.features.items.' . $key . '.title') }}

+

{{ __('app.home.features.items.' . $key . '.desc') }}

+
+ @endforeach +
+
+
+ + +
+ +
+ + +
+
+
+

{{ __('app.home.faq.title') }}

+

{{ __('app.home.faq.subtitle') }}

+
+
+ @foreach([1,2,3,4,5,6] as $i) +
+

{{ __('app.home.faq.q' . $i) }}

+

{{ __('app.home.faq.a' . $i) }}

+
+ @endforeach +
+
+
+
@@ -90,6 +93,9 @@ {{ __('app.nav.yoga') }} {{ __('app.nav.workouts') }} {{ __('app.nav.blog') }} + @auth + {{ __('app.nav.dashboard') }} + @endauth {{ __('app.nav.build_workout') }}
diff --git a/resources/views/workouts/session.blade.php b/resources/views/workouts/session.blade.php index 57be16a..8a8a999 100644 --- a/resources/views/workouts/session.blade.php +++ b/resources/views/workouts/session.blade.php @@ -38,7 +38,7 @@
-
{{ $item->exercise->name }}
+
{{ $item->exercise->display_name }}
@if($item->sets) {{ $item->sets }} {{ __('app.sessions.sets') }} @endif @if($item->reps) · {{ $item->reps }} {{ __('app.sessions.reps') }} @endif diff --git a/resources/views/workouts/show.blade.php b/resources/views/workouts/show.blade.php index abb32d5..b0da7c5 100644 --- a/resources/views/workouts/show.blade.php +++ b/resources/views/workouts/show.blade.php @@ -69,7 +69,7 @@
#{{ $item->position }}
-
{{ $item->exercise->name }}
+
{{ $item->exercise->display_name }}
@if($item->sets) {{ $item->sets }} sets @endif @if($item->reps) · {{ $item->reps }} reps @endif @@ -119,7 +119,7 @@
diff --git a/routes/web.php b/routes/web.php index d22eef3..2f3cbd4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -17,6 +17,9 @@ Route::middleware('auth')->group(function () { // Favorites Route::post('/exercises/{exercise}/favorite', [\App\Http\Controllers\FavoriteController::class, 'toggleExercise'])->name('exercises.favorite'); Route::post('/workouts/{workout}/favorite', [\App\Http\Controllers\FavoriteController::class, 'toggleWorkout'])->name('workouts.favorite'); + + // Exercise alias (per-user rename) + Route::post('/exercises/{exercise}/alias', [\App\Http\Controllers\ExerciseAliasController::class, 'store'])->name('exercises.alias'); }); Route::prefix('/exercises')->group(function () {