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
+80
View File
@@ -0,0 +1,80 @@
<x-app-layout>
<section class="bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">{{ __('app.admin.title') }}</h1>
<p class="mt-2 text-gray-600 dark:text-gray-300">{{ __('app.admin.subtitle') }}</p>
</div>
</div>
</div>
</section>
<section class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<div class="rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 p-5">
<div class="text-sm text-gray-500">{{ __('app.admin.metrics.users') }}</div>
<div class="mt-2 text-3xl font-bold text-gray-900 dark:text-white">{{ $metrics['users'] }}</div>
</div>
<div class="rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 p-5">
<div class="text-sm text-gray-500">{{ __('app.admin.metrics.workouts') }}</div>
<div class="mt-2 text-3xl font-bold text-gray-900 dark:text-white">{{ $metrics['workouts'] }}</div>
</div>
<div class="rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 p-5">
<div class="text-sm text-gray-500">{{ __('app.admin.metrics.sessions') }}</div>
<div class="mt-2 text-3xl font-bold text-gray-900 dark:text-white">{{ $metrics['sessions'] }}</div>
</div>
<div class="rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 p-5">
<div class="text-sm text-gray-500">{{ __('app.admin.metrics.exercises') }}</div>
<div class="mt-2 text-3xl font-bold text-gray-900 dark:text-white">{{ $metrics['exercises'] }}</div>
</div>
<div class="rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 p-5">
<div class="text-sm text-gray-500">{{ __('app.admin.metrics.yoga_poses') }}</div>
<div class="mt-2 text-3xl font-bold text-gray-900 dark:text-white">{{ $metrics['yogaPoses'] }}</div>
</div>
</div>
<div class="mt-8 grid grid-cols-1 lg:grid-cols-12 gap-6">
<div class="lg:col-span-7 rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 p-5">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ __('app.admin.recent_users') }}</h2>
</div>
@if($recentUsers->isEmpty())
<div class="mt-3 rounded-xl border border-dashed border-gray-300 dark:border-gray-700 p-6 text-sm text-gray-600 dark:text-gray-300">{{ __('app.admin.none') }}</div>
@else
<ul class="mt-4 space-y-3">
@foreach($recentUsers as $u)
<li class="rounded-xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-900 p-4 flex items-center justify-between">
<div>
<div class="text-sm font-semibold text-gray-900 dark:text-white">{{ $u->name }}</div>
<div class="text-xs text-gray-600 dark:text-gray-300">{{ $u->email }}</div>
</div>
<div class="text-xs text-gray-500">{{ $u->created_at->diffForHumans() }}</div>
</li>
@endforeach
</ul>
@endif
</div>
<div class="lg:col-span-5 rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 p-5">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ __('app.admin.recent_workouts') }}</h2>
@if($recentWorkouts->isEmpty())
<div class="mt-3 rounded-xl border border-dashed border-gray-300 dark:border-gray-700 p-6 text-sm text-gray-600 dark:text-gray-300">{{ __('app.admin.none') }}</div>
@else
<ul class="mt-4 space-y-3">
@foreach($recentWorkouts as $w)
<li class="rounded-xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-900 p-4 flex items-center justify-between">
<div class="text-sm font-semibold text-gray-900 dark:text-white">{{ $w->title }}</div>
<div class="text-xs text-gray-500">{{ $w->created_at->diffForHumans() }}</div>
</li>
@endforeach
</ul>
@endif
<div class="mt-6 flex items-center gap-3">
<a href="{{ route('workouts.index') }}" 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.admin.manage_workouts') }}</a>
<a href="{{ route('admin.exercises.index') }}" class="inline-flex items-center rounded-xl bg-gradient-to-r from-indigo-600 to-fuchsia-600 px-4 py-2 text-sm font-semibold text-white shadow hover:from-indigo-500 hover:to-fuchsia-500">Manage exercises</a>
</div>
</div>
</div>
</section>
</x-app-layout>
@@ -0,0 +1,53 @@
@csrf
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
<input type="text" name="name" value="{{ old('name', $exercise->name ?? '') }}" 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">
<x-input-error :messages="$errors->get('name')" class="mt-1" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Slug (optional)</label>
<input type="text" name="slug" value="{{ old('slug', $exercise->slug ?? '') }}" 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">
<p class="mt-1 text-xs text-gray-500">Leave empty to auto-generate from the name.</p>
<x-input-error :messages="$errors->get('slug')" class="mt-1" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">GIF URL</label>
<input type="url" name="gif_url" value="{{ old('gif_url', $exercise->gif_url ?? '') }}" 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">
<x-input-error :messages="$errors->get('gif_url')" class="mt-1" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Poster image</label>
<div class="mt-1 grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<input type="file" name="poster_image_file" accept="image/*" class="block w-full text-sm text-gray-900 dark:text-gray-100 file:mr-4 file:rounded-md file:border-0 file:bg-indigo-50 file:px-3 file:py-2 file:text-sm file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100 dark:file:bg-indigo-900/30 dark:file:text-indigo-200" />
<p class="mt-1 text-xs text-gray-500">Upload a new image (JPG, PNG, WebP, up to 8MB). If provided, it will override the URL below.</p>
<x-input-error :messages="$errors->get('poster_image_file')" class="mt-1" />
</div>
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400">Or use an external URL</label>
<input type="url" name="poster_image" value="{{ old('poster_image', $exercise->poster_image ?? '') }}" 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" placeholder="https://...">
<x-input-error :messages="$errors->get('poster_image')" class="mt-1" />
</div>
</div>
@php
$poster = old('poster_image', $exercise->poster_image ?? '');
$isHttp = is_string($poster) && preg_match('/^https?:\/\//i', $poster);
$src = $isHttp ? $poster : ($poster ? \Illuminate\Support\Facades\Storage::url($poster) : '');
@endphp
@if(!empty($src))
<div class="mt-2">
<img src="{{ $src }}" alt="Poster preview" class="h-32 w-auto rounded-lg border border-gray-200 dark:border-gray-700 object-cover">
</div>
@endif
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Instructions</label>
<textarea name="instructions" rows="6" 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">{{ old('instructions', $exercise->instructions ?? '') }}</textarea>
<x-input-error :messages="$errors->get('instructions')" class="mt-1" />
</div>
</div>
<div class="mt-6 flex items-center gap-3">
<a href="{{ route('admin.exercises.index') }}" class="text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">Cancel</a>
<button type="submit" 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">Save</button>
</div>
@@ -0,0 +1,23 @@
<x-app-layout>
<section class="bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">New Exercise</h1>
<p class="mt-2 text-gray-600 dark:text-gray-300">Add a new exercise to the library.</p>
</div>
</section>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-8">
@if ($errors->any())
<div class="mb-4 rounded-xl bg-red-50 text-red-700 px-4 py-3">
<ul class="list-disc ms-5 text-sm">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('admin.exercises.store') }}" method="POST" enctype="multipart/form-data" class="space-y-5 bg-white dark:bg-gray-800 rounded-2xl p-6 ring-1 ring-gray-200 dark:ring-gray-700">
@include('admin.exercises._form')
</form>
</section>
</x-app-layout>
@@ -0,0 +1,24 @@
<x-app-layout>
<section class="bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">Edit Exercise</h1>
<p class="mt-2 text-gray-600 dark:text-gray-300">Update exercise details.</p>
</div>
</section>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-8">
@if ($errors->any())
<div class="mb-4 rounded-xl bg-red-50 text-red-700 px-4 py-3">
<ul class="list-disc ms-5 text-sm">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('admin.exercises.update', $exercise) }}" method="POST" enctype="multipart/form-data" class="space-y-5 bg-white dark:bg-gray-800 rounded-2xl p-6 ring-1 ring-gray-200 dark:ring-gray-700">
@method('PATCH')
@include('admin.exercises._form')
</form>
</section>
</x-app-layout>
@@ -0,0 +1,72 @@
<x-app-layout>
<section class="bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">Manage Exercises</h1>
<p class="mt-2 text-gray-600 dark:text-gray-300">Create, edit, and delete exercises in the library.</p>
</div>
<a href="{{ route('admin.exercises.create') }}" 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">New Exercise</a>
</div>
</div>
</section>
<section class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
@if(session('status'))
<div class="mb-4 rounded-xl bg-emerald-50 text-emerald-700 px-4 py-3">{{ session('status') }}</div>
@endif
<form action="{{ route('admin.exercises.index') }}" method="GET" class="mb-4 flex items-end gap-3 bg-white dark:bg-gray-800 rounded-2xl p-4 ring-1 ring-gray-200 dark:ring-gray-700">
<div class="w-full sm:w-80">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Search</label>
<input type="text" name="q" value="{{ $q }}" placeholder="Search by name or slug" 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 placeholder-gray-400 focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2">
</div>
<div class="ml-auto">
<button type="submit" class="inline-flex items-center rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-indigo-500">Apply</button>
</div>
</form>
<div class="overflow-hidden rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800">
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-900/50">
<tr>
<th class="px-4 py-3 text-left font-semibold text-gray-700 dark:text-gray-300">Name</th>
<th class="px-4 py-3 text-left font-semibold text-gray-700 dark:text-gray-300">Slug</th>
<th class="px-4 py-3 text-left font-semibold text-gray-700 dark:text-gray-300">GIF</th>
<th class="px-4 py-3 text-right font-semibold text-gray-700 dark:text-gray-300">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($exercises as $ex)
<tr>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">{{ $ex->name }}</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-300">{{ $ex->slug }}</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-300 truncate max-w-[240px]">
@if($ex->gif_url)
<a href="{{ $ex->gif_url }}" target="_blank" class="text-indigo-600 hover:text-indigo-500">{{ \Illuminate\Support\Str::limit($ex->gif_url, 40) }}</a>
@else
<span class="text-gray-400">—</span>
@endif
</td>
<td class="px-4 py-3 text-right">
<a href="{{ route('admin.exercises.edit', $ex) }}" class="text-indigo-600 hover:text-indigo-500">Edit</a>
<form method="POST" action="{{ route('admin.exercises.destroy', $ex) }}" class="inline" onsubmit="return confirm('Delete this exercise?');">
@csrf
@method('DELETE')
<button type="submit" class="ms-3 text-red-600 hover:text-red-500">Delete</button>
</form>
</td>
</tr>
@empty
<tr><td colspan="4" class="px-4 py-6 text-center text-gray-600 dark:text-gray-300">No exercises found.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="px-4 py-3 border-t border-gray-100 dark:border-gray-800">
{{ $exercises->links() }}
</div>
</div>
</section>
</x-app-layout>