loads of progress

This commit is contained in:
2025-10-04 19:40:13 +03:00
parent 76cfd9fee6
commit 07148700a5
14 changed files with 443 additions and 28 deletions
@@ -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 -3
View File
@@ -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)) {
+22
View File
@@ -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;
}
}
+28
View File
@@ -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');
}
};
+97 -1
View File
@@ -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' => [
+97 -1
View File
@@ -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' => [
+2 -2
View File
@@ -90,9 +90,9 @@
@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">
<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 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 class="p-4">
<div class="mt-1 flex flex-wrap gap-2">
+30 -18
View File
@@ -1,7 +1,7 @@
<x-app-layout>
<section class="relative overflow-hidden">
<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>
<div class="relative mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
@@ -9,22 +9,34 @@
&#8592; Back
</a>
<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
@php $isFav = auth()->user()->hasFavoritedExercise($exercise); @endphp
<form method="POST" action="{{ route('exercises.favorite', $exercise) }}">
@csrf
<button type="submit" class="inline-flex items-center gap-2 rounded-full bg-white/90 px-3 py-1.5 text-sm font-semibold text-gray-900 shadow hover:bg-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 {{ $isFav ? 'text-rose-600' : 'text-gray-400' }}">
@if($isFav)
<path d="M11.645 20.91l-.007-.003-.022-.01a15.247 15.247 0 01-.383-.18 25.18 25.18 0 01-4.244-2.686C4.688 15.734 2.25 13.153 2.25 9.75 2.25 7.005 4.46 4.875 7.125 4.875c1.6 0 3.09.744 4.125 1.917a5.625 5.625 0 014.125-1.917c2.665 0 4.875 2.13 4.875 4.875 0 3.403-2.438 5.984-4.739 7.58a25.175 25.175 0 01-4.244 2.685 15.247 15.247 0 01-.383.18l-.022.01-.007.004-.003.001a.75.75 0 01-.592 0l-.003-.001z"/>
@else
<path fill-rule="evenodd" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 6.01 4.01 4 6.5 4c1.74 0 3.41.81 4.5 2.09A6.004 6.004 0 0115.5 4C17.99 4 20 6.01 20 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" clip-rule="evenodd"/>
@endif
</svg>
<span>{{ $isFav ? 'Favorited' : 'Favorite' }}</span>
</button>
</form>
<div class="flex items-center gap-3">
@php $isFav = auth()->user()->hasFavoritedExercise($exercise); @endphp
<form method="POST" action="{{ route('exercises.favorite', $exercise) }}">
@csrf
<button type="submit" class="inline-flex items-center gap-2 rounded-full bg-white/90 px-3 py-1.5 text-sm font-semibold text-gray-900 shadow hover:bg-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 {{ $isFav ? 'text-rose-600' : 'text-gray-400' }}">
@if($isFav)
<path d="M11.645 20.91l-.007-.003-.022-.01a15.247 15.247 0 01-.383-.18 25.18 25.18 0 01-4.244-2.686C4.688 15.734 2.25 13.153 2.25 9.75 2.25 7.005 4.46 4.875 7.125 4.875c1.6 0 3.09.744 4.125 1.917a5.625 5.625 0 014.125-1.917c2.665 0 4.875 2.13 4.875 4.875 0 3.403-2.438 5.984-4.739 7.58a25.175 25.175 0 01-4.244 2.685 15.247 15.247 0 01-.383.18l-.022.01-.007.004-.003.001a.75.75 0 01-.592 0l-.003-.001z"/>
@else
<path fill-rule="evenodd" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 6.01 4.01 4 6.5 4c1.74 0 3.41.81 4.5 2.09A6.004 6.004 0 0115.5 4C17.99 4 20 6.01 20 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" clip-rule="evenodd"/>
@endif
</svg>
<span>{{ $isFav ? 'Favorited' : 'Favorite' }}</span>
</button>
</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
</div>
<div class="mt-4 flex flex-wrap items-center gap-2">
@@ -106,9 +118,9 @@
@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">
<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 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 class="p-4">
<div class="mt-1 flex flex-wrap gap-2">
+82
View File
@@ -110,6 +110,88 @@
</div>
</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 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">
@@ -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('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>
@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>
@@ -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('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>
@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>
</div>
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-800">
+1 -1
View File
@@ -38,7 +38,7 @@
</button>
</form>
<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">
@if($item->sets) {{ $item->sets }} {{ __('app.sessions.sets') }} @endif
@if($item->reps) · {{ $item->reps }} {{ __('app.sessions.reps') }} @endif
+2 -2
View File
@@ -69,7 +69,7 @@
<div class="flex items-start justify-between gap-4">
<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">
@if($item->sets) {{ $item->sets }} sets @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>
<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)
<option value="{{ $e->id }}">{{ $e->name }}</option>
<option value="{{ $e->id }}">{{ $e->display_name }}</option>
@endforeach
</select>
</div>
+3
View File
@@ -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 () {