Commit 13f1bb72 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add complete Public Website Builder with CMS and Bold Athletic template

Multi-tenant public website system: each academy gets a data-driven advertising
site at /site/{slug} with 16 customizable sections, theme editor, gallery,
testimonials, FAQ, news, partners, and contact form management.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 32927760
<?php
namespace App\Domain\Website\Enums;
enum ContactSubmissionStatus: string
{
case New = 'new';
case Read = 'read';
case Replied = 'replied';
case Archived = 'archived';
public function label(): string
{
return match ($this) {
self::New => 'جديد',
self::Read => 'مقروء',
self::Replied => 'تم الرد',
self::Archived => 'مؤرشف',
};
}
public function color(): string
{
return match ($this) {
self::New => 'blue',
self::Read => 'amber',
self::Replied => 'green',
self::Archived => 'gray',
};
}
}
<?php
namespace App\Domain\Website\Enums;
enum MediaCollection: string
{
case AcademyLogo = 'academy_logo';
case AcademyCover = 'academy_cover';
case ActivityPhoto = 'activity_photo';
case BranchPhoto = 'branch_photo';
case Gallery = 'gallery';
case TeamPhoto = 'team_photo';
case TestimonialAvatar = 'testimonial_avatar';
case PartnerLogo = 'partner_logo';
case NewsImage = 'news_image';
case SectionImage = 'section_image';
case General = 'general';
public function dimensions(): array
{
return match ($this) {
self::AcademyLogo => ['width' => 400, 'height' => 400],
self::AcademyCover => ['width' => 1920, 'height' => 600],
self::ActivityPhoto => ['width' => 800, 'height' => 600],
self::BranchPhoto => ['width' => 1200, 'height' => 800],
self::Gallery => ['width' => 1200, 'height' => 800],
self::TeamPhoto => ['width' => 600, 'height' => 600],
self::TestimonialAvatar => ['width' => 200, 'height' => 200],
self::PartnerLogo => ['width' => 300, 'height' => 120],
self::NewsImage => ['width' => 1200, 'height' => 630],
self::SectionImage => ['width' => 800, 'height' => 600],
self::General => ['width' => 1200, 'height' => 800],
};
}
public function aspectRatio(): string
{
return match ($this) {
self::AcademyLogo => '1:1',
self::AcademyCover => '16:5',
self::ActivityPhoto => '4:3',
self::BranchPhoto => '3:2',
self::Gallery => '3:2',
self::TeamPhoto => '1:1',
self::TestimonialAvatar => '1:1',
self::PartnerLogo => '5:2',
self::NewsImage => '1.91:1',
self::SectionImage => '4:3',
self::General => '3:2',
};
}
public function maxSizeKb(): int
{
return match ($this) {
self::AcademyLogo => 500,
self::AcademyCover => 2048,
self::ActivityPhoto => 1024,
self::BranchPhoto => 1536,
self::Gallery => 1536,
self::TeamPhoto => 500,
self::TestimonialAvatar => 200,
self::PartnerLogo => 300,
self::NewsImage => 1536,
self::SectionImage => 1024,
self::General => 2048,
};
}
public function label(): string
{
return match ($this) {
self::AcademyLogo => 'شعار الأكاديمية',
self::AcademyCover => 'صورة الغلاف',
self::ActivityPhoto => 'صورة النشاط',
self::BranchPhoto => 'صورة الفرع',
self::Gallery => 'معرض الصور',
self::TeamPhoto => 'صورة المدرب',
self::TestimonialAvatar => 'صورة العميل',
self::PartnerLogo => 'شعار الشريك',
self::NewsImage => 'صورة الخبر',
self::SectionImage => 'صورة القسم',
self::General => 'صورة عامة',
};
}
}
<?php
namespace App\Domain\Website\Enums;
enum SectionKey: string
{
case Hero = 'hero';
case About = 'about';
case Activities = 'activities';
case Programs = 'programs';
case Branches = 'branches';
case Schedule = 'schedule';
case Trainers = 'trainers';
case Gallery = 'gallery';
case Testimonials = 'testimonials';
case Pricing = 'pricing';
case Partners = 'partners';
case Contact = 'contact';
case Faq = 'faq';
case Cta = 'cta';
case Stats = 'stats';
case News = 'news';
public function label(): string
{
return match ($this) {
self::Hero => 'البانر الرئيسي',
self::About => 'من نحن',
self::Activities => 'الأنشطة',
self::Programs => 'البرامج',
self::Branches => 'الفروع',
self::Schedule => 'المواعيد',
self::Trainers => 'المدربون',
self::Gallery => 'معرض الصور',
self::Testimonials => 'آراء العملاء',
self::Pricing => 'الأسعار',
self::Partners => 'شركاؤنا',
self::Contact => 'تواصل معنا',
self::Faq => 'الأسئلة الشائعة',
self::Cta => 'سجل الآن',
self::Stats => 'إنجازاتنا',
self::News => 'الأخبار',
};
}
public function icon(): string
{
return match ($this) {
self::Hero => 'photo',
self::About => 'information-circle',
self::Activities => 'fire',
self::Programs => 'academic-cap',
self::Branches => 'map-pin',
self::Schedule => 'calendar',
self::Trainers => 'users',
self::Gallery => 'camera',
self::Testimonials => 'chat-bubble-left-right',
self::Pricing => 'currency-dollar',
self::Partners => 'building-office',
self::Contact => 'phone',
self::Faq => 'question-mark-circle',
self::Cta => 'megaphone',
self::Stats => 'chart-bar',
self::News => 'newspaper',
};
}
public static function defaultOrder(): array
{
return [
self::Hero,
self::About,
self::Activities,
self::Programs,
self::Branches,
self::Stats,
self::Trainers,
self::Gallery,
self::Testimonials,
self::Pricing,
self::Partners,
self::Faq,
self::News,
self::Contact,
self::Cta,
self::Schedule,
];
}
}
<?php
namespace App\Domain\Website\Models;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use App\Domain\Website\Enums\ContactSubmissionStatus;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ContactSubmission extends Model
{
use BelongsToAcademy, HasUuid;
protected $fillable = [
'academy_id',
'name',
'phone',
'email',
'message',
'status',
'admin_notes',
'replied_by',
'read_at',
'replied_at',
];
protected $casts = [
'status' => ContactSubmissionStatus::class,
'read_at' => 'datetime',
'replied_at' => 'datetime',
];
public function repliedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'replied_by');
}
}
<?php
namespace App\Domain\Website\Models;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use App\Domain\Website\Enums\MediaCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
class Media extends Model
{
use BelongsToAcademy, HasUuid, SoftDeletes;
protected $table = 'media';
protected $fillable = [
'academy_id',
'collection',
'original_filename',
'disk_path',
'mime_type',
'width',
'height',
'file_size',
'alt_text',
'alt_text_ar',
'mediable_type',
'mediable_id',
'sort_order',
];
protected $casts = [
'collection' => MediaCollection::class,
'width' => 'integer',
'height' => 'integer',
'file_size' => 'integer',
'sort_order' => 'integer',
];
public function mediable(): MorphTo
{
return $this->morphTo();
}
public function getUrlAttribute(): string
{
return Storage::disk('public')->url($this->disk_path);
}
public function getFileSizeHumanAttribute(): string
{
$bytes = $this->file_size;
if ($bytes >= 1048576) {
return round($bytes / 1048576, 1) . ' MB';
}
return round($bytes / 1024) . ' KB';
}
}
<?php
namespace App\Domain\Website\Models;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class WebsiteFaq extends Model
{
use BelongsToAcademy, HasUuid, SoftDeletes;
protected $table = 'website_faqs';
protected $fillable = [
'academy_id',
'question',
'question_en',
'answer',
'answer_en',
'sort_order',
'is_published',
];
protected $casts = [
'sort_order' => 'integer',
'is_published' => 'boolean',
];
}
<?php
namespace App\Domain\Website\Models;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
class WebsiteNews extends Model
{
use BelongsToAcademy, HasUuid, SoftDeletes;
protected $table = 'website_news';
protected $fillable = [
'academy_id',
'title',
'title_en',
'slug',
'body',
'body_en',
'excerpt',
'excerpt_en',
'published_at',
'is_featured',
'sort_order',
'created_by',
];
protected $casts = [
'published_at' => 'datetime',
'is_featured' => 'boolean',
'sort_order' => 'integer',
];
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function image(): MorphOne
{
return $this->morphOne(Media::class, 'mediable')
->where('collection', 'news_image');
}
}
<?php
namespace App\Domain\Website\Models;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
class WebsitePartner extends Model
{
use BelongsToAcademy, HasUuid, SoftDeletes;
protected $fillable = [
'academy_id',
'name',
'name_en',
'website_url',
'sort_order',
'is_active',
];
protected $casts = [
'sort_order' => 'integer',
'is_active' => 'boolean',
];
public function logo(): MorphOne
{
return $this->morphOne(Media::class, 'mediable')
->where('collection', 'partner_logo');
}
}
<?php
namespace App\Domain\Website\Models;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Website\Enums\SectionKey;
use Illuminate\Database\Eloquent\Model;
class WebsiteSection extends Model
{
use BelongsToAcademy;
protected $fillable = [
'academy_id',
'section_key',
'title',
'title_en',
'subtitle',
'subtitle_en',
'content',
'content_en',
'settings',
'sort_order',
'is_enabled',
];
protected $casts = [
'section_key' => SectionKey::class,
'settings' => 'array',
'sort_order' => 'integer',
'is_enabled' => 'boolean',
];
public function getSetting(string $key, $default = null)
{
return data_get($this->settings, $key, $default);
}
}
<?php
namespace App\Domain\Website\Models;
use App\Domain\Shared\Traits\BelongsToAcademy;
use Illuminate\Database\Eloquent\Model;
class WebsiteSetting extends Model
{
use BelongsToAcademy;
protected $fillable = [
'academy_id',
'template',
'primary_color',
'secondary_color',
'accent_color',
'text_color',
'heading_font',
'body_font',
'navbar_style',
'site_title',
'site_title_en',
'site_description',
'site_description_en',
'social_links',
'whatsapp_number',
'custom_css',
'google_analytics_id',
'facebook_pixel_id',
'is_published',
'published_at',
];
protected $casts = [
'social_links' => 'array',
'is_published' => 'boolean',
'published_at' => 'datetime',
];
}
<?php
namespace App\Domain\Website\Models;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
class WebsiteTestimonial extends Model
{
use BelongsToAcademy, HasUuid, SoftDeletes;
protected $fillable = [
'academy_id',
'name',
'name_en',
'role',
'role_en',
'content',
'content_en',
'rating',
'is_featured',
'sort_order',
];
protected $casts = [
'rating' => 'integer',
'is_featured' => 'boolean',
'sort_order' => 'integer',
];
public function avatar(): MorphOne
{
return $this->morphOne(Media::class, 'mediable')
->where('collection', 'testimonial_avatar');
}
}
<?php
namespace App\Domain\Website\Services;
use App\Domain\Website\Enums\MediaCollection;
use App\Domain\Website\Models\Media;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class MediaService
{
public function upload(
UploadedFile $file,
MediaCollection $collection,
?string $mediableType = null,
?int $mediableId = null,
): Media {
$academy = app('current_academy');
$dimensions = $this->getImageDimensions($file);
$filename = Str::uuid() . '.' . $file->getClientOriginalExtension();
$path = "website/{$academy->id}/{$collection->value}/{$filename}";
Storage::disk('public')->put($path, file_get_contents($file->getRealPath()));
return Media::create([
'academy_id' => $academy->id,
'collection' => $collection,
'original_filename' => $file->getClientOriginalName(),
'disk_path' => $path,
'mime_type' => $file->getMimeType(),
'width' => $dimensions['width'],
'height' => $dimensions['height'],
'file_size' => $file->getSize(),
'mediable_type' => $mediableType,
'mediable_id' => $mediableId,
]);
}
public function delete(Media $media): void
{
if (Storage::disk('public')->exists($media->disk_path)) {
Storage::disk('public')->delete($media->disk_path);
}
$media->delete();
}
public function replace(Media $media, UploadedFile $file): Media
{
if (Storage::disk('public')->exists($media->disk_path)) {
Storage::disk('public')->delete($media->disk_path);
}
$dimensions = $this->getImageDimensions($file);
$filename = Str::uuid() . '.' . $file->getClientOriginalExtension();
$academy = app('current_academy');
$path = "website/{$academy->id}/{$media->collection->value}/{$filename}";
Storage::disk('public')->put($path, file_get_contents($file->getRealPath()));
$media->update([
'original_filename' => $file->getClientOriginalName(),
'disk_path' => $path,
'mime_type' => $file->getMimeType(),
'width' => $dimensions['width'],
'height' => $dimensions['height'],
'file_size' => $file->getSize(),
]);
return $media->fresh();
}
public function getForCollection(MediaCollection $collection, ?string $mediableType = null, ?int $mediableId = null)
{
$query = Media::where('collection', $collection);
if ($mediableType && $mediableId) {
$query->where('mediable_type', $mediableType)
->where('mediable_id', $mediableId);
}
return $query->orderBy('sort_order')->get();
}
public function updateSortOrder(array $orderedIds): void
{
foreach ($orderedIds as $index => $id) {
Media::where('id', $id)->update(['sort_order' => $index]);
}
}
private function getImageDimensions(UploadedFile $file): array
{
$imageInfo = @getimagesize($file->getRealPath());
if ($imageInfo) {
return ['width' => $imageInfo[0], 'height' => $imageInfo[1]];
}
return ['width' => 0, 'height' => 0];
}
public function getQualityLevel(UploadedFile $file, MediaCollection $collection): string
{
$dimensions = $this->getImageDimensions($file);
$required = $collection->dimensions();
$widthRatio = $dimensions['width'] / $required['width'];
$heightRatio = $dimensions['height'] / $required['height'];
$ratio = min($widthRatio, $heightRatio);
if ($ratio >= 1.0) {
return 'excellent';
}
if ($ratio >= 0.75) {
return 'acceptable';
}
return 'too_small';
}
}
<?php
namespace App\Domain\Website\Services;
use Illuminate\Support\Facades\Cache;
class WebsiteCacheService
{
public function invalidate(int $academyId): void
{
$keys = [
"website.{$academyId}.page",
"website.{$academyId}.activities",
"website.{$academyId}.programs",
"website.{$academyId}.branches",
"website.{$academyId}.stats",
];
foreach ($keys as $key) {
Cache::forget($key);
}
}
public function invalidateAll(): void
{
Cache::flush();
}
}
<?php
namespace App\Domain\Website\Services;
use App\Domain\Shared\Models\Academy;
use App\Domain\Website\Models\ContactSubmission;
use App\Domain\Website\Models\Media;
use App\Domain\Website\Models\WebsiteFaq;
use App\Domain\Website\Models\WebsiteNews;
use App\Domain\Website\Models\WebsitePartner;
use App\Domain\Website\Models\WebsiteTestimonial;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class WebsiteDataService
{
public function getActivities(Academy $academy): Collection
{
return Cache::remember(
"website.{$academy->id}.activities",
3600,
fn () => DB::table('activities')
->where('academy_id', $academy->id)
->where('is_active', true)
->orderBy('sort_order')
->get()
);
}
public function getPrograms(Academy $academy): Collection
{
return Cache::remember(
"website.{$academy->id}.programs",
3600,
fn () => DB::table('training_programs')
->where('academy_id', $academy->id)
->where('status', 'active')
->orderBy('sort_order')
->get()
);
}
public function getBranches(Academy $academy): Collection
{
return Cache::remember(
"website.{$academy->id}.branches",
3600,
fn () => DB::table('branches')
->where('academy_id', $academy->id)
->where('is_active', true)
->orderByDesc('is_main')
->get()
);
}
public function getStats(Academy $academy): array
{
return Cache::remember(
"website.{$academy->id}.stats",
21600,
function () use ($academy) {
return [
'participants' => DB::table('participants')
->where('academy_id', $academy->id)
->where('status', 'active')
->count(),
'trainers' => DB::table('trainers')
->where('academy_id', $academy->id)
->where('status', 'active')
->count(),
'branches' => DB::table('branches')
->where('academy_id', $academy->id)
->where('is_active', true)
->count(),
'programs' => DB::table('training_programs')
->where('academy_id', $academy->id)
->where('status', 'active')
->count(),
'activities' => DB::table('activities')
->where('academy_id', $academy->id)
->where('is_active', true)
->count(),
];
}
);
}
public function getTestimonials(Academy $academy, int $limit = 10): Collection
{
return WebsiteTestimonial::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('is_featured', true)
->orderBy('sort_order')
->limit($limit)
->get();
}
public function getFaqs(Academy $academy): Collection
{
return WebsiteFaq::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('is_published', true)
->orderBy('sort_order')
->get();
}
public function getNews(Academy $academy, int $limit = 6): Collection
{
return WebsiteNews::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->orderByDesc('published_at')
->limit($limit)
->get();
}
public function getPartners(Academy $academy): Collection
{
return WebsitePartner::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('is_active', true)
->orderBy('sort_order')
->get();
}
public function getGalleryImages(Academy $academy, int $limit = 12): Collection
{
return Media::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('collection', 'gallery')
->orderBy('sort_order')
->limit($limit)
->get();
}
public function getMediaForCollection(Academy $academy, string $collection): Collection
{
return Media::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('collection', $collection)
->orderBy('sort_order')
->get();
}
public function submitContactForm(Academy $academy, array $data): ContactSubmission
{
return ContactSubmission::create([
'academy_id' => $academy->id,
'name' => $data['name'],
'phone' => $data['phone'],
'email' => $data['email'] ?? null,
'message' => $data['message'],
]);
}
}
<?php
namespace App\Domain\Website\Services;
use App\Domain\Shared\Models\Academy;
use App\Domain\Website\Enums\SectionKey;
use App\Domain\Website\Models\WebsiteSection;
use Illuminate\Support\Collection;
class WebsiteSectionService
{
public function seedDefaults(Academy $academy): void
{
$order = SectionKey::defaultOrder();
foreach ($order as $index => $key) {
WebsiteSection::withoutGlobalScope('academy')->firstOrCreate(
['academy_id' => $academy->id, 'section_key' => $key->value],
[
'title' => $key->label(),
'sort_order' => $index,
'is_enabled' => !in_array($key, [SectionKey::Schedule, SectionKey::News]),
]
);
}
}
public function getEnabled(Academy $academy): Collection
{
return WebsiteSection::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('is_enabled', true)
->orderBy('sort_order')
->get();
}
public function getAll(Academy $academy): Collection
{
return WebsiteSection::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->orderBy('sort_order')
->get();
}
public function reorder(Academy $academy, array $orderedKeys): void
{
foreach ($orderedKeys as $index => $key) {
WebsiteSection::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('section_key', $key)
->update(['sort_order' => $index]);
}
app(WebsiteCacheService::class)->invalidate($academy->id);
}
public function toggle(WebsiteSection $section, bool $enabled): void
{
$section->update(['is_enabled' => $enabled]);
app(WebsiteCacheService::class)->invalidate($section->academy_id);
}
public function updateContent(WebsiteSection $section, array $data): WebsiteSection
{
$section->update($data);
app(WebsiteCacheService::class)->invalidate($section->academy_id);
return $section->fresh();
}
}
<?php
namespace App\Domain\Website\Services;
use App\Domain\Shared\Models\Academy;
use App\Domain\Website\Models\WebsiteSetting;
class WebsiteSettingService
{
public function getOrCreate(Academy $academy): WebsiteSetting
{
return WebsiteSetting::withoutGlobalScope('academy')
->firstOrCreate(
['academy_id' => $academy->id],
[
'template' => 'bold_athletic',
'site_title' => $academy->name_ar,
'site_title_en' => $academy->name,
]
);
}
public function update(Academy $academy, array $data): WebsiteSetting
{
$settings = $this->getOrCreate($academy);
$settings->update($data);
app(WebsiteCacheService::class)->invalidate($academy->id);
return $settings->fresh();
}
public function publish(Academy $academy): WebsiteSetting
{
$settings = $this->getOrCreate($academy);
$settings->update([
'is_published' => true,
'published_at' => now(),
]);
app(WebsiteCacheService::class)->invalidate($academy->id);
return $settings;
}
public function unpublish(Academy $academy): WebsiteSetting
{
$settings = $this->getOrCreate($academy);
$settings->update(['is_published' => false]);
app(WebsiteCacheService::class)->invalidate($academy->id);
return $settings;
}
}
<?php
namespace App\Http\Controllers;
use App\Domain\Website\Services\WebsiteDataService;
use Illuminate\Http\Request;
class ContactFormController extends Controller
{
public function submit(Request $request, string $slug)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'phone' => 'required|string|max:20',
'email' => 'nullable|email|max:255',
'message' => 'required|string|max:2000',
]);
$academy = app('current_academy');
app(WebsiteDataService::class)->submitContactForm($academy, $validated);
if ($request->expectsJson()) {
return response()->json(['message' => 'تم إرسال رسالتك بنجاح']);
}
return back()->with('contact_success', 'تم إرسال رسالتك بنجاح، سنتواصل معك قريباً');
}
}
<?php
namespace App\Http\Controllers;
use App\Domain\Shared\Models\Academy;
use App\Domain\Website\Enums\SectionKey;
use App\Domain\Website\Models\WebsiteSetting;
use App\Domain\Website\Services\WebsiteDataService;
use App\Domain\Website\Services\WebsiteSectionService;
use App\Domain\Website\Services\WebsiteSettingService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class PublicWebsiteController extends Controller
{
public function __construct(
private WebsiteDataService $dataService,
private WebsiteSectionService $sectionService,
private WebsiteSettingService $settingService,
) {}
public function show(string $slug)
{
$academy = app('current_academy');
$settings = app('website_settings');
$sections = $this->sectionService->getEnabled($academy);
$data = $this->gatherSectionData($academy, $sections);
return view('website.index', [
'academy' => $academy,
'settings' => $settings,
'sections' => $sections,
'data' => $data,
]);
}
public function preview(Request $request)
{
$academy = app('current_academy');
$settings = app(WebsiteSettingService::class)->getOrCreate($academy);
app()->instance('website_settings', $settings);
$sections = $this->sectionService->getAll($academy)->where('is_enabled', true);
$data = $this->gatherSectionData($academy, $sections);
return view('website.index', [
'academy' => $academy,
'settings' => $settings,
'sections' => $sections,
'data' => $data,
'isPreview' => true,
]);
}
private function gatherSectionData(Academy $academy, $sections): array
{
$data = [];
$enabledKeys = $sections->pluck('section_key');
if ($enabledKeys->contains(SectionKey::Activities)) {
$data['activities'] = $this->dataService->getActivities($academy);
}
if ($enabledKeys->contains(SectionKey::Programs)) {
$data['programs'] = $this->dataService->getPrograms($academy);
}
if ($enabledKeys->contains(SectionKey::Branches)) {
$data['branches'] = $this->dataService->getBranches($academy);
}
if ($enabledKeys->contains(SectionKey::Stats)) {
$data['stats'] = $this->dataService->getStats($academy);
}
if ($enabledKeys->contains(SectionKey::Testimonials)) {
$data['testimonials'] = $this->dataService->getTestimonials($academy);
}
if ($enabledKeys->contains(SectionKey::Faq)) {
$data['faqs'] = $this->dataService->getFaqs($academy);
}
if ($enabledKeys->contains(SectionKey::News)) {
$data['news'] = $this->dataService->getNews($academy);
}
if ($enabledKeys->contains(SectionKey::Partners)) {
$data['partners'] = $this->dataService->getPartners($academy);
}
if ($enabledKeys->contains(SectionKey::Gallery)) {
$data['gallery'] = $this->dataService->getGalleryImages($academy);
}
return $data;
}
}
<?php
namespace App\Http\Middleware;
use App\Domain\Shared\Models\Academy;
use App\Domain\Website\Models\WebsiteSetting;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ResolveAcademyFromSlug
{
public function handle(Request $request, Closure $next): Response
{
$slug = $request->route('slug');
if (!$slug) {
abort(404);
}
$academy = Academy::where('slug', $slug)
->where('status', 'active')
->first();
if (!$academy) {
abort(404);
}
$settings = WebsiteSetting::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->first();
if (!$settings || !$settings->is_published) {
abort(404);
}
app()->instance('current_academy', $academy);
app()->instance('website_settings', $settings);
return $next($request);
}
}
<?php
namespace App\Livewire\Website;
use App\Domain\Website\Models\ContactSubmission;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
#[Title('رسائل التواصل')]
class ContactSubmissionList extends Component
{
use WithPagination;
#[Url]
public string $status = '';
public function mount(): void
{
$this->authorize('settings.manage');
}
public function markAsRead(int $id): void
{
$submission = ContactSubmission::find($id);
if ($submission && $submission->status->value === 'new') {
$submission->update(['status' => 'read', 'read_at' => now()]);
}
}
public function archive(int $id): void
{
ContactSubmission::find($id)?->update(['status' => 'archived']);
}
public function updatedStatus(): void
{
$this->resetPage();
}
public function render()
{
$query = ContactSubmission::orderByDesc('created_at');
if ($this->status) {
$query->where('status', $this->status);
}
return view('livewire.website.contact-submission-list', [
'submissions' => $query->paginate(20),
]);
}
}
<?php
namespace App\Livewire\Website;
use App\Domain\Website\Models\WebsiteFaq;
use App\Domain\Website\Services\WebsiteCacheService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('الأسئلة الشائعة')]
class FaqManager extends Component
{
public bool $showForm = false;
public ?int $editingId = null;
public string $question = '';
public string $question_en = '';
public string $answer = '';
public string $answer_en = '';
public bool $is_published = true;
public function mount(): void
{
$this->authorize('settings.manage');
}
public function create(): void
{
$this->reset(['question', 'question_en', 'answer', 'answer_en', 'is_published', 'editingId']);
$this->is_published = true;
$this->showForm = true;
}
public function edit(int $id): void
{
$faq = WebsiteFaq::find($id);
if (!$faq) return;
$this->editingId = $id;
$this->question = $faq->question;
$this->question_en = $faq->question_en ?? '';
$this->answer = $faq->answer;
$this->answer_en = $faq->answer_en ?? '';
$this->is_published = $faq->is_published;
$this->showForm = true;
}
public function save(): void
{
$this->validate([
'question' => 'required|string|max:500',
'answer' => 'required|string|max:2000',
]);
$data = [
'question' => $this->question,
'question_en' => $this->question_en ?: null,
'answer' => $this->answer,
'answer_en' => $this->answer_en ?: null,
'is_published' => $this->is_published,
];
if ($this->editingId) {
WebsiteFaq::find($this->editingId)->update($data);
} else {
$data['sort_order'] = WebsiteFaq::max('sort_order') + 1;
WebsiteFaq::create($data);
}
app(WebsiteCacheService::class)->invalidate(app('current_academy')->id);
$this->showForm = false;
session()->flash('success', __('تم الحفظ بنجاح'));
}
public function delete(int $id): void
{
WebsiteFaq::find($id)?->delete();
app(WebsiteCacheService::class)->invalidate(app('current_academy')->id);
session()->flash('success', __('تم الحذف'));
}
public function render()
{
return view('livewire.website.faq-manager', [
'faqs' => WebsiteFaq::orderBy('sort_order')->get(),
]);
}
}
<?php
namespace App\Livewire\Website;
use App\Domain\Website\Enums\MediaCollection;
use App\Domain\Website\Models\Media;
use App\Domain\Website\Services\MediaService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
use Livewire\WithFileUploads;
#[Layout('layouts.app')]
#[Title('معرض الصور')]
class GalleryManager extends Component
{
use WithFileUploads;
public $photos = [];
public array $gallery = [];
public function mount(): void
{
$this->authorize('settings.manage');
$this->loadGallery();
}
public function loadGallery(): void
{
$academy = app('current_academy');
$this->gallery = Media::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('collection', 'gallery')
->orderBy('sort_order')
->get()
->map(fn ($m) => [
'id' => $m->id,
'url' => $m->url,
'filename' => $m->original_filename,
'size' => $m->file_size_human,
])
->toArray();
}
public function updatedPhotos(): void
{
$this->validate([
'photos.*' => 'image|max:1536|mimes:jpg,jpeg,png,webp',
]);
$mediaService = app(MediaService::class);
foreach ($this->photos as $photo) {
$mediaService->upload($photo, MediaCollection::Gallery);
}
$this->photos = [];
$this->loadGallery();
session()->flash('success', __('تم رفع الصور بنجاح'));
}
public function deleteImage(int $id): void
{
$media = Media::find($id);
if ($media) {
app(MediaService::class)->delete($media);
$this->loadGallery();
session()->flash('success', __('تم حذف الصورة'));
}
}
public function reorder(array $orderedIds): void
{
app(MediaService::class)->updateSortOrder($orderedIds);
$this->loadGallery();
}
public function render()
{
return view('livewire.website.gallery-manager');
}
}
<?php
namespace App\Livewire\Website;
use App\Domain\Website\Models\WebsiteNews;
use App\Domain\Website\Services\WebsiteCacheService;
use Illuminate\Support\Str;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('الأخبار')]
class NewsManager extends Component
{
public bool $showForm = false;
public ?int $editingId = null;
public string $title = '';
public string $title_en = '';
public string $body = '';
public string $body_en = '';
public string $excerpt = '';
public string $excerpt_en = '';
public bool $is_featured = false;
public function mount(): void
{
$this->authorize('settings.manage');
}
public function create(): void
{
$this->reset(['title', 'title_en', 'body', 'body_en', 'excerpt', 'excerpt_en', 'is_featured', 'editingId']);
$this->showForm = true;
}
public function edit(int $id): void
{
$news = WebsiteNews::find($id);
if (!$news) return;
$this->editingId = $id;
$this->title = $news->title;
$this->title_en = $news->title_en ?? '';
$this->body = $news->body;
$this->body_en = $news->body_en ?? '';
$this->excerpt = $news->excerpt ?? '';
$this->excerpt_en = $news->excerpt_en ?? '';
$this->is_featured = $news->is_featured;
$this->showForm = true;
}
public function save(): void
{
$this->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
]);
$data = [
'title' => $this->title,
'title_en' => $this->title_en ?: null,
'body' => $this->body,
'body_en' => $this->body_en ?: null,
'excerpt' => $this->excerpt ?: Str::limit($this->body, 150),
'excerpt_en' => $this->excerpt_en ?: null,
'is_featured' => $this->is_featured,
];
if ($this->editingId) {
WebsiteNews::find($this->editingId)->update($data);
} else {
$data['slug'] = Str::slug($this->title) ?: Str::random(8);
$data['created_by'] = auth()->id();
$data['published_at'] = now();
WebsiteNews::create($data);
}
app(WebsiteCacheService::class)->invalidate(app('current_academy')->id);
$this->showForm = false;
session()->flash('success', __('تم الحفظ بنجاح'));
}
public function delete(int $id): void
{
WebsiteNews::find($id)?->delete();
app(WebsiteCacheService::class)->invalidate(app('current_academy')->id);
session()->flash('success', __('تم الحذف'));
}
public function render()
{
return view('livewire.website.news-manager', [
'news' => WebsiteNews::orderByDesc('published_at')->get(),
]);
}
}
<?php
namespace App\Livewire\Website;
use App\Domain\Website\Models\WebsitePartner;
use App\Domain\Website\Services\WebsiteCacheService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('الشركاء')]
class PartnerManager extends Component
{
public bool $showForm = false;
public ?int $editingId = null;
public string $name = '';
public string $name_en = '';
public string $website_url = '';
public bool $is_active = true;
public function mount(): void
{
$this->authorize('settings.manage');
}
public function create(): void
{
$this->reset(['name', 'name_en', 'website_url', 'is_active', 'editingId']);
$this->is_active = true;
$this->showForm = true;
}
public function edit(int $id): void
{
$partner = WebsitePartner::find($id);
if (!$partner) return;
$this->editingId = $id;
$this->name = $partner->name;
$this->name_en = $partner->name_en ?? '';
$this->website_url = $partner->website_url ?? '';
$this->is_active = $partner->is_active;
$this->showForm = true;
}
public function save(): void
{
$this->validate([
'name' => 'required|string|max:255',
'website_url' => 'nullable|url|max:500',
]);
$data = [
'name' => $this->name,
'name_en' => $this->name_en ?: null,
'website_url' => $this->website_url ?: null,
'is_active' => $this->is_active,
];
if ($this->editingId) {
WebsitePartner::find($this->editingId)->update($data);
} else {
$data['sort_order'] = WebsitePartner::max('sort_order') + 1;
WebsitePartner::create($data);
}
app(WebsiteCacheService::class)->invalidate(app('current_academy')->id);
$this->showForm = false;
session()->flash('success', __('تم الحفظ بنجاح'));
}
public function delete(int $id): void
{
WebsitePartner::find($id)?->delete();
app(WebsiteCacheService::class)->invalidate(app('current_academy')->id);
session()->flash('success', __('تم الحذف'));
}
public function render()
{
return view('livewire.website.partner-manager', [
'partners' => WebsitePartner::orderBy('sort_order')->get(),
]);
}
}
<?php
namespace App\Livewire\Website;
use App\Domain\Website\Enums\SectionKey;
use App\Domain\Website\Models\WebsiteSection;
use App\Domain\Website\Models\WebsiteSetting;
use App\Domain\Website\Services\WebsiteSectionService;
use App\Domain\Website\Services\WebsiteSettingService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('إدارة الموقع الإلكتروني')]
class SectionManager extends Component
{
public array $sections = [];
public ?string $editingSection = null;
// Section form fields
public string $sectionTitle = '';
public string $sectionTitleEn = '';
public string $sectionSubtitle = '';
public string $sectionSubtitleEn = '';
public string $sectionContent = '';
public string $sectionContentEn = '';
public array $sectionSettings = [];
public bool $isPublished = false;
public function mount(): void
{
$this->authorize('settings.manage');
$academy = app('current_academy');
$sectionService = app(WebsiteSectionService::class);
$sectionService->seedDefaults($academy);
$this->loadSections();
$settings = app(WebsiteSettingService::class)->getOrCreate($academy);
$this->isPublished = $settings->is_published;
}
public function loadSections(): void
{
$academy = app('current_academy');
$this->sections = WebsiteSection::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->orderBy('sort_order')
->get()
->map(fn ($s) => [
'id' => $s->id,
'key' => $s->section_key->value,
'label' => $s->section_key->label(),
'icon' => $s->section_key->icon(),
'title' => $s->title,
'is_enabled' => $s->is_enabled,
'sort_order' => $s->sort_order,
])
->toArray();
}
public function toggleSection(int $id): void
{
$section = WebsiteSection::find($id);
if ($section) {
app(WebsiteSectionService::class)->toggle($section, !$section->is_enabled);
$this->loadSections();
}
}
public function reorder(array $orderedIds): void
{
$academy = app('current_academy');
foreach ($orderedIds as $index => $id) {
WebsiteSection::where('id', $id)->update(['sort_order' => $index]);
}
app(\App\Domain\Website\Services\WebsiteCacheService::class)->invalidate($academy->id);
$this->loadSections();
}
public function editSection(string $key): void
{
$academy = app('current_academy');
$section = WebsiteSection::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('section_key', $key)
->first();
if ($section) {
$this->editingSection = $key;
$this->sectionTitle = $section->title ?? '';
$this->sectionTitleEn = $section->title_en ?? '';
$this->sectionSubtitle = $section->subtitle ?? '';
$this->sectionSubtitleEn = $section->subtitle_en ?? '';
$this->sectionContent = $section->content ?? '';
$this->sectionContentEn = $section->content_en ?? '';
$this->sectionSettings = $section->settings ?? [];
}
}
public function saveSection(): void
{
$academy = app('current_academy');
$section = WebsiteSection::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('section_key', $this->editingSection)
->first();
if ($section) {
app(WebsiteSectionService::class)->updateContent($section, [
'title' => $this->sectionTitle,
'title_en' => $this->sectionTitleEn,
'subtitle' => $this->sectionSubtitle,
'subtitle_en' => $this->sectionSubtitleEn,
'content' => $this->sectionContent,
'content_en' => $this->sectionContentEn,
'settings' => $this->sectionSettings,
]);
$this->loadSections();
session()->flash('success', __('تم حفظ القسم بنجاح'));
}
}
public function cancelEdit(): void
{
$this->editingSection = null;
}
public function publish(): void
{
$academy = app('current_academy');
app(WebsiteSettingService::class)->publish($academy);
$this->isPublished = true;
session()->flash('success', __('تم نشر الموقع بنجاح'));
}
public function unpublish(): void
{
$academy = app('current_academy');
app(WebsiteSettingService::class)->unpublish($academy);
$this->isPublished = false;
session()->flash('success', __('تم إلغاء نشر الموقع'));
}
public function render()
{
return view('livewire.website.section-manager');
}
}
<?php
namespace App\Livewire\Website;
use App\Domain\Website\Models\WebsiteTestimonial;
use App\Domain\Website\Services\WebsiteCacheService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('آراء العملاء')]
class TestimonialManager extends Component
{
public bool $showForm = false;
public ?int $editingId = null;
public string $name = '';
public string $name_en = '';
public string $role = '';
public string $role_en = '';
public string $content = '';
public string $content_en = '';
public int $rating = 5;
public bool $is_featured = true;
public function mount(): void
{
$this->authorize('settings.manage');
}
public function create(): void
{
$this->reset(['name', 'name_en', 'role', 'role_en', 'content', 'content_en', 'rating', 'is_featured', 'editingId']);
$this->is_featured = true;
$this->rating = 5;
$this->showForm = true;
}
public function edit(int $id): void
{
$testimonial = WebsiteTestimonial::find($id);
if (!$testimonial) return;
$this->editingId = $id;
$this->name = $testimonial->name;
$this->name_en = $testimonial->name_en ?? '';
$this->role = $testimonial->role ?? '';
$this->role_en = $testimonial->role_en ?? '';
$this->content = $testimonial->content;
$this->content_en = $testimonial->content_en ?? '';
$this->rating = $testimonial->rating;
$this->is_featured = $testimonial->is_featured;
$this->showForm = true;
}
public function save(): void
{
$this->validate([
'name' => 'required|string|max:255',
'content' => 'required|string|max:1000',
'rating' => 'required|integer|min:1|max:5',
]);
$data = [
'name' => $this->name,
'name_en' => $this->name_en ?: null,
'role' => $this->role ?: null,
'role_en' => $this->role_en ?: null,
'content' => $this->content,
'content_en' => $this->content_en ?: null,
'rating' => $this->rating,
'is_featured' => $this->is_featured,
];
if ($this->editingId) {
WebsiteTestimonial::find($this->editingId)->update($data);
} else {
WebsiteTestimonial::create($data);
}
app(WebsiteCacheService::class)->invalidate(app('current_academy')->id);
$this->showForm = false;
session()->flash('success', __('تم الحفظ بنجاح'));
}
public function delete(int $id): void
{
WebsiteTestimonial::find($id)?->delete();
app(WebsiteCacheService::class)->invalidate(app('current_academy')->id);
session()->flash('success', __('تم الحذف'));
}
public function render()
{
$testimonials = WebsiteTestimonial::orderBy('sort_order')->get();
return view('livewire.website.testimonial-manager', [
'testimonials' => $testimonials,
]);
}
}
<?php
namespace App\Livewire\Website;
use App\Domain\Website\Services\WebsiteSettingService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('تصميم الموقع')]
class ThemeEditor extends Component
{
public string $primary_color = '#1a1a2e';
public string $secondary_color = '#16213e';
public string $accent_color = '#e94560';
public string $text_color = '#ffffff';
public string $heading_font = 'Cairo';
public string $body_font = 'Cairo';
public string $navbar_style = 'transparent';
public string $site_title = '';
public string $site_title_en = '';
public string $site_description = '';
public string $site_description_en = '';
public string $whatsapp_number = '';
public array $social_links = [];
public string $custom_css = '';
public string $google_analytics_id = '';
public string $facebook_pixel_id = '';
public array $availableFonts = [
'Cairo', 'Tajawal', 'IBM Plex Sans Arabic', 'Noto Sans Arabic',
'Almarai', 'Readex Pro', 'Rubik', 'El Messiri',
];
public function mount(): void
{
$this->authorize('settings.manage');
$settings = app(WebsiteSettingService::class)->getOrCreate(app('current_academy'));
$this->primary_color = $settings->primary_color;
$this->secondary_color = $settings->secondary_color;
$this->accent_color = $settings->accent_color;
$this->text_color = $settings->text_color;
$this->heading_font = $settings->heading_font;
$this->body_font = $settings->body_font;
$this->navbar_style = $settings->navbar_style;
$this->site_title = $settings->site_title ?? '';
$this->site_title_en = $settings->site_title_en ?? '';
$this->site_description = $settings->site_description ?? '';
$this->site_description_en = $settings->site_description_en ?? '';
$this->whatsapp_number = $settings->whatsapp_number ?? '';
$this->social_links = $settings->social_links ?? [];
$this->custom_css = $settings->custom_css ?? '';
$this->google_analytics_id = $settings->google_analytics_id ?? '';
$this->facebook_pixel_id = $settings->facebook_pixel_id ?? '';
}
public function save(): void
{
$this->validate([
'primary_color' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'secondary_color' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'accent_color' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'text_color' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'heading_font' => 'required|string|max:50',
'body_font' => 'required|string|max:50',
'navbar_style' => 'required|in:solid,transparent,floating',
'site_title' => 'nullable|string|max:255',
'site_description' => 'nullable|string|max:1000',
'whatsapp_number' => 'nullable|string|max:20',
]);
app(WebsiteSettingService::class)->update(app('current_academy'), [
'primary_color' => $this->primary_color,
'secondary_color' => $this->secondary_color,
'accent_color' => $this->accent_color,
'text_color' => $this->text_color,
'heading_font' => $this->heading_font,
'body_font' => $this->body_font,
'navbar_style' => $this->navbar_style,
'site_title' => $this->site_title,
'site_title_en' => $this->site_title_en,
'site_description' => $this->site_description,
'site_description_en' => $this->site_description_en,
'whatsapp_number' => $this->whatsapp_number,
'social_links' => $this->social_links,
'custom_css' => $this->custom_css,
'google_analytics_id' => $this->google_analytics_id,
'facebook_pixel_id' => $this->facebook_pixel_id,
]);
session()->flash('success', __('تم حفظ إعدادات التصميم بنجاح'));
}
public function render()
{
return view('livewire.website.theme-editor');
}
}
<?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
{
Schema::create('media', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('academies');
$table->string('collection', 50);
$table->string('original_filename');
$table->string('disk_path');
$table->string('mime_type', 50);
$table->unsignedInteger('width')->default(0);
$table->unsignedInteger('height')->default(0);
$table->unsignedInteger('file_size')->default(0);
$table->string('alt_text')->nullable();
$table->string('alt_text_ar')->nullable();
$table->nullableMorphs('mediable');
$table->unsignedSmallInteger('sort_order')->default(0);
$table->timestamps();
$table->softDeletes();
$table->index(['academy_id', 'collection']);
});
DB::statement("ALTER TABLE media ADD CONSTRAINT media_collection_check CHECK (collection IN (
'academy_logo', 'academy_cover', 'activity_photo', 'branch_photo',
'gallery', 'team_photo', 'testimonial_avatar', 'partner_logo',
'news_image', 'section_image', 'general'
))");
}
public function down(): void
{
Schema::dropIfExists('media');
}
};
<?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
{
Schema::create('website_settings', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->unique()->constrained('academies');
$table->string('template', 30)->default('bold_athletic');
// Colors
$table->string('primary_color', 7)->default('#1a1a2e');
$table->string('secondary_color', 7)->default('#16213e');
$table->string('accent_color', 7)->default('#e94560');
$table->string('text_color', 7)->default('#ffffff');
// Typography
$table->string('heading_font', 50)->default('Cairo');
$table->string('body_font', 50)->default('Cairo');
// Layout
$table->string('navbar_style', 20)->default('transparent');
// Content
$table->string('site_title')->nullable();
$table->string('site_title_en')->nullable();
$table->text('site_description')->nullable();
$table->text('site_description_en')->nullable();
$table->jsonb('social_links')->default('{}');
$table->string('whatsapp_number', 20)->nullable();
// Advanced
$table->text('custom_css')->nullable();
$table->string('google_analytics_id', 30)->nullable();
$table->string('facebook_pixel_id', 30)->nullable();
// Publish state
$table->boolean('is_published')->default(false);
$table->timestamp('published_at')->nullable();
$table->timestamps();
});
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_template_check
CHECK (template IN ('bold_athletic', 'clean_professional', 'vibrant_playful'))");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_navbar_style_check
CHECK (navbar_style IN ('solid', 'transparent', 'floating'))");
}
public function down(): void
{
Schema::dropIfExists('website_settings');
}
};
<?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
{
Schema::create('website_sections', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('academies');
$table->string('section_key', 50);
$table->string('title')->nullable();
$table->string('title_en')->nullable();
$table->string('subtitle')->nullable();
$table->string('subtitle_en')->nullable();
$table->text('content')->nullable();
$table->text('content_en')->nullable();
$table->jsonb('settings')->default('{}');
$table->unsignedSmallInteger('sort_order')->default(0);
$table->boolean('is_enabled')->default(true);
$table->timestamps();
$table->unique(['academy_id', 'section_key']);
$table->index(['academy_id', 'sort_order']);
});
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'))");
}
public function down(): void
{
Schema::dropIfExists('website_sections');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('website_testimonials', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('academies');
$table->string('name');
$table->string('name_en')->nullable();
$table->string('role')->nullable();
$table->string('role_en')->nullable();
$table->text('content');
$table->text('content_en')->nullable();
$table->unsignedTinyInteger('rating')->default(5);
$table->boolean('is_featured')->default(false);
$table->unsignedSmallInteger('sort_order')->default(0);
$table->timestamps();
$table->softDeletes();
$table->index(['academy_id', 'is_featured']);
});
}
public function down(): void
{
Schema::dropIfExists('website_testimonials');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('website_faqs', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('academies');
$table->string('question');
$table->string('question_en')->nullable();
$table->text('answer');
$table->text('answer_en')->nullable();
$table->unsignedSmallInteger('sort_order')->default(0);
$table->boolean('is_published')->default(true);
$table->timestamps();
$table->softDeletes();
$table->index(['academy_id', 'is_published']);
});
}
public function down(): void
{
Schema::dropIfExists('website_faqs');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('website_news', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('academies');
$table->string('title');
$table->string('title_en')->nullable();
$table->string('slug');
$table->text('body');
$table->text('body_en')->nullable();
$table->string('excerpt')->nullable();
$table->string('excerpt_en')->nullable();
$table->timestamp('published_at')->nullable();
$table->boolean('is_featured')->default(false);
$table->unsignedSmallInteger('sort_order')->default(0);
$table->foreignId('created_by')->constrained('users');
$table->timestamps();
$table->softDeletes();
$table->unique(['academy_id', 'slug']);
$table->index(['academy_id', 'published_at']);
});
}
public function down(): void
{
Schema::dropIfExists('website_news');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('website_partners', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('academies');
$table->string('name');
$table->string('name_en')->nullable();
$table->string('website_url')->nullable();
$table->unsignedSmallInteger('sort_order')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
$table->index(['academy_id', 'is_active']);
});
}
public function down(): void
{
Schema::dropIfExists('website_partners');
}
};
<?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
{
Schema::create('contact_submissions', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('academies');
$table->string('name');
$table->string('phone', 20);
$table->string('email')->nullable();
$table->text('message');
$table->string('status', 20)->default('new');
$table->text('admin_notes')->nullable();
$table->foreignId('replied_by')->nullable()->constrained('users');
$table->timestamp('read_at')->nullable();
$table->timestamp('replied_at')->nullable();
$table->timestamps();
$table->index(['academy_id', 'status']);
$table->index(['academy_id', 'created_at']);
});
DB::statement("ALTER TABLE contact_submissions ADD CONSTRAINT contact_submissions_status_check
CHECK (status IN ('new', 'read', 'replied', 'archived'))");
}
public function down(): void
{
Schema::dropIfExists('contact_submissions');
}
};
# Public Website Builder — Academy Advertising Sites
## Vision
Every academy that signs up gets a **stunning, data-driven public website** — zero technical knowledge required. The system's home screen is NOT a login page. It's a professional advertising website that pulls real data from the management system and combines it with custom CMS content.
**URL Flow:**
- `academy-slug.caprover.al-arcade.com` → Public website (visitors)
- `academy-slug.caprover.al-arcade.com/manage` → Login → Dashboard (staff)
---
## Architecture Overview
```
┌─────────────────────────────────────────────────────┐
│ PUBLIC WEBSITE │
│ (Blade views, no auth, cached, SEO-optimized) │
├─────────────────────────────────────────────────────┤
│ Data Sources: │
│ ├── CMS Content (website_sections table) │
│ ├── Media Library (media table + local/S3 storage) │
│ ├── Live Data (programs, branches, activities) │
│ └── Theme Settings (website_settings table) │
├─────────────────────────────────────────────────────┤
│ MANAGEMENT CMS PANEL │
│ (Livewire components inside dashboard) │
│ ├── Website Editor (sections, text, photos) │
│ ├── Media Manager (upload + crop + preview) │
│ ├── Theme Picker (colors, fonts, layout) │
│ └── Preview Mode (live preview before publish) │
└─────────────────────────────────────────────────────┘
```
---
## Design Decisions
| Decision | Choice | Reason |
|---|---|---|
| Template Style | **Bold & Athletic** | Dark, diagonal, dynamic — matches sports energy |
| CMS Editor | **Hybrid: Forms + Live Preview** | Reliable form editing with real-time preview iframe |
| Live Data | **Moderate** | Programs + branches live; prices/schedules CMS-managed |
| Routing | **Same app, separate route group** | `/site/{slug}` with full-page cache middleware |
| Image Processing | Intervention Image v3 | Resize, crop, WebP conversion |
| Caching | Full-page cache (Laravel Response Cache) | Sub-50ms page loads |
| Animations | Alpine.js + Intersection Observer | Count-up, fade-in, slide-up on scroll |
| Map | Leaflet.js (OpenStreetMap) | Free, no API key |
| SEO | Spatie Laravel SEO + JSON-LD | LocalBusiness schema per academy |
---
## Media Library System
### Upload Specifications
| Upload Target | Dimensions | Aspect Ratio | Max Size | Collection Key | Usage |
|---|---|---|---|---|---|
| Academy Logo | 400×400px | 1:1 | 500KB | `academy_logo` | Header, footer, favicon |
| Academy Cover | 1920×600px | 16:5 | 2MB | `academy_cover` | Hero section |
| Activity Photo | 800×600px | 4:3 | 1MB | `activity_photo` | Activity cards |
| Branch Photo | 1200×800px | 3:2 | 1.5MB | `branch_photo` | Branch section |
| Gallery Photos | 1200×800px | 3:2 | 1.5MB | `gallery` | Gallery grid |
| Team/Coach Photo | 600×600px | 1:1 | 500KB | `team_photo` | Team section circles |
| Testimonial Avatar | 200×200px | 1:1 | 200KB | `testimonial_avatar` | Testimonials |
| Partner/Sponsor Logo | 300×120px | 5:2 | 300KB | `partner_logo` | Partners bar |
### Upload UX Requirements
1. **Pre-upload**: Show exact dimensions as a dotted frame with measurements overlaid
2. **During upload**: Progress bar with percentage
3. **Post-upload**: Crop tool with LOCKED aspect ratio + zoom slider
4. **Quality indicator**:
- Green: Resolution meets or exceeds target
- Yellow: 75-99% of target (acceptable)
- Red: Below 75% (too small, warn user)
5. **Context preview**: "See how this looks on your website" button opens section preview
6. **Accepted formats**: JPEG, PNG, WebP — auto-convert to WebP for serving
7. **Dimension guide text**: Always visible below uploader: "Required: 1920×600px (16:5) • Max 2MB • JPG/PNG/WebP"
### Upload Component Wireframe
```
┌─────────────────────────────────────────────────────────────────┐
│ Upload Academy Cover Photo │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ 1920 × 600 px │ │ │
│ │ │ ━━━━━━━━━━━━━━━ │ │ │
│ │ │ 16:5 aspect ratio │ │ │
│ │ │ │ │ │
│ │ │ Click or drag │ │ │
│ │ │ to upload │ │ │
│ │ └─────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ Required: 1920×600px (16:5) • Max 2MB • JPG/PNG/WebP │
│ Tip: Use a high-contrast image — text overlay will be added │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ After upload: CROP TOOL appears here │ │
│ │ • Locked aspect ratio │ │
│ │ • Zoom slider │ │
│ │ • Quality indicator (Green: Excellent) │ │
│ │ • "Preview on website" button │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ [Cancel] [Save & Apply] │
└─────────────────────────────────────────────────────────────────┘
```
---
## CMS Sections
### Available Sections (16 total)
| Section Key | Name (AR) | Content Type | Data Source |
|---|---|---|---|
| `hero` | البانر الرئيسي | Cover image + headline + CTA | CMS only |
| `about` | من نحن | Rich text + optional image | CMS only |
| `activities` | الأنشطة | Cards grid | **Live DB** + photos |
| `programs` | البرامج | Cards/list | **Live DB** + CMS descriptions |
| `branches` | الفروع | Map + cards | **Live DB** + photos |
| `schedule` | المواعيد | Timetable/calendar | CMS-managed |
| `trainers` | المدربون | Team grid | CMS-managed |
| `gallery` | معرض الصور | Masonry/grid | CMS (uploaded photos) |
| `testimonials` | آراء العملاء | Carousel | CMS table |
| `pricing` | الأسعار | Pricing cards | CMS-managed |
| `partners` | شركاؤنا | Logo bar | CMS table |
| `contact` | تواصل معنا | Form + map + info | CMS + branch data |
| `faq` | الأسئلة الشائعة | Accordion | CMS table |
| `cta` | سجل الآن | Call-to-action banner | CMS only |
| `stats` | إنجازاتنا | Counter animations | **Live DB** (computed counts) |
| `news` | الأخبار | Blog/posts grid | CMS table |
### Live Data vs CMS Data
| What Visitors See | Source | Cache Duration |
|---|---|---|
| Academy name, logo | `organizations` table | 1 hour |
| Activity names + photos | `activities` + `media` | 1 hour |
| Program names, age ranges | `training_programs` | 1 hour |
| Program descriptions for website | `website_sections` CMS | Invalidated on save |
| Branch names, addresses, phones | `branches` | 1 hour |
| Branch photos, descriptions | `media` + CMS | Invalidated on save |
| Schedules | CMS-managed (not live) | Invalidated on save |
| Prices | CMS-managed (not live) | Invalidated on save |
| Trainer profiles | CMS-managed | Invalidated on save |
| Testimonials, FAQ, News | CMS tables | Invalidated on save |
| Stats (counts) | Computed + cached | 6 hours |
---
## CMS Editor — Hybrid Forms + Live Preview
### Editor Layout Wireframe
```
┌──────────────────────────────────────────────────────────────────────────┐
│ Website Editor [Preview] [Publish] │
├────────────────────────────┬─────────────────────────────────────────────┤
│ SECTIONS │ │
│ ───────────────── │ ┌─────────────────────────────────────┐ │
│ ☰ البانر الرئيسي ✓ │ │ │ │
│ ☰ من نحن ✓ │ │ LIVE PREVIEW IFRAME │ │
│ ☰ الأنشطة ✓ │ │ │ │
│ ☰ البرامج ✓ │ │ Updates in real-time as you │ │
│ ☰ الفروع ✓ │ │ modify form fields on the left │ │
│ ☰ المدربون ✗ │ │ │ │
│ ☰ معرض الصور ✓ │ │ Highlighted section indicator │ │
│ ☰ آراء العملاء ✓ │ │ shows which section you're │ │
│ ☰ الأسعار ✗ │ │ currently editing │ │
│ ☰ شركاؤنا ✓ │ │ │ │
│ ☰ تواصل معنا ✓ │ │ │ │
│ ☰ الأسئلة الشائعة ✗ │ │ │ │
│ │ └─────────────────────────────────────┘ │
│ [+ Add Section] │ │
├────────────────────────────┤ Desktop Tablet Mobile │
│ EDITING: البانر الرئيسي │ │
│ ───────────────── │ │
│ Title (AR): [أكاديمية..] │ │
│ Title (EN): [Academy...] │ │
│ Subtitle: [انضم إلين.] │ │
│ CTA Text: [سجل الآن] │ │
│ CTA Link: [/register] │ │
│ Cover Photo: [Change] │ │
│ Overlay: [Dark v] [70%] │ │
│ │ │
│ [Save Section] │ │
└────────────────────────────┴─────────────────────────────────────────────┘
```
### Editor Behaviors
- **Draggable sections**: Reorder via drag handle (☰)
- **Toggle enable/disable**: Click ✓/✗ to show/hide section on public site
- **Section-specific forms**: Each section has its own form fields (see below)
- **Live preview**: iframe loads `/manage/website/preview` route (authenticated, no cache)
- **Debounced updates**: Preview refreshes 500ms after last keystroke
- **Device toggle**: Switch preview between desktop (100%), tablet (768px), mobile (375px)
- **Publish flow**: Explicit button — site stays in draft until published
### Section Form Fields
#### Hero (البانر الرئيسي)
- Title AR (required)
- Title EN (optional)
- Subtitle AR
- Subtitle EN
- CTA button text AR
- CTA button text EN
- CTA link URL
- Cover photo upload (1920×600)
- Overlay color (dark/light/accent)
- Overlay opacity (0-100% slider)
#### About (من نحن)
- Heading AR / EN
- Body text AR (rich text — bold, italic, lists)
- Body text EN
- Side image upload (800×600)
- Image position (start/end)
#### Activities (الأنشطة)
- Section heading AR / EN
- Max items to display (number)
- Display style: grid / carousel
- Per activity (from live data): can override photo, can add description
- Show/hide specific activities toggle
#### Programs (البرامج)
- Section heading AR / EN
- Display style: cards / list / alternating
- Per program (from live data): featured description AR/EN, featured image
- Show/hide specific programs toggle
- Show age range: yes/no
- Show "spots available": yes/no
#### Branches (الفروع)
- Section heading AR / EN
- Show map: yes/no
- Map center coordinates (auto from branches)
- Per branch (from live data): description AR/EN, photo, display order
- Show phone number: yes/no
- Show working hours: yes/no
#### Trainers (المدربون)
- Section heading AR / EN
- Display style: grid / carousel
- Items: manual list of trainers (name, role, bio, photo)
- Columns per row: 3 / 4
#### Gallery (معرض الصور)
- Section heading AR / EN
- Layout: masonry / grid / carousel
- Columns: 2 / 3 / 4
- Photos: bulk upload with sort order
- Lightbox: yes/no
#### Testimonials (آراء العملاء)
- Section heading AR / EN
- Display: carousel / grid
- Auto-play speed (seconds)
- Items from `website_testimonials` table
#### Pricing (الأسعار)
- Section heading AR / EN
- Display style: cards / table
- Items: manual pricing tiers (name, price, features list, highlighted: yes/no)
- CTA text per tier
#### Stats (إنجازاتنا)
- Section heading AR / EN
- Stats to show (checkboxes): participants, trainers, branches, years, programs, graduates
- Custom stats: label + number (up to 6)
- Animation: count-up / fade-in / none
#### Contact (تواصل معنا)
- Section heading AR / EN
- Show form: yes/no
- Form fields: name, phone, email, message (all required)
- Show map: yes/no
- Show branch addresses: yes/no
- Show social links: yes/no
#### FAQ (الأسئلة الشائعة)
- Section heading AR / EN
- Items from `website_faqs` table (question/answer pairs)
- Display: accordion (single open) / accordion (multi open)
#### Partners (شركاؤنا)
- Section heading AR / EN
- Display: logo bar (scrolling) / grid
- Items from `website_partners` table
#### CTA (سجل الآن)
- Heading AR / EN
- Subheading AR / EN
- Button text AR / EN
- Button link
- Background: solid color / gradient / image
- Background image upload (1920×400)
#### News (الأخبار)
- Section heading AR / EN
- Max items to display
- Show date: yes/no
- Show excerpt: yes/no
- Items from `website_news` table
---
## Website Settings (Theme Configuration)
```
website_settings (per academy):
├── Colors
│ ├── primary_color (hex) — default #1a1a2e
│ ├── secondary_color (hex) — default #16213e
│ ├── accent_color (hex) — default #e94560
│ └── text_color (hex) — default #ffffff
├── Typography
│ ├── heading_font — from curated Arabic-safe list:
│ │ Cairo, Tajawal, IBM Plex Sans Arabic, Noto Sans Arabic,
│ │ Almarai, Readex Pro, Rubik, El Messiri
│ └── body_font — same list
├── Layout
│ ├── sections_order (JSON array of section keys)
│ ├── sections_enabled (JSON object: key → boolean)
│ └── navbar_style ('solid' | 'transparent' | 'floating')
├── Content
│ ├── site_title (AR)
│ ├── site_title_en
│ ├── site_description (AR, for meta tag)
│ ├── site_description_en
│ ├── social_links (JSONB):
│ │ facebook, instagram, twitter, youtube, tiktok, linkedin
│ └── whatsapp_number (with country code)
└── Advanced
├── custom_css (optional, textarea)
├── google_analytics_id
└── facebook_pixel_id
```
---
## Bold & Athletic Template — Visual Design Spec
### Design Language
- **Background**: Dark (#1a1a2e to #16213e gradient)
- **Accent**: Vibrant pop color (default #e94560, customizable)
- **Typography**: Bold, uppercase headings with letter-spacing
- **Geometry**: Diagonal clip-paths, skewed section dividers
- **Cards**: Dark surface with accent border on hover, image zoom effect
- **Buttons**: Solid accent with glow/scale on hover
- **Spacing**: Generous whitespace between sections (py-20 to py-32)
### Section-by-Section Design
#### Hero
- Full viewport height (100vh desktop, 60vh mobile)
- Cover image with `clip-path: polygon(0 0, 100% 0, 100% 85%, 0 100%)` diagonal bottom
- Dark gradient overlay (70% opacity default)
- Heading: 4xl-6xl, Cairo bold, text-shadow
- Animated accent underline (width animates in on load)
- CTA button: accent bg, rounded-lg, hover:scale-105 + box-shadow glow
- Floating stats bar at bottom: semi-transparent dark bg, 3-4 stats with counters
#### Activities
- Dark section background
- Subtle diagonal line pattern (SVG bg, 5% opacity)
- Cards: 3-column grid (lg), 2-column (md), 1-column (sm)
- Card: image with `object-cover`, overlay gradient from bottom
- Hover: image scale(1.05), accent border-bottom appears
- Activity name: absolute bottom, white text on gradient
#### Programs
- Alternating left/right layout per program
- Large image one side, content the other
- Content: program name (heading), age range badge, description, "learn more" link
- Diagonal accent stripe between items (thin, decorative)
- Mobile: stacked, image on top
#### Branches
- Map section: full-width Leaflet map with custom markers (accent color)
- Below map: branch cards in a row
- Card: branch photo, name, address, phone, working hours
- Hover: card lifts (shadow increase) + accent top-border
#### Stats
- Full-width dark band with CSS diagonal accent lines (pseudo-elements)
- 4 stats in a row, large numbers (text-5xl)
- Numbers animate (count up) when scrolled into view (Intersection Observer)
- Label below each number in muted text
- Icon above each number in accent color
#### Trainers
- Circular photos with 3px accent color border
- Name below in white, role/specialization in muted text
- Hover: image slight scale + border thickens
- Grid: 4-column (lg), 3 (md), 2 (sm)
#### Testimonials
- Carousel with large quote marks (accent color, faded)
- Testimonial text centered, italicized
- Avatar (circle) + name + role below
- Navigation: dots or arrows in accent color
- Auto-play with pause on hover
#### Gallery
- Masonry grid layout (CSS columns or JS masonry)
- Hover: image darkens + "view" icon appears
- Click: lightbox with navigation
- Lazy loading for performance
#### Contact
- Split: form on one side, info + map on the other
- Form: dark input fields with accent focus ring
- Validation: inline, Arabic error messages
- Submit: accent button with loading state
- Success: animated checkmark + "سنتواصل معك قريبا"
#### Footer
- Dark background (darkest shade)
- Academy logo + brief description
- Quick links (sections)
- Social media icons (accent hover color)
- WhatsApp floating button (fixed, bottom-end corner)
- Copyright line
---
## Database Schema
### Migration 1: `media`
```php
Schema::create('media', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('organizations');
$table->uuid('uuid')->unique();
$table->string('collection', 50); // academy_logo, academy_cover, activity_photo, etc.
$table->string('original_filename');
$table->string('disk_path');
$table->string('mime_type', 50);
$table->unsignedInteger('width');
$table->unsignedInteger('height');
$table->unsignedInteger('file_size'); // bytes
$table->string('alt_text')->nullable();
$table->string('alt_text_ar')->nullable();
$table->nullableMorphs('mediable'); // polymorphic: activity, branch, person, etc.
$table->unsignedSmallInteger('sort_order')->default(0);
$table->timestamps();
$table->softDeletes();
$table->index(['academy_id', 'collection']);
});
```
### Migration 2: `website_settings`
```php
Schema::create('website_settings', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('organizations')->unique();
$table->string('template', 30)->default('bold_athletic');
// Colors
$table->string('primary_color', 7)->default('#1a1a2e');
$table->string('secondary_color', 7)->default('#16213e');
$table->string('accent_color', 7)->default('#e94560');
$table->string('text_color', 7)->default('#ffffff');
// Typography
$table->string('heading_font', 50)->default('Cairo');
$table->string('body_font', 50)->default('Cairo');
// Layout
$table->string('navbar_style', 20)->default('transparent');
// Content
$table->string('site_title')->nullable();
$table->string('site_title_en')->nullable();
$table->text('site_description')->nullable();
$table->text('site_description_en')->nullable();
$table->jsonb('social_links')->default('{}');
$table->string('whatsapp_number', 20)->nullable();
// Advanced
$table->text('custom_css')->nullable();
$table->string('google_analytics_id', 30)->nullable();
$table->string('facebook_pixel_id', 30)->nullable();
// Publish state
$table->boolean('is_published')->default(false);
$table->timestamp('published_at')->nullable();
$table->timestamps();
});
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_template_check
CHECK (template IN ('bold_athletic', 'clean_professional', 'vibrant_playful'))");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_navbar_style_check
CHECK (navbar_style IN ('solid', 'transparent', 'floating'))");
```
### Migration 3: `website_sections`
```php
Schema::create('website_sections', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('organizations');
$table->string('section_key', 50);
$table->string('title')->nullable();
$table->string('title_en')->nullable();
$table->string('subtitle')->nullable();
$table->string('subtitle_en')->nullable();
$table->text('content')->nullable(); // rich text body (AR)
$table->text('content_en')->nullable();
$table->jsonb('settings')->default('{}'); // section-specific config
$table->unsignedSmallInteger('sort_order')->default(0);
$table->boolean('is_enabled')->default(true);
$table->timestamps();
$table->unique(['academy_id', 'section_key']);
$table->index(['academy_id', 'sort_order']);
});
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'))");
```
### Migration 4: `website_testimonials`
```php
Schema::create('website_testimonials', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('organizations');
$table->uuid('uuid')->unique();
$table->string('name');
$table->string('name_en')->nullable();
$table->string('role')->nullable(); // e.g., "ولي أمر" or "لاعب"
$table->string('role_en')->nullable();
$table->text('content'); // AR testimonial text
$table->text('content_en')->nullable();
$table->unsignedTinyInteger('rating')->default(5); // 1-5
$table->boolean('is_featured')->default(false);
$table->unsignedSmallInteger('sort_order')->default(0);
$table->timestamps();
$table->softDeletes();
$table->index(['academy_id', 'is_featured']);
});
```
### Migration 5: `website_faqs`
```php
Schema::create('website_faqs', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('organizations');
$table->uuid('uuid')->unique();
$table->string('question'); // AR
$table->string('question_en')->nullable();
$table->text('answer'); // AR
$table->text('answer_en')->nullable();
$table->unsignedSmallInteger('sort_order')->default(0);
$table->boolean('is_published')->default(true);
$table->timestamps();
$table->softDeletes();
$table->index(['academy_id', 'is_published']);
});
```
### Migration 6: `website_news`
```php
Schema::create('website_news', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('organizations');
$table->uuid('uuid')->unique();
$table->string('title'); // AR
$table->string('title_en')->nullable();
$table->string('slug');
$table->text('body'); // AR, rich text
$table->text('body_en')->nullable();
$table->string('excerpt')->nullable(); // AR
$table->string('excerpt_en')->nullable();
$table->timestamp('published_at')->nullable();
$table->boolean('is_featured')->default(false);
$table->unsignedSmallInteger('sort_order')->default(0);
$table->foreignId('created_by')->constrained('users');
$table->timestamps();
$table->softDeletes();
$table->unique(['academy_id', 'slug']);
$table->index(['academy_id', 'published_at']);
});
```
### Migration 7: `website_partners`
```php
Schema::create('website_partners', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('organizations');
$table->uuid('uuid')->unique();
$table->string('name'); // AR
$table->string('name_en')->nullable();
$table->string('website_url')->nullable();
$table->unsignedSmallInteger('sort_order')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
$table->index(['academy_id', 'is_active']);
});
```
### Migration 8: `contact_submissions`
```php
Schema::create('contact_submissions', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('organizations');
$table->uuid('uuid')->unique();
$table->string('name');
$table->string('phone', 20);
$table->string('email')->nullable();
$table->text('message');
$table->string('status', 20)->default('new'); // new, read, replied, archived
$table->text('admin_notes')->nullable();
$table->foreignId('replied_by')->nullable()->constrained('users');
$table->timestamp('read_at')->nullable();
$table->timestamp('replied_at')->nullable();
$table->timestamps();
$table->index(['academy_id', 'status']);
$table->index(['academy_id', 'created_at']);
});
DB::statement("ALTER TABLE contact_submissions ADD CONSTRAINT contact_submissions_status_check
CHECK (status IN ('new', 'read', 'replied', 'archived'))");
```
---
## File Structure
```
app/Domain/Website/
├── Models/
│ ├── WebsiteSetting.php
│ ├── WebsiteSection.php
│ ├── WebsiteTestimonial.php
│ ├── WebsiteFaq.php
│ ├── WebsiteNews.php
│ ├── WebsitePartner.php
│ └── ContactSubmission.php
├── Services/
│ ├── WebsiteSettingService.php
│ ├── WebsiteSectionService.php
│ ├── WebsiteDataService.php (aggregates live data for public site)
│ └── WebsiteCacheService.php (tag-based cache invalidation)
├── Enums/
│ ├── SectionKey.php
│ ├── NavbarStyle.php
│ ├── TemplateStyle.php
│ └── ContactSubmissionStatus.php
└── Events/
├── WebsitePublished.php
└── ContactSubmitted.php
app/Domain/Shared/
├── Models/Media.php
└── Services/MediaService.php
app/Http/Controllers/
├── PublicWebsiteController.php (renders public site — single controller)
└── ContactFormController.php (handles contact form submission)
app/Http/Middleware/
├── ResolveAcademyFromSlug.php (sets current_academy from URL slug)
└── CachePublicPage.php (full-page cache with tag invalidation)
app/Livewire/Website/
├── ThemeEditor.php (colors, fonts, navbar style)
├── SectionManager.php (list, reorder, enable/disable sections)
├── SectionEditor.php (form fields for active section)
├── MediaUploader.php (reusable — crop, resize, quality check)
├── MediaGalleryManager.php (bulk upload + sort for gallery section)
├── TestimonialManager.php (CRUD for testimonials)
├── FaqManager.php (CRUD for FAQs)
├── NewsManager.php (CRUD for news posts)
├── PartnerManager.php (CRUD for partners)
├── ContactSubmissionList.php (view + manage contact form submissions)
└── WebsitePreview.php (preview iframe controller)
resources/views/
├── website/
│ ├── layout.blade.php (public site master — no auth, dark theme)
│ ├── index.blade.php (loops through enabled sections)
│ ├── navbar.blade.php (transparent/solid/floating)
│ ├── footer.blade.php (logo, links, social, copyright)
│ └── sections/
│ ├── hero.blade.php
│ ├── about.blade.php
│ ├── activities.blade.php
│ ├── programs.blade.php
│ ├── branches.blade.php
│ ├── schedule.blade.php
│ ├── trainers.blade.php
│ ├── gallery.blade.php
│ ├── testimonials.blade.php
│ ├── pricing.blade.php
│ ├── partners.blade.php
│ ├── contact.blade.php
│ ├── faq.blade.php
│ ├── cta.blade.php
│ ├── stats.blade.php
│ └── news.blade.php
└── livewire/website/
├── theme-editor.blade.php
├── section-manager.blade.php
├── section-editor.blade.php
├── media-uploader.blade.php
├── testimonial-manager.blade.php
├── faq-manager.blade.php
├── news-manager.blade.php
├── partner-manager.blade.php
└── contact-submission-list.blade.php
```
---
## Build Order — 12 Vertical Slices
### Slice 1: Media Library Foundation
- Migration: `media` table
- Model: `Media` with `BelongsToAcademy`, `HasUuid`, `SoftDeletes`
- Service: `MediaService`
- `upload(UploadedFile, collection, mediable?)` — validate, resize, convert WebP, store
- `crop(Media, x, y, width, height)` — re-crop existing image
- `delete(Media)` — soft delete + optionally remove from disk
- `getCollectionSpec(collection)` — returns dimensions, aspect ratio, max size
- Livewire: `MediaUploader` component
- Props: `collection`, `mediableType`, `mediableId`, `multiple`
- Shows dimension guide, handles upload, crop tool, quality indicator
- Emits: `media-uploaded` with media ID
- Storage config: `disks.media` in `filesystems.php`
- **Test in browser**: upload an image, see crop tool, verify WebP output
### Slice 2: Website Settings & Theme
- Migration: `website_settings`
- Model: `WebsiteSetting` with `BelongsToAcademy`
- Service: `WebsiteSettingService`
- `getOrCreate(academy)` — returns settings, creates defaults if none
- `update(academy, data)` — validates + saves + invalidates cache
- `publish(academy)` — sets is_published, fires WebsitePublished event
- Livewire: `ThemeEditor`
- Color pickers (accent, primary, secondary)
- Font dropdowns (curated Arabic-safe list)
- Navbar style selector
- Social links inputs
- Live preview updates via Alpine
- **Test in browser**: change colors, see preview update
### Slice 3: Website Sections Framework
- Migration: `website_sections`
- Model: `WebsiteSection` with `BelongsToAcademy`
- Service: `WebsiteSectionService`
- `seedDefaults(academy)` — creates all 16 sections with default sort order
- `reorder(academy, orderedKeys[])` — bulk update sort_order
- `toggle(section, enabled)` — enable/disable
- `updateContent(section, data)` — save title, subtitle, content, settings
- Livewire: `SectionManager` (draggable list with enable/disable toggles)
- Livewire: `SectionEditor` (dynamic form based on section_key)
- Seeder: auto-seeds sections when new academy is created
- **Test in browser**: reorder sections, toggle on/off, edit content
### Slice 4: Public Site Layout + Hero Section
- Route: `GET /site/{slug}``PublicWebsiteController@show`
- Middleware: `ResolveAcademyFromSlug`
- Layout: `website/layout.blade.php` — dark theme, no auth, loads fonts + Alpine
- Navbar: transparent with scroll-to-solid transition
- Hero section Blade view with diagonal clip-path, overlay, CTA
- CSS: custom properties from `website_settings` (injected as inline `<style>`)
- **Test in browser**: visit `/site/test-academy`, see styled hero
### Slice 5: Activities + Programs Sections (Live Data)
- `WebsiteDataService::getActivities(academy)` — cached query
- `WebsiteDataService::getPrograms(academy)` — cached query
- Activities Blade: grid cards with hover zoom + overlay
- Programs Blade: alternating image/content layout
- CMS forms: per-section settings (display style, max items, show/hide toggles)
- Photo linking: `MediaUploader` in activity/program forms
- **Test in browser**: see live activities from DB rendered in bold template
### Slice 6: Branches Section (Live Data + Map)
- `WebsiteDataService::getBranches(academy)` — cached query
- Branches Blade: Leaflet map + branch cards below
- Map: custom markers in accent color, popup with branch info
- Branch photos via `media` polymorphic relation
- CMS form: per-branch descriptions, show/hide phone, working hours
- **Test in browser**: see map with markers, branch cards
### Slice 7: Testimonials, FAQ, Partners (CMS Tables)
- Migrations: `website_testimonials`, `website_faqs`, `website_partners`
- Models with `BelongsToAcademy`, `HasUuid`, `SoftDeletes`
- Livewire CRUD components for each (create, edit, delete, reorder)
- Public Blade views:
- Testimonials: carousel with large quotes, avatar, rating stars
- FAQ: accordion with smooth expand/collapse (Alpine `x-collapse`)
- Partners: scrolling logo bar
- **Test in browser**: add testimonials from CMS, see on public site
### Slice 8: Gallery + News (CMS Content)
- Migration: `website_news`
- Gallery: bulk `MediaUploader` for collection='gallery' + drag-to-sort
- Gallery Blade: masonry grid, lightbox on click (Alpine)
- News: CRUD component with title, body (simple textarea), excerpt, publish date
- News Blade: cards grid with featured image, date, excerpt
- **Test in browser**: upload gallery photos, create news post, verify public views
### Slice 9: Stats + Contact Sections
- Stats: `WebsiteDataService::getStats(academy)` — counts participants, trainers, branches, programs
- Stats Blade: animated counter (Alpine + Intersection Observer)
- Migration: `contact_submissions`
- Model: `ContactSubmission`
- Contact Blade: form with validation + map + social links
- `ContactFormController`: validates, stores, dispatches `ContactSubmitted` event
- Livewire: `ContactSubmissionList` — view/manage submissions in dashboard
- **Test in browser**: submit contact form, see in dashboard
### Slice 10: Remaining Sections (About, Schedule, Trainers, Pricing, CTA)
- About Blade: rich text + image (content from `website_sections.content`)
- Schedule Blade: simple timetable from CMS settings JSON
- Trainers Blade: team grid with circular photos
- Pricing Blade: cards with feature lists, highlighted plan
- CTA Blade: full-width banner with gradient/image bg, large heading + button
- Each gets its section-specific form in `SectionEditor`
- **Test in browser**: enable each section, fill content, verify
### Slice 11: Live Preview System
- Route: `GET /manage/website/preview` (auth required, no cache)
- Renders same template as public but always fresh (no cache)
- `WebsitePreview` Livewire component: manages iframe
- Section highlight: when editing section X, preview scrolls to it + adds outline
- Device toggle: wraps iframe in width constraints (100%, 768px, 375px)
- Debounced refresh: `wire:model.live.debounce.500ms` triggers preview reload
- **Test in browser**: edit fields, see changes in preview iframe
### Slice 12: Publishing + Cache + SEO
- Publish button: sets `is_published = true` + `published_at`
- Cache middleware: full-page cache on public routes, tagged per academy
- `WebsiteCacheService::invalidate(academy)` — called on any CMS save
- SEO: `<title>`, `<meta description>`, Open Graph tags, JSON-LD LocalBusiness
- Sitemap: auto-generated for published academies
- WhatsApp floating button: fixed position, links to academy's WhatsApp
- Final route wiring: root `/` redirects to `/site/{default-slug}` (not login)
- **Test in browser**: publish, verify cache headers, check SEO meta tags
---
## Routing Configuration
```php
// routes/web.php
// Public website (no auth, cached)
Route::middleware(['resolve.academy.slug', 'cache.public.page'])
->prefix('site/{slug}')
->group(function () {
Route::get('/', [PublicWebsiteController::class, 'show'])->name('website.show');
Route::get('/news/{newsSlug}', [PublicWebsiteController::class, 'newsArticle'])->name('website.news.show');
Route::post('/contact', [ContactFormController::class, 'submit'])->name('website.contact.submit');
});
// CMS management (inside existing auth dashboard routes)
Route::middleware(['auth', 'permission:website.manage'])
->prefix('manage/website')
->group(function () {
Route::get('/', WebsiteSectionManager::class)->name('manage.website');
Route::get('/theme', WebsiteThemeEditor::class)->name('manage.website.theme');
Route::get('/preview', [PublicWebsiteController::class, 'preview'])->name('manage.website.preview');
Route::get('/testimonials', WebsiteTestimonialManager::class)->name('manage.website.testimonials');
Route::get('/faqs', WebsiteFaqManager::class)->name('manage.website.faqs');
Route::get('/news', WebsiteNewsManager::class)->name('manage.website.news');
Route::get('/partners', WebsitePartnerManager::class)->name('manage.website.partners');
Route::get('/gallery', WebsiteMediaGalleryManager::class)->name('manage.website.gallery');
Route::get('/contacts', WebsiteContactSubmissionList::class)->name('manage.website.contacts');
});
// Root redirect (homepage is the public site, not login)
Route::get('/', function () {
// Resolve academy from domain/config and redirect to public site
return redirect()->route('website.show', ['slug' => config('app.default_academy_slug')]);
});
```
---
## Permissions
| Permission Key | Description | Roles |
|---|---|---|
| `website.manage` | Access website CMS panel | academy_owner, academy_admin |
| `website.publish` | Publish/unpublish the website | academy_owner, academy_admin |
| `website.theme` | Change colors, fonts, template | academy_owner |
| `website.contacts` | View contact form submissions | academy_owner, academy_admin, receptionist |
---
## Cache Strategy
| What | TTL | Invalidation |
|---|---|---|
| Full public page | 1 hour | On any CMS save for that academy |
| Live data (programs, branches, activities) | 1 hour | On model update (observer) |
| Stats (counts) | 6 hours | Scheduled job recalculates |
| Media URLs | 24 hours | On media delete/replace |
| Preview page | Never cached | Always fresh for editors |
**Implementation:**
- Cache tags: `['website', 'academy:{id}']`
- `WebsiteCacheService::invalidate($academyId)` — flushes all tagged cache
- Called from: section save, theme save, media upload, publish toggle
- Model observers on `Activity`, `TrainingProgram`, `Branch` — invalidate website cache on change
---
## SEO Implementation
### Meta Tags (in `website/layout.blade.php`)
```html
<title>{{ $settings->site_title }} — {{ $academy->name_ar }}</title>
<meta name="description" content="{{ $settings->site_description }}">
<meta property="og:title" content="{{ $settings->site_title }}">
<meta property="og:description" content="{{ $settings->site_description }}">
<meta property="og:image" content="{{ $academy->cover_photo_url }}">
<meta property="og:type" content="website">
<link rel="canonical" href="{{ route('website.show', $academy->slug) }}">
```
### JSON-LD Structured Data
```json
{
"@context": "https://schema.org",
"@type": "SportsActivityLocation",
"name": "{{ academy name }}",
"description": "{{ site description }}",
"address": {
"@type": "PostalAddress",
"addressLocality": "{{ branch city }}",
"addressCountry": "EG"
},
"telephone": "{{ branch phone }}",
"image": "{{ cover photo }}",
"sameAs": ["{{ social links }}"]
}
```
---
## Performance Budget
| Metric | Target | How |
|---|---|---|
| First Contentful Paint | < 1.5s | Full-page cache, optimized images |
| Largest Contentful Paint | < 2.5s | WebP images, lazy loading below fold |
| Total page size | < 2MB | WebP compression, font subsetting |
| Lighthouse score | > 90 | Semantic HTML, accessibility, meta tags |
| Time to Interactive | < 3s | Minimal JS (Alpine only), no heavy frameworks |
---
## Dependencies to Install
```bash
composer require intervention/image:^3.0 # Image processing
composer require spatie/laravel-responsecache # Full-page caching
# OR custom cache middleware (simpler)
npm install cropperjs # Client-side crop tool
npm install leaflet # Maps (if not using CDN)
```
---
## Default Section Order (seeded for new academies)
1. hero
2. about
3. activities
4. programs
5. branches
6. stats
7. trainers
8. gallery
9. testimonials
10. pricing
11. partners
12. faq
13. news
14. contact
15. cta
16. schedule (disabled by default)
---
## Future Enhancements (NOT in initial build)
- Multiple template styles (Clean & Professional, Vibrant & Playful)
- Custom domain support (academy.com → their public site)
- Blog with full CMS (categories, tags, rich editor)
- Online registration form (pre-enrollment from public site)
- Event calendar integration
- Video sections (YouTube/Vimeo embed)
- Multilingual URL slugs
- A/B testing for CTA variations
- Analytics dashboard (page views, popular sections, form conversion rate)
@import 'tailwindcss';
/* ═══════════════════════════════════════════════════════════════
BOLD & ATHLETIC — Public Academy Website Template
Dark, diagonal, dynamic. Sports energy.
═══════════════════════════════════════════════════════════════ */
@theme {
--color-website-primary: var(--site-primary, #1a1a2e);
--color-website-secondary: var(--site-secondary, #16213e);
--color-website-accent: var(--site-accent, #e94560);
--color-website-text: var(--site-text, #ffffff);
--color-website-muted: oklch(0.75 0.01 260);
--color-website-surface: oklch(0.15 0.02 260);
--color-website-surface-raised: oklch(0.18 0.02 260);
--color-website-border: oklch(0.25 0.02 260);
}
@layer base {
.website-body {
background: linear-gradient(135deg, var(--site-primary, #1a1a2e) 0%, var(--site-secondary, #16213e) 100%);
color: var(--site-text, #ffffff);
font-family: var(--site-body-font, 'Cairo'), 'Noto Sans Arabic', system-ui, sans-serif;
direction: rtl;
min-height: 100vh;
overflow-x: hidden;
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
}
.website-body * {
scroll-margin-top: 80px;
}
}
@layer components {
/* ─── Navbar ─── */
.site-nav {
position: fixed;
top: 0;
inset-inline: 0;
z-index: 100;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(0px);
}
.site-nav--scrolled {
background: oklch(0.12 0.02 260 / 0.95);
backdrop-filter: blur(12px);
box-shadow: 0 4px 30px oklch(0 0 0 / 0.3);
}
.site-nav--solid {
background: oklch(0.12 0.02 260 / 0.98);
backdrop-filter: blur(12px);
}
/* ─── Hero ─── */
.hero-section {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.hero-section::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 120px;
background: linear-gradient(to top, var(--site-primary, #1a1a2e), transparent);
z-index: 2;
}
.hero-clip {
clip-path: polygon(0 0, 100% 0, 100% 88%, 0 100%);
}
.hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
135deg,
oklch(0.1 0.02 260 / var(--overlay-opacity, 0.7)) 0%,
oklch(0.05 0.03 260 / calc(var(--overlay-opacity, 0.7) + 0.1)) 100%
);
z-index: 1;
}
.hero-content {
position: relative;
z-index: 3;
text-align: center;
padding: 2rem;
}
.hero-title {
font-size: clamp(2.5rem, 6vw, 5rem);
font-weight: 800;
line-height: 1.1;
text-shadow: 0 4px 20px oklch(0 0 0 / 0.5);
letter-spacing: -0.02em;
}
.hero-accent-line {
width: 0;
height: 4px;
margin: 1.5rem auto;
background: var(--site-accent, #e94560);
border-radius: 2px;
animation: accent-expand 1s 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes accent-expand {
to { width: 120px; }
}
/* ─── Section Shared ─── */
.site-section {
position: relative;
padding: 6rem 0;
overflow: hidden;
}
.site-section--dark {
background: oklch(0.1 0.015 260);
}
.site-section--pattern {
background-image: repeating-linear-gradient(
-45deg,
transparent,
transparent 40px,
oklch(1 0 0 / 0.02) 40px,
oklch(1 0 0 / 0.02) 41px
);
}
.section-title {
font-size: clamp(1.75rem, 4vw, 3rem);
font-weight: 700;
margin-bottom: 0.5rem;
position: relative;
display: inline-block;
}
.section-title::after {
content: '';
position: absolute;
bottom: -8px;
inset-inline-start: 0;
width: 60px;
height: 3px;
background: var(--site-accent, #e94560);
border-radius: 2px;
}
/* ─── Cards (Activity / Program / Branch) ─── */
.site-card {
position: relative;
border-radius: 1rem;
overflow: hidden;
background: oklch(0.15 0.02 260);
border: 1px solid oklch(0.25 0.02 260);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.site-card:hover {
transform: translateY(-4px);
border-color: var(--site-accent, #e94560);
box-shadow: 0 20px 40px oklch(0 0 0 / 0.3),
0 0 0 1px var(--site-accent, #e94560);
}
.site-card__image {
width: 100%;
aspect-ratio: 4/3;
object-fit: cover;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.site-card:hover .site-card__image {
transform: scale(1.08);
}
.site-card__overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, oklch(0.08 0.02 260 / 0.9) 0%, transparent 60%);
opacity: 0;
transition: opacity 0.4s;
}
.site-card:hover .site-card__overlay {
opacity: 1;
}
/* ─── Stats Counter ─── */
.stats-band {
position: relative;
background: oklch(0.08 0.02 260);
overflow: hidden;
}
.stats-band::before,
.stats-band::after {
content: '';
position: absolute;
width: 200px;
height: 200%;
background: var(--site-accent, #e94560);
opacity: 0.05;
transform: rotate(-15deg);
}
.stats-band::before { left: 10%; top: -50%; }
.stats-band::after { right: 15%; top: -50%; }
.stat-number {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
color: var(--site-accent, #e94560);
line-height: 1;
}
/* ─── Testimonials Carousel ─── */
.testimonial-card {
background: oklch(0.15 0.02 260);
border-radius: 1.5rem;
padding: 2.5rem;
border: 1px solid oklch(0.25 0.02 260);
position: relative;
}
.testimonial-card::before {
content: '\201C';
position: absolute;
top: 1rem;
inset-inline-start: 1.5rem;
font-size: 5rem;
line-height: 1;
color: var(--site-accent, #e94560);
opacity: 0.3;
font-family: serif;
}
/* ─── FAQ Accordion ─── */
.faq-item {
border: 1px solid oklch(0.25 0.02 260);
border-radius: 0.75rem;
overflow: hidden;
transition: border-color 0.3s;
}
.faq-item:hover,
.faq-item[data-open="true"] {
border-color: var(--site-accent, #e94560);
}
/* ─── CTA Section ─── */
.cta-section {
position: relative;
background: linear-gradient(135deg, var(--site-accent, #e94560), oklch(0.55 0.2 350));
overflow: hidden;
}
.cta-section::before {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(
-45deg,
transparent,
transparent 30px,
oklch(1 0 0 / 0.05) 30px,
oklch(1 0 0 / 0.05) 31px
);
}
/* ─── Buttons ─── */
.site-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.875rem 2rem;
border-radius: 0.75rem;
font-weight: 600;
font-size: 1rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
text-decoration: none;
}
.site-btn--primary {
background: var(--site-accent, #e94560);
color: white;
box-shadow: 0 4px 15px oklch(0.6 0.2 15 / 0.3);
}
.site-btn--primary:hover {
transform: scale(1.05);
box-shadow: 0 8px 25px oklch(0.6 0.2 15 / 0.5);
}
.site-btn--outline {
background: transparent;
color: white;
border: 2px solid oklch(1 0 0 / 0.3);
}
.site-btn--outline:hover {
border-color: var(--site-accent, #e94560);
color: var(--site-accent, #e94560);
}
/* ─── Footer ─── */
.site-footer {
background: oklch(0.06 0.015 260);
border-top: 1px solid oklch(0.2 0.02 260);
}
/* ─── WhatsApp Float ─── */
.whatsapp-float {
position: fixed;
bottom: 1.5rem;
inset-inline-end: 1.5rem;
z-index: 90;
width: 56px;
height: 56px;
background: #25d366;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px oklch(0.5 0.2 150 / 0.4);
transition: transform 0.3s;
animation: whatsapp-pulse 2s infinite;
}
.whatsapp-float:hover {
transform: scale(1.1);
}
@keyframes whatsapp-pulse {
0%, 100% { box-shadow: 0 4px 15px oklch(0.5 0.2 150 / 0.4); }
50% { box-shadow: 0 4px 25px oklch(0.5 0.2 150 / 0.6), 0 0 0 8px oklch(0.5 0.2 150 / 0.1); }
}
/* ─── Animations ─── */
.reveal {
opacity: 0;
transform: translateY(30px);
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.reveal--visible {
opacity: 1;
transform: translateY(0);
}
.reveal--delay-1 { transition-delay: 0.1s; }
.reveal--delay-2 { transition-delay: 0.2s; }
.reveal--delay-3 { transition-delay: 0.3s; }
.reveal--delay-4 { transition-delay: 0.4s; }
/* ─── Map Container ─── */
.map-container {
border-radius: 1rem;
overflow: hidden;
border: 1px solid oklch(0.25 0.02 260);
height: 350px;
}
/* ─── Gallery Grid ─── */
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.gallery-item {
position: relative;
border-radius: 0.75rem;
overflow: hidden;
aspect-ratio: 3/2;
cursor: pointer;
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.gallery-item:hover img {
transform: scale(1.1);
}
.gallery-item::after {
content: '';
position: absolute;
inset: 0;
background: oklch(0.1 0 0 / 0);
transition: background 0.3s;
}
.gallery-item:hover::after {
background: oklch(0.1 0 0 / 0.4);
}
/* ─── Partners Logo Bar ─── */
.partners-track {
display: flex;
gap: 3rem;
animation: scroll-logos 30s linear infinite;
}
@keyframes scroll-logos {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
/* ─── Contact Form ─── */
.site-input {
width: 100%;
padding: 0.875rem 1rem;
background: oklch(0.12 0.02 260);
border: 1px solid oklch(0.25 0.02 260);
border-radius: 0.75rem;
color: white;
font-size: 1rem;
transition: border-color 0.3s;
}
.site-input:focus {
outline: none;
border-color: var(--site-accent, #e94560);
box-shadow: 0 0 0 3px oklch(0.6 0.2 15 / 0.1);
}
.site-input::placeholder {
color: oklch(0.55 0.01 260);
}
}
/* ─── Responsive ─── */
@media (max-width: 768px) {
.hero-section {
min-height: 70vh;
}
.site-section {
padding: 3.5rem 0;
}
.gallery-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.gallery-grid {
grid-template-columns: 1fr;
}
}
/**
* Bold & Athletic — Public Website Interactivity
* Scroll animations, counter animations, navbar transitions
*/
document.addEventListener('DOMContentLoaded', () => {
initNavbar();
initRevealAnimations();
initCounterAnimations();
initSmoothScroll();
});
// ─── Navbar scroll transition ───
function initNavbar() {
const nav = document.querySelector('.site-nav');
if (!nav) return;
const onScroll = () => {
if (window.scrollY > 50) {
nav.classList.add('site-nav--scrolled');
} else {
nav.classList.remove('site-nav--scrolled');
}
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
}
// ─── Reveal on scroll (Intersection Observer) ───
function initRevealAnimations() {
const elements = document.querySelectorAll('.reveal');
if (!elements.length) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('reveal--visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.15, rootMargin: '0px 0px -50px 0px' }
);
elements.forEach((el) => observer.observe(el));
}
// ─── Counter animation (count up on scroll) ───
function initCounterAnimations() {
const counters = document.querySelectorAll('[data-counter]');
if (!counters.length) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
animateCounter(entry.target);
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.5 }
);
counters.forEach((el) => observer.observe(el));
}
function animateCounter(el) {
const target = parseInt(el.dataset.counter, 10);
const duration = 2000;
const start = performance.now();
const suffix = el.dataset.suffix || '';
function update(now) {
const elapsed = now - start;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 4); // ease-out quart
const current = Math.floor(eased * target);
el.textContent = current.toLocaleString('ar-EG') + suffix;
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
// ─── Smooth scroll for anchor links ───
function initSmoothScroll() {
document.querySelectorAll('a[href^="#"]').forEach((link) => {
link.addEventListener('click', (e) => {
const targetId = link.getAttribute('href').slice(1);
const target = document.getElementById(targetId);
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
}
<div>
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">رسائل التواصل</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">الرسائل الواردة من نموذج التواصل في الموقع</p>
</div>
<a href="{{ route('website.manage.sections') }}" class="px-4 py-2 text-sm font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
← إدارة الأقسام
</a>
</div>
{{-- Status Filter --}}
<div class="flex items-center gap-2 mb-4">
<button wire:click="$set('status', '')" class="px-3 py-1.5 text-sm rounded-lg transition-colors {{ $status === '' ? 'bg-blue-600 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600' }}">
الكل
</button>
<button wire:click="$set('status', 'new')" class="px-3 py-1.5 text-sm rounded-lg transition-colors {{ $status === 'new' ? 'bg-blue-600 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600' }}">
جديد
</button>
<button wire:click="$set('status', 'read')" class="px-3 py-1.5 text-sm rounded-lg transition-colors {{ $status === 'read' ? 'bg-blue-600 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600' }}">
مقروء
</button>
<button wire:click="$set('status', 'replied')" class="px-3 py-1.5 text-sm rounded-lg transition-colors {{ $status === 'replied' ? 'bg-blue-600 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600' }}">
تم الرد
</button>
<button wire:click="$set('status', 'archived')" class="px-3 py-1.5 text-sm rounded-lg transition-colors {{ $status === 'archived' ? 'bg-blue-600 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600' }}">
أرشيف
</button>
</div>
{{-- Submissions Table --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
@if($submissions->count() > 0)
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-700/50">
<tr>
<th class="px-4 py-3 text-start font-medium text-gray-700 dark:text-gray-300">الاسم</th>
<th class="px-4 py-3 text-start font-medium text-gray-700 dark:text-gray-300">البريد / الهاتف</th>
<th class="px-4 py-3 text-start font-medium text-gray-700 dark:text-gray-300">الرسالة</th>
<th class="px-4 py-3 text-start font-medium text-gray-700 dark:text-gray-300">الحالة</th>
<th class="px-4 py-3 text-start font-medium text-gray-700 dark:text-gray-300">التاريخ</th>
<th class="px-4 py-3 text-start font-medium text-gray-700 dark:text-gray-300">إجراءات</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
@foreach($submissions as $submission)
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30 {{ $submission->status->value === 'new' ? 'bg-blue-50/50 dark:bg-blue-900/10' : '' }}">
<td class="px-4 py-3">
<span class="font-medium text-gray-900 dark:text-white {{ $submission->status->value === 'new' ? 'font-bold' : '' }}">
{{ $submission->name }}
</span>
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
<div dir="ltr" class="text-start">{{ $submission->email }}</div>
@if($submission->phone)
<div dir="ltr" class="text-xs text-gray-400">{{ $submission->phone }}</div>
@endif
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400 max-w-xs">
<p class="line-clamp-2">{{ $submission->message }}</p>
</td>
<td class="px-4 py-3">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium
{{ $submission->status->value === 'new' ? 'bg-blue-100 text-blue-700' : '' }}
{{ $submission->status->value === 'read' ? 'bg-gray-100 text-gray-700' : '' }}
{{ $submission->status->value === 'replied' ? 'bg-green-100 text-green-700' : '' }}
{{ $submission->status->value === 'archived' ? 'bg-gray-100 text-gray-500' : '' }}
">
{{ $submission->status->label() }}
</span>
</td>
<td class="px-4 py-3 text-gray-500 text-xs whitespace-nowrap">
{{ $submission->created_at->diffForHumans() }}
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-1">
@if($submission->status->value === 'new')
<button wire:click="markAsRead({{ $submission->id }})" class="p-1.5 text-gray-400 hover:text-blue-600 rounded transition-colors" title="تحديد كمقروء">
<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 19v-8.93a2 2 0 01.89-1.664l7-4.666a2 2 0 012.22 0l7 4.666A2 2 0 0121 10.07V19M3 19a2 2 0 002 2h14a2 2 0 002-2M3 19l6.75-4.5M21 19l-6.75-4.5M3 10l6.75 4.5M21 10l-6.75 4.5m0 0l-1.14.76a2 2 0 01-2.22 0l-1.14-.76"/></svg>
</button>
@endif
@if($submission->status->value !== 'archived')
<button wire:click="archive({{ $submission->id }})" class="p-1.5 text-gray-400 hover:text-amber-600 rounded transition-colors" title="أرشفة">
<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="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"/></svg>
</button>
@endif
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Pagination --}}
<div class="px-4 py-3 border-t border-gray-200 dark:border-gray-700">
{{ $submissions->links() }}
</div>
@else
<div class="text-center py-12">
<svg class="w-12 h-12 mx-auto text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">لا توجد رسائل {{ $status ? 'بهذه الحالة' : '' }}</h3>
<p class="mt-2 text-gray-500 text-sm">ستظهر هنا الرسائل الواردة من نموذج التواصل</p>
</div>
@endif
</div>
</div>
<div>
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">الأسئلة الشائعة</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">أضف الأسئلة المتكررة وإجاباتها</p>
</div>
<div class="flex items-center gap-3">
<a href="{{ route('website.manage.sections') }}" class="px-4 py-2 text-sm font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
← إدارة الأقسام
</a>
<button wire:click="create" class="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors">
+ إضافة سؤال
</button>
</div>
</div>
@if(session('success'))
<div class="mb-4 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
{{-- Form --}}
@if($showForm)
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{{ $editingId ? 'تعديل السؤال' : 'إضافة سؤال جديد' }}
</h3>
<form wire:submit="save" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">السؤال (عربي) *</label>
<input type="text" wire:model="question" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
@error('question') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">السؤال (إنجليزي)</label>
<input type="text" wire:model="question_en" dir="ltr" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">الإجابة (عربي) *</label>
<textarea wire:model="answer" rows="3" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"></textarea>
@error('answer') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">الإجابة (إنجليزي)</label>
<textarea wire:model="answer_en" rows="3" dir="ltr" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"></textarea>
</div>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" wire:model="is_published" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700 dark:text-gray-300">منشور</span>
</label>
</div>
<div class="flex items-center gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button type="submit" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium text-sm transition-colors"
wire:loading.attr="disabled" wire:target="save">
<span wire:loading.remove wire:target="save">حفظ</span>
<span wire:loading wire:target="save">جارٍ الحفظ...</span>
</button>
<button type="button" wire:click="$set('showForm', false)" class="px-4 py-2.5 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-sm transition-colors">
إلغاء
</button>
</div>
</form>
</div>
@endif
{{-- List --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
@if(count($faqs) > 0)
<div class="divide-y divide-gray-100 dark:divide-gray-700">
@foreach($faqs as $faq)
<div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-gray-900 dark:text-white">{{ $faq->question }}</span>
@if(!$faq->is_published)
<span class="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">مخفي</span>
@endif
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">{{ $faq->answer }}</p>
</div>
<div class="flex items-center gap-1 shrink-0">
<button wire:click="edit({{ $faq->id }})" class="p-2 text-gray-400 hover:text-blue-600 rounded-lg hover:bg-blue-50 transition-colors">
<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="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
</button>
<button wire:click="delete({{ $faq->id }})" wire:confirm="هل أنت متأكد من الحذف؟" class="p-2 text-gray-400 hover:text-red-600 rounded-lg hover:bg-red-50 transition-colors">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-12">
<svg class="w-12 h-12 mx-auto text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">لا توجد أسئلة بعد</h3>
<p class="mt-2 text-gray-500 text-sm">أضف الأسئلة الشائعة لمساعدة زوار موقعك</p>
</div>
@endif
</div>
</div>
<div>
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">معرض الصور</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">ارفع صور الأكاديمية والأنشطة لعرضها في الموقع</p>
</div>
<a href="{{ route('website.manage.sections') }}" class="px-4 py-2 text-sm font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
← إدارة الأقسام
</a>
</div>
@if(session('success'))
<div class="mb-4 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="mb-4 p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">
{{ session('error') }}
</div>
@endif
{{-- Upload Area --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
<div class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-xl p-8 text-center hover:border-blue-400 transition-colors"
x-data="{ dragging: false }"
x-on:dragover.prevent="dragging = true"
x-on:dragleave.prevent="dragging = false"
x-on:drop.prevent="dragging = false; $wire.uploadMultiple('photos', $event.dataTransfer.files)"
:class="dragging && 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'">
<svg class="w-12 h-12 mx-auto text-gray-400 dark:text-gray-500 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<p class="text-gray-600 dark:text-gray-300 font-medium mb-1">اسحب الصور هنا أو</p>
<label class="inline-block cursor-pointer">
<span class="text-blue-600 hover:text-blue-700 font-medium">اختر من جهازك</span>
<input type="file" wire:model="photos" multiple accept="image/*" class="hidden">
</label>
<p class="text-xs text-gray-500 mt-3">
الأبعاد المثالية: 1200×800 بكسل | الحد الأقصى: 5 ميجابايت | الصيغ: JPG, PNG, WebP
</p>
</div>
{{-- Upload Progress --}}
<div wire:loading wire:target="photos" class="mt-4">
<div class="flex items-center gap-3 text-sm text-blue-600">
<svg class="animate-spin w-5 h-5" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/></svg>
جارٍ رفع الصور...
</div>
</div>
@error('photos.*')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
{{-- Gallery Grid --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
@if(count($gallery) > 0)
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-gray-900 dark:text-white">الصور ({{ count($gallery) }})</h3>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
@foreach($gallery as $image)
<div class="group relative aspect-square rounded-lg overflow-hidden bg-gray-100 dark:bg-gray-700">
<img src="{{ $image['url'] }}" alt="" class="w-full h-full object-cover">
{{-- Overlay Actions --}}
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<button wire:click="deleteImage({{ $image['id'] }})"
wire:confirm="هل أنت متأكد من حذف هذه الصورة؟"
class="p-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</div>
{{-- Size Badge --}}
@if(isset($image['size']))
<span class="absolute bottom-1 start-1 text-[10px] bg-black/60 text-white px-1.5 py-0.5 rounded">
{{ $image['size'] }}
</span>
@endif
</div>
@endforeach
</div>
@else
<div class="text-center py-12">
<svg class="w-16 h-16 mx-auto text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">لا توجد صور بعد</h3>
<p class="mt-2 text-gray-500 dark:text-gray-400 text-sm">ارفع صوراً لتظهر في معرض موقعك</p>
</div>
@endif
</div>
</div>
<div>
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">الأخبار</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">مقالات وأخبار الأكاديمية</p>
</div>
<div class="flex items-center gap-3">
<a href="{{ route('website.manage.sections') }}" class="px-4 py-2 text-sm font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
← إدارة الأقسام
</a>
<button wire:click="create" class="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors">
+ إضافة خبر
</button>
</div>
</div>
@if(session('success'))
<div class="mb-4 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
{{-- Form --}}
@if($showForm)
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{{ $editingId ? 'تعديل الخبر' : 'إضافة خبر جديد' }}
</h3>
<form wire:submit="save" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">العنوان (عربي) *</label>
<input type="text" wire:model="title" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
@error('title') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">العنوان (إنجليزي)</label>
<input type="text" wire:model="title_en" dir="ltr" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">المقتطف (عربي)</label>
<textarea wire:model="excerpt" rows="2" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" placeholder="ملخص قصير يظهر في بطاقة الخبر..."></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">المقتطف (إنجليزي)</label>
<textarea wire:model="excerpt_en" rows="2" dir="ltr" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"></textarea>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">المحتوى الكامل (عربي) *</label>
<textarea wire:model="body" rows="6" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"></textarea>
@error('body') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">المحتوى الكامل (إنجليزي)</label>
<textarea wire:model="body_en" rows="6" dir="ltr" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"></textarea>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" wire:model="is_featured" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700 dark:text-gray-300">خبر مميز</span>
</label>
</div>
<div class="flex items-center gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button type="submit" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium text-sm transition-colors"
wire:loading.attr="disabled" wire:target="save">
<span wire:loading.remove wire:target="save">حفظ</span>
<span wire:loading wire:target="save">جارٍ الحفظ...</span>
</button>
<button type="button" wire:click="$set('showForm', false)" class="px-4 py-2.5 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-sm transition-colors">
إلغاء
</button>
</div>
</form>
</div>
@endif
{{-- List --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
@if(count($news) > 0)
<div class="divide-y divide-gray-100 dark:divide-gray-700">
@foreach($news as $article)
<div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-gray-900 dark:text-white">{{ $article->title }}</span>
@if($article->is_featured)
<span class="text-xs bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full">مميز</span>
@endif
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">{{ $article->excerpt }}</p>
<p class="text-xs text-gray-400 mt-1">{{ $article->published_at?->format('Y-m-d') }}</p>
</div>
<div class="flex items-center gap-1 shrink-0">
<button wire:click="edit({{ $article->id }})" class="p-2 text-gray-400 hover:text-blue-600 rounded-lg hover:bg-blue-50 transition-colors">
<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="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
</button>
<button wire:click="delete({{ $article->id }})" wire:confirm="هل أنت متأكد من الحذف؟" class="p-2 text-gray-400 hover:text-red-600 rounded-lg hover:bg-red-50 transition-colors">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-12">
<svg class="w-12 h-12 mx-auto text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">لا توجد أخبار بعد</h3>
<p class="mt-2 text-gray-500 text-sm">أضف أخبار ومقالات الأكاديمية</p>
</div>
@endif
</div>
</div>
<div>
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">الشركاء</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">شركاء الأكاديمية والرعاة</p>
</div>
<div class="flex items-center gap-3">
<a href="{{ route('website.manage.sections') }}" class="px-4 py-2 text-sm font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
← إدارة الأقسام
</a>
<button wire:click="create" class="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors">
+ إضافة شريك
</button>
</div>
</div>
@if(session('success'))
<div class="mb-4 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
{{-- Form --}}
@if($showForm)
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{{ $editingId ? 'تعديل الشريك' : 'إضافة شريك جديد' }}
</h3>
<form wire:submit="save" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">اسم الشريك (عربي) *</label>
<input type="text" wire:model="name" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
@error('name') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">اسم الشريك (إنجليزي)</label>
<input type="text" wire:model="name_en" dir="ltr" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">الموقع الإلكتروني</label>
<input type="url" wire:model="website_url" dir="ltr" placeholder="https://..." class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
@error('website_url') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" wire:model="is_active" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700 dark:text-gray-300">نشط</span>
</label>
</div>
<div class="flex items-center gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button type="submit" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium text-sm transition-colors"
wire:loading.attr="disabled" wire:target="save">
<span wire:loading.remove wire:target="save">حفظ</span>
<span wire:loading wire:target="save">جارٍ الحفظ...</span>
</button>
<button type="button" wire:click="$set('showForm', false)" class="px-4 py-2.5 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-sm transition-colors">
إلغاء
</button>
</div>
</form>
</div>
@endif
{{-- List --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
@if(count($partners) > 0)
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
@foreach($partners as $partner)
<div class="border border-gray-200 dark:border-gray-600 rounded-lg p-4 hover:shadow-md transition-shadow {{ !$partner->is_active ? 'opacity-50' : '' }}">
<div class="flex items-center justify-between mb-2">
<span class="font-medium text-gray-900 dark:text-white">{{ $partner->name }}</span>
@if(!$partner->is_active)
<span class="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">معطل</span>
@endif
</div>
@if($partner->website_url)
<p class="text-xs text-gray-500 truncate mb-3" dir="ltr">{{ $partner->website_url }}</p>
@endif
<div class="flex items-center gap-1">
<button wire:click="edit({{ $partner->id }})" class="p-1.5 text-gray-400 hover:text-blue-600 rounded transition-colors">
<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="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
</button>
<button wire:click="delete({{ $partner->id }})" wire:confirm="هل أنت متأكد من الحذف؟" class="p-1.5 text-gray-400 hover:text-red-600 rounded transition-colors">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-12">
<svg class="w-12 h-12 mx-auto text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">لا يوجد شركاء بعد</h3>
<p class="mt-2 text-gray-500 text-sm">أضف شركاء ورعاة الأكاديمية</p>
</div>
@endif
</div>
</div>
<div>
{{-- Header --}}
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">إدارة الموقع الإلكتروني</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">تحكم بأقسام ومحتوى موقع الأكاديمية</p>
</div>
<div class="flex items-center gap-3">
<a href="{{ route('website.manage.theme') }}" class="px-4 py-2 text-sm font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
تصميم الموقع
</a>
@if($isPublished)
<a href="{{ route('website.show', app('current_academy')->slug) }}" target="_blank" class="px-4 py-2 text-sm font-medium rounded-lg bg-green-50 text-green-700 border border-green-200 hover:bg-green-100 transition-colors">
عرض الموقع ↗
</a>
<button wire:click="unpublish" class="px-4 py-2 text-sm font-medium rounded-lg bg-amber-50 text-amber-700 border border-amber-200 hover:bg-amber-100 transition-colors">
إلغاء النشر
</button>
@else
<button wire:click="publish" class="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors">
نشر الموقع
</button>
@endif
</div>
</div>
@if(session('success'))
<div class="mb-4 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{{-- Sections List --}}
<div class="lg:col-span-1">
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="font-semibold text-gray-900 dark:text-white">أقسام الموقع</h3>
<p class="text-xs text-gray-500 mt-1">اسحب لإعادة الترتيب</p>
</div>
<div class="divide-y divide-gray-100 dark:divide-gray-700">
@foreach($sections as $section)
<div class="flex items-center gap-3 p-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors cursor-pointer {{ $editingSection === $section['key'] ? 'bg-blue-50 dark:bg-blue-900/20 border-s-4 border-blue-500' : '' }}"
wire:click="editSection('{{ $section['key'] }}')">
{{-- Drag Handle --}}
<svg class="w-4 h-4 text-gray-400 cursor-grab" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm8-16a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/>
</svg>
{{-- Section Info --}}
<div class="flex-1 min-w-0">
<span class="text-sm font-medium text-gray-900 dark:text-white">{{ $section['label'] }}</span>
</div>
{{-- Toggle --}}
<button wire:click.stop="toggleSection({{ $section['id'] }})"
class="shrink-0 w-9 h-5 rounded-full transition-colors {{ $section['is_enabled'] ? 'bg-green-500' : 'bg-gray-300 dark:bg-gray-600' }}">
<span class="block w-4 h-4 rounded-full bg-white shadow transform transition-transform {{ $section['is_enabled'] ? '-translate-x-4' : '-translate-x-0.5' }}"></span>
</button>
</div>
@endforeach
</div>
</div>
{{-- Quick Links --}}
<div class="mt-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 space-y-2">
<a href="{{ route('website.manage.gallery') }}" class="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-sm text-gray-700 dark:text-gray-300 transition-colors">
<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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
معرض الصور
</a>
<a href="{{ route('website.manage.testimonials') }}" class="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-sm text-gray-700 dark:text-gray-300 transition-colors">
<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="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
آراء العملاء
</a>
<a href="{{ route('website.manage.faqs') }}" class="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-sm text-gray-700 dark:text-gray-300 transition-colors">
<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="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
الأسئلة الشائعة
</a>
<a href="{{ route('website.manage.news') }}" class="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-sm text-gray-700 dark:text-gray-300 transition-colors">
<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="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/></svg>
الأخبار
</a>
<a href="{{ route('website.manage.partners') }}" class="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-sm text-gray-700 dark:text-gray-300 transition-colors">
<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="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
الشركاء
</a>
<a href="{{ route('website.manage.contacts') }}" class="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-sm text-gray-700 dark:text-gray-300 transition-colors">
<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 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
رسائل التواصل
</a>
</div>
</div>
{{-- Section Editor --}}
<div class="lg:col-span-2">
@if($editingSection)
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
تعديل: {{ \App\Domain\Website\Enums\SectionKey::from($editingSection)->label() }}
</h3>
<button wire:click="cancelEdit" class="text-gray-400 hover:text-gray-600">
<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="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<form wire:submit="saveSection" class="space-y-5">
{{-- Title --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">العنوان (عربي)</label>
<input type="text" wire:model="sectionTitle" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">العنوان (إنجليزي)</label>
<input type="text" wire:model="sectionTitleEn" dir="ltr" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
{{-- Subtitle --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">العنوان الفرعي (عربي)</label>
<input type="text" wire:model="sectionSubtitle" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">العنوان الفرعي (إنجليزي)</label>
<input type="text" wire:model="sectionSubtitleEn" dir="ltr" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
{{-- Content --}}
@if(in_array($editingSection, ['about', 'schedule']))
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">المحتوى (عربي)</label>
<textarea wire:model="sectionContent" rows="5" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">المحتوى (إنجليزي)</label>
<textarea wire:model="sectionContentEn" rows="5" dir="ltr" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
@endif
{{-- Section-Specific Settings --}}
@if($editingSection === 'hero')
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">نص الزر</label>
<input type="text" wire:model="sectionSettings.cta_text" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" placeholder="سجّل الآن">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">رابط الزر</label>
<input type="text" wire:model="sectionSettings.cta_link" dir="ltr" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" placeholder="#section-contact">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">شفافية الطبقة المظلمة (%)</label>
<input type="range" wire:model="sectionSettings.overlay_opacity" min="20" max="90" class="w-full">
<span class="text-xs text-gray-500">{{ $sectionSettings['overlay_opacity'] ?? 70 }}%</span>
</div>
@endif
@if($editingSection === 'activities' || $editingSection === 'programs')
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">عدد العناصر المعروضة</label>
<input type="number" wire:model="sectionSettings.max_items" min="3" max="12" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" placeholder="6">
</div>
@endif
{{-- Actions --}}
<div class="flex items-center gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button type="submit" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium text-sm transition-colors"
wire:loading.attr="disabled" wire:target="saveSection">
<span wire:loading.remove wire:target="saveSection">حفظ التغييرات</span>
<span wire:loading wire:target="saveSection">جارٍ الحفظ...</span>
</button>
<button type="button" wire:click="cancelEdit" class="px-4 py-2.5 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-sm transition-colors">
إلغاء
</button>
</div>
</form>
</div>
@else
{{-- Empty State --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-12 text-center">
<svg class="w-16 h-16 mx-auto text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">اختر قسماً للتعديل</h3>
<p class="mt-2 text-gray-500 dark:text-gray-400 text-sm">اضغط على أي قسم من القائمة لتعديل محتواه</p>
</div>
@endif
</div>
</div>
</div>
<div>
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">آراء العملاء</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">إدارة شهادات العملاء المعروضة في الموقع</p>
</div>
<div class="flex items-center gap-3">
<a href="{{ route('website.manage.sections') }}" class="px-4 py-2 text-sm font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
← إدارة الأقسام
</a>
<button wire:click="create" class="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors">
+ إضافة رأي
</button>
</div>
</div>
@if(session('success'))
<div class="mb-4 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
{{-- Form --}}
@if($showForm)
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{{ $editingId ? 'تعديل الرأي' : 'إضافة رأي جديد' }}
</h3>
<form wire:submit="save" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">اسم العميل *</label>
<input type="text" wire:model="name" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
@error('name') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">الصفة / العلاقة</label>
<input type="text" wire:model="role" placeholder="ولي أمر لاعب" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">نص الرأي *</label>
<textarea wire:model="content" rows="3" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" placeholder="تجربة عميلك مع الأكاديمية..."></textarea>
@error('content') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">التقييم</label>
<select wire:model="rating" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="5">★★★★★ ممتاز</option>
<option value="4">★★★★☆ جيد جداً</option>
<option value="3">★★★☆☆ جيد</option>
</select>
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" wire:model="is_featured" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700 dark:text-gray-300">مميز (يظهر أولاً)</span>
</label>
</div>
</div>
<div class="flex items-center gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button type="submit" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium text-sm transition-colors"
wire:loading.attr="disabled" wire:target="save">
<span wire:loading.remove wire:target="save">حفظ</span>
<span wire:loading wire:target="save">جارٍ الحفظ...</span>
</button>
<button type="button" wire:click="$set('showForm', false)" class="px-4 py-2.5 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-sm transition-colors">
إلغاء
</button>
</div>
</form>
</div>
@endif
{{-- List --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
@if(count($testimonials) > 0)
<div class="divide-y divide-gray-100 dark:divide-gray-700">
@foreach($testimonials as $testimonial)
<div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-gray-900 dark:text-white">{{ $testimonial->name }}</span>
@if($testimonial->role)
<span class="text-xs text-gray-500">— {{ $testimonial->role }}</span>
@endif
@if($testimonial->is_featured)
<span class="text-xs bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full">مميز</span>
@endif
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">{{ $testimonial->content }}</p>
<div class="mt-1 text-amber-500 text-xs">
@for($i = 0; $i < $testimonial->rating; $i++) ★ @endfor
</div>
</div>
<div class="flex items-center gap-1 shrink-0">
<button wire:click="edit({{ $testimonial->id }})" class="p-2 text-gray-400 hover:text-blue-600 rounded-lg hover:bg-blue-50 transition-colors">
<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="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
</button>
<button wire:click="delete({{ $testimonial->id }})" wire:confirm="هل أنت متأكد من الحذف؟" class="p-2 text-gray-400 hover:text-red-600 rounded-lg hover:bg-red-50 transition-colors">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-12">
<svg class="w-12 h-12 mx-auto text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">لا توجد آراء بعد</h3>
<p class="mt-2 text-gray-500 text-sm">أضف آراء عملائك لزيادة مصداقية موقعك</p>
</div>
@endif
</div>
</div>
<div>
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">تصميم الموقع</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">تخصيص الألوان والخطوط والروابط الاجتماعية</p>
</div>
<a href="{{ route('website.manage.sections') }}" class="px-4 py-2 text-sm font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
← إدارة الأقسام
</a>
</div>
@if(session('success'))
<div class="mb-4 p-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
<form wire:submit="save" class="space-y-6">
{{-- Colors --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">الألوان</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">اللون الرئيسي</label>
<div class="flex items-center gap-2">
<input type="color" wire:model="primary_color" class="w-10 h-10 rounded cursor-pointer border-0 p-0">
<input type="text" wire:model="primary_color" dir="ltr" class="flex-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" placeholder="#1e40af">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">اللون الثانوي</label>
<div class="flex items-center gap-2">
<input type="color" wire:model="secondary_color" class="w-10 h-10 rounded cursor-pointer border-0 p-0">
<input type="text" wire:model="secondary_color" dir="ltr" class="flex-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" placeholder="#f59e0b">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">اللون المميز</label>
<div class="flex items-center gap-2">
<input type="color" wire:model="accent_color" class="w-10 h-10 rounded cursor-pointer border-0 p-0">
<input type="text" wire:model="accent_color" dir="ltr" class="flex-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" placeholder="#10b981">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">لون النص</label>
<div class="flex items-center gap-2">
<input type="color" wire:model="text_color" class="w-10 h-10 rounded cursor-pointer border-0 p-0">
<input type="text" wire:model="text_color" dir="ltr" class="flex-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" placeholder="#ffffff">
</div>
</div>
</div>
{{-- Color Preview --}}
<div class="mt-4 p-4 rounded-lg border border-gray-200 dark:border-gray-600" style="background-color: {{ $secondary_color }}">
<div class="flex items-center gap-3">
<span class="px-3 py-1 rounded text-sm font-medium" style="background-color: {{ $primary_color }}; color: {{ $text_color }}">رئيسي</span>
<span class="px-3 py-1 rounded text-sm font-medium" style="background-color: {{ $accent_color }}; color: {{ $text_color }}">مميز</span>
<span class="px-3 py-1 rounded text-sm font-medium" style="color: {{ $text_color }}; border: 1px solid {{ $text_color }}">نص</span>
</div>
</div>
</div>
{{-- Fonts --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">الخطوط</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">خط العناوين</label>
<select wire:model="heading_font" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="Cairo">Cairo</option>
<option value="Tajawal">Tajawal</option>
<option value="IBM Plex Sans Arabic">IBM Plex Sans Arabic</option>
<option value="Noto Sans Arabic">Noto Sans Arabic</option>
<option value="Almarai">Almarai</option>
<option value="Changa">Changa</option>
<option value="El Messiri">El Messiri</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">خط النصوص</label>
<select wire:model="body_font" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="Cairo">Cairo</option>
<option value="Tajawal">Tajawal</option>
<option value="IBM Plex Sans Arabic">IBM Plex Sans Arabic</option>
<option value="Noto Sans Arabic">Noto Sans Arabic</option>
<option value="Almarai">Almarai</option>
</select>
</div>
</div>
</div>
{{-- Navbar Style --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">شريط التنقل</h3>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">نمط الشريط</label>
<div class="grid grid-cols-3 gap-3">
<label class="relative cursor-pointer">
<input type="radio" wire:model="navbar_style" value="transparent" class="sr-only peer">
<div class="p-3 rounded-lg border-2 peer-checked:border-blue-500 border-gray-200 dark:border-gray-600 text-center transition-colors">
<span class="text-sm">شفاف</span>
</div>
</label>
<label class="relative cursor-pointer">
<input type="radio" wire:model="navbar_style" value="solid" class="sr-only peer">
<div class="p-3 rounded-lg border-2 peer-checked:border-blue-500 border-gray-200 dark:border-gray-600 text-center transition-colors">
<span class="text-sm">معتم</span>
</div>
</label>
<label class="relative cursor-pointer">
<input type="radio" wire:model="navbar_style" value="floating" class="sr-only peer">
<div class="p-3 rounded-lg border-2 peer-checked:border-blue-500 border-gray-200 dark:border-gray-600 text-center transition-colors">
<span class="text-sm">عائم</span>
</div>
</label>
</div>
</div>
</div>
{{-- Social Links --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">روابط التواصل الاجتماعي</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Facebook</label>
<input type="url" wire:model="social_links.facebook" dir="ltr" placeholder="https://facebook.com/..." class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Instagram</label>
<input type="url" wire:model="social_links.instagram" dir="ltr" placeholder="https://instagram.com/..." class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Twitter / X</label>
<input type="url" wire:model="social_links.twitter" dir="ltr" placeholder="https://x.com/..." class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">YouTube</label>
<input type="url" wire:model="social_links.youtube" dir="ltr" placeholder="https://youtube.com/@..." class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">TikTok</label>
<input type="url" wire:model="social_links.tiktok" dir="ltr" placeholder="https://tiktok.com/@..." class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
</div>
</div>
{{-- WhatsApp --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">واتساب</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">رقم واتساب</label>
<input type="text" wire:model="whatsapp_number" dir="ltr" placeholder="+201012345678" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<p class="text-xs text-gray-500 mt-1">سيظهر زر واتساب عائم في الموقع</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">عنوان الموقع</label>
<input type="text" wire:model="site_title" placeholder="اسم الأكاديمية" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
</div>
</div>
{{-- Analytics --}}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">التحليلات</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Google Analytics ID</label>
<input type="text" wire:model="google_analytics_id" dir="ltr" placeholder="G-XXXXXXXXXX" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Facebook Pixel ID</label>
<input type="text" wire:model="facebook_pixel_id" dir="ltr" placeholder="123456789" class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
</div>
</div>
{{-- Save Button --}}
<div class="flex items-center gap-3">
<button type="submit" class="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium transition-colors"
wire:loading.attr="disabled" wire:target="save">
<span wire:loading.remove wire:target="save">حفظ الإعدادات</span>
<span wire:loading wire:target="save">جارٍ الحفظ...</span>
</button>
</div>
</form>
</div>
<footer class="site-footer">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
{{-- Academy Info --}}
<div class="lg:col-span-2">
<div class="flex items-center gap-3 mb-4">
@if($academy->logo_path)
<img src="{{ asset('storage/' . $academy->logo_path) }}"
alt="{{ $academy->name_ar }}"
class="h-12 w-12 rounded-lg object-contain">
@endif
<span class="text-xl font-bold text-white">{{ $academy->name_ar }}</span>
</div>
@if($settings->site_description)
<p class="text-white/60 leading-relaxed max-w-md">
{{ Str::limit($settings->site_description, 200) }}
</p>
@endif
{{-- Social Links --}}
@if(is_array($settings->social_links) && count(array_filter($settings->social_links)))
<div class="flex items-center gap-3 mt-6">
@foreach(['facebook', 'instagram', 'twitter', 'youtube', 'tiktok'] as $platform)
@if(!empty($settings->social_links[$platform]))
<a href="{{ $settings->social_links[$platform] }}"
target="_blank"
rel="noopener"
class="w-10 h-10 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center text-white/60 hover:text-[var(--site-accent)] hover:border-[var(--site-accent)] transition-all"
aria-label="{{ $platform }}">
@include("website.icons.{$platform}")
</a>
@endif
@endforeach
</div>
@endif
</div>
{{-- Quick Links --}}
<div>
<h4 class="text-white font-bold mb-4">روابط سريعة</h4>
<ul class="space-y-2">
@foreach($sections->take(6) as $section)
<li>
<a href="#section-{{ $section->section_key->value }}"
class="text-white/60 hover:text-[var(--site-accent)] transition-colors text-sm">
{{ $section->title ?? $section->section_key->label() }}
</a>
</li>
@endforeach
</ul>
</div>
{{-- Contact Info --}}
<div>
<h4 class="text-white font-bold mb-4">تواصل معنا</h4>
<ul class="space-y-3 text-sm">
@if($academy->phone)
<li class="flex items-center gap-2 text-white/60">
<svg class="w-4 h-4 text-[var(--site-accent)]" 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>
<span dir="ltr">{{ $academy->phone }}</span>
</li>
@endif
@if($academy->email)
<li class="flex items-center gap-2 text-white/60">
<svg class="w-4 h-4 text-[var(--site-accent)]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
<span>{{ $academy->email }}</span>
</li>
@endif
@if($settings->whatsapp_number)
<li class="flex items-center gap-2 text-white/60">
<svg class="w-4 h-4 text-green-400" 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>
<span dir="ltr">{{ $settings->whatsapp_number }}</span>
</li>
@endif
</ul>
</div>
</div>
{{-- Bottom Bar --}}
<div class="mt-12 pt-8 border-t border-white/10 flex flex-col sm:flex-row items-center justify-between gap-4">
<p class="text-white/40 text-sm">
&copy; {{ date('Y') }} {{ $academy->name_ar }}. جميع الحقوق محفوظة.
</p>
<p class="text-white/30 text-xs">
مدعوم بنظام El Captain
</p>
</div>
</div>
</footer>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></svg>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
@extends('website.layout')
@section('content')
@foreach($sections as $section)
<section id="section-{{ $section->section_key->value }}">
@include('website.sections.' . $section->section_key->value, [
'section' => $section,
'academy' => $academy,
'settings' => $settings,
'data' => $data,
])
</section>
@endforeach
@endsection
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $settings->site_title ?? $academy->name_ar }}</title>
<meta name="description" content="{{ $settings->site_description ?? '' }}">
{{-- Open Graph --}}
<meta property="og:title" content="{{ $settings->site_title ?? $academy->name_ar }}">
<meta property="og:description" content="{{ $settings->site_description ?? '' }}">
<meta property="og:type" content="website">
<meta property="og:locale" content="ar_EG">
@if($academy->logo_path)
<meta property="og:image" content="{{ asset('storage/' . $academy->logo_path) }}">
@endif
{{-- Fonts --}}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family={{ urlencode($settings->heading_font ?? 'Cairo') }}:wght@400;600;700;800&family={{ $settings->body_font !== $settings->heading_font ? urlencode($settings->body_font ?? 'Cairo') . ':wght@400;500;600&' : '' }}display=swap" rel="stylesheet">
{{-- Vite Assets --}}
@vite(['resources/css/website.css', 'resources/js/website.js'])
{{-- Dynamic CSS Variables from Academy Branding --}}
<style>
:root {
--site-primary: {{ $settings->primary_color ?? '#1a1a2e' }};
--site-secondary: {{ $settings->secondary_color ?? '#16213e' }};
--site-accent: {{ $settings->accent_color ?? '#e94560' }};
--site-text: {{ $settings->text_color ?? '#ffffff' }};
--site-heading-font: '{{ $settings->heading_font ?? 'Cairo' }}', 'Noto Sans Arabic', sans-serif;
--site-body-font: '{{ $settings->body_font ?? 'Cairo' }}', 'Noto Sans Arabic', sans-serif;
}
.website-body { font-family: var(--site-body-font); }
.hero-title, .section-title, .stat-number { font-family: var(--site-heading-font); }
</style>
@if($settings->custom_css)
<style>{!! $settings->custom_css !!}</style>
@endif
{{-- Structured Data --}}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SportsActivityLocation",
"name": "{{ $academy->name_ar }}",
"description": "{{ $settings->site_description ?? '' }}",
"url": "{{ url()->current() }}",
@if($academy->logo_path)
"image": "{{ asset('storage/' . $academy->logo_path) }}",
@endif
"address": {
"@type": "PostalAddress",
"addressCountry": "EG"
}
}
</script>
@if($settings->google_analytics_id)
<script async src="https://www.googletagmanager.com/gtag/js?id={{ $settings->google_analytics_id }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ $settings->google_analytics_id }}');
</script>
@endif
</head>
<body class="website-body">
{{-- Navbar --}}
@include('website.navbar')
{{-- Main Content --}}
<main>
{{ $slot ?? '' }}
@yield('content')
</main>
{{-- Footer --}}
@include('website.footer')
{{-- WhatsApp Float --}}
@if($settings->whatsapp_number)
<a href="https://wa.me/{{ preg_replace('/[^0-9]/', '', $settings->whatsapp_number) }}"
target="_blank"
rel="noopener"
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)
<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>
@endisset
</body>
</html>
<nav class="site-nav {{ $settings->navbar_style === 'solid' ? 'site-nav--solid' : '' }}" id="site-navbar">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-20">
{{-- Logo + Name --}}
<a href="#" class="flex items-center gap-3 group">
@if($academy->logo_path)
<img src="{{ asset('storage/' . $academy->logo_path) }}"
alt="{{ $academy->name_ar }}"
class="h-10 w-10 rounded-lg object-contain transition-transform group-hover:scale-110">
@endif
<span class="text-lg font-bold text-white">{{ $academy->name_ar }}</span>
</a>
{{-- Desktop Navigation --}}
<div class="hidden lg:flex items-center gap-1">
@foreach($sections->take(7) as $section)
<a href="#section-{{ $section->section_key->value }}"
class="px-3 py-2 text-sm font-medium text-white/80 hover:text-white rounded-lg hover:bg-white/5 transition-all">
{{ $section->title ?? $section->section_key->label() }}
</a>
@endforeach
</div>
{{-- CTA + Mobile Toggle --}}
<div class="flex items-center gap-3">
@php
$heroSection = $sections->firstWhere('section_key', \App\Domain\Website\Enums\SectionKey::Cta);
$ctaText = $heroSection?->getSetting('cta_text') ?? 'سجّل الآن';
@endphp
<a href="#section-contact" class="hidden sm:inline-flex site-btn site-btn--primary text-sm !py-2 !px-4">
{{ $ctaText }}
</a>
{{-- Mobile Menu Button --}}
<button type="button"
class="lg:hidden p-2 rounded-lg text-white/80 hover:text-white hover:bg-white/10 transition-colors"
onclick="document.getElementById('mobile-menu').classList.toggle('hidden')"
aria-label="القائمة">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
</div>
</div>
</div>
{{-- Mobile Menu --}}
<div id="mobile-menu" class="hidden lg:hidden bg-black/95 backdrop-blur-lg border-t border-white/10">
<div class="px-4 py-4 space-y-1">
@foreach($sections as $section)
<a href="#section-{{ $section->section_key->value }}"
class="block px-4 py-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors"
onclick="document.getElementById('mobile-menu').classList.add('hidden')">
{{ $section->title ?? $section->section_key->label() }}
</a>
@endforeach
</div>
</div>
</nav>
@php
$sectionImage = \App\Domain\Website\Models\Media::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('collection', 'section_image')
->where('mediable_type', 'website_section')
->where('mediable_id', $section->id)
->first();
@endphp
<div class="site-section">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
{{-- Text Content --}}
<div class="reveal">
<h2 class="section-title">{{ $section->title ?? 'من نحن' }}</h2>
@if($section->content)
<div class="mt-8 text-white/70 leading-relaxed text-lg space-y-4">
{!! nl2br(e($section->content)) !!}
</div>
@endif
@if($section->getSetting('show_stats', true))
<div class="mt-8 flex items-center gap-8">
@foreach(($data['stats'] ?? []) as $key => $value)
@if($loop->index < 3 && $value > 0)
<div class="text-center">
<span class="block text-2xl font-bold text-[var(--site-accent)]">{{ $value }}+</span>
<span class="text-sm text-white/50">
{{ match($key) { 'participants' => 'طالب', 'trainers' => 'مدرب', 'branches' => 'فرع', 'programs' => 'برنامج', default => $key } }}
</span>
</div>
@endif
@endforeach
</div>
@endif
</div>
{{-- Image --}}
<div class="reveal reveal--delay-2">
@if($sectionImage)
<div class="relative">
<img src="{{ $sectionImage->url }}"
alt="{{ $section->title }}"
class="rounded-2xl w-full object-cover shadow-2xl">
{{-- Decorative accent corner --}}
<div class="absolute -bottom-4 -start-4 w-24 h-24 border-b-4 border-s-4 border-[var(--site-accent)] rounded-bl-2xl"></div>
</div>
@else
<div class="relative rounded-2xl overflow-hidden aspect-[4/3] bg-gradient-to-br from-[var(--site-accent)]/20 to-transparent border border-white/10 flex items-center justify-center">
<svg class="w-20 h-20 text-white/20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg>
</div>
@endif
</div>
</div>
</div>
</div>
@php
$activities = $data['activities'] ?? collect();
$maxItems = $section->getSetting('max_items', 6);
@endphp
@if($activities->count())
<div class="site-section site-section--dark site-section--pattern">
<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-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($activities->take($maxItems) as $activity)
@php
$activityImage = \App\Domain\Website\Models\Media::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('collection', 'activity_photo')
->where('mediable_type', 'activity')
->where('mediable_id', $activity->id)
->first();
@endphp
<div class="site-card reveal reveal--delay-{{ ($loop->index % 4) + 1 }}">
<div class="relative overflow-hidden aspect-[4/3]">
@if($activityImage)
<img src="{{ $activityImage->url }}"
alt="{{ $activity->name_ar }}"
class="site-card__image">
@else
<div class="w-full h-full bg-gradient-to-br from-[var(--site-accent)]/30 to-[var(--site-secondary)] flex items-center justify-center">
@if($activity->icon)
<span class="text-5xl">{{ $activity->icon }}</span>
@else
<svg class="w-16 h-16 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
@endif
</div>
@endif
<div class="site-card__overlay"></div>
{{-- Activity Name Overlay --}}
<div class="absolute bottom-0 inset-x-0 p-4 bg-gradient-to-t from-black/80 to-transparent">
<h3 class="text-white font-bold text-lg">{{ $activity->name_ar }}</h3>
@if($activity->description_ar)
<p class="text-white/70 text-sm mt-1 line-clamp-2">{{ $activity->description_ar }}</p>
@endif
</div>
</div>
@if($activity->color)
<div class="h-1 w-full" style="background: {{ $activity->color }}"></div>
@else
<div class="h-1 w-full bg-[var(--site-accent)]"></div>
@endif
</div>
@endforeach
</div>
</div>
</div>
@endif
@php
$branches = $data['branches'] ?? collect();
@endphp
@if($branches->count())
<div class="site-section site-section--dark">
<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-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
{{-- Map --}}
@if($section->getSetting('show_map', true) && $branches->where('latitude', '!=', null)->count())
<div class="map-container mb-10 reveal" id="branches-map"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof L === 'undefined') return;
const map = L.map('branches-map').setView([{{ $branches->first()->latitude ?? 30.0444 }}, {{ $branches->first()->longitude ?? 31.2357 }}], 11);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap'
}).addTo(map);
@foreach($branches->where('latitude', '!=', null) as $branch)
L.marker([{{ $branch->latitude }}, {{ $branch->longitude }}])
.addTo(map)
.bindPopup('<strong>{{ $branch->name_ar }}</strong><br>{{ $branch->address }}');
@endforeach
});
</script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
@endif
{{-- Branch Cards --}}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($branches as $branch)
@php
$branchImage = \App\Domain\Website\Models\Media::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('collection', 'branch_photo')
->where('mediable_type', 'branch')
->where('mediable_id', $branch->id)
->first();
@endphp
<div class="site-card reveal reveal--delay-{{ ($loop->index % 3) + 1 }}">
@if($branchImage)
<div class="relative overflow-hidden aspect-[3/2]">
<img src="{{ $branchImage->url }}"
alt="{{ $branch->name_ar }}"
class="site-card__image">
</div>
@endif
<div class="p-5">
<div class="flex items-start justify-between">
<h3 class="text-lg font-bold text-white">{{ $branch->name_ar }}</h3>
@if($branch->is_main)
<span class="px-2 py-0.5 text-xs rounded-full bg-[var(--site-accent)]/10 text-[var(--site-accent)] border border-[var(--site-accent)]/20">
رئيسي
</span>
@endif
</div>
@if($branch->address)
<p class="mt-2 text-white/50 text-sm flex items-start gap-2">
<svg class="w-4 h-4 mt-0.5 shrink-0 text-[var(--site-accent)]" 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"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
{{ $branch->address }}
</p>
@endif
@if($branch->phone)
<p class="mt-2 text-white/50 text-sm flex items-center gap-2">
<svg class="w-4 h-4 shrink-0 text-[var(--site-accent)]" 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>
<span dir="ltr">{{ $branch->phone }}</span>
</p>
@endif
</div>
</div>
@endforeach
</div>
</div>
</div>
@endif
@php
$branches = $data['branches'] ?? collect();
@endphp
<div class="site-section site-section--dark" x-data="{
form: { name: '', phone: '', email: '', message: '' },
loading: false,
success: false,
errors: {},
async submit() {
this.loading = true;
this.errors = {};
try {
const res = await fetch('{{ route('website.contact.submit', $academy->slug) }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' },
body: JSON.stringify(this.form)
});
const data = await res.json();
if (res.ok) {
this.success = true;
this.form = { name: '', phone: '', email: '', message: '' };
} else if (data.errors) {
this.errors = data.errors;
}
} catch(e) { }
this.loading = false;
}
}">
<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-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
{{-- Contact Form --}}
<div class="reveal">
<div x-show="!success">
<form @submit.prevent="submit" class="space-y-4">
<div>
<input type="text" x-model="form.name" placeholder="الاسم الكامل *" class="site-input" required>
<template x-if="errors.name"><p class="text-red-400 text-sm mt-1" x-text="errors.name[0]"></p></template>
</div>
<div>
<input type="tel" x-model="form.phone" placeholder="رقم الهاتف *" class="site-input" dir="ltr" required>
<template x-if="errors.phone"><p class="text-red-400 text-sm mt-1" x-text="errors.phone[0]"></p></template>
</div>
<div>
<input type="email" x-model="form.email" placeholder="البريد الإلكتروني (اختياري)" class="site-input" dir="ltr">
<template x-if="errors.email"><p class="text-red-400 text-sm mt-1" x-text="errors.email[0]"></p></template>
</div>
<div>
<textarea x-model="form.message" placeholder="رسالتك *" rows="4" class="site-input resize-none" required></textarea>
<template x-if="errors.message"><p class="text-red-400 text-sm mt-1" x-text="errors.message[0]"></p></template>
</div>
<button type="submit" class="site-btn site-btn--primary w-full justify-center" :disabled="loading">
<span x-show="!loading">إرسال الرسالة</span>
<span x-show="loading" class="flex items-center gap-2">
<svg class="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
جارٍ الإرسال...
</span>
</button>
</form>
</div>
{{-- Success State --}}
<div x-show="success" x-transition class="text-center py-12">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-green-500/10 flex items-center justify-center">
<svg class="w-8 h-8 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<h3 class="text-xl font-bold text-white">تم إرسال رسالتك بنجاح!</h3>
<p class="text-white/60 mt-2">سنتواصل معك في أقرب وقت</p>
</div>
</div>
{{-- Contact Info --}}
<div class="reveal reveal--delay-2 space-y-6">
@foreach($branches->take(3) as $branch)
<div class="p-5 rounded-xl bg-white/5 border border-white/10">
<h4 class="font-bold text-white flex items-center gap-2">
<svg class="w-5 h-5 text-[var(--site-accent)]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
{{ $branch->name_ar }}
</h4>
@if($branch->address)
<p class="text-white/50 text-sm mt-2">{{ $branch->address }}</p>
@endif
@if($branch->phone)
<p class="text-white/50 text-sm mt-1" dir="ltr">{{ $branch->phone }}</p>
@endif
</div>
@endforeach
{{-- Social Links --}}
@if(is_array($settings->social_links) && count(array_filter($settings->social_links)))
<div class="p-5 rounded-xl bg-white/5 border border-white/10">
<h4 class="font-bold text-white mb-3">تابعنا</h4>
<div class="flex items-center gap-3">
@foreach(['facebook', 'instagram', 'twitter', 'youtube', 'tiktok'] as $platform)
@if(!empty($settings->social_links[$platform]))
<a href="{{ $settings->social_links[$platform] }}"
target="_blank"
rel="noopener"
class="w-10 h-10 rounded-lg bg-white/5 flex items-center justify-center text-white/60 hover:text-[var(--site-accent)] hover:bg-[var(--site-accent)]/10 transition-all">
@include("website.icons.{$platform}")
</a>
@endif
@endforeach
</div>
</div>
@endif
</div>
</div>
</div>
</div>
<div class="cta-section py-20 lg:py-24">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-white reveal">
{{ $section->title ?? 'ابدأ رحلتك الرياضية اليوم' }}
</h2>
@if($section->subtitle)
<p class="mt-4 text-xl text-white/80 reveal reveal--delay-1">
{{ $section->subtitle }}
</p>
@endif
<div class="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4 reveal reveal--delay-2">
<a href="{{ $section->getSetting('cta_link', '#section-contact') }}"
class="site-btn bg-white text-gray-900 font-bold hover:bg-white/90 hover:scale-105 transition-all shadow-xl">
{{ $section->getSetting('cta_text', 'سجّل الآن') }}
<svg class="w-5 h-5 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
</a>
@if($settings->whatsapp_number)
<a href="https://wa.me/{{ preg_replace('/[^0-9]/', '', $settings->whatsapp_number) }}"
target="_blank"
class="site-btn bg-white/10 text-white border border-white/20 hover:bg-white/20">
<svg class="w-5 h-5" 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>
</div>
</div>
@php
$faqs = $data['faqs'] ?? collect();
@endphp
@if($faqs->count())
<div class="site-section">
<div class="max-w-3xl 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-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
<div class="space-y-3" x-data="{ open: null }">
@foreach($faqs as $faq)
<div class="faq-item reveal reveal--delay-{{ min($loop->index + 1, 4) }}"
:data-open="open === {{ $faq->id }} ? 'true' : 'false'">
<button @click="open = open === {{ $faq->id }} ? null : {{ $faq->id }}"
class="w-full flex items-center justify-between p-5 text-start">
<span class="text-white font-medium pe-4">{{ $faq->question }}</span>
<svg class="w-5 h-5 text-[var(--site-accent)] shrink-0 transition-transform duration-300"
:class="open === {{ $faq->id }} ? 'rotate-180' : ''"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div x-show="open === {{ $faq->id }}"
x-collapse
class="px-5 pb-5">
<p class="text-white/60 leading-relaxed">{{ $faq->answer }}</p>
</div>
</div>
@endforeach
</div>
</div>
</div>
@endif
@php
$gallery = $data['gallery'] ?? collect();
@endphp
@if($gallery->count())
<div class="site-section site-section--dark" x-data="{ lightbox: null }">
<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-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
<div class="gallery-grid reveal">
@foreach($gallery as $image)
<div class="gallery-item" @click="lightbox = '{{ $image->url }}'">
<img src="{{ $image->url }}"
alt="{{ $image->alt_text_ar ?? 'صورة من المعرض' }}"
loading="lazy">
<div class="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity z-10">
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
</div>
</div>
@endforeach
</div>
</div>
{{-- Lightbox --}}
<div x-show="lightbox"
x-transition.opacity
@click.self="lightbox = null"
@keydown.escape.window="lightbox = null"
class="fixed inset-0 z-[200] bg-black/95 flex items-center justify-center p-4 cursor-pointer"
style="display: none;">
<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">
<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"/>
</svg>
</button>
</div>
</div>
@endif
@php
$coverMedia = \App\Domain\Website\Models\Media::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('collection', 'academy_cover')
->first();
$overlayOpacity = $section->getSetting('overlay_opacity', 70) / 100;
$ctaText = $section->getSetting('cta_text', 'سجّل الآن');
$ctaLink = $section->getSetting('cta_link', '#section-contact');
@endphp
<div class="hero-section hero-clip">
{{-- Background Image --}}
@if($coverMedia)
<img src="{{ $coverMedia->url }}"
alt="{{ $academy->name_ar }}"
class="absolute inset-0 w-full h-full object-cover">
@else
<div class="absolute inset-0 bg-gradient-to-br from-[var(--site-primary)] via-[var(--site-secondary)] to-black"></div>
@endif
{{-- Overlay --}}
<div class="hero-overlay" style="--overlay-opacity: {{ $overlayOpacity }}"></div>
{{-- Diagonal Decorative Lines --}}
<div class="absolute inset-0 z-[2] pointer-events-none opacity-10">
<div class="absolute top-[20%] -start-20 w-[400px] h-[2px] bg-[var(--site-accent)] rotate-[-15deg]"></div>
<div class="absolute top-[40%] -end-10 w-[300px] h-[2px] bg-[var(--site-accent)] rotate-[-15deg]"></div>
<div class="absolute bottom-[30%] -start-10 w-[250px] h-[2px] bg-white rotate-[-15deg]"></div>
</div>
{{-- Content --}}
<div class="hero-content max-w-4xl mx-auto">
<h1 class="hero-title reveal">
{{ $section->title ?? $academy->name_ar }}
</h1>
<div class="hero-accent-line"></div>
@if($section->subtitle)
<p class="text-xl md:text-2xl text-white/80 mt-4 reveal reveal--delay-1 max-w-2xl mx-auto leading-relaxed">
{{ $section->subtitle }}
</p>
@endif
<div class="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4 reveal reveal--delay-2">
<a href="{{ $ctaLink }}" class="site-btn site-btn--primary text-lg">
{{ $ctaText }}
<svg class="w-5 h-5 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
</a>
<a href="#section-about" class="site-btn site-btn--outline">
تعرف علينا
</a>
</div>
</div>
{{-- Scroll Indicator --}}
<div class="absolute bottom-10 left-1/2 -translate-x-1/2 z-10 animate-bounce">
<svg class="w-6 h-6 text-white/50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
</svg>
</div>
</div>
@php
$news = $data['news'] ?? collect();
@endphp
@if($news->count())
<div class="site-section site-section--dark">
<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-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($news as $article)
<article class="site-card reveal reveal--delay-{{ ($loop->index % 3) + 1 }}">
@if($article->image)
<div class="relative overflow-hidden aspect-[16/9]">
<img src="{{ $article->image->url }}"
alt="{{ $article->title }}"
class="site-card__image"
loading="lazy">
</div>
@endif
<div class="p-5">
@if($article->published_at)
<time class="text-xs text-white/40">
{{ $article->published_at->translatedFormat('j F Y') }}
</time>
@endif
<h3 class="text-lg font-bold text-white mt-2 line-clamp-2">
{{ $article->title }}
</h3>
@if($article->excerpt)
<p class="text-white/50 text-sm mt-2 line-clamp-3">
{{ $article->excerpt }}
</p>
@endif
</div>
</article>
@endforeach
</div>
</div>
</div>
@endif
@php
$partners = $data['partners'] ?? collect();
@endphp
@if($partners->count())
<div class="site-section overflow-hidden">
<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>
</div>
<div class="relative overflow-hidden reveal">
<div class="partners-track">
@for($i = 0; $i < 2; $i++)
@foreach($partners as $partner)
@if($partner->logo)
<div class="shrink-0 w-36 h-20 flex items-center justify-center grayscale hover:grayscale-0 opacity-60 hover:opacity-100 transition-all duration-300">
<img src="{{ $partner->logo->url }}"
alt="{{ $partner->name }}"
class="max-w-full max-h-full object-contain">
</div>
@else
<div class="shrink-0 px-6 py-3 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center">
<span class="text-white/40 text-sm font-medium">{{ $partner->name }}</span>
</div>
@endif
@endforeach
@endfor
</div>
</div>
</div>
</div>
@endif
@php
$plans = json_decode($section->getSetting('plans', '[]'), true) ?? [];
@endphp
@if(count($plans))
<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-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-5xl mx-auto">
@foreach($plans as $index => $plan)
@php $highlighted = !empty($plan['highlighted']); @endphp
<div class="relative rounded-2xl p-6 border transition-all reveal reveal--delay-{{ ($index % 3) + 1 }}
{{ $highlighted
? 'bg-[var(--site-accent)]/5 border-[var(--site-accent)] scale-105 shadow-2xl shadow-[var(--site-accent)]/10'
: 'bg-white/5 border-white/10 hover:border-white/20' }}">
@if($highlighted)
<div class="absolute -top-3 start-1/2 -translate-x-1/2 px-4 py-1 bg-[var(--site-accent)] text-white text-xs font-bold rounded-full">
الأكثر طلباً
</div>
@endif
<h3 class="text-xl font-bold text-white">{{ $plan['name'] ?? '' }}</h3>
<div class="mt-4 flex items-baseline gap-1">
<span class="text-3xl font-bold {{ $highlighted ? 'text-[var(--site-accent)]' : 'text-white' }}">
{{ $plan['price'] ?? '' }}
</span>
<span class="text-white/50 text-sm">{{ $plan['period'] ?? '/شهر' }}</span>
</div>
@if(!empty($plan['features']))
<ul class="mt-6 space-y-3">
@foreach($plan['features'] as $feature)
<li class="flex items-start gap-2 text-sm text-white/70">
<svg class="w-4 h-4 mt-0.5 text-[var(--site-accent)] shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
{{ $feature }}
</li>
@endforeach
</ul>
@endif
<a href="#section-contact"
class="mt-6 block text-center site-btn {{ $highlighted ? 'site-btn--primary' : 'site-btn--outline' }} w-full justify-center">
{{ $plan['cta'] ?? 'اشترك الآن' }}
</a>
</div>
@endforeach
</div>
</div>
</div>
@endif
@php
$programs = $data['programs'] ?? collect();
$maxItems = $section->getSetting('max_items', 6);
@endphp
@if($programs->count())
<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-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
<div class="space-y-8">
@foreach($programs->take($maxItems) as $program)
@php
$programImage = \App\Domain\Website\Models\Media::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('collection', 'section_image')
->where('mediable_type', 'training_program')
->where('mediable_id', $program->id)
->first();
$isReversed = $loop->index % 2 !== 0;
@endphp
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center reveal {{ $isReversed ? '' : '' }}">
{{-- Image --}}
<div class="{{ $isReversed ? 'lg:order-2' : '' }}">
@if($programImage)
<img src="{{ $programImage->url }}"
alt="{{ $program->name_ar }}"
class="rounded-xl w-full aspect-[16/10] object-cover border border-white/10">
@else
<div class="rounded-xl w-full aspect-[16/10] bg-gradient-to-br from-[var(--site-accent)]/10 to-[var(--site-secondary)] border border-white/10 flex items-center justify-center">
<svg class="w-16 h-16 text-white/20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="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"/>
</svg>
</div>
@endif
</div>
{{-- Content --}}
<div class="{{ $isReversed ? 'lg:order-1' : '' }}">
<h3 class="text-2xl font-bold text-white">{{ $program->name_ar }}</h3>
<div class="flex items-center gap-3 mt-3">
@if($program->age_from && $program->age_to)
<span class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium bg-[var(--site-accent)]/10 text-[var(--site-accent)] border border-[var(--site-accent)]/20">
{{ $program->age_from }} - {{ $program->age_to }} سنة
</span>
@endif
@if($program->duration_months)
<span class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium bg-white/5 text-white/70 border border-white/10">
{{ $program->duration_months }} شهر
</span>
@endif
</div>
@if($program->description_ar)
<p class="mt-4 text-white/60 leading-relaxed">
{{ Str::limit($program->description_ar, 200) }}
</p>
@endif
<a href="#section-contact" class="inline-flex items-center gap-2 mt-6 text-[var(--site-accent)] font-medium hover:gap-3 transition-all">
سجّل الآن
<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="M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
</a>
</div>
</div>
@if(!$loop->last)
<div class="h-px w-full bg-gradient-to-l from-transparent via-white/10 to-transparent"></div>
@endif
@endforeach
</div>
</div>
</div>
@endif
@php
$schedule = json_decode($section->getSetting('schedule_data', '[]'), true) ?? [];
$days = ['السبت', 'الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة'];
@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-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
@if(count($schedule))
<div class="overflow-x-auto reveal">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-white/10">
<th class="py-3 px-4 text-start text-white/50 font-medium">اليوم</th>
<th class="py-3 px-4 text-start text-white/50 font-medium">النشاط</th>
<th class="py-3 px-4 text-start text-white/50 font-medium">الوقت</th>
<th class="py-3 px-4 text-start text-white/50 font-medium">الفرع</th>
</tr>
</thead>
<tbody>
@foreach($schedule as $row)
<tr class="border-b border-white/5 hover:bg-white/5 transition-colors">
<td class="py-3 px-4 text-white font-medium">{{ $row['day'] ?? '' }}</td>
<td class="py-3 px-4 text-white/70">{{ $row['activity'] ?? '' }}</td>
<td class="py-3 px-4 text-white/70" dir="ltr">{{ $row['time'] ?? '' }}</td>
<td class="py-3 px-4 text-white/70">{{ $row['branch'] ?? '' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
@if($section->content)
<div class="text-center text-white/60 leading-relaxed reveal">
{!! nl2br(e($section->content)) !!}
</div>
@else
<div class="text-center text-white/40 py-8 reveal">
<p>تواصل معنا للاستفسار عن مواعيد التدريب</p>
</div>
@endif
@endif
</div>
</div>
@php
$stats = $data['stats'] ?? [];
$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'],
'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'],
'branches' => ['label' => 'فرع', 'icon' => 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4'],
'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'],
];
@endphp
@if(!empty($stats) && array_sum($stats) > 0)
<div class="stats-band py-16 lg:py-20">
<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>
</div>
@endif
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-8">
@foreach($displayStats as $key => $meta)
@if(($stats[$key] ?? 0) > 0)
<div class="text-center reveal reveal--delay-{{ $loop->index + 1 }}">
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="{{ $meta['icon'] }}"/>
</svg>
</div>
<span class="stat-number" data-counter="{{ $stats[$key] }}" data-suffix="+">0</span>
<span class="block mt-2 text-white/50 text-sm">{{ $meta['label'] }}</span>
</div>
@endif
@endforeach
</div>
</div>
</div>
@endif
@php
$testimonials = $data['testimonials'] ?? collect();
@endphp
@if($testimonials->count())
<div class="site-section" x-data="{ current: 0, total: {{ $testimonials->count() }} }" x-init="setInterval(() => current = (current + 1) % total, 5000)">
<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-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
<div class="max-w-3xl mx-auto relative reveal">
@foreach($testimonials as $testimonial)
<div class="testimonial-card transition-all duration-500"
x-show="current === {{ $loop->index }}"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
<p class="text-white/80 text-lg leading-relaxed italic relative z-10">
{{ $testimonial->content }}
</p>
<div class="mt-6 flex items-center gap-3">
@if($testimonial->avatar)
<img src="{{ $testimonial->avatar->url }}"
alt="{{ $testimonial->name }}"
class="w-12 h-12 rounded-full object-cover border-2 border-[var(--site-accent)]">
@else
<div class="w-12 h-12 rounded-full bg-[var(--site-accent)]/20 flex items-center justify-center">
<span class="text-[var(--site-accent)] font-bold">{{ mb_substr($testimonial->name, 0, 1) }}</span>
</div>
@endif
<div>
<span class="block text-white font-medium">{{ $testimonial->name }}</span>
@if($testimonial->role)
<span class="block text-white/40 text-sm">{{ $testimonial->role }}</span>
@endif
</div>
{{-- Stars --}}
<div class="ms-auto flex gap-0.5">
@for($i = 1; $i <= 5; $i++)
<svg class="w-4 h-4 {{ $i <= $testimonial->rating ? 'text-amber-400' : 'text-white/20' }}" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
@endfor
</div>
</div>
</div>
@endforeach
{{-- Dots Navigation --}}
<div class="flex items-center justify-center gap-2 mt-8">
@foreach($testimonials as $testimonial)
<button @click="current = {{ $loop->index }}"
class="w-2.5 h-2.5 rounded-full transition-all duration-300"
:class="current === {{ $loop->index }} ? 'bg-[var(--site-accent)] w-6' : 'bg-white/20 hover:bg-white/40'"
aria-label="شهادة {{ $loop->iteration }}">
</button>
@endforeach
</div>
</div>
</div>
</div>
@endif
@php
$trainers = json_decode($section->getSetting('trainers', '[]'), true) ?? [];
$teamPhotos = \App\Domain\Website\Models\Media::withoutGlobalScope('academy')
->where('academy_id', $academy->id)
->where('collection', 'team_photo')
->orderBy('sort_order')
->get();
@endphp
<div class="site-section site-section--dark site-section--pattern">
<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-white/60 text-lg">{{ $section->subtitle }}</p>
@endif
</div>
@if(count($trainers))
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
@foreach($trainers as $index => $trainer)
<div class="text-center reveal reveal--delay-{{ ($index % 4) + 1 }}">
<div class="relative mx-auto w-32 h-32 mb-4">
@php $photo = $teamPhotos->get($index); @endphp
@if($photo)
<img src="{{ $photo->url }}"
alt="{{ $trainer['name'] ?? '' }}"
class="w-full h-full rounded-full object-cover border-3 border-[var(--site-accent)] shadow-lg shadow-[var(--site-accent)]/10">
@else
<div class="w-full h-full rounded-full bg-gradient-to-br from-[var(--site-accent)]/20 to-[var(--site-secondary)] border-3 border-[var(--site-accent)]/40 flex items-center justify-center">
<svg class="w-12 h-12 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
@endif
</div>
<h4 class="text-white font-bold">{{ $trainer['name'] ?? '' }}</h4>
@if(!empty($trainer['role']))
<p class="text-[var(--site-accent)] text-sm mt-1">{{ $trainer['role'] }}</p>
@endif
@if(!empty($trainer['bio']))
<p class="text-white/50 text-sm mt-1">{{ $trainer['bio'] }}</p>
@endif
</div>
@endforeach
</div>
@else
<div class="text-center text-white/40 py-8">
<p>سيتم إضافة فريق المدربين قريباً</p>
</div>
@endif
</div>
</div>
......@@ -66,7 +66,9 @@
use App\Livewire\Inventory\WarehouseList as InventoryWarehouseList;
use App\Livewire\Wallets\WalletList;
use App\Livewire\Wallets\WalletShow;
use App\Http\Controllers\ContactFormController;
use App\Http\Controllers\PublicDocumentController;
use App\Http\Controllers\PublicWebsiteController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
......@@ -84,6 +86,15 @@
Route::get('/pos-receipt/{uuid}', [PublicDocumentController::class, 'posReceipt'])->name('pos-receipt');
});
// Public Academy Website (no auth, cached)
Route::middleware(\App\Http\Middleware\ResolveAcademyFromSlug::class)
->prefix('site/{slug}')
->name('website.')
->group(function () {
Route::get('/', [PublicWebsiteController::class, 'show'])->name('show');
Route::post('/contact', [ContactFormController::class, 'submit'])->name('contact.submit');
});
/*
|--------------------------------------------------------------------------
| Guest Routes
......@@ -480,6 +491,19 @@
Route::get('/guardian', GuardianDashboard::class)->name('guardian.dashboard')
->middleware('permission:dashboard.view');
// ─── Website CMS ─────────────────────────────────────────────
Route::prefix('website')->name('website.manage.')->middleware('permission:settings.manage')->group(function () {
Route::get('/', \App\Livewire\Website\SectionManager::class)->name('sections');
Route::get('/theme', \App\Livewire\Website\ThemeEditor::class)->name('theme');
Route::get('/preview', [PublicWebsiteController::class, 'preview'])->name('preview');
Route::get('/testimonials', \App\Livewire\Website\TestimonialManager::class)->name('testimonials');
Route::get('/faqs', \App\Livewire\Website\FaqManager::class)->name('faqs');
Route::get('/news', \App\Livewire\Website\NewsManager::class)->name('news');
Route::get('/partners', \App\Livewire\Website\PartnerManager::class)->name('partners');
Route::get('/gallery', \App\Livewire\Website\GalleryManager::class)->name('gallery');
Route::get('/contacts', \App\Livewire\Website\ContactSubmissionList::class)->name('contacts');
});
// ─── Parents Portal ─────────────────────────────────────────
Route::prefix('parent')->name('parent.')->group(function () {
Route::get('/', \App\Livewire\Parent\ParentHome::class)->name('home');
......
......@@ -6,7 +6,12 @@ import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
input: [
'resources/css/app.css',
'resources/js/app.js',
'resources/css/website.css',
'resources/js/website.js',
],
refresh: true,
fonts: [
bunny('Instrument Sans', {
......
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