diff --git a/app/Console/Commands/FetchYogaDataCommand.php b/app/Console/Commands/FetchYogaDataCommand.php new file mode 100644 index 0000000..181f7ce --- /dev/null +++ b/app/Console/Commands/FetchYogaDataCommand.php @@ -0,0 +1,103 @@ +info('Fetching: ' . $url); + + $response = Http::timeout(30)->acceptJson()->get($url); + if (!$response->ok()) { + $this->error('Failed to fetch data: ' . $response->status()); + return self::FAILURE; + } + + $data = $response->json(); + if (!is_array($data)) { + $this->error('Unexpected response format'); + return self::FAILURE; + } + + $countCategories = 0; $countPoses = 0; + + foreach ($data as $cat) { + $extId = (int) ($cat['id'] ?? 0); + if ($extId <= 0) { continue; } + + $category = YogaCategory::updateOrCreate( + ['external_id' => $extId], + [ + 'name' => (string) ($cat['category_name'] ?? 'Category ' . $extId), + 'description' => (string) ($cat['category_description'] ?? ''), + ] + ); + $countCategories++; + + $poses = $cat['poses'] ?? []; + if (!is_array($poses)) { $poses = []; } + + foreach ($poses as $pose) { + $pExt = (int) ($pose['id'] ?? 0); + if ($pExt <= 0) { continue; } + + $payload = [ + 'category_id' => $category->id, + 'english_name' => (string) ($pose['english_name'] ?? ''), + 'sanskrit_name_adapted' => (string) ($pose['sanskrit_name_adapted'] ?? ''), + 'sanskrit_name' => (string) ($pose['sanskrit_name'] ?? ''), + 'translation_name' => (string) ($pose['translation_name'] ?? ''), + 'description' => (string) ($pose['pose_description'] ?? ''), + 'benefits' => (string) ($pose['pose_benefits'] ?? ''), + 'url_svg' => (string) ($pose['url_svg'] ?? ''), + 'url_png' => (string) ($pose['url_png'] ?? ''), + 'url_svg_alt' => (string) ($pose['url_svg_alt'] ?? ''), + ]; + + $model = YogaPose::updateOrCreate( + ['external_id' => $pExt], + $payload + [ + // slug may be regenerated with --force + 'slug' => $this->makeSlug($payload['english_name'], $pExt, $category->name), + ] + ); + + if ($this->option('force')) { + $model->slug = $this->makeSlug($payload['english_name'], $pExt, $category->name, true); + $model->save(); + } + + $countPoses++; + } + } + + $this->info("Imported {$countCategories} categories, {$countPoses} poses."); + return self::SUCCESS; + } + + protected function makeSlug(string $englishName, int $externalId, string $categoryName = '', bool $force = false): string + { + $base = Str::slug($englishName !== '' ? $englishName : ('pose-' . $externalId)); + if ($base === '') { $base = 'pose-' . $externalId; } + $slug = $base; + $i = 1; + while (YogaPose::where('slug', $slug)->exists()) { + // If force-regenerating we allow different category-based suffix to reduce collisions + $suffix = $force && $categoryName ? '-' . Str::slug($categoryName) : ''; + $slug = $base . $suffix . '-' . (++$i); + } + return $slug; + } +} diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php new file mode 100644 index 0000000..6413e9e --- /dev/null +++ b/app/Http/Controllers/DashboardController.php @@ -0,0 +1,61 @@ +workouts()->count(); + $sessionsQuery = WorkoutSession::where('user_id', $user->id); + $sessionsCount = (clone $sessionsQuery)->count(); + $completedSessions = (clone $sessionsQuery)->whereNotNull('completed_at')->count(); + $completionRate = $sessionsCount > 0 ? round(($completedSessions / $sessionsCount) * 100) : 0; + + // Recent sessions with progress + $recentSessions = (clone $sessionsQuery) + ->latest('started_at') + ->latest('id') + ->with(['workout:id,title', 'items:id,session_id,done']) + ->limit(5) + ->get() + ->map(function ($s) { + $total = $s->items->count(); + $done = $s->items->where('done', true)->count(); + $pct = $total > 0 ? (int) round(($done / $total) * 100) : 0; + return [ + 'id' => $s->id, + 'workout_title' => optional($s->workout)->title ?? 'Workout', + 'started_at' => $s->started_at, + 'completed_at' => $s->completed_at, + 'done' => $done, + 'total' => $total, + 'pct' => $pct, + ]; + }); + + // Activity in the last 30 days (number of sessions per day) + $since = now()->subDays(29)->startOfDay(); + $dailyActivity = (clone $sessionsQuery) + ->where('created_at', '>=', $since) + ->get(['id', 'created_at']) + ->groupBy(function ($s) { return $s->created_at->format('Y-m-d'); }) + ->map->count(); + + return view('dashboard', compact( + 'workoutsCount', + 'sessionsCount', + 'completedSessions', + 'completionRate', + 'recentSessions', + 'dailyActivity' + )); + } +} diff --git a/app/Http/Controllers/ExerciseController.php b/app/Http/Controllers/ExerciseController.php index 46f21c5..e7a600b 100644 --- a/app/Http/Controllers/ExerciseController.php +++ b/app/Http/Controllers/ExerciseController.php @@ -69,8 +69,40 @@ 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']); + + // Find similar exercises by shared target muscles, body parts, or equipment + $targetIds = $exercise->targetMuscles->pluck('id')->all(); + $bodyPartIds = $exercise->bodyParts->pluck('id')->all(); + $equipmentIds = $exercise->equipments->pluck('id')->all(); + + $similarQuery = Exercise::query() + ->with(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'equipments:id,name,slug']) + ->where('id', '!=', $exercise->id) + ->where(function ($q) use ($targetIds, $bodyPartIds, $equipmentIds) { + if (!empty($targetIds)) { + $q->orWhereHas('targetMuscles', function ($rel) use ($targetIds) { + $rel->whereIn('id', $targetIds); + }); + } + if (!empty($bodyPartIds)) { + $q->orWhereHas('bodyParts', function ($rel) use ($bodyPartIds) { + $rel->whereIn('id', $bodyPartIds); + }); + } + if (!empty($equipmentIds)) { + $q->orWhereHas('equipments', function ($rel) use ($equipmentIds) { + $rel->whereIn('id', $equipmentIds); + }); + } + }) + ->orderBy('name') + ->limit(9); + + $similarExercises = $similarQuery->get(); + return view('exercises.show', [ 'exercise' => $exercise, + 'similarExercises' => $similarExercises, ]); } } diff --git a/app/Http/Controllers/WorkoutSessionController.php b/app/Http/Controllers/WorkoutSessionController.php new file mode 100644 index 0000000..deb26b3 --- /dev/null +++ b/app/Http/Controllers/WorkoutSessionController.php @@ -0,0 +1,95 @@ +authorizeWorkout($workout); + + $session = WorkoutSession::create([ + 'user_id' => Auth::id(), + 'workout_id' => $workout->id, + 'started_at' => now(), + ]); + + // Pre-create items for convenience + $workout->loadMissing('items'); + foreach ($workout->items as $item) { + WorkoutSessionItem::create([ + 'session_id' => $session->id, + 'workout_exercise_id' => $item->id, + 'done' => false, + ]); + } + + return redirect()->route('sessions.show', $session); + } + + public function show(WorkoutSession $session): View + { + $this->authorizeSession($session); + $session->load(['workout.items.exercise', 'items']); + + // Index session items by workout_exercise_id for quick lookup + $doneByItemId = $session->items->keyBy('workout_exercise_id'); + + return view('workouts.session', [ + 'session' => $session, + 'workout' => $session->workout, + 'doneByItemId' => $doneByItemId, + ]); + } + + public function toggleItem(Request $request, WorkoutSession $session, WorkoutExercise $item): RedirectResponse + { + $this->authorizeSession($session); + if ($item->workout_id !== $session->workout_id) { + abort(404); + } + + $si = WorkoutSessionItem::firstOrCreate([ + 'session_id' => $session->id, + 'workout_exercise_id' => $item->id, + ], [ + 'done' => false, + ]); + + $si->done = !$si->done; + $si->save(); + + return back(); + } + + public function complete(WorkoutSession $session): RedirectResponse + { + $this->authorizeSession($session); + $session->completed_at = now(); + $session->save(); + return redirect()->route('workouts.show', $session->workout)->with('status', __('app.sessions.completed')); + } + + protected function authorizeWorkout(Workout $workout): void + { + if ($workout->user_id !== Auth::id()) { + abort(403); + } + } + + protected function authorizeSession(WorkoutSession $session): void + { + if ($session->user_id !== Auth::id()) { + abort(403); + } + } +} diff --git a/app/Http/Controllers/YogaPoseController.php b/app/Http/Controllers/YogaPoseController.php new file mode 100644 index 0000000..b2a464a --- /dev/null +++ b/app/Http/Controllers/YogaPoseController.php @@ -0,0 +1,84 @@ +query('q', '')); + $category = (int) $request->query('category', 0); + $benefit = trim((string) $request->query('benefit', '')); + $perPage = (int) $request->query('per_page', 12); + if ($perPage < 6 || $perPage > 60) { $perPage = 12; } + + $query = YogaPose::query()->with('category') + ->when($q !== '', function ($qb) use ($q) { + $qb->where(function ($sub) use ($q) { + $sub->where('english_name', 'like', "%{$q}%") + ->orWhere('sanskrit_name', 'like', "%{$q}%") + ->orWhere('sanskrit_name_adapted', 'like', "%{$q}%"); + }); + }) + ->when($category > 0, function ($qb) use ($category) { + $qb->where('category_id', $category); + }) + ->when($benefit !== '', function ($qb) use ($benefit) { + $qb->where('benefits', 'like', "%{$benefit}%"); + }) + ->orderBy('english_name'); + + $poses = $query->paginate($perPage)->withQueryString(); + $categories = YogaCategory::orderBy('name')->get(['id','name']); + + return view('yoga.index', [ + 'poses' => $poses, + 'filters' => [ + 'q' => $q, + 'category' => $category, + 'benefit' => $benefit, + 'per_page' => $perPage, + ], + 'categories' => $categories, + ]); + } + + public function show(YogaPose $pose): View + { + $pose->load('category'); + + // Find similar poses by same category or overlapping benefits keywords + $benefits = (string) $pose->benefits; + $tokens = collect(preg_split('/[^\p{L}]+/u', mb_strtolower($benefits))) + ->filter(fn($t) => $t !== null && $t !== '' && mb_strlen($t) >= 4) + ->unique() + ->take(8) + ->values() + ->all(); + + $similarQuery = YogaPose::query() + ->with('category') + ->where('id', '!=', $pose->id) + ->where(function ($q) use ($pose, $tokens) { + // Same category + if ($pose->category_id) { + $q->orWhere('category_id', $pose->category_id); + } + // Overlapping benefits terms + foreach ($tokens as $tk) { + $q->orWhere('benefits', 'like', "%{$tk}%"); + } + }) + ->orderBy('english_name') + ->limit(9); + + $similarPoses = $similarQuery->get(); + + return view('yoga.show', compact('pose', 'similarPoses')); + } +} diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php new file mode 100644 index 0000000..14f1668 --- /dev/null +++ b/app/Http/Middleware/SetLocale.php @@ -0,0 +1,29 @@ +query('lang'); + if ($locale && in_array($locale, ['en', 'ro'], true)) { + session(['locale' => $locale]); + } + + $current = session('locale', config('app.locale')); + App::setLocale($current); + + return $next($request); + } +} diff --git a/app/Models/WorkoutSession.php b/app/Models/WorkoutSession.php new file mode 100644 index 0000000..39e4c59 --- /dev/null +++ b/app/Models/WorkoutSession.php @@ -0,0 +1,40 @@ + 'datetime', + 'completed_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function workout(): BelongsTo + { + return $this->belongsTo(Workout::class); + } + + public function items(): HasMany + { + return $this->hasMany(WorkoutSessionItem::class, 'session_id'); + } +} diff --git a/app/Models/WorkoutSessionItem.php b/app/Models/WorkoutSessionItem.php new file mode 100644 index 0000000..6fa71f2 --- /dev/null +++ b/app/Models/WorkoutSessionItem.php @@ -0,0 +1,32 @@ + 'boolean', + ]; + + public function session(): BelongsTo + { + return $this->belongsTo(WorkoutSession::class, 'session_id'); + } + + public function workoutExercise(): BelongsTo + { + return $this->belongsTo(WorkoutExercise::class); + } +} diff --git a/app/Models/YogaCategory.php b/app/Models/YogaCategory.php new file mode 100644 index 0000000..21a9f8e --- /dev/null +++ b/app/Models/YogaCategory.php @@ -0,0 +1,23 @@ +hasMany(YogaPose::class, 'category_id'); + } +} diff --git a/app/Models/YogaPose.php b/app/Models/YogaPose.php new file mode 100644 index 0000000..0dda379 --- /dev/null +++ b/app/Models/YogaPose.php @@ -0,0 +1,32 @@ +belongsTo(YogaCategory::class, 'category_id'); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..0ddfb23 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use App\Http\Middleware\SetLocale; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -11,7 +12,9 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->web([ + SetLocale::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/database/migrations/2025_10_04_000001_create_workout_sessions_table.php b/database/migrations/2025_10_04_000001_create_workout_sessions_table.php new file mode 100644 index 0000000..e1e483a --- /dev/null +++ b/database/migrations/2025_10_04_000001_create_workout_sessions_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('workout_id')->constrained()->cascadeOnDelete(); + $table->timestamp('started_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + + $table->index(['user_id', 'workout_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('workout_sessions'); + } +}; diff --git a/database/migrations/2025_10_04_000002_create_workout_session_items_table.php b/database/migrations/2025_10_04_000002_create_workout_session_items_table.php new file mode 100644 index 0000000..d406b48 --- /dev/null +++ b/database/migrations/2025_10_04_000002_create_workout_session_items_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignId('session_id')->constrained('workout_sessions')->cascadeOnDelete(); + $table->foreignId('workout_exercise_id')->constrained()->cascadeOnDelete(); + $table->boolean('done')->default(false); + $table->timestamps(); + + $table->unique(['session_id', 'workout_exercise_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('workout_session_items'); + } +}; diff --git a/database/migrations/2025_10_04_100000_create_yoga_categories_table.php b/database/migrations/2025_10_04_100000_create_yoga_categories_table.php new file mode 100644 index 0000000..6411e89 --- /dev/null +++ b/database/migrations/2025_10_04_100000_create_yoga_categories_table.php @@ -0,0 +1,23 @@ +id(); + $table->unsignedBigInteger('external_id')->unique(); + $table->string('name'); + $table->text('description')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('yoga_categories'); + } +}; diff --git a/database/migrations/2025_10_04_100010_create_yoga_poses_table.php b/database/migrations/2025_10_04_100010_create_yoga_poses_table.php new file mode 100644 index 0000000..5389b22 --- /dev/null +++ b/database/migrations/2025_10_04_100010_create_yoga_poses_table.php @@ -0,0 +1,34 @@ +id(); + $table->unsignedBigInteger('external_id')->unique(); + $table->foreignId('category_id')->constrained('yoga_categories')->cascadeOnDelete(); + $table->string('slug')->unique(); + $table->string('english_name'); + $table->string('sanskrit_name_adapted')->nullable(); + $table->string('sanskrit_name')->nullable(); + $table->string('translation_name')->nullable(); + $table->text('description')->nullable(); + $table->text('benefits')->nullable(); + $table->string('url_svg')->nullable(); + $table->string('url_png')->nullable(); + $table->string('url_svg_alt')->nullable(); + $table->timestamps(); + + $table->index(['category_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('yoga_poses'); + } +}; diff --git a/lang/en/app.php b/lang/en/app.php new file mode 100644 index 0000000..e05ebc5 --- /dev/null +++ b/lang/en/app.php @@ -0,0 +1,164 @@ + [ + 'exercises' => 'Exercises', + 'yoga' => 'Yoga', + 'workouts' => 'Workouts', + 'blog' => 'Blog', + 'build_workout' => 'Build a Workout', + 'profile' => 'Profile', + 'logout' => 'Log Out', + 'login' => 'Log in', + 'register' => 'Sign up', + 'enable_dark' => 'Enable Dark Mode', + 'disable_dark' => 'Disable Dark Mode', + 'language' => 'Language', + 'english' => 'English', + 'romanian' => 'Romanian', + ], + + 'home' => [ + 'badge' => 'New • Explore 1,000+ moves', + 'title' => 'Discover 1,000+ Exercises. Build Your Perfect Workout.', + 'subtitle' => 'Explore FitAtlas to find exercises by muscle group and equipment, then craft routines that match your goals.', + 'search_placeholder' => 'Search exercises (e.g. push up, dumbbell, back)', + 'search' => 'Search', + 'filters' => [ + 'arms' => 'Arms', + 'chest' => 'Chest', + 'legs' => 'Legs', + 'core' => 'Core', + 'back' => 'Back', + ], + 'featured' => 'Featured Exercises', + 'browse_all' => 'Browse all', + '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', + 'footer' => [ + 'about' => 'About', + 'privacy' => 'Privacy', + 'terms' => 'Terms', + 'contact' => 'Contact', + ], + ], + + 'workouts' => [ + 'my_workouts' => 'My Workouts', + 'manage_desc' => 'Create routines and manage your exercises.', + 'new_workout' => 'New Workout', + 'none_yet' => 'No workouts yet. Create your first workout.', + 'open' => 'Open', + 'delete' => 'Delete', + 'confirm_delete' => 'Delete this workout?', + + 'create_title' => 'Create Workout', + 'create_desc' => 'Give your workout a name and optional details.', + 'title_label' => 'Title', + 'estimated_minutes' => 'Estimated Minutes', + 'notes' => 'Notes', + 'cancel' => 'Cancel', + 'create_submit' => 'Create Workout', + + 'back_to_workouts' => '← Back to Workouts', + 'favorited' => 'Favorited', + 'favorite' => 'Favorite', + 'min' => 'min', + 'exercises_count' => 'exercises', + 'exercises' => 'Exercises', + 'no_exercises_yet' => 'No exercises yet. Add your first one on the right.', + 'remove_exercise_confirm' => 'Remove this exercise?', + 'remove' => 'Remove', + 'edit' => 'Edit', + 'position_ph' => 'Position', + 'sets' => 'Sets', + 'reps' => 'Reps', + 'weight' => 'Weight (kg)', + 'rest' => 'Rest (s)', + 'save' => 'Save', + 'add_exercise' => 'Add Exercise', + 'exercise_label' => 'Exercise', + 'add_exercise_submit' => 'Add Exercise', + 'rest_suffix' => 's rest', + 'kg' => 'kg', + ], + + 'exercises' => [ + 'browse' => 'Browse Exercises', + 'browse_desc' => 'Explore the full FitAtlas library. Filter by body part, muscle, or equipment.', + 'search' => 'Search', + 'search_placeholder' => 'Search by name or instruction', + 'body_part' => 'Body Part', + 'muscle' => 'Muscle', + 'equipment' => 'Equipment', + 'any' => 'Any', + 'reset' => 'Reset', + 'apply_filters' => 'Apply Filters', + 'showing' => 'Showing', + 'exercises_noun' => 'exercises', + 'per_page' => 'Per page', + 'none_found' => 'No exercises found. Try adjusting your filters.', + 'similar' => 'Similar exercises', + 'similar_none' => 'No similar exercises found.', + ], + + 'sessions' => [ + 'start' => 'Start Session', + 'back_to_workout' => 'Back to workout', + 'progress' => 'Progress', + 'mark_complete' => 'Mark as complete', + 'mark_complete_confirm' => 'Mark this session as complete?', + 'no_items' => 'No exercises in this workout.', + 'sets' => 'sets', + 'reps' => 'reps', + 'rest' => 'rest', + 'completed' => 'Workout session completed! Great job!' + ], + + 'dashboard' => [ + 'title' => 'Your Dashboard', + 'subtitle' => 'See your workouts and training progress at a glance.', + 'metrics' => [ + 'workouts' => 'Workouts', + 'sessions' => 'Sessions', + 'completed' => 'Completed', + 'completion_rate' => 'Completion rate', + ], + 'recent_sessions' => 'Recent Sessions', + 'no_sessions' => 'No sessions yet. Start a workout to track your progress.', + 'completed' => 'Completed', + 'view_session' => 'View session', + 'activity_30d' => 'Last 30 Days Activity', + 'activity_30d_help' => 'Number of sessions per day (higher is better).', + ], + + 'yoga' => [ + 'browse' => 'Browse Yoga Poses', + 'browse_desc' => 'Explore yoga poses by category or benefits.', + 'search' => 'Search', + 'search_placeholder' => 'Search by English or Sanskrit name', + 'category' => 'Category', + 'benefit' => 'Benefit contains', + 'benefit_placeholder' => 'e.g. stress, digestion', + 'any' => 'Any', + 'reset' => 'Reset', + 'apply_filters' => 'Apply Filters', + 'showing' => 'Showing', + 'poses_noun' => 'poses', + 'none_found' => 'No poses found. Try adjusting your filters.', + 'no_image' => 'No image', + 'back' => 'Back', + 'description' => 'Description', + 'no_description' => 'No description available.', + 'benefits' => 'Benefits', + 'no_benefits' => 'No documented benefits.', + 'pose_info' => 'Pose Info', + 'translation' => 'Translation', + 'sanskrit' => 'Sanskrit', + 'sanskrit_adapted' => 'Sanskrit (adapted)', + 'browse_all' => 'Browse all poses', + 'similar' => 'Similar poses', + 'similar_none' => 'No similar poses found.', + ], +]; diff --git a/lang/en/auth.php b/lang/en/auth.php new file mode 100644 index 0000000..6598e2c --- /dev/null +++ b/lang/en/auth.php @@ -0,0 +1,20 @@ + 'These credentials do not match our records.', + 'password' => 'The provided password is incorrect.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + +]; diff --git a/lang/en/pagination.php b/lang/en/pagination.php new file mode 100644 index 0000000..d481411 --- /dev/null +++ b/lang/en/pagination.php @@ -0,0 +1,19 @@ + '« Previous', + 'next' => 'Next »', + +]; diff --git a/lang/en/passwords.php b/lang/en/passwords.php new file mode 100644 index 0000000..fad3a7d --- /dev/null +++ b/lang/en/passwords.php @@ -0,0 +1,22 @@ + 'Your password has been reset.', + 'sent' => 'We have emailed your password reset link.', + 'throttled' => 'Please wait before retrying.', + 'token' => 'This password reset token is invalid.', + 'user' => "We can't find a user with that email address.", + +]; diff --git a/lang/en/validation.php b/lang/en/validation.php new file mode 100644 index 0000000..bd9cc11 --- /dev/null +++ b/lang/en/validation.php @@ -0,0 +1,199 @@ + 'The :attribute field must be accepted.', + 'accepted_if' => 'The :attribute field must be accepted when :other is :value.', + 'active_url' => 'The :attribute field must be a valid URL.', + 'after' => 'The :attribute field must be a date after :date.', + 'after_or_equal' => 'The :attribute field must be a date after or equal to :date.', + 'alpha' => 'The :attribute field must only contain letters.', + 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.', + 'alpha_num' => 'The :attribute field must only contain letters and numbers.', + 'any_of' => 'The :attribute field is invalid.', + 'array' => 'The :attribute field must be an array.', + 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.', + 'before' => 'The :attribute field must be a date before :date.', + 'before_or_equal' => 'The :attribute field must be a date before or equal to :date.', + 'between' => [ + 'array' => 'The :attribute field must have between :min and :max items.', + 'file' => 'The :attribute field must be between :min and :max kilobytes.', + 'numeric' => 'The :attribute field must be between :min and :max.', + 'string' => 'The :attribute field must be between :min and :max characters.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'can' => 'The :attribute field contains an unauthorized value.', + 'confirmed' => 'The :attribute field confirmation does not match.', + 'contains' => 'The :attribute field is missing a required value.', + 'current_password' => 'The password is incorrect.', + 'date' => 'The :attribute field must be a valid date.', + 'date_equals' => 'The :attribute field must be a date equal to :date.', + 'date_format' => 'The :attribute field must match the format :format.', + 'decimal' => 'The :attribute field must have :decimal decimal places.', + 'declined' => 'The :attribute field must be declined.', + 'declined_if' => 'The :attribute field must be declined when :other is :value.', + 'different' => 'The :attribute field and :other must be different.', + 'digits' => 'The :attribute field must be :digits digits.', + 'digits_between' => 'The :attribute field must be between :min and :max digits.', + 'dimensions' => 'The :attribute field has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.', + 'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.', + 'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.', + 'email' => 'The :attribute field must be a valid email address.', + 'ends_with' => 'The :attribute field must end with one of the following: :values.', + 'enum' => 'The selected :attribute is invalid.', + 'exists' => 'The selected :attribute is invalid.', + 'extensions' => 'The :attribute field must have one of the following extensions: :values.', + 'file' => 'The :attribute field must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'array' => 'The :attribute field must have more than :value items.', + 'file' => 'The :attribute field must be greater than :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than :value.', + 'string' => 'The :attribute field must be greater than :value characters.', + ], + 'gte' => [ + 'array' => 'The :attribute field must have :value items or more.', + 'file' => 'The :attribute field must be greater than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than or equal to :value.', + 'string' => 'The :attribute field must be greater than or equal to :value characters.', + ], + 'hex_color' => 'The :attribute field must be a valid hexadecimal color.', + 'image' => 'The :attribute field must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field must exist in :other.', + 'in_array_keys' => 'The :attribute field must contain at least one of the following keys: :values.', + 'integer' => 'The :attribute field must be an integer.', + 'ip' => 'The :attribute field must be a valid IP address.', + 'ipv4' => 'The :attribute field must be a valid IPv4 address.', + 'ipv6' => 'The :attribute field must be a valid IPv6 address.', + 'json' => 'The :attribute field must be a valid JSON string.', + 'list' => 'The :attribute field must be a list.', + 'lowercase' => 'The :attribute field must be lowercase.', + 'lt' => [ + 'array' => 'The :attribute field must have less than :value items.', + 'file' => 'The :attribute field must be less than :value kilobytes.', + 'numeric' => 'The :attribute field must be less than :value.', + 'string' => 'The :attribute field must be less than :value characters.', + ], + 'lte' => [ + 'array' => 'The :attribute field must not have more than :value items.', + 'file' => 'The :attribute field must be less than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be less than or equal to :value.', + 'string' => 'The :attribute field must be less than or equal to :value characters.', + ], + 'mac_address' => 'The :attribute field must be a valid MAC address.', + 'max' => [ + 'array' => 'The :attribute field must not have more than :max items.', + 'file' => 'The :attribute field must not be greater than :max kilobytes.', + 'numeric' => 'The :attribute field must not be greater than :max.', + 'string' => 'The :attribute field must not be greater than :max characters.', + ], + 'max_digits' => 'The :attribute field must not have more than :max digits.', + 'mimes' => 'The :attribute field must be a file of type: :values.', + 'mimetypes' => 'The :attribute field must be a file of type: :values.', + 'min' => [ + 'array' => 'The :attribute field must have at least :min items.', + 'file' => 'The :attribute field must be at least :min kilobytes.', + 'numeric' => 'The :attribute field must be at least :min.', + 'string' => 'The :attribute field must be at least :min characters.', + ], + 'min_digits' => 'The :attribute field must have at least :min digits.', + 'missing' => 'The :attribute field must be missing.', + 'missing_if' => 'The :attribute field must be missing when :other is :value.', + 'missing_unless' => 'The :attribute field must be missing unless :other is :value.', + 'missing_with' => 'The :attribute field must be missing when :values is present.', + 'missing_with_all' => 'The :attribute field must be missing when :values are present.', + 'multiple_of' => 'The :attribute field must be a multiple of :value.', + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute field format is invalid.', + 'numeric' => 'The :attribute field must be a number.', + 'password' => [ + 'letters' => 'The :attribute field must contain at least one letter.', + 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.', + 'numbers' => 'The :attribute field must contain at least one number.', + 'symbols' => 'The :attribute field must contain at least one symbol.', + 'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.', + ], + 'present' => 'The :attribute field must be present.', + 'present_if' => 'The :attribute field must be present when :other is :value.', + 'present_unless' => 'The :attribute field must be present unless :other is :value.', + 'present_with' => 'The :attribute field must be present when :values is present.', + 'present_with_all' => 'The :attribute field must be present when :values are present.', + 'prohibited' => 'The :attribute field is prohibited.', + 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.', + 'prohibited_if_accepted' => 'The :attribute field is prohibited when :other is accepted.', + 'prohibited_if_declined' => 'The :attribute field is prohibited when :other is declined.', + 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.', + 'prohibits' => 'The :attribute field prohibits :other from being present.', + 'regex' => 'The :attribute field format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_array_keys' => 'The :attribute field must contain entries for: :values.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_if_accepted' => 'The :attribute field is required when :other is accepted.', + 'required_if_declined' => 'The :attribute field is required when :other is declined.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute field must match :other.', + 'size' => [ + 'array' => 'The :attribute field must contain :size items.', + 'file' => 'The :attribute field must be :size kilobytes.', + 'numeric' => 'The :attribute field must be :size.', + 'string' => 'The :attribute field must be :size characters.', + ], + 'starts_with' => 'The :attribute field must start with one of the following: :values.', + 'string' => 'The :attribute field must be a string.', + 'timezone' => 'The :attribute field must be a valid timezone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'uppercase' => 'The :attribute field must be uppercase.', + 'url' => 'The :attribute field must be a valid URL.', + 'ulid' => 'The :attribute field must be a valid ULID.', + 'uuid' => 'The :attribute field must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + +]; diff --git a/lang/ro/app.php b/lang/ro/app.php new file mode 100644 index 0000000..f5240ce --- /dev/null +++ b/lang/ro/app.php @@ -0,0 +1,164 @@ + [ + 'exercises' => 'Exerciții', + 'yoga' => 'Yoga', + 'workouts' => 'Antrenamente', + 'blog' => 'Blog', + 'build_workout' => 'Creează un antrenament', + 'profile' => 'Profil', + 'logout' => 'Deconectare', + 'login' => 'Autentificare', + 'register' => 'Înregistrare', + 'enable_dark' => 'Activează Modul Închis', + 'disable_dark' => 'Dezactivează Modul Închis', + 'language' => 'Limba', + 'english' => 'Engleză', + 'romanian' => 'Română', + ], + + 'home' => [ + 'badge' => 'Nou • Descoperă peste 1.000 de mișcări', + 'title' => 'Descoperă peste 1.000 de exerciții. Construiește antrenamentul perfect.', + 'subtitle' => 'Explorează FitAtlas pentru a găsi exerciții după grupă musculară și echipament, apoi creează rutine potrivite obiectivelor tale.', + 'search_placeholder' => 'Caută exerciții (ex: flotări, gantere, spate)', + 'search' => 'Caută', + 'filters' => [ + 'arms' => 'Brațe', + 'chest' => 'Piept', + 'legs' => 'Picioare', + 'core' => 'Trunchi', + 'back' => 'Spate', + ], + 'featured' => 'Exerciții recomandate', + 'browse_all' => 'Vezi toate', + '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ă', + 'footer' => [ + 'about' => 'Despre', + 'privacy' => 'Confidențialitate', + 'terms' => 'Termeni', + 'contact' => 'Contact', + ], + ], + + 'workouts' => [ + 'my_workouts' => 'Antrenamentele mele', + 'manage_desc' => 'Creează rutine și gestionează exercițiile.', + 'new_workout' => 'Antrenament nou', + 'none_yet' => 'Încă nu există antrenamente. Creează primul tău antrenament.', + 'open' => 'Deschide', + 'delete' => 'Șterge', + 'confirm_delete' => 'Ștergi acest antrenament?', + + 'create_title' => 'Creează antrenament', + 'create_desc' => 'Dă un nume antrenamentului și, opțional, adaugă detalii.', + 'title_label' => 'Titlu', + 'estimated_minutes' => 'Minute estimate', + 'notes' => 'Notițe', + 'cancel' => 'Anulează', + 'create_submit' => 'Creează antrenament', + + 'back_to_workouts' => '← Înapoi la Antrenamente', + 'favorited' => 'La favorite', + 'favorite' => 'Adaugă la favorite', + 'min' => 'min', + 'exercises_count' => 'exerciții', + 'exercises' => 'Exerciții', + 'no_exercises_yet' => 'Încă nu există exerciții. Adaugă primul în dreapta.', + 'remove_exercise_confirm' => 'Elimini acest exercițiu?', + 'remove' => 'Elimină', + 'edit' => 'Editează', + 'position_ph' => 'Poziție', + 'sets' => 'Seturi', + 'reps' => 'Repetări', + 'weight' => 'Greutate (kg)', + 'rest' => 'Pauză (s)', + 'save' => 'Salvează', + 'add_exercise' => 'Adaugă exercițiu', + 'exercise_label' => 'Exercițiu', + 'add_exercise_submit' => 'Adaugă exercițiu', + 'rest_suffix' => 's pauză', + 'kg' => 'kg', + ], + + 'exercises' => [ + 'browse' => 'Răsfoiește exercițiile', + 'browse_desc' => 'Explorează întreaga bibliotecă FitAtlas. Filtrează după zonă, mușchi sau echipament.', + 'search' => 'Căutare', + 'search_placeholder' => 'Caută după nume sau instrucțiuni', + 'body_part' => 'Zonă', + 'muscle' => 'Mușchi', + 'equipment' => 'Echipament', + 'any' => 'Oricare', + 'reset' => 'Resetează', + 'apply_filters' => 'Aplică filtrele', + 'showing' => 'Se afișează', + 'exercises_noun' => 'exerciții', + 'per_page' => 'Pe pagină', + '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.', + ], + + 'sessions' => [ + 'start' => 'Începe sesiunea', + 'back_to_workout' => 'Înapoi la antrenament', + 'progress' => 'Progres', + 'mark_complete' => 'Marchează ca finalizată', + 'mark_complete_confirm' => 'Marchezi această sesiune ca finalizată?', + 'no_items' => 'Nu există exerciții în acest antrenament.', + 'sets' => 'seturi', + 'reps' => 'repetări', + 'rest' => 'pauză', + 'completed' => 'Sesiune de antrenament finalizată! Bravo!' + ], + + 'dashboard' => [ + 'title' => 'Tabloul tău de bord', + 'subtitle' => 'Vezi pe scurt antrenamentele și progresul tău.', + 'metrics' => [ + 'workouts' => 'Antrenamente', + 'sessions' => 'Sesiuni', + 'completed' => 'Finalizate', + 'completion_rate' => 'Rată de finalizare', + ], + 'recent_sessions' => 'Sesiuni recente', + 'no_sessions' => 'Nu există sesiuni încă. Începe un antrenament pentru a-ți urmări progresul.', + 'completed' => 'Finalizată', + 'view_session' => 'Vezi sesiunea', + 'activity_30d' => 'Activitate în ultimele 30 de zile', + 'activity_30d_help' => 'Numărul de sesiuni pe zi (mai mult este mai bine).', + ], + + 'yoga' => [ + 'browse' => 'Răsfoiește pozițiile de yoga', + 'browse_desc' => 'Explorează pozițiile de yoga după categorie sau beneficii.', + 'search' => 'Căutare', + 'search_placeholder' => 'Caută după nume în engleză sau sanscrită', + 'category' => 'Categorie', + 'benefit' => 'Beneficiu conține', + 'benefit_placeholder' => 'ex: stres, digestie', + 'any' => 'Oricare', + 'reset' => 'Resetează', + 'apply_filters' => 'Aplică filtrele', + 'showing' => 'Se afișează', + 'poses_noun' => 'poziții', + 'none_found' => 'Nu s-au găsit poziții. Încearcă să ajustezi filtrele.', + 'no_image' => 'Fără imagine', + 'back' => 'Înapoi', + 'description' => 'Descriere', + 'no_description' => 'Nu există descriere disponibilă.', + 'benefits' => 'Beneficii', + 'no_benefits' => 'Nu există beneficii documentate.', + 'pose_info' => 'Informații despre poziție', + 'translation' => 'Traducere', + 'sanskrit' => 'Sanscrită', + 'sanskrit_adapted' => 'Sanscrită (adaptată)', + 'browse_all' => 'Vezi toate pozițiile', + 'similar' => 'Poziții similare', + 'similar_none' => 'Nu s-au găsit poziții similare.', + ], +]; diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 4024c64..8d58be7 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,17 +1,92 @@ - -

- {{ __('Dashboard') }} -

-
- -
-
-
-
- {{ __("You're logged in!") }} +
+
+
+
+

{{ __('app.dashboard.title') }}

+

{{ __('app.dashboard.subtitle') }}

-
+ + +
+
+
+
{{ __('app.dashboard.metrics.workouts') }}
+
{{ $workoutsCount }}
+
+
+
{{ __('app.dashboard.metrics.sessions') }}
+
{{ $sessionsCount }}
+
+
+
{{ __('app.dashboard.metrics.completed') }}
+
{{ $completedSessions }}
+
+
+
+
{{ __('app.dashboard.metrics.completion_rate') }}
+
{{ $completionRate }}%
+
+
+
+
+
+
+ +
+
+
+

{{ __('app.dashboard.recent_sessions') }}

+
+ @if($recentSessions->isEmpty()) +
{{ __('app.dashboard.no_sessions') }}
+ @else +
    + @foreach($recentSessions as $s) +
  • +
    +
    +
    {{ $s['workout_title'] }}
    +
    + {{ $s['done'] }} / {{ $s['total'] }} · {{ $s['pct'] }}% + @if($s['completed_at']) + · {{ __('app.dashboard.completed') }} + @endif +
    +
    +
    +
    +
    + {{ __('app.dashboard.view_session') }} +
    +
  • + @endforeach +
+ @endif +
+ +
+

{{ __('app.dashboard.activity_30d') }}

+

{{ __('app.dashboard.activity_30d_help') }}

+ @php + $today = now()->startOfDay(); + $days = []; + for ($i = 29; $i >= 0; $i--) { $d = $today->copy()->subDays($i)->format('Y-m-d'); $days[] = $d; } + $max = max(1, $dailyActivity->max() ?? 0); + @endphp +
+ @foreach($days as $d) + @php $count = $dailyActivity[$d] ?? 0; $pct = $max ? intval(($count / $max) * 100) : 0; @endphp +
+
+ {{ $count }} +
+
+ @endforeach +
+
+
+
diff --git a/resources/views/exercises/show.blade.php b/resources/views/exercises/show.blade.php index ad75a4a..775d161 100644 --- a/resources/views/exercises/show.blade.php +++ b/resources/views/exercises/show.blade.php @@ -90,4 +90,41 @@
+ + @if(isset($similarExercises)) +
+
+
+

{{ __('app.exercises.similar') }}

+ {{ __('app.home.browse_all') }} +
+ @if($similarExercises->isEmpty()) +
{{ __('app.exercises.similar_none') }}
+ @else + @php $badgePalette = ['bg-rose-100 text-rose-700','bg-amber-100 text-amber-700','bg-emerald-100 text-emerald-700','bg-sky-100 text-sky-700','bg-violet-100 text-violet-700']; @endphp + + @endif +
+
+ @endif diff --git a/resources/views/home/index.blade.php b/resources/views/home/index.blade.php index c866c66..dec93e5 100644 --- a/resources/views/home/index.blade.php +++ b/resources/views/home/index.blade.php @@ -11,41 +11,41 @@
- New • Explore 1,000+ moves + {{ __('app.home.badge') }}

- Discover 1,000+ Exercises. Build Your Perfect Workout. + {{ __('app.home.title') }}

- Explore FitAtlas to find exercises by muscle group and equipment, then craft routines that match your goals. + {{ __('app.home.subtitle') }}

- - + +
@php - $filters = ['Arms', 'Chest', 'Legs', 'Core', 'Back']; + $filters = ['arms', 'chest', 'legs', 'core', 'back']; $filterColors = [ - 'Arms' => 'from-amber-400 to-orange-500', - 'Chest' => 'from-rose-400 to-pink-500', - 'Legs' => 'from-emerald-400 to-teal-500', - 'Core' => 'from-sky-400 to-cyan-500', - 'Back' => 'from-violet-400 to-indigo-500', + 'arms' => 'from-amber-400 to-orange-500', + 'chest' => 'from-rose-400 to-pink-500', + 'legs' => 'from-emerald-400 to-teal-500', + 'core' => 'from-sky-400 to-cyan-500', + 'back' => 'from-violet-400 to-indigo-500', ]; @endphp @foreach($filters as $filter) - - {{ $filter }} + + {{ __('app.home.filters.' . $filter) }} @endforeach
@@ -54,8 +54,8 @@
-

Featured Exercises

- Browse all +

{{ __('app.home.featured') }}

+ {{ __('app.home.browse_all') }}
@php $exercises = $exercises ?? [ @@ -96,9 +96,9 @@
-

Favorites

-

A balanced routine targeting all major muscle groups. Suitable for most levels and easy to adapt with available equipment.

- Try This Routine +

{{ __('app.home.favorites') }}

+

{{ __('app.home.favorites_desc') }}

+ {{ __('app.home.try_routine') }}
@@ -70,14 +76,21 @@
+
+ {{ __('app.nav.language') }}: + EN + | + RO +
- Exercises - Workouts - Blog - Build a Workout + {{ __('app.nav.exercises') }} + {{ __('app.nav.yoga') }} + {{ __('app.nav.workouts') }} + {{ __('app.nav.blog') }} + {{ __('app.nav.build_workout') }}
@auth diff --git a/resources/views/workouts/index.blade.php b/resources/views/workouts/index.blade.php index c10b800..7cd2a38 100644 --- a/resources/views/workouts/index.blade.php +++ b/resources/views/workouts/index.blade.php @@ -3,10 +3,10 @@
-

My Workouts

-

Create routines and manage your exercises.

+

{{ __('app.workouts.my_workouts') }}

+

{{ __('app.workouts.manage_desc') }}

- New Workout + {{ __('app.workouts.new_workout') }}
@@ -16,7 +16,7 @@
{{ session('status') }}
@endif @if($workouts->count() === 0) -
No workouts yet. Create your first workout.
+
{{ __('app.workouts.none_yet') }}
@else
@foreach($workouts as $workout) @@ -31,11 +31,11 @@ @endif
- Open -
+ {{ __('app.workouts.open') }} + @csrf @method('DELETE') - +
diff --git a/resources/views/workouts/session.blade.php b/resources/views/workouts/session.blade.php new file mode 100644 index 0000000..57be16a --- /dev/null +++ b/resources/views/workouts/session.blade.php @@ -0,0 +1,125 @@ + +
+
+
+
+ ← {{ __('app.sessions.back_to_workout') }} +

{{ $workout->title }}

+

{{ __('app.sessions.progress') }}: {{ $doneByItemId->where('done', true)->count() }} / {{ $workout->items->count() }}

+
+
+ @csrf + +
+
+
+
+ +
+
+ @if($workout->items->count() === 0) +
{{ __('app.sessions.no_items') }}
+ @else +
    + @foreach($workout->items as $item) + @php $si = $doneByItemId->get($item->id); $isDone = $si && $si->done; @endphp +
  • +
    +
    + @csrf + @method('PATCH') + +
    +
    +
    {{ $item->exercise->name }}
    +
    + @if($item->sets) {{ $item->sets }} {{ __('app.sessions.sets') }} @endif + @if($item->reps) · {{ $item->reps }} {{ __('app.sessions.reps') }} @endif + @if($item->weight) · {{ $item->weight }} kg @endif + @if($item->rest_seconds) · {{ $item->rest_seconds }}s {{ __('app.sessions.rest') }} @endif +
    + @if($item->notes) +
    {{ $item->notes }}
    + @endif +
    +
    +
  • + @endforeach +
+ @endif +
+
+ +@push('scripts') + +@endpush +
diff --git a/resources/views/workouts/show.blade.php b/resources/views/workouts/show.blade.php index 808a388..abb32d5 100644 --- a/resources/views/workouts/show.blade.php +++ b/resources/views/workouts/show.blade.php @@ -25,11 +25,19 @@

@if($workout->estimated_minutes) ~ {{ $workout->estimated_minutes }} min · @endif {{ $workout->items->count() }} exercises

-
- @csrf - @method('DELETE') - -
+
+ @if(Auth::id() === $workout->user_id) +
+ @csrf + +
+
+ @csrf + @method('DELETE') + +
+ @endif +
diff --git a/resources/views/yoga/index.blade.php b/resources/views/yoga/index.blade.php new file mode 100644 index 0000000..e4fb066 --- /dev/null +++ b/resources/views/yoga/index.blade.php @@ -0,0 +1,100 @@ + +
+
+
+
+

{{ __('app.yoga.browse') }}

+

{{ __('app.yoga.browse_desc') }}

+
+
+
+
+ +
+
+ + +
+
+

{{ __('app.yoga.showing') }} {{ $poses->total() }} {{ __('app.yoga.poses_noun') }}

+
+ + + + +
+
+ + @if($poses->count() === 0) +
{{ __('app.yoga.none_found') }}
+ @else + + +
{{ $poses->links() }}
+ @endif +
+
+
+
diff --git a/resources/views/yoga/show.blade.php b/resources/views/yoga/show.blade.php new file mode 100644 index 0000000..b182292 --- /dev/null +++ b/resources/views/yoga/show.blade.php @@ -0,0 +1,124 @@ + +
+
+ @php $img = $pose->url_png ?: $pose->url_svg ?: $pose->url_svg_alt; @endphp + @if($img) + {{ $pose->english_name }} + @else +
+ @endif +
+
+
+ ← {{ __('app.yoga.back') }} +
+

{{ $pose->english_name }}

+
+
+ @if($pose->category) + {{ $pose->category->name }} + @endif + @if($pose->sanskrit_name) + Sanskrit: {{ $pose->sanskrit_name }} + @endif + @if($pose->sanskrit_name_adapted) + {{ $pose->sanskrit_name_adapted }} + @endif +
+
+
+ +
+
+
+
+

{{ __('app.yoga.description') }}

+
+ @php $desc = trim((string) $pose->description); @endphp + @if($desc !== '') + @foreach(preg_split('/\r?\n+/', $desc) as $line) + @if(trim($line) !== '')

{{ $line }}

@endif + @endforeach + @else +

{{ __('app.yoga.no_description') }}

+ @endif +
+
+ +
+

{{ __('app.yoga.benefits') }}

+
+ @php $benef = trim((string) $pose->benefits); @endphp + @if($benef !== '') + @foreach(preg_split('/\r?\n+/', $benef) as $line) + @if(trim($line) !== '')

{{ $line }}

@endif + @endforeach + @else +

{{ __('app.yoga.no_benefits') }}

+ @endif +
+
+
+
+
+

{{ __('app.yoga.pose_info') }}

+
+ @if($pose->translation_name) +
{{ __('app.yoga.translation') }}
{{ $pose->translation_name }}
+ @endif + @if($pose->sanskrit_name) +
{{ __('app.yoga.sanskrit') }}
{{ $pose->sanskrit_name }}
+ @endif + @if($pose->sanskrit_name_adapted) +
{{ __('app.yoga.sanskrit_adapted') }}
{{ $pose->sanskrit_name_adapted }}
+ @endif + @if($pose->category) +
{{ __('app.yoga.category') }}
{{ $pose->category->name }}
+ @endif +
+ +
+
+
+
+ + @isset($similarPoses) +
+
+
+

{{ __('app.yoga.similar') }}

+ {{ __('app.yoga.browse_all') }} +
+ @if($similarPoses->isEmpty()) +
{{ __('app.yoga.similar_none') }}
+ @else + + @endif +
+
+ @endisset +
diff --git a/routes/web.php b/routes/web.php index 1e7764c..d22eef3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,9 +6,8 @@ use Illuminate\Support\Facades\Route; Route::get('/', [HomeController::class, 'index'])->name('home'); -Route::get('/dashboard', function () { - return view('dashboard'); -})->middleware(['auth', 'verified'])->name('dashboard'); +Route::get('/dashboard', [\App\Http\Controllers\DashboardController::class, 'index']) + ->middleware(['auth', 'verified'])->name('dashboard'); Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); @@ -38,10 +37,25 @@ Route::middleware('auth')->prefix('/workouts')->group(function () { Route::post('/{workout}/exercises', [\App\Http\Controllers\WorkoutController::class, 'addExercise'])->name('workouts.exercises.add'); Route::patch('/{workout}/exercises/{item}', [\App\Http\Controllers\WorkoutController::class, 'updateExercise'])->name('workouts.exercises.update'); Route::delete('/{workout}/exercises/{item}', [\App\Http\Controllers\WorkoutController::class, 'removeExercise'])->name('workouts.exercises.remove'); + + // Sessions + Route::post('/{workout}/sessions', [\App\Http\Controllers\WorkoutSessionController::class, 'store'])->name('sessions.store'); +}); + +Route::middleware('auth')->group(function () { + Route::get('/sessions/{session}', [\App\Http\Controllers\WorkoutSessionController::class, 'show'])->name('sessions.show'); + Route::patch('/sessions/{session}/items/{item}', [\App\Http\Controllers\WorkoutSessionController::class, 'toggleItem'])->name('sessions.items.toggle'); + Route::post('/sessions/{session}/complete', [\App\Http\Controllers\WorkoutSessionController::class, 'complete'])->name('sessions.complete'); }); Route::prefix('/blog')->group(function () { Route::get('/', [\App\Http\Controllers\BlogController::class, 'index'])->name('blog.index'); }); +// Yoga poses +Route::prefix('/yoga')->group(function () { + Route::get('/', [\App\Http\Controllers\YogaPoseController::class, 'index'])->name('yoga.index'); + Route::get('/{pose:slug}', [\App\Http\Controllers\YogaPoseController::class, 'show'])->name('yoga.show'); +}); + require __DIR__.'/auth.php';