loads of progress
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Exercise;
|
||||||
|
use App\Models\ExerciseAlias;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class ExerciseAliasController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request, Exercise $exercise): RedirectResponse
|
||||||
|
{
|
||||||
|
// Auth is enforced at the route level. Extra guard below for safety.
|
||||||
|
if (!Auth::check()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use App\Models\Equipment;
|
|||||||
use App\Models\Exercise;
|
use App\Models\Exercise;
|
||||||
use App\Models\Muscle;
|
use App\Models\Muscle;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class ExerciseController extends Controller
|
class ExerciseController extends Controller
|
||||||
{
|
{
|
||||||
@@ -20,7 +21,8 @@ class ExerciseController extends Controller
|
|||||||
if ($perPage < 6 || $perPage > 60) { $perPage = 12; }
|
if ($perPage < 6 || $perPage > 60) { $perPage = 12; }
|
||||||
|
|
||||||
$query = Exercise::query()
|
$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) {
|
->when($q !== '', function ($qBuilder) use ($q) {
|
||||||
$qBuilder->where(function ($sub) use ($q) {
|
$qBuilder->where(function ($sub) use ($q) {
|
||||||
$sub->where('name', 'like', "%{$q}%")
|
$sub->where('name', 'like', "%{$q}%")
|
||||||
@@ -68,7 +70,8 @@ class ExerciseController extends Controller
|
|||||||
|
|
||||||
public function show(Exercise $exercise)
|
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
|
// Find similar exercises by shared target muscles, body parts, or equipment
|
||||||
$targetIds = $exercise->targetMuscles->pluck('id')->all();
|
$targetIds = $exercise->targetMuscles->pluck('id')->all();
|
||||||
@@ -76,7 +79,8 @@ class ExerciseController extends Controller
|
|||||||
$equipmentIds = $exercise->equipments->pluck('id')->all();
|
$equipmentIds = $exercise->equipments->pluck('id')->all();
|
||||||
|
|
||||||
$similarQuery = Exercise::query()
|
$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('id', '!=', $exercise->id)
|
||||||
->where(function ($q) use ($targetIds, $bodyPartIds, $equipmentIds) {
|
->where(function ($q) use ($targetIds, $bodyPartIds, $equipmentIds) {
|
||||||
if (!empty($targetIds)) {
|
if (!empty($targetIds)) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class Exercise extends Model
|
class Exercise extends Model
|
||||||
{
|
{
|
||||||
@@ -44,4 +45,25 @@ class Exercise extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsToMany(User::class, 'exercise_favorites')->withTimestamps();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class ExerciseAlias extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'exercise_id',
|
||||||
|
'name',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exercise(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Exercise::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('exercise_aliases', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ return [
|
|||||||
'workouts' => 'Workouts',
|
'workouts' => 'Workouts',
|
||||||
'blog' => 'Blog',
|
'blog' => 'Blog',
|
||||||
'build_workout' => 'Build a Workout',
|
'build_workout' => 'Build a Workout',
|
||||||
|
'dashboard' => 'Dashboard',
|
||||||
'profile' => 'Profile',
|
'profile' => 'Profile',
|
||||||
'logout' => 'Log Out',
|
'logout' => 'Log Out',
|
||||||
'login' => 'Log in',
|
'login' => 'Log in',
|
||||||
@@ -36,6 +37,95 @@ return [
|
|||||||
'favorites' => 'Favorites',
|
'favorites' => 'Favorites',
|
||||||
'favorites_desc' => 'A balanced routine targeting all major muscle groups. Suitable for most levels and easy to adapt with available equipment.',
|
'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',
|
'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' => [
|
'footer' => [
|
||||||
'about' => 'About',
|
'about' => 'About',
|
||||||
'privacy' => 'Privacy',
|
'privacy' => 'Privacy',
|
||||||
@@ -101,6 +191,12 @@ return [
|
|||||||
'none_found' => 'No exercises found. Try adjusting your filters.',
|
'none_found' => 'No exercises found. Try adjusting your filters.',
|
||||||
'similar' => 'Similar exercises',
|
'similar' => 'Similar exercises',
|
||||||
'similar_none' => 'No similar exercises found.',
|
'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' => [
|
'sessions' => [
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ return [
|
|||||||
'workouts' => 'Antrenamente',
|
'workouts' => 'Antrenamente',
|
||||||
'blog' => 'Blog',
|
'blog' => 'Blog',
|
||||||
'build_workout' => 'Creează un antrenament',
|
'build_workout' => 'Creează un antrenament',
|
||||||
|
'dashboard' => 'Tabloul de bord',
|
||||||
'profile' => 'Profil',
|
'profile' => 'Profil',
|
||||||
'logout' => 'Deconectare',
|
'logout' => 'Deconectare',
|
||||||
'login' => 'Autentificare',
|
'login' => 'Autentificare',
|
||||||
@@ -36,6 +37,95 @@ return [
|
|||||||
'favorites' => 'Favorite',
|
'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.',
|
'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ă',
|
'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' => [
|
'footer' => [
|
||||||
'about' => 'Despre',
|
'about' => 'Despre',
|
||||||
'privacy' => 'Confidențialitate',
|
'privacy' => 'Confidențialitate',
|
||||||
@@ -101,6 +191,12 @@ return [
|
|||||||
'none_found' => 'Nu s-au găsit exerciții. Încearcă să ajustezi filtrele.',
|
'none_found' => 'Nu s-au găsit exerciții. Încearcă să ajustezi filtrele.',
|
||||||
'similar' => 'Exerciții similare',
|
'similar' => 'Exerciții similare',
|
||||||
'similar_none' => 'Nu s-au găsit 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' => [
|
'sessions' => [
|
||||||
|
|||||||
@@ -90,9 +90,9 @@
|
|||||||
@foreach($exercises as $exercise)
|
@foreach($exercises as $exercise)
|
||||||
<a href="{{ route('exercises.show', $exercise->slug) }}" class="group rounded-3xl overflow-hidden bg-white dark:bg-gray-800 ring-1 ring-gray-200 dark:ring-gray-700 shadow-sm hover:shadow-xl transition">
|
<a href="{{ route('exercises.show', $exercise->slug) }}" class="group rounded-3xl overflow-hidden bg-white dark:bg-gray-800 ring-1 ring-gray-200 dark:ring-gray-700 shadow-sm hover:shadow-xl transition">
|
||||||
<div class="relative aspect-[16/9] overflow-hidden">
|
<div class="relative aspect-[16/9] overflow-hidden">
|
||||||
<img src="{{ $exercise->gif_url ?: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?q=80&w=1200&auto=format&fit=crop' }}" alt="{{ $exercise->name }}" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105">
|
<img src="{{ $exercise->gif_url ?: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?q=80&w=1200&auto=format&fit=crop' }}" alt="{{ $exercise->display_name }}" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105">
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
||||||
<div class="absolute bottom-3 left-3 inline-flex items-center rounded-full bg-white/90 px-3 py-1 text-xs font-semibold text-gray-900 shadow">{{ $exercise->name }}</div>
|
<div class="absolute bottom-3 left-3 inline-flex items-center rounded-full bg-white/90 px-3 py-1 text-xs font-semibold text-gray-900 shadow">{{ $exercise->display_name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="mt-1 flex flex-wrap gap-2">
|
<div class="mt-1 flex flex-wrap gap-2">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<x-app-layout>
|
<x-app-layout>
|
||||||
<section class="relative overflow-hidden">
|
<section class="relative overflow-hidden">
|
||||||
<div class="absolute inset-0">
|
<div class="absolute inset-0">
|
||||||
<img src="{{ $exercise->gif_url ?: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?q=80&w=1920&auto=format&fit=crop' }}" alt="{{ $exercise->name }}" class="h-full w-full object-cover">
|
<img src="{{ $exercise->gif_url ?: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?q=80&w=1920&auto=format&fit=crop' }}" alt="{{ $exercise->display_name }}" class="h-full w-full object-cover">
|
||||||
<div class="absolute inset-0 bg-gradient-to-b from-indigo-900/70 via-indigo-800/60 to-transparent"></div>
|
<div class="absolute inset-0 bg-gradient-to-b from-indigo-900/70 via-indigo-800/60 to-transparent"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
|
<div class="relative mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
|
||||||
@@ -9,8 +9,9 @@
|
|||||||
← Back
|
← Back
|
||||||
</a>
|
</a>
|
||||||
<div class="mt-3 flex items-start justify-between gap-4">
|
<div class="mt-3 flex items-start justify-between gap-4">
|
||||||
<h1 class="text-3xl sm:text-5xl font-extrabold tracking-tight text-white drop-shadow">{{ $exercise->name }}</h1>
|
<h1 class="text-3xl sm:text-5xl font-extrabold tracking-tight text-white drop-shadow">{{ $exercise->display_name }}</h1>
|
||||||
@auth
|
@auth
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
@php $isFav = auth()->user()->hasFavoritedExercise($exercise); @endphp
|
@php $isFav = auth()->user()->hasFavoritedExercise($exercise); @endphp
|
||||||
<form method="POST" action="{{ route('exercises.favorite', $exercise) }}">
|
<form method="POST" action="{{ route('exercises.favorite', $exercise) }}">
|
||||||
@csrf
|
@csrf
|
||||||
@@ -25,6 +26,17 @@
|
|||||||
<span>{{ $isFav ? 'Favorited' : 'Favorite' }}</span>
|
<span>{{ $isFav ? 'Favorited' : 'Favorite' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@php $alias = optional($exercise->aliases->first())->name ?? ''; @endphp
|
||||||
|
<form method="POST" action="{{ route('exercises.alias', $exercise) }}" class="hidden sm:flex items-center gap-2 bg-white/90 rounded-full px-2 py-1.5 shadow">
|
||||||
|
@csrf
|
||||||
|
<input type="text" name="name" value="{{ old('name', $alias) }}" placeholder="{{ __('app.exercises.rename_placeholder') }}" class="w-40 sm:w-56 bg-transparent text-sm text-gray-900 placeholder-gray-500 focus:outline-none">
|
||||||
|
<button type="submit" class="text-xs font-semibold text-indigo-600 hover:text-indigo-500">{{ __('app.exercises.rename_save') }}</button>
|
||||||
|
@if($alias !== '')
|
||||||
|
<button type="submit" name="name" value="" class="text-xs text-gray-500 hover:text-gray-700">{{ __('app.exercises.rename_reset') }}</button>
|
||||||
|
@endif
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@endauth
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex flex-wrap items-center gap-2">
|
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||||||
@@ -106,9 +118,9 @@
|
|||||||
@foreach($similarExercises as $ex)
|
@foreach($similarExercises as $ex)
|
||||||
<a href="{{ route('exercises.show', $ex->slug) }}" class="group rounded-3xl overflow-hidden bg-white dark:bg-gray-800 ring-1 ring-gray-200 dark:ring-gray-700 shadow-sm hover:shadow-xl transition">
|
<a href="{{ route('exercises.show', $ex->slug) }}" class="group rounded-3xl overflow-hidden bg-white dark:bg-gray-800 ring-1 ring-gray-200 dark:ring-gray-700 shadow-sm hover:shadow-xl transition">
|
||||||
<div class="relative aspect-[16/9] overflow-hidden">
|
<div class="relative aspect-[16/9] overflow-hidden">
|
||||||
<img src="{{ $ex->gif_url ?: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?q=80&w=1200&auto=format&fit=crop' }}" alt="{{ $ex->name }}" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105">
|
<img src="{{ $ex->gif_url ?: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?q=80&w=1200&auto=format&fit=crop' }}" alt="{{ $ex->display_name }}" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105">
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
||||||
<div class="absolute bottom-3 left-3 inline-flex items-center rounded-full bg-white/90 px-3 py-1 text-xs font-semibold text-gray-900 shadow">{{ $ex->name }}</div>
|
<div class="absolute bottom-3 left-3 inline-flex items-center rounded-full bg-white/90 px-3 py-1 text-xs font-semibold text-gray-900 shadow">{{ $ex->display_name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="mt-1 flex flex-wrap gap-2">
|
<div class="mt-1 flex flex-wrap gap-2">
|
||||||
|
|||||||
@@ -110,6 +110,88 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Features -->
|
||||||
|
<section class="relative bg-white dark:bg-gray-900 py-12 sm:py-16 border-t border-gray-100 dark:border-gray-800">
|
||||||
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center max-w-3xl mx-auto">
|
||||||
|
<h2 class="text-2xl sm:text-3xl font-extrabold text-gray-900 dark:text-white">{{ __('app.home.features.title') }}</h2>
|
||||||
|
<p class="mt-2 text-gray-600 dark:text-gray-300">{{ __('app.home.features.subtitle') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-10 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
@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)
|
||||||
|
<div class="rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 p-6">
|
||||||
|
<div class="h-10 w-10 rounded-lg bg-gradient-to-br from-fuchsia-500 to-indigo-600 text-white flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-6 w-6">
|
||||||
|
<path d="{{ $icons[$key] }}" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">{{ __('app.home.features.items.' . $key . '.title') }}</h3>
|
||||||
|
<p class="mt-1 text-gray-600 dark:text-gray-300">{{ __('app.home.features.items.' . $key . '.desc') }}</p>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Pricing -->
|
||||||
|
<section class="relative bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-900/60 py-12 sm:py-16 border-t border-gray-100 dark:border-gray-800">
|
||||||
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center max-w-3xl mx-auto">
|
||||||
|
<h2 class="text-2xl sm:text-3xl font-extrabold text-gray-900 dark:text-white">{{ __('app.home.pricing.title') }}</h2>
|
||||||
|
<p class="mt-2 text-gray-600 dark:text-gray-300">{{ __('app.home.pricing.subtitle') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-10 grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
@php $plans = ['free','pro','ultimate']; @endphp
|
||||||
|
@foreach($plans as $plan)
|
||||||
|
<div class="rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 p-6 flex flex-col">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ __('app.home.pricing.plans.' . $plan . '.name') }}</h3>
|
||||||
|
<div class="mt-1 text-3xl font-extrabold text-gray-900 dark:text-white">{{ __('app.home.pricing.plans.' . $plan . '.price') }} <span class="text-base font-medium text-gray-600 dark:text-gray-300">{{ __('app.home.pricing.plans.' . $plan . '.period') }}</span></div>
|
||||||
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">{{ __('app.home.pricing.plans.' . $plan . '.desc') }}</p>
|
||||||
|
</div>
|
||||||
|
<ul class="mt-4 space-y-2 text-sm text-gray-700 dark:text-gray-300 flex-1">
|
||||||
|
@for($i=1;$i<=5;$i++)
|
||||||
|
@php $key = 'feature'.$i; @endphp
|
||||||
|
@if(trans()->has('app.home.pricing.plans.' . $plan . '.features.' . $key))
|
||||||
|
<li class="flex items-start gap-2"><span class="text-emerald-500">✓</span><span>{{ __('app.home.pricing.plans.' . $plan . '.features.' . $key) }}</span></li>
|
||||||
|
@endif
|
||||||
|
@endfor
|
||||||
|
</ul>
|
||||||
|
<div class="mt-6">
|
||||||
|
<a href="{{ route('register') }}" class="inline-flex items-center rounded-xl bg-gradient-to-r from-fuchsia-600 to-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow hover:from-fuchsia-500 hover:to-indigo-500">{{ __('app.home.pricing.cta') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FAQ -->
|
||||||
|
<section class="relative bg-white dark:bg-gray-900 py-12 sm:py-16 border-t border-gray-100 dark:border-gray-800">
|
||||||
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center max-w-3xl mx-auto">
|
||||||
|
<h2 class="text-2xl sm:text-3xl font-extrabold text-gray-900 dark:text-white">{{ __('app.home.faq.title') }}</h2>
|
||||||
|
<p class="mt-2 text-gray-600 dark:text-gray-300">{{ __('app.home.faq.subtitle') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
@foreach([1,2,3,4,5,6] as $i)
|
||||||
|
<div class="rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 p-6">
|
||||||
|
<h3 class="text-base font-semibold text-gray-900 dark:text-white">{{ __('app.home.faq.q' . $i) }}</h3>
|
||||||
|
<p class="mt-2 text-gray-600 dark:text-gray-300">{{ __('app.home.faq.a' . $i) }}</p>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="border-t border-gray-200 dark:border-gray-800">
|
<footer class="border-t border-gray-200 dark:border-gray-800">
|
||||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
<a href="{{ route('yoga.index') }}" class="text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">{{ __('app.nav.yoga') }}</a>
|
<a href="{{ route('yoga.index') }}" class="text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">{{ __('app.nav.yoga') }}</a>
|
||||||
<a href="{{ route('workouts.index', [], false) }}" class="text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">{{ __('app.nav.workouts') }}</a>
|
<a href="{{ route('workouts.index', [], false) }}" class="text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">{{ __('app.nav.workouts') }}</a>
|
||||||
<a href="{{ route('blog.index', [], false) }}" class="text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">{{ __('app.nav.blog') }}</a>
|
<a href="{{ route('blog.index', [], false) }}" class="text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">{{ __('app.nav.blog') }}</a>
|
||||||
|
@auth
|
||||||
|
<a href="{{ route('dashboard') }}" class="text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">{{ __('app.nav.dashboard') }}</a>
|
||||||
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -90,6 +93,9 @@
|
|||||||
<x-responsive-nav-link :href="route('yoga.index')">{{ __('app.nav.yoga') }}</x-responsive-nav-link>
|
<x-responsive-nav-link :href="route('yoga.index')">{{ __('app.nav.yoga') }}</x-responsive-nav-link>
|
||||||
<x-responsive-nav-link :href="route('workouts.index', [], false)">{{ __('app.nav.workouts') }}</x-responsive-nav-link>
|
<x-responsive-nav-link :href="route('workouts.index', [], false)">{{ __('app.nav.workouts') }}</x-responsive-nav-link>
|
||||||
<x-responsive-nav-link :href="route('blog.index', [], false)">{{ __('app.nav.blog') }}</x-responsive-nav-link>
|
<x-responsive-nav-link :href="route('blog.index', [], false)">{{ __('app.nav.blog') }}</x-responsive-nav-link>
|
||||||
|
@auth
|
||||||
|
<x-responsive-nav-link :href="route('dashboard')">{{ __('app.nav.dashboard') }}</x-responsive-nav-link>
|
||||||
|
@endauth
|
||||||
<x-responsive-nav-link :href="route('workouts.builder')">{{ __('app.nav.build_workout') }}</x-responsive-nav-link>
|
<x-responsive-nav-link :href="route('workouts.builder')">{{ __('app.nav.build_workout') }}</x-responsive-nav-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-800">
|
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div>
|
<div>
|
||||||
<div class="exercise-title text-base font-semibold {{ $isDone ? 'line-through text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-white' }}">{{ $item->exercise->name }}</div>
|
<div class="exercise-title text-base font-semibold {{ $isDone ? 'line-through text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-white' }}">{{ $item->exercise->display_name }}</div>
|
||||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||||
@if($item->sets) {{ $item->sets }} {{ __('app.sessions.sets') }} @endif
|
@if($item->sets) {{ $item->sets }} {{ __('app.sessions.sets') }} @endif
|
||||||
@if($item->reps) · {{ $item->reps }} {{ __('app.sessions.reps') }} @endif
|
@if($item->reps) · {{ $item->reps }} {{ __('app.sessions.reps') }} @endif
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-gray-500">#{{ $item->position }}</div>
|
<div class="text-sm text-gray-500">#{{ $item->position }}</div>
|
||||||
<div class="text-base font-semibold text-gray-900 dark:text-white">{{ $item->exercise->name }}</div>
|
<div class="text-base font-semibold text-gray-900 dark:text-white">{{ $item->exercise->display_name }}</div>
|
||||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||||
@if($item->sets) {{ $item->sets }} sets @endif
|
@if($item->sets) {{ $item->sets }} sets @endif
|
||||||
@if($item->reps) · {{ $item->reps }} reps @endif
|
@if($item->reps) · {{ $item->reps }} reps @endif
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Exercise</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Exercise</label>
|
||||||
<select name="exercise_id" required class="mt-1 w-full rounded-xl border-gray-300 dark:border-gray-700 bg-white/80 dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2">
|
<select name="exercise_id" required class="mt-1 w-full rounded-xl border-gray-300 dark:border-gray-700 bg-white/80 dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2">
|
||||||
@foreach($exercises as $e)
|
@foreach($exercises as $e)
|
||||||
<option value="{{ $e->id }}">{{ $e->name }}</option>
|
<option value="{{ $e->id }}">{{ $e->display_name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ Route::middleware('auth')->group(function () {
|
|||||||
// Favorites
|
// Favorites
|
||||||
Route::post('/exercises/{exercise}/favorite', [\App\Http\Controllers\FavoriteController::class, 'toggleExercise'])->name('exercises.favorite');
|
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');
|
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 () {
|
Route::prefix('/exercises')->group(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user