some progress

This commit is contained in:
2025-10-06 22:35:04 +03:00
parent 07148700a5
commit beeda8139b
25 changed files with 770 additions and 32 deletions
@@ -0,0 +1,145 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Exercise;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\View\View;
class ExerciseController extends Controller
{
public function index(Request $request): View
{
$q = trim((string) $request->query('q', ''));
$exercises = Exercise::query()
->when($q !== '', function ($qb) use ($q) {
$qb->where('name', 'like', "%{$q}%")
->orWhere('slug', 'like', "%{$q}%");
})
->orderBy('name')
->paginate(20)
->withQueryString();
return view('admin.exercises.index', compact('exercises', 'q'));
}
public function create(): View
{
return view('admin.exercises.create');
}
public function store(Request $request): RedirectResponse
{
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'slug' => ['nullable', 'string', 'max:255', 'unique:exercises,slug'],
'gif_url' => ['nullable', 'string', 'max:2048'],
'poster_image' => ['nullable', 'string', 'max:2048'],
'poster_image_file' => ['nullable', 'image', 'mimes:jpeg,jpg,png,webp,avif,gif', 'max:8192'],
'instructions' => ['nullable', 'string'],
]);
$name = trim($data['name']);
$slug = trim((string) ($data['slug'] ?? ''));
if ($slug === '') {
$slug = $this->uniqueSlug($name);
}
// Determine poster image value (uploaded file takes precedence)
$posterImageValue = $data['poster_image'] ?? null;
if ($request->hasFile('poster_image_file')) {
$path = $request->file('poster_image_file')->store('exercises/posters', 'public');
$posterImageValue = $path; // store relative path; use Storage::url to display
}
Exercise::create([
'name' => $name,
'slug' => $slug,
'gif_url' => $data['gif_url'] ?? null,
'poster_image' => $posterImageValue,
'instructions' => $data['instructions'] ?? null,
]);
return redirect()->route('admin.exercises.index')->with('status', 'Exercise created');
}
public function edit(Exercise $exercise): View
{
return view('admin.exercises.edit', compact('exercise'));
}
public function update(Request $request, Exercise $exercise): RedirectResponse
{
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'slug' => ['nullable', 'string', 'max:255', 'unique:exercises,slug,' . $exercise->id],
'gif_url' => ['nullable', 'string', 'max:2048'],
'poster_image' => ['nullable', 'string', 'max:2048'],
'poster_image_file' => ['nullable', 'image', 'mimes:jpeg,jpg,png,webp,avif,gif', 'max:8192'],
'instructions' => ['nullable', 'string'],
]);
$name = trim($data['name']);
$slug = trim((string) ($data['slug'] ?? ''));
if ($slug === '') {
// only (re)generate if empty
$slug = $this->uniqueSlug($name, $exercise->id);
}
// Determine poster image value for update (uploaded file > provided URL > keep existing)
$posterImageValue = $data['poster_image'] ?? $exercise->poster_image;
if ($request->hasFile('poster_image_file')) {
$path = $request->file('poster_image_file')->store('exercises/posters', 'public');
$posterImageValue = $path;
}
$exercise->update([
'name' => $name,
'slug' => $slug,
'gif_url' => $data['gif_url'] ?? null,
'poster_image' => $posterImageValue,
'instructions' => $data['instructions'] ?? null,
]);
return redirect()->route('admin.exercises.index')->with('status', 'Exercise updated');
}
public function destroy(Exercise $exercise): RedirectResponse
{
// Safely remove related data to avoid FK constraint errors
DB::transaction(function () use ($exercise) {
// Detach pivot relations if defined
try { $exercise->bodyParts()->detach(); } catch (\Throwable $e) {}
try { $exercise->targetMuscles()->detach(); } catch (\Throwable $e) {}
try { $exercise->secondaryMuscles()->detach(); } catch (\Throwable $e) {}
try { $exercise->equipments()->detach(); } catch (\Throwable $e) {}
try { $exercise->favoritedBy()->detach(); } catch (\Throwable $e) {}
// Delete aliases and workout_exercises if relations exist
try { $exercise->aliases()->delete(); } catch (\Throwable $e) {}
try { $exercise->workoutExercises()->delete(); } catch (\Throwable $e) {}
$exercise->delete();
});
return redirect()->route('admin.exercises.index')->with('status', 'Exercise deleted');
}
protected function uniqueSlug(string $name, ?int $ignoreId = null): string
{
$base = Str::slug($name);
if ($base === '') { $base = 'exercise'; }
$slug = $base;
$i = 1;
while (Exercise::where('slug', $slug)
->when($ignoreId, fn($q)=>$q->where('id','!=',$ignoreId))
->exists()) {
$slug = $base . '-' . (++$i);
}
return $slug;
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use App\Models\Exercise;
use App\Models\User;
use App\Models\Workout;
use App\Models\WorkoutSession;
use App\Models\YogaPose;
use Illuminate\View\View;
class AdminDashboardController extends Controller
{
public function index(): View
{
$metrics = [
'users' => User::count(),
'workouts' => Workout::count(),
'sessions' => WorkoutSession::count(),
'exercises' => Exercise::count(),
'yogaPoses' => YogaPose::count(),
];
$recentUsers = User::latest()->limit(5)->get(['id','name','email','created_at']);
$recentWorkouts = Workout::latest()->limit(5)->get(['id','title','created_at']);
return view('admin.dashboard', compact('metrics','recentUsers','recentWorkouts'));
}
}
@@ -33,6 +33,7 @@ class RegisteredUserController extends Controller
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'is_trainer' => ['nullable', 'boolean'],
// Fitness profile (all optional to avoid breaking tests unless specified)
'age' => ['nullable', 'integer', 'min:5', 'max:120'],
'weight_kg' => ['nullable', 'numeric', 'min:20', 'max:400'],
@@ -49,6 +50,7 @@ class RegisteredUserController extends Controller
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'is_trainer' => (bool) $request->boolean('is_trainer'),
'age' => $request->age,
'weight_kg' => $request->weight_kg,
'height_cm' => $request->height_cm,
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class Admin
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$user = Auth::user();
if (!$user || !$user->is_admin) {
abort(403);
}
return $next($request);
}
}
+14
View File
@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
class Exercise extends Model
{
@@ -12,6 +13,7 @@ class Exercise extends Model
'slug',
'exercise_db_id',
'gif_url',
'poster_image',
'instructions',
];
@@ -66,4 +68,16 @@ class Exercise extends Model
$alias = $this->aliases()->where('user_id', $userId)->first();
return $alias?->name ?: $name;
}
public function getPosterUrlAttribute(): ?string
{
$poster = (string)($this->attributes['poster_image'] ?? '');
if ($poster === '') {
return null;
}
if (preg_match('/^https?:\/\//i', $poster)) {
return $poster;
}
return Storage::url($poster);
}
}
+3
View File
@@ -30,6 +30,7 @@ class User extends Authenticatable
'available_equipment',
'days_per_week',
'minutes_per_session',
'is_trainer',
];
/**
@@ -57,6 +58,8 @@ class User extends Authenticatable
'height_cm' => 'decimal:2',
'days_per_week' => 'integer',
'minutes_per_session' => 'integer',
'is_admin' => 'boolean',
'is_trainer' => 'boolean',
];
}
+4
View File
@@ -14,10 +14,14 @@ class WorkoutSessionItem extends Model
'session_id',
'workout_exercise_id',
'done',
'exercise_ms',
'rest_ms',
];
protected $casts = [
'done' => 'boolean',
'exercise_ms' => 'integer',
'rest_ms' => 'integer',
];
public function session(): BelongsTo