some progress
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user