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; } }