Commit dc79c433 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Website Builder V2: production polish + route fixes + dashboard enrollment breakdown

- Add admin UIs for Video, Trainers, Schedule, and Pricing Plan sections
- Redesign schedule section with interactive day-filter cards and Alpine.js
- Fix video section click handler with Alpine.js reactive state
- Add section reordering (up/down arrows) in SectionManager
- Add SEO meta tags (OG, Twitter Card, canonical, robots) to website layout
- Add site-btn--outline-accent CSS variant for pricing cards
- Add scrollbar-hide utility to website.css
- Fix broken routes: website.preview → website.manage.preview,
  parent.child-detail → parent.profile.child,
  parent.invoice-detail → parent.finances.invoice
- Fix attendance "not_taken" filter to match dashboard logic (include scheduled sessions)
- Add enrollment status breakdown (active/pending/cancelled) to subscription revenue widget
- Add cancelled/suspended revenue explanation in activity breakdown
- Add Phase C migration + events section key migration
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 9d9ae754
...@@ -20,22 +20,40 @@ Each client (academy) gets their own app uploaded to Play Store / App Store unde ...@@ -20,22 +20,40 @@ Each client (academy) gets their own app uploaded to Play Store / App Store unde
flutter_app/ flutter_app/
├── lib/ ├── lib/
│ ├── config/ │ ├── config/
│ │ └── instance.dart ← THE ONLY FILE YOU CHANGE PER CLIENT │ │ └── instance.dart ← Config: URL + package ID
│ ├── core/ │ ├── core/
│ ├── features/ │ ├── features/
│ └── main.dart │ └── main.dart
├── android/ ├── android/
│ ├── app/src/main/ │ ├── app/src/main/
│ │ └── google-services.json ← Per-client Firebase config (Android) │ │ ├── google-services.json ← Per-client Firebase config (Android)
│ │ └── res/
│ │ ├── mipmap-hdpi/ic_launcher.png ← App icon
│ │ ├── mipmap-xhdpi/ic_launcher.png
│ │ ├── mipmap-xxhdpi/ic_launcher.png
│ │ └── mipmap-xxxhdpi/ic_launcher.png
│ └── ... │ └── ...
├── ios/ ├── ios/
│ ├── Runner/ │ ├── Runner/
│ │ └── GoogleService-Info.plist ← Per-client Firebase config (iOS) │ │ ├── GoogleService-Info.plist ← Per-client Firebase config (iOS)
│ │ └── Assets.xcassets/AppIcon.appiconset/ ← iOS app icon
│ └── ... │ └── ...
└── assets/ └── assets/
└── branding/ ← Optional: splash screen override └── branding/
├── logo.png ← Academy logo (used in splash + login)
├── logo_dark.png ← Logo variant for dark backgrounds
└── splash.png ← Full splash screen image (1080x1920)
``` ```
### What You Edit Per Client (4 things only)
| File | What |
|------|------|
| `lib/config/instance.dart` | Instance URL + package ID + app name |
| `assets/branding/logo.png` + `splash.png` | Academy logo + splash screen |
| `android/app/google-services.json` | Firebase config (Android) |
| Android `ic_launcher.png` (all sizes) | App icon on home screen |
### instance.dart — The Single Source of Truth ### instance.dart — The Single Source of Truth
```dart ```dart
...@@ -71,12 +89,14 @@ class InstanceConfig { ...@@ -71,12 +89,14 @@ class InstanceConfig {
### To Deploy for a New Client ### To Deploy for a New Client
1. Clone the Flutter repo 1. Clone the Flutter repo
2. Edit `lib/config/instance.dart` → paste the instance URL + package ID 2. Edit `lib/config/instance.dart` → paste the instance URL + package ID + name
3. Drop in `google-services.json` (from their Firebase project) 3. Replace `assets/branding/logo.png` + `splash.png` with the client's branding
4. Run `flutter build apk --release` (or `appbundle`) 4. Replace Android `ic_launcher.png` (use Android Studio Image Asset tool or https://icon.kitchen)
5. Upload to Play Store under the client's developer account 5. Drop in `google-services.json` (from their Firebase project)
6. On the Laravel instance: add the Firebase service account JSON to system settings 6. Run `flutter build appbundle --release`
7. Done — notifications flow from their instance to their app 7. Upload to Play Store under the client's developer account
8. On the Laravel instance: add the Firebase service account JSON to system settings
9. Done — notifications flow from their instance to their app
--- ---
...@@ -581,6 +601,9 @@ Add a "Mobile App" section in system settings: ...@@ -581,6 +601,9 @@ Add a "Mobile App" section in system settings:
- `baseUrl` = client's instance URL - `baseUrl` = client's instance URL
- `appName` / `appNameAr` = client's name - `appName` / `appNameAr` = client's name
- `packageId` = `com.alcaptain.{clientslug}` - `packageId` = `com.alcaptain.{clientslug}`
- [ ] Replace `assets/branding/logo.png` with client's logo
- [ ] Replace `assets/branding/splash.png` with client's splash (1080x1920)
- [ ] Generate app icons from logo (use icon.kitchen or Android Studio)
- [ ] Replace `android/app/google-services.json` - [ ] Replace `android/app/google-services.json`
- [ ] (iOS) Replace `ios/Runner/GoogleService-Info.plist` - [ ] (iOS) Replace `ios/Runner/GoogleService-Info.plist`
- [ ] Update `android/app/build.gradle``applicationId` matches packageId - [ ] Update `android/app/build.gradle``applicationId` matches packageId
......
...@@ -20,6 +20,9 @@ ...@@ -20,6 +20,9 @@
case Cta = 'cta'; case Cta = 'cta';
case Stats = 'stats'; case Stats = 'stats';
case News = 'news'; case News = 'news';
case Events = 'events';
case Video = 'video';
case CustomHtml = 'custom_html';
public function label(): string public function label(): string
{ {
...@@ -40,6 +43,9 @@ public function label(): string ...@@ -40,6 +43,9 @@ public function label(): string
self::Cta => 'سجل الآن', self::Cta => 'سجل الآن',
self::Stats => 'إنجازاتنا', self::Stats => 'إنجازاتنا',
self::News => 'الأخبار', self::News => 'الأخبار',
self::Events => 'الأحداث والبطولات',
self::Video => 'فيديو',
self::CustomHtml => 'محتوى مخصص',
}; };
} }
...@@ -62,6 +68,9 @@ public function icon(): string ...@@ -62,6 +68,9 @@ public function icon(): string
self::Cta => 'megaphone', self::Cta => 'megaphone',
self::Stats => 'chart-bar', self::Stats => 'chart-bar',
self::News => 'newspaper', self::News => 'newspaper',
self::Events => 'trophy',
self::Video => 'play-circle',
self::CustomHtml => 'code-bracket',
}; };
} }
...@@ -81,6 +90,7 @@ public static function defaultOrder(): array ...@@ -81,6 +90,7 @@ public static function defaultOrder(): array
self::Partners, self::Partners,
self::Faq, self::Faq,
self::News, self::News,
self::Events,
self::Contact, self::Contact,
self::Cta, self::Cta,
self::Schedule, self::Schedule,
......
...@@ -19,6 +19,8 @@ class WebsiteFaq extends Model ...@@ -19,6 +19,8 @@ class WebsiteFaq extends Model
'question_en', 'question_en',
'answer', 'answer',
'answer_en', 'answer_en',
'category',
'category_en',
'sort_order', 'sort_order',
'is_published', 'is_published',
]; ];
......
...@@ -25,6 +25,8 @@ class WebsiteNews extends Model ...@@ -25,6 +25,8 @@ class WebsiteNews extends Model
'body_en', 'body_en',
'excerpt', 'excerpt',
'excerpt_en', 'excerpt_en',
'category',
'category_en',
'published_at', 'published_at',
'is_featured', 'is_featured',
'sort_order', 'sort_order',
......
...@@ -51,6 +51,9 @@ class WebsiteSetting extends Model ...@@ -51,6 +51,9 @@ class WebsiteSetting extends Model
'footer_blocks', 'footer_blocks',
'footer_bottom_text', 'footer_bottom_text',
'footer_style', 'footer_style',
'floating_elements',
'announcement_bar',
'popup_config',
'is_published', 'is_published',
'published_at', 'published_at',
]; ];
...@@ -58,6 +61,9 @@ class WebsiteSetting extends Model ...@@ -58,6 +61,9 @@ class WebsiteSetting extends Model
protected $casts = [ protected $casts = [
'social_links' => 'array', 'social_links' => 'array',
'footer_blocks' => 'array', 'footer_blocks' => 'array',
'floating_elements' => 'array',
'announcement_bar' => 'array',
'popup_config' => 'array',
'is_published' => 'boolean', 'is_published' => 'boolean',
'animations_enabled' => 'boolean', 'animations_enabled' => 'boolean',
'navbar_show_social' => 'boolean', 'navbar_show_social' => 'boolean',
......
...@@ -20,6 +20,7 @@ class WebsiteTestimonial extends Model ...@@ -20,6 +20,7 @@ class WebsiteTestimonial extends Model
'role_en', 'role_en',
'content', 'content',
'content_en', 'content_en',
'video_url',
'rating', 'rating',
'is_featured', 'is_featured',
'sort_order', 'sort_order',
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Domain\Event\Models\Event;
use App\Domain\Shared\Models\Academy; use App\Domain\Shared\Models\Academy;
use App\Domain\Website\Enums\SectionKey; use App\Domain\Website\Enums\SectionKey;
use App\Domain\Website\Models\WebsiteNews;
use App\Domain\Website\Models\WebsiteSetting; use App\Domain\Website\Models\WebsiteSetting;
use App\Domain\Website\Services\WebsiteDataService; use App\Domain\Website\Services\WebsiteDataService;
use App\Domain\Website\Services\WebsiteSectionService; use App\Domain\Website\Services\WebsiteSectionService;
...@@ -86,6 +88,67 @@ public function preview(Request $request) ...@@ -86,6 +88,67 @@ public function preview(Request $request)
]); ]);
} }
public function newsIndex(string $slug)
{
$academy = app('current_academy');
$settings = app('website_settings');
$category = request('category');
$query = WebsiteNews::where('academy_id', $academy->id)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->orderByDesc('published_at');
if ($category) {
$query->where('category', $category);
}
$news = $query->paginate(12);
$categories = WebsiteNews::where('academy_id', $academy->id)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->whereNotNull('category')
->distinct()
->pluck('category');
return view('website.news-index', [
'academy' => $academy,
'settings' => $settings,
'news' => $news,
'categories' => $categories,
'activeCategory' => $category,
]);
}
public function newsShow(string $slug, string $news)
{
$academy = app('current_academy');
$settings = app('website_settings');
$article = WebsiteNews::where('academy_id', $academy->id)
->where('slug', $news)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->firstOrFail();
$related = WebsiteNews::where('academy_id', $academy->id)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->where('id', '!=', $article->id)
->when($article->category, fn($q) => $q->where('category', $article->category))
->latest('published_at')
->take(3)
->get();
return view('website.news-show', [
'academy' => $academy,
'settings' => $settings,
'article' => $article,
'related' => $related,
]);
}
private function gatherSectionData(Academy $academy, $sections): array private function gatherSectionData(Academy $academy, $sections): array
{ {
$data = []; $data = [];
...@@ -127,6 +190,17 @@ private function gatherSectionData(Academy $academy, $sections): array ...@@ -127,6 +190,17 @@ private function gatherSectionData(Academy $academy, $sections): array
$data['gallery'] = $this->dataService->getGalleryImages($academy); $data['gallery'] = $this->dataService->getGalleryImages($academy);
} }
if ($enabledKeys->contains(SectionKey::Events)) {
$data['events'] = Event::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->published()
->where('starts_at', '>', now()->subDay())
->with('cover')
->orderBy('starts_at')
->take(6)
->get();
}
return $data; return $data;
} }
} }
...@@ -78,10 +78,9 @@ public function render() ...@@ -78,10 +78,9 @@ public function render()
->when($this->dateTo, fn ($q) => $q->where('session_date', '<=', $this->dateTo)) ->when($this->dateTo, fn ($q) => $q->where('session_date', '<=', $this->dateTo))
->when($this->search, fn ($q) => $q->whereHas('group', fn ($gq) => $gq->where('name_ar', 'ilike', "%{$this->search}%"))) ->when($this->search, fn ($q) => $q->whereHas('group', fn ($gq) => $gq->where('name_ar', 'ilike', "%{$this->search}%")))
->when($this->filter === 'not_taken', function ($q) { ->when($this->filter === 'not_taken', function ($q) {
$q->whereIn('status', ['completed', 'in_progress']) $q->whereIn('status', ['scheduled', 'in_progress', 'completed'])
->where(function ($sub) { ->where('end_time', '<', now()->format('H:i:s'))
$sub->whereDoesntHave('attendanceRecords', fn ($ar) => $ar->whereNotIn('status', ['expected'])); ->whereDoesntHave('attendanceRecords', fn ($ar) => $ar->whereNotIn('status', ['expected']));
});
}) })
->orderByDesc('session_date') ->orderByDesc('session_date')
->orderByDesc('start_time'); ->orderByDesc('start_time');
......
...@@ -48,9 +48,13 @@ public function render() ...@@ -48,9 +48,13 @@ public function render()
$topPrograms = collect($this->getTopPrograms($startDate, $academyId)); $topPrograms = collect($this->getTopPrograms($startDate, $academyId));
// Enrollment counts // Enrollment counts
$newEnrollments = Enrollment::where('created_at', '>=', $startDate) $enrollmentQuery = Enrollment::where('created_at', '>=', $startDate)
->when($academyId, fn ($q) => $q->where('academy_id', $academyId)) ->when($academyId, fn ($q) => $q->where('academy_id', $academyId));
->count();
$newEnrollments = (clone $enrollmentQuery)->count();
$activeEnrollments = (clone $enrollmentQuery)->where('status', 'active')->count();
$pendingEnrollments = (clone $enrollmentQuery)->where('status', 'pending')->count();
$cancelledEnrollments = (clone $enrollmentQuery)->where('status', 'cancelled')->count();
$previousEnrollments = Enrollment::whereBetween('created_at', [$previousStart, $startDate]) $previousEnrollments = Enrollment::whereBetween('created_at', [$previousStart, $startDate])
->when($academyId, fn ($q) => $q->where('academy_id', $academyId)) ->when($academyId, fn ($q) => $q->where('academy_id', $academyId))
...@@ -72,6 +76,9 @@ public function render() ...@@ -72,6 +76,9 @@ public function render()
'byActivity' => $byActivity, 'byActivity' => $byActivity,
'topPrograms' => $topPrograms, 'topPrograms' => $topPrograms,
'newEnrollments' => $newEnrollments, 'newEnrollments' => $newEnrollments,
'activeEnrollments' => $activeEnrollments,
'pendingEnrollments' => $pendingEnrollments,
'cancelledEnrollments' => $cancelledEnrollments,
'previousEnrollments' => $previousEnrollments, 'previousEnrollments' => $previousEnrollments,
'subscriptionPercent' => $subscriptionPercent, 'subscriptionPercent' => $subscriptionPercent,
'totalRevenue' => $totalRevenue, 'totalRevenue' => $totalRevenue,
......
...@@ -96,6 +96,24 @@ public function reorder(array $orderedIds): void ...@@ -96,6 +96,24 @@ public function reorder(array $orderedIds): void
$this->loadSections(); $this->loadSections();
} }
public function moveSection(int $id, string $direction): void
{
$currentIndex = collect($this->sections)->search(fn($s) => $s['id'] === $id);
if ($currentIndex === false) return;
$targetIndex = $direction === 'up' ? $currentIndex - 1 : $currentIndex + 1;
if ($targetIndex < 0 || $targetIndex >= count($this->sections)) return;
$currentId = $this->sections[$currentIndex]['id'];
$targetId = $this->sections[$targetIndex]['id'];
WebsiteSection::where('id', $currentId)->update(['sort_order' => $targetIndex]);
WebsiteSection::where('id', $targetId)->update(['sort_order' => $currentIndex]);
app(\App\Domain\Website\Services\WebsiteCacheService::class)->invalidate(app('current_academy')->id);
$this->loadSections();
}
public function editSection(string $key): void public function editSection(string $key): void
{ {
$academy = app('current_academy'); $academy = app('current_academy');
...@@ -142,7 +160,7 @@ public function saveSection(): void ...@@ -142,7 +160,7 @@ public function saveSection(): void
$settings['variant'] = $this->heroVariant; $settings['variant'] = $this->heroVariant;
} }
$layoutSections = ['activities', 'programs', 'trainers', 'news', 'branches', 'testimonials']; $layoutSections = ['activities', 'programs', 'trainers', 'news', 'branches', 'testimonials', 'events'];
if (in_array($this->editingSection, $layoutSections)) { if (in_array($this->editingSection, $layoutSections)) {
$settings['layout'] = $this->sectionLayout; $settings['layout'] = $this->sectionLayout;
} }
......
...@@ -57,6 +57,15 @@ class ThemeEditor extends Component ...@@ -57,6 +57,15 @@ class ThemeEditor extends Component
public array $footer_blocks = []; public array $footer_blocks = [];
public string $footer_bottom_text = ''; public string $footer_bottom_text = '';
// Announcement Bar
public array $announcement_bar = [];
// Floating Elements
public array $floating_elements = [];
// Popup
public array $popup_config = [];
// General // General
public string $site_title = ''; public string $site_title = '';
public string $site_title_en = ''; public string $site_title_en = '';
...@@ -122,6 +131,9 @@ public function mount(): void ...@@ -122,6 +131,9 @@ public function mount(): void
$this->footer_columns = $settings->footer_columns ?? 4; $this->footer_columns = $settings->footer_columns ?? 4;
$this->footer_blocks = $settings->footer_blocks ?? []; $this->footer_blocks = $settings->footer_blocks ?? [];
$this->footer_bottom_text = $settings->footer_bottom_text ?? ''; $this->footer_bottom_text = $settings->footer_bottom_text ?? '';
$this->announcement_bar = $settings->announcement_bar ?? [];
$this->floating_elements = $settings->floating_elements ?? [];
$this->popup_config = $settings->popup_config ?? [];
$this->site_title = $settings->site_title ?? ''; $this->site_title = $settings->site_title ?? '';
$this->site_title_en = $settings->site_title_en ?? ''; $this->site_title_en = $settings->site_title_en ?? '';
$this->site_description = $settings->site_description ?? ''; $this->site_description = $settings->site_description ?? '';
...@@ -251,6 +263,9 @@ public function save(): void ...@@ -251,6 +263,9 @@ public function save(): void
'footer_columns' => $this->footer_columns, 'footer_columns' => $this->footer_columns,
'footer_blocks' => $this->footer_blocks, 'footer_blocks' => $this->footer_blocks,
'footer_bottom_text' => $this->footer_bottom_text, 'footer_bottom_text' => $this->footer_bottom_text,
'announcement_bar' => $this->announcement_bar,
'floating_elements' => $this->floating_elements,
'popup_config' => $this->popup_config,
'site_title' => $this->site_title, 'site_title' => $this->site_title,
'site_title_en' => $this->site_title_en, 'site_title_en' => $this->site_title_en,
'site_description' => $this->site_description, 'site_description' => $this->site_description,
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// FAQ categories
Schema::table('website_faqs', function (Blueprint $table) {
$table->string('category', 100)->nullable()->after('answer_en');
$table->string('category_en', 100)->nullable()->after('category');
});
// News categories
Schema::table('website_news', function (Blueprint $table) {
$table->string('category', 100)->nullable()->after('excerpt_en');
$table->string('category_en', 100)->nullable()->after('category');
});
// Testimonials video support
Schema::table('website_testimonials', function (Blueprint $table) {
$table->string('video_url', 500)->nullable()->after('content_en');
});
// Gallery category on media
if (!Schema::hasColumn('media', 'gallery_category')) {
Schema::table('media', function (Blueprint $table) {
$table->string('gallery_category', 50)->nullable()->after('alt_text_en');
});
}
// Floating elements, announcement bar, popup config
Schema::table('website_settings', function (Blueprint $table) {
$table->jsonb('floating_elements')->default('{}')->after('footer_style');
$table->jsonb('announcement_bar')->default('{}')->after('floating_elements');
$table->jsonb('popup_config')->default('{}')->after('announcement_bar');
});
// Update section_key CHECK to include video + custom_html
DB::statement("ALTER TABLE website_sections DROP CONSTRAINT IF EXISTS website_sections_section_key_check");
DB::statement("ALTER TABLE website_sections ADD CONSTRAINT website_sections_section_key_check CHECK (section_key IN (
'hero', 'about', 'activities', 'programs', 'branches', 'schedule',
'trainers', 'gallery', 'testimonials', 'pricing', 'partners',
'contact', 'faq', 'cta', 'stats', 'news', 'video', 'custom_html'
))");
}
public function down(): void
{
Schema::table('website_faqs', function (Blueprint $table) {
$table->dropColumn(['category', 'category_en']);
});
Schema::table('website_news', function (Blueprint $table) {
$table->dropColumn(['category', 'category_en']);
});
Schema::table('website_testimonials', function (Blueprint $table) {
$table->dropColumn('video_url');
});
if (Schema::hasColumn('media', 'gallery_category')) {
Schema::table('media', function (Blueprint $table) {
$table->dropColumn('gallery_category');
});
}
Schema::table('website_settings', function (Blueprint $table) {
$table->dropColumn(['floating_elements', 'announcement_bar', 'popup_config']);
});
DB::statement("ALTER TABLE website_sections DROP CONSTRAINT IF EXISTS website_sections_section_key_check");
DB::statement("ALTER TABLE website_sections ADD CONSTRAINT website_sections_section_key_check CHECK (section_key IN (
'hero', 'about', 'activities', 'programs', 'branches', 'schedule',
'trainers', 'gallery', 'testimonials', 'pricing', 'partners',
'contact', 'faq', 'cta', 'stats', 'news'
))");
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
DB::statement("ALTER TABLE website_sections DROP CONSTRAINT IF EXISTS website_sections_section_key_check");
DB::statement("ALTER TABLE website_sections ADD CONSTRAINT website_sections_section_key_check CHECK (section_key IN (
'hero', 'about', 'activities', 'programs', 'branches', 'schedule',
'trainers', 'gallery', 'testimonials', 'pricing', 'partners',
'contact', 'faq', 'cta', 'stats', 'news', 'events', 'video', 'custom_html'
))");
}
public function down(): void
{
DB::statement("ALTER TABLE website_sections DROP CONSTRAINT IF EXISTS website_sections_section_key_check");
DB::statement("ALTER TABLE website_sections ADD CONSTRAINT website_sections_section_key_check CHECK (section_key IN (
'hero', 'about', 'activities', 'programs', 'branches', 'schedule',
'trainers', 'gallery', 'testimonials', 'pricing', 'partners',
'contact', 'faq', 'cta', 'stats', 'news', 'video', 'custom_html'
))");
}
};
...@@ -358,6 +358,28 @@ ...@@ -358,6 +358,28 @@
transform: translateY(-2px); transform: translateY(-2px);
} }
.site-btn--outline-accent {
background: transparent;
color: var(--site-accent);
border: 2px solid var(--site-accent);
}
.site-btn--outline-accent:hover {
background: var(--site-accent);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 14px color-mix(in srgb, var(--site-accent) 25%, transparent);
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* ─── Footer ─── */ /* ─── Footer ─── */
.site-footer { .site-footer {
background: var(--site-primary); background: var(--site-primary);
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<!-- Main Stats Row --> <!-- Main Stats Row -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6"> <div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<!-- Total Subscription Revenue --> <!-- Total Subscription Revenue -->
<div class="bg-gradient-to-br from-emerald-50 to-teal-50 rounded-xl border border-emerald-200 p-4"> <div class="bg-gradient-to-br from-emerald-50 to-teal-50 rounded-xl border border-emerald-200 p-4" title="{{ __('جميع المدفوعات المؤكدة على فواتير الاشتراكات — بما فيها لاعبون ألغوا اشتراكهم بعد الدفع') }}">
<p class="text-xs text-emerald-600 font-medium mb-1">{{ __('إجمالي الاشتراكات') }}</p> <p class="text-xs text-emerald-600 font-medium mb-1">{{ __('إجمالي الاشتراكات') }}</p>
<p class="text-xl font-bold text-emerald-700" dir="ltr">{{ number_format($totalSubscriptionRevenue / 100, 0) }} <span class="text-sm font-normal">{{ __('ج.م') }}</span></p> <p class="text-xl font-bold text-emerald-700" dir="ltr">{{ number_format($totalSubscriptionRevenue / 100, 0) }} <span class="text-sm font-normal">{{ __('ج.م') }}</span></p>
@if($change != 0) @if($change != 0)
...@@ -41,9 +41,14 @@ ...@@ -41,9 +41,14 @@
</div> </div>
<!-- New Enrollments --> <!-- New Enrollments -->
<div class="bg-gradient-to-br from-purple-50 to-violet-50 rounded-xl border border-purple-200 p-4"> <div class="bg-gradient-to-br from-purple-50 to-violet-50 rounded-xl border border-purple-200 p-4" title="{{ __('عدد التسجيلات الجديدة بجميع حالاتها (نشط + ملغى + معلّق)') }}">
<p class="text-xs text-purple-600 font-medium mb-1">{{ __('اشتراكات جديدة') }}</p> <p class="text-xs text-purple-600 font-medium mb-1">{{ __('تسجيلات جديدة') }}</p>
<p class="text-xl font-bold text-purple-700" dir="ltr">{{ number_format($newEnrollments) }}</p> <p class="text-xl font-bold text-purple-700" dir="ltr">{{ number_format($newEnrollments) }}</p>
<div class="flex items-center gap-2 mt-1.5 flex-wrap">
<span class="inline-flex items-center gap-0.5 text-[11px] text-emerald-600"><span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>{{ $activeEnrollments }} {{ __('نشط') }}</span>
<span class="inline-flex items-center gap-0.5 text-[11px] text-amber-600"><span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>{{ $pendingEnrollments }} {{ __('معلّق') }}</span>
<span class="inline-flex items-center gap-0.5 text-[11px] text-red-500"><span class="w-1.5 h-1.5 rounded-full bg-red-400"></span>{{ $cancelledEnrollments }} {{ __('ملغى') }}</span>
</div>
@if($previousEnrollments > 0) @if($previousEnrollments > 0)
<p class="text-xs text-gray-500 mt-1"> <p class="text-xs text-gray-500 mt-1">
{{ __('الفترة السابقة:') }} {{ number_format($previousEnrollments) }} {{ __('الفترة السابقة:') }} {{ number_format($previousEnrollments) }}
...@@ -80,14 +85,18 @@ ...@@ -80,14 +85,18 @@
</div> </div>
@else @else
<div class="space-y-3"> <div class="space-y-3">
@php $maxTotal = $byActivity->max('total') ?: 1; @endphp @php
$maxTotal = $byActivity->max('total') ?: 1;
$activityTotal = $byActivity->sum('total');
$unattributed = $totalSubscriptionRevenue - $activityTotal;
@endphp
@foreach($byActivity as $activity) @foreach($byActivity as $activity)
<div> <div>
<div class="flex items-center justify-between mb-1"> <div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full {{ match($loop->index % 6) { 0 => 'bg-emerald-500', 1 => 'bg-blue-500', 2 => 'bg-purple-500', 3 => 'bg-amber-500', 4 => 'bg-pink-500', 5 => 'bg-cyan-500', default => 'bg-gray-500' } }}"></span> <span class="w-2.5 h-2.5 rounded-full {{ match($loop->index % 6) { 0 => 'bg-emerald-500', 1 => 'bg-blue-500', 2 => 'bg-purple-500', 3 => 'bg-amber-500', 4 => 'bg-pink-500', 5 => 'bg-cyan-500', default => 'bg-gray-500' } }}"></span>
<span class="text-sm font-medium text-gray-700">{{ $activity->activity_name }}</span> <span class="text-sm font-medium text-gray-700">{{ $activity->activity_name }}</span>
<span class="text-xs text-gray-400">({{ $activity->subscriber_count }} {{ __('مشترك') }})</span> <span class="text-xs text-gray-400">({{ $activity->subscriber_count }} {{ __('مشترك نشط') }})</span>
</div> </div>
<span class="text-sm font-bold text-gray-800" dir="ltr">{{ number_format($activity->total / 100, 0) }} {{ __('ج.م') }}</span> <span class="text-sm font-bold text-gray-800" dir="ltr">{{ number_format($activity->total / 100, 0) }} {{ __('ج.م') }}</span>
</div> </div>
...@@ -97,6 +106,18 @@ ...@@ -97,6 +106,18 @@
</div> </div>
</div> </div>
@endforeach @endforeach
@if($unattributed > 0)
<div class="pt-2 mt-2 border-t border-gray-100">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-gray-300"></span>
<span class="text-sm text-gray-500">{{ __('اشتراكات ملغاة/معلّقة') }}</span>
</div>
<span class="text-sm font-medium text-gray-500" dir="ltr">{{ number_format($unattributed / 100, 0) }} {{ __('ج.م') }}</span>
</div>
<p class="text-[11px] text-gray-400 mt-1 ms-4">{{ __('مدفوعات لاعبين ألغوا أو علّقوا اشتراكهم بعد الدفع') }}</p>
</div>
@endif
</div> </div>
@endif @endif
</div> </div>
......
<div> <div>
{{-- Back Button --}} {{-- Back Button --}}
<div class="mb-4"> <div class="mb-4">
<a href="{{ route('parent.child-detail', ['child' => $childId ?? '']) }}" wire:navigate <a href="{{ route('parent.profile.child', ['participant' => $childId ?? '']) }}" wire:navigate
class="inline-flex items-center gap-1.5 text-sm text-[#64748B] hover:text-[#2563EB] transition-colors min-h-[44px]"> class="inline-flex items-center gap-1.5 text-sm text-[#64748B] hover:text-[#2563EB] transition-colors min-h-[44px]">
<svg class="w-4 h-4 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
......
...@@ -102,7 +102,7 @@ class="px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-a ...@@ -102,7 +102,7 @@ class="px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-a
$invoiceStatus = $invoice->status->value ?? $invoice->status ?? ''; $invoiceStatus = $invoice->status->value ?? $invoice->status ?? '';
$statusConf = $invoiceStatusConfig[$invoiceStatus] ?? ['label' => $invoiceStatus, 'bg' => 'bg-gray-100', 'text' => 'text-gray-700']; $statusConf = $invoiceStatusConfig[$invoiceStatus] ?? ['label' => $invoiceStatus, 'bg' => 'bg-gray-100', 'text' => 'text-gray-700'];
@endphp @endphp
<a href="{{ route('parent.invoice-detail', ['invoice' => $invoice->uuid]) }}" wire:navigate <a href="{{ route('parent.finances.invoice', ['invoice' => $invoice->uuid]) }}" wire:navigate
class="block bg-white rounded-2xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow"> class="block bg-white rounded-2xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div> <div>
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
@foreach($children as $child) @foreach($children as $child)
@php $childConf = $childStatusConfig[$child['status'] ?? ''] ?? ['label' => $child['status'] ?? '', 'bg' => 'bg-gray-100', 'text' => 'text-gray-700']; @endphp @php $childConf = $childStatusConfig[$child['status'] ?? ''] ?? ['label' => $child['status'] ?? '', 'bg' => 'bg-gray-100', 'text' => 'text-gray-700']; @endphp
<a href="{{ route('parent.child-detail', ['child' => $child['id'] ?? '']) }}" wire:navigate <a href="{{ route('parent.profile.child', ['participant' => $child['id'] ?? '']) }}" wire:navigate
class="block bg-white rounded-2xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow"> class="block bg-white rounded-2xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-full bg-indigo-100 flex items-center justify-center shrink-0"> <div class="w-12 h-12 rounded-full bg-indigo-100 flex items-center justify-center shrink-0">
......
...@@ -6,15 +6,32 @@ ...@@ -6,15 +6,32 @@
<title>{{ $settings->site_title ?? $academy->name_ar }}</title> <title>{{ $settings->site_title ?? $academy->name_ar }}</title>
<meta name="description" content="{{ $settings->site_description ?? '' }}"> <meta name="description" content="{{ $settings->site_description ?? '' }}">
{{-- SEO --}}
<meta name="robots" content="index, follow">
<link rel="canonical" href="{{ url()->current() }}">
@if($academy->logo_path)
<link rel="icon" type="image/png" href="{{ asset('storage/' . $academy->logo_path) }}">
<link rel="apple-touch-icon" href="{{ asset('storage/' . $academy->logo_path) }}">
@endif
{{-- Open Graph --}} {{-- Open Graph --}}
<meta property="og:title" content="{{ $settings->site_title ?? $academy->name_ar }}"> <meta property="og:title" content="{{ $settings->site_title ?? $academy->name_ar }}">
<meta property="og:description" content="{{ $settings->site_description ?? '' }}"> <meta property="og:description" content="{{ $settings->site_description ?? '' }}">
<meta property="og:url" content="{{ url()->current() }}">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:locale" content="ar_EG"> <meta property="og:locale" content="ar_EG">
@if($academy->logo_path) @if($academy->logo_path)
<meta property="og:image" content="{{ asset('storage/' . $academy->logo_path) }}"> <meta property="og:image" content="{{ asset('storage/' . $academy->logo_path) }}">
@endif @endif
{{-- Twitter Card --}}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ $settings->site_title ?? $academy->name_ar }}">
<meta name="twitter:description" content="{{ $settings->site_description ?? '' }}">
@if($academy->logo_path)
<meta name="twitter:image" content="{{ asset('storage/' . $academy->logo_path) }}">
@endif
{{-- Fonts --}} {{-- Fonts --}}
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
...@@ -138,6 +155,9 @@ function gtag(){dataLayer.push(arguments);} ...@@ -138,6 +155,9 @@ function gtag(){dataLayer.push(arguments);}
@endif @endif
</head> </head>
<body class="website-body" data-template="{{ $settings->template ?? 'bold_athletic' }}" data-density="{{ $settings->density ?? 'comfortable' }}" data-radius="{{ $settings->border_radius_preset ?? 'rounded' }}" data-card="{{ $settings->card_style ?? 'raised' }}" data-animations="{{ ($settings->animations_enabled ?? true) ? 'true' : 'false' }}" data-anim-speed="{{ $settings->animation_speed ?? 'normal' }}"> <body class="website-body" data-template="{{ $settings->template ?? 'bold_athletic' }}" data-density="{{ $settings->density ?? 'comfortable' }}" data-radius="{{ $settings->border_radius_preset ?? 'rounded' }}" data-card="{{ $settings->card_style ?? 'raised' }}" data-animations="{{ ($settings->animations_enabled ?? true) ? 'true' : 'false' }}" data-anim-speed="{{ $settings->animation_speed ?? 'normal' }}">
{{-- Announcement Bar --}}
@include('website.partials.announcement-bar')
{{-- Navbar --}} {{-- Navbar --}}
@include('website.navbar') @include('website.navbar')
...@@ -150,18 +170,11 @@ function gtag(){dataLayer.push(arguments);} ...@@ -150,18 +170,11 @@ function gtag(){dataLayer.push(arguments);}
{{-- Footer --}} {{-- Footer --}}
@include('website.footer') @include('website.footer')
{{-- WhatsApp Float --}} {{-- Floating Elements (WhatsApp, scroll-to-top, custom button) --}}
@if($settings->whatsapp_number) @include('website.partials.floating-elements')
<a href="https://wa.me/{{ preg_replace('/[^0-9]/', '', $settings->whatsapp_number) }}"
target="_blank" {{-- Popup Modal --}}
rel="noopener" @include('website.partials.popup-modal')
class="whatsapp-float"
aria-label="تواصل عبر واتساب">
<svg width="28" height="28" viewBox="0 0 24 24" fill="white">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
</svg>
</a>
@endif
@isset($isPreview) @isset($isPreview)
<div class="fixed top-4 left-1/2 -translate-x-1/2 z-[200] bg-amber-500 text-black px-4 py-2 rounded-full text-sm font-bold shadow-lg"> <div class="fixed top-4 left-1/2 -translate-x-1/2 z-[200] bg-amber-500 text-black px-4 py-2 rounded-full text-sm font-bold shadow-lg">
......
@php
$bar = $settings->announcement_bar ?? [];
$enabled = $bar['enabled'] ?? false;
$showUntil = $bar['show_until'] ?? null;
$isExpired = $showUntil && now()->isAfter($showUntil);
@endphp
@if($enabled && !$isExpired)
<div x-data="{ dismissed: localStorage.getItem('announcement_dismissed_{{ $academy->id }}') === 'true' }"
x-show="!dismissed"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 max-h-16"
x-transition:leave-end="opacity-0 max-h-0"
class="relative overflow-hidden"
style="background-color: {{ $bar['bg_color'] ?? 'var(--site-accent)' }}; color: {{ $bar['text_color'] ?? '#ffffff' }}">
<div class="max-w-7xl mx-auto px-4 py-2.5 flex items-center justify-center gap-3 text-sm font-medium">
@if(!empty($bar['link']))
<a href="{{ $bar['link'] }}" class="hover:underline">
{{ $bar['text'] ?? '' }}
</a>
@else
<span>{{ $bar['text'] ?? '' }}</span>
@endif
@if($bar['dismissible'] ?? true)
<button @click="dismissed = true; localStorage.setItem('announcement_dismissed_{{ $academy->id }}', 'true')"
class="absolute end-3 top-1/2 -translate-y-1/2 p-1 rounded-full hover:bg-white/20 transition-colors"
aria-label="إغلاق">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
@endif
</div>
</div>
@endif
@php
$elements = $settings->floating_elements ?? [];
$whatsapp = $elements['whatsapp'] ?? [];
$scrollTop = $elements['scroll_to_top'] ?? [];
$customBtn = $elements['custom_button'] ?? [];
$whatsappNumber = $whatsapp['number'] ?? $settings->whatsapp_number ?? null;
@endphp
<div class="fixed bottom-6 end-6 z-50 flex flex-col items-end gap-3" x-data="{ showScrollTop: false }" @scroll.window="showScrollTop = window.scrollY > 600">
{{-- Scroll to Top --}}
@if($scrollTop['enabled'] ?? true)
<button x-show="showScrollTop"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-75"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-75"
@click="window.scrollTo({ top: 0, behavior: 'smooth' })"
class="w-11 h-11 rounded-full bg-gray-800/80 text-white shadow-lg hover:bg-gray-900 backdrop-blur-sm flex items-center justify-center transition-all hover:scale-110"
aria-label="العودة للأعلى">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/></svg>
</button>
@endif
{{-- Custom Button --}}
@if(($customBtn['enabled'] ?? false) && !empty($customBtn['url']))
<a href="{{ $customBtn['url'] }}"
class="px-4 py-2.5 rounded-full text-white text-sm font-medium shadow-lg hover:shadow-xl transition-all hover:scale-105 flex items-center gap-2"
style="background-color: {{ $customBtn['color'] ?? 'var(--site-accent)' }}">
@if(!empty($customBtn['icon']) && $customBtn['icon'] === 'phone')
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/></svg>
@endif
{{ $customBtn['text'] ?? 'تواصل معنا' }}
</a>
@endif
{{-- WhatsApp --}}
@if(($whatsapp['enabled'] ?? true) && $whatsappNumber)
@php
$waMessage = $whatsapp['message'] ?? 'مرحبا، أريد الاستفسار عن...';
$waLink = whatsapp_link($whatsappNumber, $waMessage);
@endphp
<a href="{{ $waLink }}" target="_blank" rel="noopener"
class="w-14 h-14 rounded-full bg-green-500 text-white shadow-lg hover:shadow-xl hover:bg-green-600 flex items-center justify-center transition-all hover:scale-110 group"
aria-label="تواصل عبر واتساب">
<svg class="w-7 h-7 group-hover:scale-110 transition-transform" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z"/>
</svg>
</a>
@endif
</div>
@php
$popup = $settings->popup_config ?? [];
$enabled = $popup['enabled'] ?? false;
$delay = $popup['delay_seconds'] ?? 3;
$showOnce = $popup['show_once'] ?? true;
@endphp
@if($enabled && !empty($popup['title']))
<div x-data="{
open: false,
init() {
const key = 'popup_shown_{{ $academy->id }}';
const wasShown = {{ $showOnce ? 'true' : 'false' }} && localStorage.getItem(key);
if (!wasShown) {
setTimeout(() => {
this.open = true;
if ({{ $showOnce ? 'true' : 'false' }}) localStorage.setItem(key, 'true');
}, {{ $delay * 1000 }});
}
}
}"
x-show="open"
x-cloak
class="fixed inset-0 z-[9999] flex items-center justify-center p-4"
@keydown.escape.window="open = false">
{{-- Backdrop --}}
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
{{-- Modal --}}
<div x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-90 translate-y-4"
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-90"
class="relative w-full max-w-md bg-white rounded-2xl shadow-2xl overflow-hidden">
{{-- Close --}}
<button @click="open = false"
class="absolute top-3 end-3 z-10 w-8 h-8 rounded-full bg-black/10 hover:bg-black/20 flex items-center justify-center transition-colors"
aria-label="إغلاق">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
{{-- Image --}}
@if(!empty($popup['image_url']))
<div class="w-full h-48 overflow-hidden">
<img src="{{ $popup['image_url'] }}" alt="{{ $popup['title'] }}" class="w-full h-full object-cover">
</div>
@endif
{{-- Content --}}
<div class="p-6 text-center">
<h3 class="text-xl font-bold text-gray-900">{{ $popup['title'] }}</h3>
@if(!empty($popup['body']))
<p class="mt-3 text-gray-600 leading-relaxed text-sm">{{ $popup['body'] }}</p>
@endif
@if(!empty($popup['cta_text']) && !empty($popup['cta_url']))
<a href="{{ $popup['cta_url'] }}"
class="mt-5 inline-block px-6 py-2.5 rounded-full text-white text-sm font-medium shadow-lg hover:shadow-xl transition-all hover:scale-105"
style="background-color: {{ $popup['cta_color'] ?? 'var(--site-accent)' }}">
{{ $popup['cta_text'] }}
</a>
@endif
</div>
</div>
</div>
@endif
<div class="site-section">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@if($section->title)
<div class="text-center mb-12 reveal">
<h2 class="section-title mx-auto">{{ $section->title }}</h2>
@if($section->subtitle)
<p class="mt-4 text-gray-500 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
@endif
<div class="reveal prose prose-lg max-w-none dark:prose-invert">
{!! $section->content !!}
</div>
</div>
</div>
@php @php
$faqs = $data['faqs'] ?? collect(); $faqs = $data['faqs'] ?? collect();
$showCategories = $section->getSetting('show_categories', true);
$showSearch = $section->getSetting('show_search', true);
$expandFirst = $section->getSetting('expand_first', true);
$categories = $faqs->pluck('category')->filter()->unique()->values();
@endphp @endphp
<div class="site-section site-section--alt"> <div class="site-section site-section--alt">
...@@ -12,9 +16,61 @@ ...@@ -12,9 +16,61 @@
</div> </div>
@if($faqs->count()) @if($faqs->count())
<div class="space-y-3" x-data="{ open: null }"> <div x-data="{
open: {{ $expandFirst ? $faqs->first()->id : 'null' }},
search: '',
activeCategory: 'all',
get filteredFaqs() {
return Array.from(this.$refs.faqList.children).forEach(el => {
const q = el.dataset.question?.toLowerCase() || '';
const a = el.dataset.answer?.toLowerCase() || '';
const cat = el.dataset.category || '';
const matchSearch = !this.search || q.includes(this.search.toLowerCase()) || a.includes(this.search.toLowerCase());
const matchCat = this.activeCategory === 'all' || cat === this.activeCategory;
el.style.display = (matchSearch && matchCat) ? '' : 'none';
});
}
}" x-init="$watch('search', () => filteredFaqs); $watch('activeCategory', () => filteredFaqs)">
{{-- Search --}}
@if($showSearch)
<div class="mb-6 reveal">
<div class="relative">
<svg class="absolute start-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input type="text" x-model="search"
placeholder="ابحث في الأسئلة..."
class="w-full ps-12 pe-4 py-3 rounded-xl border border-gray-200 bg-white focus:ring-2 focus:ring-[var(--site-accent)]/20 focus:border-[var(--site-accent)] text-sm transition-all">
</div>
</div>
@endif
{{-- Category Tabs --}}
@if($showCategories && $categories->count() > 1)
<div class="flex items-center gap-2 mb-6 overflow-x-auto pb-2 scrollbar-hide reveal">
<button @click="activeCategory = 'all'"
:class="activeCategory === 'all' ? 'bg-[var(--site-accent)] text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap">
الكل
</button>
@foreach($categories as $cat)
<button @click="activeCategory = '{{ $cat }}'"
:class="activeCategory === '{{ $cat }}' ? 'bg-[var(--site-accent)] text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap">
{{ $cat }}
</button>
@endforeach
</div>
@endif
{{-- FAQ List --}}
<div class="space-y-3" x-ref="faqList">
@foreach($faqs as $faq) @foreach($faqs as $faq)
<div class="faq-item reveal reveal--delay-{{ min($loop->index + 1, 4) }}" <div class="faq-item reveal reveal--delay-{{ min($loop->index + 1, 4) }}"
data-question="{{ mb_strtolower($faq->question) }}"
data-answer="{{ mb_strtolower($faq->answer) }}"
data-category="{{ $faq->category ?? '' }}"
:data-open="open === {{ $faq->id }} ? 'true' : 'false'"> :data-open="open === {{ $faq->id }} ? 'true' : 'false'">
<button @click="open = open === {{ $faq->id }} ? null : {{ $faq->id }}" <button @click="open = open === {{ $faq->id }} ? null : {{ $faq->id }}"
class="w-full flex items-center justify-between p-5 text-start"> class="w-full flex items-center justify-between p-5 text-start">
...@@ -33,6 +89,14 @@ class="px-5 pb-5"> ...@@ -33,6 +89,14 @@ class="px-5 pb-5">
</div> </div>
@endforeach @endforeach
</div> </div>
{{-- No Results --}}
<div x-show="search && !Array.from($refs.faqList.children).some(el => el.style.display !== 'none')"
x-cloak
class="text-center py-8 text-gray-500 text-sm">
لا توجد نتائج لـ "<span x-text="search"></span>"
</div>
</div>
@else @else
<div class="space-y-3"> <div class="space-y-3">
@for($i = 0; $i < 3; $i++) @for($i = 0; $i < 3; $i++)
......
@php @php
$gallery = $data['gallery'] ?? collect(); $gallery = $data['gallery'] ?? collect();
$showCategories = $section->getSetting('show_categories', true);
$layout = $section->getSetting('layout', 'grid');
$categories = $gallery->pluck('gallery_category')->filter()->unique()->values();
@endphp @endphp
<div class="site-section" x-data="{ lightbox: null }"> <div class="site-section" x-data="{ lightbox: null, lightboxIdx: 0, activeCategory: 'all', images: [] }"
x-init="images = Array.from($refs.galleryGrid?.children || [])">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12 reveal"> <div class="text-center mb-12 reveal">
<h2 class="section-title mx-auto">{{ $section->title ?? 'معرض الصور' }}</h2> <h2 class="section-title mx-auto">{{ $section->title ?? 'معرض الصور' }}</h2>
...@@ -12,9 +16,32 @@ ...@@ -12,9 +16,32 @@
</div> </div>
@if($gallery->count()) @if($gallery->count())
<div class="gallery-grid reveal"> {{-- Category Filter Tabs --}}
@foreach($gallery as $image) @if($showCategories && $categories->count() > 1)
<div class="gallery-item" @click="lightbox = '{{ $image->url }}'"> <div class="flex items-center justify-center gap-2 mb-8 overflow-x-auto pb-2 scrollbar-hide reveal">
<button @click="activeCategory = 'all'; images.forEach(el => el.style.display = '')"
:class="activeCategory === 'all' ? 'bg-[var(--site-accent)] text-white shadow-md' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'"
class="px-5 py-2 rounded-full text-sm font-medium transition-all whitespace-nowrap">
الكل
</button>
@foreach($categories as $cat)
<button @click="activeCategory = '{{ $cat }}'; images.forEach(el => el.style.display = el.dataset.category === '{{ $cat }}' || !el.dataset.category ? '' : 'none')"
:class="activeCategory === '{{ $cat }}' ? 'bg-[var(--site-accent)] text-white shadow-md' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'"
class="px-5 py-2 rounded-full text-sm font-medium transition-all whitespace-nowrap">
{{ $cat }}
</button>
@endforeach
</div>
@endif
{{-- Gallery Grid --}}
<div class="gallery-grid reveal" x-ref="galleryGrid"
@if($layout === 'masonry') style="columns: 3; column-gap: 1rem;" @endif>
@foreach($gallery as $index => $image)
<div class="gallery-item"
data-category="{{ $image->gallery_category ?? '' }}"
@click="lightbox = '{{ $image->url }}'; lightboxIdx = {{ $index }}"
@if($layout === 'masonry') style="break-inside: avoid; margin-bottom: 1rem;" @endif>
<img src="{{ $image->url }}" <img src="{{ $image->url }}"
alt="{{ $image->alt_text_ar ?? 'صورة من المعرض' }}" alt="{{ $image->alt_text_ar ?? 'صورة من المعرض' }}"
loading="lazy"> loading="lazy">
...@@ -23,6 +50,11 @@ ...@@ -23,6 +50,11 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"/>
</svg> </svg>
</div> </div>
@if($image->gallery_category)
<span class="absolute bottom-2 start-2 z-10 px-2 py-0.5 bg-black/50 text-white text-xs rounded-full backdrop-blur-sm">
{{ $image->gallery_category }}
</span>
@endif
</div> </div>
@endforeach @endforeach
</div> </div>
...@@ -38,18 +70,39 @@ ...@@ -38,18 +70,39 @@
@endif @endif
</div> </div>
{{-- Lightbox --}} {{-- Lightbox with Navigation --}}
<div x-show="lightbox" <div x-show="lightbox"
x-transition.opacity x-transition.opacity
@click.self="lightbox = null" @click.self="lightbox = null"
@keydown.escape.window="lightbox = null" @keydown.escape.window="lightbox = null"
@keydown.left.window="if(lightbox) { lightboxIdx = (lightboxIdx - 1 + {{ $gallery->count() }}) % {{ $gallery->count() }}; lightbox = [{{ $gallery->map(fn($i) => "'" . $i->url . "'")->implode(',') }}][lightboxIdx]; }"
@keydown.right.window="if(lightbox) { lightboxIdx = (lightboxIdx + 1) % {{ $gallery->count() }}; lightbox = [{{ $gallery->map(fn($i) => "'" . $i->url . "'")->implode(',') }}][lightboxIdx]; }"
class="fixed inset-0 z-[200] bg-black/95 flex items-center justify-center p-4 cursor-pointer" class="fixed inset-0 z-[200] bg-black/95 flex items-center justify-center p-4 cursor-pointer"
style="display: none;"> x-cloak>
<img :src="lightbox" class="max-w-full max-h-[90vh] rounded-lg object-contain"> <img :src="lightbox" class="max-w-full max-h-[90vh] rounded-lg object-contain">
<button @click="lightbox = null" class="absolute top-4 end-4 text-white/60 hover:text-white p-2">
{{-- Navigation Arrows --}}
@if($gallery->count() > 1)
<button @click.stop="lightboxIdx = (lightboxIdx - 1 + {{ $gallery->count() }}) % {{ $gallery->count() }}; lightbox = [{{ $gallery->map(fn($i) => "'" . $i->url . "'")->implode(',') }}][lightboxIdx]"
class="absolute start-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
</button>
<button @click.stop="lightboxIdx = (lightboxIdx + 1) % {{ $gallery->count() }}; lightbox = [{{ $gallery->map(fn($i) => "'" . $i->url . "'")->implode(',') }}][lightboxIdx]"
class="absolute end-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</button>
@endif
{{-- Close --}}
<button @click="lightbox = null" class="absolute top-4 end-4 text-white/60 hover:text-white p-2 transition-colors">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg> </svg>
</button> </button>
{{-- Counter --}}
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 text-white/60 text-sm">
<span x-text="lightboxIdx + 1"></span> / {{ $gallery->count() }}
</div>
</div> </div>
</div> </div>
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
@endif @endif
<a href="#section-contact" <a href="#section-contact"
class="mt-6 block text-center site-btn {{ $highlighted ? 'site-btn--primary' : 'site-btn--outline' }} w-full justify-center"> class="mt-6 block text-center site-btn {{ $highlighted ? 'site-btn--primary' : 'site-btn--outline-accent' }} w-full justify-center">
{{ $plan['cta'] ?? 'اشترك الآن' }} {{ $plan['cta'] ?? 'اشترك الآن' }}
</a> </a>
</div> </div>
......
@php @php
$schedule = json_decode($section->getSetting('schedule_data', '[]'), true) ?? []; $schedule = json_decode($section->getSetting('schedule_data', '[]'), true) ?? [];
$days = ['السبت', 'الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة']; $days = ['السبت', 'الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة'];
$groupedByDay = collect($schedule)->groupBy('day');
$activeDays = $days;
$todayIdx = (int) now()->dayOfWeek;
$arabicDayMap = [6 => 'السبت', 0 => 'الأحد', 1 => 'الاثنين', 2 => 'الثلاثاء', 3 => 'الأربعاء', 4 => 'الخميس', 5 => 'الجمعة'];
$todayArabic = $arabicDayMap[$todayIdx] ?? 'السبت';
@endphp @endphp
<div class="site-section site-section--alt"> <div class="site-section site-section--alt">
...@@ -13,27 +18,72 @@ ...@@ -13,27 +18,72 @@
</div> </div>
@if(count($schedule)) @if(count($schedule))
<div class="overflow-x-auto reveal bg-white rounded-xl shadow-sm border border-gray-200"> <div class="reveal" x-data="{ activeDay: '{{ $todayArabic }}' }">
<table class="w-full text-sm"> {{-- Day Tabs --}}
<thead> <div class="flex overflow-x-auto gap-2 pb-4 mb-6 scrollbar-hide">
<tr class="border-b border-gray-200 bg-gray-50"> <button type="button" @click="activeDay = ''" class="shrink-0 px-4 py-2.5 rounded-xl text-sm font-medium transition-all"
<th class="py-3 px-4 text-start text-gray-600 font-medium">اليوم</th> :class="activeDay === '' ? 'bg-[var(--site-accent)] text-white shadow-lg shadow-[var(--site-accent)]/20' : 'bg-white text-gray-600 hover:bg-gray-100 border border-gray-200'">
<th class="py-3 px-4 text-start text-gray-600 font-medium">النشاط</th> الكل
<th class="py-3 px-4 text-start text-gray-600 font-medium">الوقت</th> </button>
<th class="py-3 px-4 text-start text-gray-600 font-medium">الفرع</th> @foreach($days as $day)
</tr> @if($groupedByDay->has($day))
</thead> <button type="button" @click="activeDay = '{{ $day }}'" class="shrink-0 px-4 py-2.5 rounded-xl text-sm font-medium transition-all"
<tbody> :class="activeDay === '{{ $day }}' ? 'bg-[var(--site-accent)] text-white shadow-lg shadow-[var(--site-accent)]/20' : 'bg-white text-gray-600 hover:bg-gray-100 border border-gray-200'">
@foreach($schedule as $row) {{ $day }}
<tr class="border-b border-gray-100 hover:bg-gray-50 transition-colors"> @if($day === $todayArabic)
<td class="py-3 px-4 text-gray-900 font-medium">{{ $row['day'] ?? '' }}</td> <span class="inline-block w-1.5 h-1.5 rounded-full bg-green-400 ms-1 animate-pulse"></span>
<td class="py-3 px-4 text-gray-600">{{ $row['activity'] ?? '' }}</td> @endif
<td class="py-3 px-4 text-gray-600" dir="ltr">{{ $row['time'] ?? '' }}</td> </button>
<td class="py-3 px-4 text-gray-600">{{ $row['branch'] ?? '' }}</td> @endif
</tr>
@endforeach @endforeach
</tbody> </div>
</table>
{{-- Schedule Cards --}}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@foreach($schedule as $index => $row)
<div x-show="activeDay === '' || activeDay === '{{ $row['day'] ?? '' }}'"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
class="bg-white rounded-xl border border-gray-100 p-4 shadow-sm hover:shadow-md transition-shadow group">
<div class="flex items-start gap-3">
{{-- Time Badge --}}
<div class="shrink-0 w-16 text-center">
<div class="bg-[var(--site-accent)]/5 rounded-lg p-2 group-hover:bg-[var(--site-accent)]/10 transition-colors">
<svg class="w-5 h-5 mx-auto text-[var(--site-accent)] mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span class="text-[11px] font-bold text-[var(--site-accent)]" dir="ltr">{{ $row['time'] ?? '' }}</span>
</div>
</div>
{{-- Info --}}
<div class="flex-1 min-w-0">
<h4 class="text-gray-900 font-bold text-sm">{{ $row['activity'] ?? '' }}</h4>
<div class="flex items-center gap-3 mt-2">
<span class="inline-flex items-center gap-1 text-xs text-gray-500">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{{ $row['day'] ?? '' }}
</span>
@if(!empty($row['branch']))
<span class="inline-flex items-center gap-1 text-xs text-gray-500">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/></svg>
{{ $row['branch'] }}
</span>
@endif
</div>
</div>
</div>
</div>
@endforeach
</div>
{{-- Today highlight --}}
<div class="mt-6 text-center" x-show="activeDay === '{{ $todayArabic }}'">
<p class="text-xs text-gray-400 flex items-center justify-center gap-1">
<span class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
اليوم — {{ $todayArabic }}
</p>
</div>
</div> </div>
@else @else
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
......
@php @php
$stats = $data['stats'] ?? []; $stats = $data['stats'] ?? [];
$customStats = $section->getSetting('custom_stats', []);
$layout = $section->getSetting('layout', 'default');
$displayStats = [ $displayStats = [
'participants' => ['label' => 'طالب نشط', 'icon' => 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z'], 'participants' => ['label' => 'طالب نشط', 'icon' => 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z'],
'trainers' => ['label' => 'مدرب محترف', 'icon' => 'M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z'], 'trainers' => ['label' => 'مدرب محترف', 'icon' => 'M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z'],
...@@ -7,7 +10,9 @@ ...@@ -7,7 +10,9 @@
'programs' => ['label' => 'برنامج تدريبي', 'icon' => 'M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253'], 'programs' => ['label' => 'برنامج تدريبي', 'icon' => 'M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253'],
'activities' => ['label' => 'نشاط رياضي', 'icon' => 'M13 10V3L4 14h7v7l9-11h-7z'], 'activities' => ['label' => 'نشاط رياضي', 'icon' => 'M13 10V3L4 14h7v7l9-11h-7z'],
]; ];
$hasStats = !empty($stats) && array_sum($stats) > 0; $hasStats = !empty($stats) && array_sum($stats) > 0;
$hasCustom = !empty($customStats);
@endphp @endphp
<div class="stats-band py-16 lg:py-20"> <div class="stats-band py-16 lg:py-20">
...@@ -15,25 +20,78 @@ ...@@ -15,25 +20,78 @@
@if($section->title) @if($section->title)
<div class="text-center mb-12 reveal"> <div class="text-center mb-12 reveal">
<h2 class="section-title mx-auto">{{ $section->title ?? 'إنجازاتنا' }}</h2> <h2 class="section-title mx-auto">{{ $section->title ?? 'إنجازاتنا' }}</h2>
@if($section->subtitle)
<p class="mt-4 text-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div> </div>
@endif @endif
@if($hasStats) @if($hasStats || $hasCustom)
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-8"> @php
@foreach($displayStats as $key => $meta) $allStats = [];
@if(($stats[$key] ?? 0) > 0) foreach ($displayStats as $key => $meta) {
<div class="text-center reveal reveal--delay-{{ $loop->index + 1 }}"> if (($stats[$key] ?? 0) > 0) {
$allStats[] = ['value' => $stats[$key], 'label' => $meta['label'], 'icon' => $meta['icon'], 'suffix' => '+'];
}
}
foreach ($customStats as $cs) {
if (!empty($cs['value']) && !empty($cs['label'])) {
$allStats[] = ['value' => $cs['value'], 'label' => $cs['label'], 'icon' => $cs['icon'] ?? null, 'suffix' => $cs['suffix'] ?? '+'];
}
}
$colClass = match(count($allStats)) {
1, 2 => 'grid-cols-1 sm:grid-cols-2',
3 => 'grid-cols-1 sm:grid-cols-3',
4 => 'grid-cols-2 lg:grid-cols-4',
default => 'grid-cols-2 md:grid-cols-3 lg:grid-cols-' . min(count($allStats), 5),
};
@endphp
@if($layout === 'cards')
<div class="grid {{ $colClass }} gap-6">
@foreach($allStats as $i => $stat)
<div class="text-center p-6 rounded-2xl bg-white/5 border border-white/10 hover:bg-white/10 transition-colors reveal reveal--delay-{{ min($i + 1, 4) }}">
@if($stat['icon'])
<div class="w-12 h-12 mx-auto mb-3 rounded-full bg-[var(--site-accent)]/20 flex items-center justify-center">
<svg class="w-6 h-6 text-[var(--site-accent)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="{{ $stat['icon'] }}"/>
</svg>
</div>
@endif
<span class="stat-number text-3xl" data-counter="{{ $stat['value'] }}" data-suffix="{{ $stat['suffix'] }}">0</span>
<span class="block mt-2 text-white/60 text-sm">{{ $stat['label'] }}</span>
</div>
@endforeach
</div>
@elseif($layout === 'minimal')
<div class="flex flex-wrap items-center justify-center gap-12 md:gap-16">
@foreach($allStats as $i => $stat)
<div class="text-center reveal reveal--delay-{{ min($i + 1, 4) }}">
<span class="stat-number text-4xl lg:text-5xl" data-counter="{{ $stat['value'] }}" data-suffix="{{ $stat['suffix'] }}">0</span>
<span class="block mt-2 text-white/60 text-sm tracking-wide uppercase">{{ $stat['label'] }}</span>
</div>
@endforeach
</div>
@else
{{-- Default --}}
<div class="grid {{ $colClass }} gap-8">
@foreach($allStats as $i => $stat)
<div class="text-center reveal reveal--delay-{{ min($i + 1, 4) }}">
@if($stat['icon'])
<div class="w-14 h-14 mx-auto mb-4 rounded-xl bg-[var(--site-accent)]/10 flex items-center justify-center"> <div class="w-14 h-14 mx-auto mb-4 rounded-xl bg-[var(--site-accent)]/10 flex items-center justify-center">
<svg class="w-7 h-7 text-[var(--site-accent)]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-7 h-7 text-[var(--site-accent)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="{{ $meta['icon'] }}"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="{{ $stat['icon'] }}"/>
</svg> </svg>
</div> </div>
<span class="stat-number" data-counter="{{ $stats[$key] }}" data-suffix="+">0</span>
<span class="block mt-2 text-white/60 text-sm">{{ $meta['label'] }}</span>
</div>
@endif @endif
<span class="stat-number" data-counter="{{ $stat['value'] }}" data-suffix="{{ $stat['suffix'] }}">0</span>
<span class="block mt-2 text-white/60 text-sm">{{ $stat['label'] }}</span>
</div>
@endforeach @endforeach
</div> </div>
@endif
@else @else
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-8"> <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-8">
@foreach($displayStats as $key => $meta) @foreach($displayStats as $key => $meta)
......
@php
$videos = $section->getSetting('videos', []);
$layout = $section->getSetting('layout', 'featured');
$showTitles = $section->getSetting('show_titles', true);
@endphp
<div class="site-section">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12 reveal">
<h2 class="section-title mx-auto">{{ $section->title ?? 'فيديو' }}</h2>
@if($section->subtitle)
<p class="mt-4 text-gray-500 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
@if(count($videos))
@if($layout === 'featured' && count($videos) > 1)
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 reveal" x-data="{
activeIdx: 0,
videos: @js(collect($videos)->map(fn($v) => ['url' => video_embed_url($v['url'] ?? ''), 'title' => $v['title'] ?? ''])->toArray())
}">
{{-- Featured (active video large) --}}
<div class="lg:col-span-2">
<div class="relative aspect-video rounded-xl overflow-hidden shadow-lg">
<iframe :src="videos[activeIdx]?.url" class="absolute inset-0 w-full h-full" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
<h3 x-show="videos[activeIdx]?.title" x-text="videos[activeIdx]?.title" class="mt-3 text-lg font-bold text-gray-900"></h3>
</div>
{{-- Sidebar thumbnails --}}
<div class="space-y-3 max-h-[400px] overflow-y-auto">
@foreach($videos as $vIdx => $video)
<div class="relative aspect-video rounded-lg overflow-hidden shadow-sm cursor-pointer group transition-all"
:class="activeIdx === {{ $vIdx }} ? 'ring-2 ring-[var(--site-accent)] scale-[1.02]' : 'opacity-80 hover:opacity-100'"
@click="activeIdx = {{ $vIdx }}">
@php $thumbUrl = video_embed_url($video['url'] ?? ''); @endphp
@if($thumbUrl)
<iframe src="{{ $thumbUrl }}" class="absolute inset-0 w-full h-full pointer-events-none" frameborder="0" loading="lazy" tabindex="-1"></iframe>
@endif
<div class="absolute inset-0 bg-black/30 group-hover:bg-black/10 transition-colors flex items-center justify-center">
<svg class="w-8 h-8 text-white opacity-90" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
</div>
</div>
@if($showTitles && !empty($video['title']))
<p class="text-sm font-medium text-gray-700 -mt-1.5 mb-1">{{ $video['title'] }}</p>
@endif
@endforeach
</div>
</div>
@elseif($layout === 'single' || count($videos) === 1)
<div class="max-w-4xl mx-auto reveal">
<div class="relative aspect-video rounded-xl overflow-hidden shadow-xl">
@php $embedUrl = video_embed_url($videos[0]['url'] ?? ''); @endphp
@if($embedUrl)
<iframe src="{{ $embedUrl }}" class="absolute inset-0 w-full h-full" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen loading="lazy"></iframe>
@endif
</div>
@if($showTitles && !empty($videos[0]['title']))
<h3 class="mt-4 text-xl font-bold text-gray-900 text-center">{{ $videos[0]['title'] }}</h3>
@endif
</div>
@elseif($layout === 'grid')
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@foreach($videos as $index => $video)
<div class="reveal reveal--delay-{{ ($index % 4) + 1 }}">
<div class="relative aspect-video rounded-xl overflow-hidden shadow-md">
@php $embedUrl = video_embed_url($video['url'] ?? ''); @endphp
@if($embedUrl)
<iframe src="{{ $embedUrl }}" class="absolute inset-0 w-full h-full" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen loading="lazy"></iframe>
@endif
</div>
@if($showTitles && !empty($video['title']))
<h4 class="mt-2 font-semibold text-gray-800">{{ $video['title'] }}</h4>
@endif
</div>
@endforeach
</div>
@else
{{-- Carousel --}}
<div class="flex overflow-x-auto snap-x snap-mandatory gap-6 pb-4 -mx-4 px-4 scrollbar-hide">
@foreach($videos as $index => $video)
<div class="min-w-[400px] sm:min-w-[500px] snap-start shrink-0 reveal">
<div class="relative aspect-video rounded-xl overflow-hidden shadow-md">
@php $embedUrl = video_embed_url($video['url'] ?? ''); @endphp
@if($embedUrl)
<iframe src="{{ $embedUrl }}" class="absolute inset-0 w-full h-full" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen loading="lazy"></iframe>
@endif
</div>
@if($showTitles && !empty($video['title']))
<h4 class="mt-2 font-semibold text-gray-800">{{ $video['title'] }}</h4>
@endif
</div>
@endforeach
</div>
@endif
@else
<div class="max-w-lg mx-auto text-center py-12">
<svg class="w-16 h-16 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664zM21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<p class="text-gray-500 text-sm">لم تُضف فيديوهات بعد</p>
</div>
@endif
</div>
</div>
...@@ -98,6 +98,8 @@ ...@@ -98,6 +98,8 @@
Route::post('/contact', [ContactFormController::class, 'submit'])->name('contact.submit'); Route::post('/contact', [ContactFormController::class, 'submit'])->name('contact.submit');
Route::get('/events', [\App\Http\Controllers\PublicEventController::class, 'index'])->name('events.index'); Route::get('/events', [\App\Http\Controllers\PublicEventController::class, 'index'])->name('events.index');
Route::get('/events/{eventSlug}', [\App\Http\Controllers\PublicEventController::class, 'show'])->name('events.show'); Route::get('/events/{eventSlug}', [\App\Http\Controllers\PublicEventController::class, 'show'])->name('events.show');
Route::get('/news', [PublicWebsiteController::class, 'newsIndex'])->name('news.index');
Route::get('/news/{news}', [PublicWebsiteController::class, 'newsShow'])->name('news.show');
}); });
/* /*
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment