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');
}
};
This diff is collapsed.
This diff is collapsed.
/**
* 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>
This diff is collapsed.
<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>
This diff is collapsed.
<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>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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