Commit 379cefca authored by Mahmoud Aglan's avatar Mahmoud Aglan

Website Builder V2 Phase A: theme engine, colors, typography, spacing, shapes

- Add 8 template presets with one-click color scheme application
- Expand color palette from 4 to 8 (add background, surface, border, muted)
- Add auto-palette generation from accent color (dark/light detection)
- Add typography controls: font size scale, heading weight, line height, letter spacing
- Add density control (compact/comfortable/spacious) affecting section padding and card gaps
- Add border radius presets (sharp/subtle/rounded/pill)
- Add card styles (flat/raised/bordered/glass)
- Add section dividers (none/line/wave/angle/curve) rendered as SVG between sections
- Add image shape control (square/rounded/circle/blob)
- Add animation toggle and speed control
- Add per-section: background type (solid/gradient/image/video), custom animation, padding override, mobile visibility toggle
- All new columns have defaults — existing sites render unchanged until admin modifies settings
- CSS variables drive all theming; public layout emits full token set
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4343122d
...@@ -22,6 +22,17 @@ class WebsiteSection extends Model ...@@ -22,6 +22,17 @@ class WebsiteSection extends Model
'settings', 'settings',
'sort_order', 'sort_order',
'is_enabled', 'is_enabled',
'bg_type',
'bg_color',
'bg_gradient_from',
'bg_gradient_to',
'bg_gradient_angle',
'bg_image_path',
'bg_overlay_opacity',
'bg_video_url',
'animation',
'padding_y',
'hidden_on_mobile',
]; ];
protected $casts = [ protected $casts = [
...@@ -29,6 +40,9 @@ class WebsiteSection extends Model ...@@ -29,6 +40,9 @@ class WebsiteSection extends Model
'settings' => 'array', 'settings' => 'array',
'sort_order' => 'integer', 'sort_order' => 'integer',
'is_enabled' => 'boolean', 'is_enabled' => 'boolean',
'bg_gradient_angle' => 'integer',
'bg_overlay_opacity' => 'integer',
'hidden_on_mobile' => 'boolean',
]; ];
public function getSetting(string $key, $default = null) public function getSetting(string $key, $default = null)
......
...@@ -16,8 +16,23 @@ class WebsiteSetting extends Model ...@@ -16,8 +16,23 @@ class WebsiteSetting extends Model
'secondary_color', 'secondary_color',
'accent_color', 'accent_color',
'text_color', 'text_color',
'background_color',
'surface_color',
'border_color',
'muted_text_color',
'heading_font', 'heading_font',
'body_font', 'body_font',
'font_size_scale',
'heading_weight',
'line_height',
'letter_spacing',
'density',
'border_radius_preset',
'card_style',
'section_divider',
'image_shape',
'animations_enabled',
'animation_speed',
'navbar_style', 'navbar_style',
'site_title', 'site_title',
'site_title_en', 'site_title_en',
...@@ -35,6 +50,7 @@ class WebsiteSetting extends Model ...@@ -35,6 +50,7 @@ class WebsiteSetting extends Model
protected $casts = [ protected $casts = [
'social_links' => 'array', 'social_links' => 'array',
'is_published' => 'boolean', 'is_published' => 'boolean',
'animations_enabled' => 'boolean',
'published_at' => 'datetime', 'published_at' => 'datetime',
]; ];
} }
This diff is collapsed.
<?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
{
// --- website_settings: new columns for Phase A (template, colors, typography, density, shapes) ---
Schema::table('website_settings', function (Blueprint $table) {
$table->string('background_color', 7)->default('#ffffff')->after('text_color');
$table->string('surface_color', 7)->default('#f8fafc')->after('background_color');
$table->string('border_color', 7)->default('#e2e8f0')->after('surface_color');
$table->string('muted_text_color', 7)->default('#64748b')->after('border_color');
$table->string('font_size_scale', 10)->default('medium')->after('body_font');
$table->string('heading_weight', 10)->default('700')->after('font_size_scale');
$table->string('line_height', 10)->default('normal')->after('heading_weight');
$table->string('letter_spacing', 10)->default('normal')->after('line_height');
$table->string('density', 10)->default('comfortable')->after('letter_spacing');
$table->string('border_radius_preset', 10)->default('rounded')->after('density');
$table->string('card_style', 10)->default('raised')->after('border_radius_preset');
$table->string('section_divider', 10)->default('none')->after('card_style');
$table->string('image_shape', 10)->default('rounded')->after('section_divider');
$table->boolean('animations_enabled')->default(true)->after('image_shape');
$table->string('animation_speed', 10)->default('normal')->after('animations_enabled');
});
// Update template CHECK constraint to add new templates
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_template_check");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_template_check CHECK (template IN ('bold_athletic', 'clean_professional', 'vibrant_playful', 'dark_sport', 'minimal_corporate', 'gradient_modern', 'neon_energy', 'earth_natural'))");
// Add CHECK constraints for new columns
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_font_size_scale_check CHECK (font_size_scale IN ('small', 'medium', 'large', 'xlarge'))");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_heading_weight_check CHECK (heading_weight IN ('400', '500', '600', '700', '800', '900'))");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_line_height_check CHECK (line_height IN ('tight', 'normal', 'relaxed', 'loose'))");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_letter_spacing_check CHECK (letter_spacing IN ('tight', 'normal', 'wide'))");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_density_check CHECK (density IN ('compact', 'comfortable', 'spacious'))");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_border_radius_preset_check CHECK (border_radius_preset IN ('sharp', 'subtle', 'rounded', 'pill'))");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_card_style_check CHECK (card_style IN ('flat', 'raised', 'bordered', 'glass'))");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_section_divider_check CHECK (section_divider IN ('none', 'line', 'wave', 'angle', 'curve'))");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_image_shape_check CHECK (image_shape IN ('square', 'rounded', 'circle', 'blob'))");
DB::statement("ALTER TABLE website_settings ADD CONSTRAINT website_settings_animation_speed_check CHECK (animation_speed IN ('slow', 'normal', 'fast'))");
// --- website_sections: per-section background + animation + padding ---
Schema::table('website_sections', function (Blueprint $table) {
$table->string('bg_type', 20)->default('inherit')->after('is_enabled');
$table->string('bg_color', 7)->nullable()->after('bg_type');
$table->string('bg_gradient_from', 7)->nullable()->after('bg_color');
$table->string('bg_gradient_to', 7)->nullable()->after('bg_gradient_from');
$table->smallInteger('bg_gradient_angle')->default(180)->after('bg_gradient_to');
$table->string('bg_image_path', 500)->nullable()->after('bg_gradient_angle');
$table->smallInteger('bg_overlay_opacity')->default(60)->after('bg_image_path');
$table->string('bg_video_url', 500)->nullable()->after('bg_overlay_opacity');
$table->string('animation', 20)->default('fade-up')->after('bg_video_url');
$table->string('padding_y', 10)->default('inherit')->after('animation');
$table->boolean('hidden_on_mobile')->default(false)->after('padding_y');
});
DB::statement("ALTER TABLE website_sections ADD CONSTRAINT website_sections_bg_type_check CHECK (bg_type IN ('inherit', 'solid', 'gradient', 'image', 'video'))");
DB::statement("ALTER TABLE website_sections ADD CONSTRAINT website_sections_animation_check CHECK (animation IN ('none', 'fade-up', 'fade-down', 'fade-left', 'fade-right', 'zoom-in', 'flip', 'bounce'))");
DB::statement("ALTER TABLE website_sections ADD CONSTRAINT website_sections_padding_y_check CHECK (padding_y IN ('inherit', 'none', 'small', 'medium', 'large', 'xlarge'))");
}
public function down(): void
{
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_font_size_scale_check");
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_heading_weight_check");
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_line_height_check");
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_letter_spacing_check");
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_density_check");
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_border_radius_preset_check");
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_card_style_check");
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_section_divider_check");
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_image_shape_check");
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_animation_speed_check");
Schema::table('website_settings', function (Blueprint $table) {
$table->dropColumn([
'background_color', 'surface_color', 'border_color', 'muted_text_color',
'font_size_scale', 'heading_weight', 'line_height', 'letter_spacing',
'density', 'border_radius_preset', 'card_style', 'section_divider', 'image_shape',
'animations_enabled', 'animation_speed',
]);
});
DB::statement("ALTER TABLE website_settings DROP CONSTRAINT IF EXISTS website_settings_template_check");
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_sections DROP CONSTRAINT IF EXISTS website_sections_bg_type_check");
DB::statement("ALTER TABLE website_sections DROP CONSTRAINT IF EXISTS website_sections_animation_check");
DB::statement("ALTER TABLE website_sections DROP CONSTRAINT IF EXISTS website_sections_padding_y_check");
Schema::table('website_sections', function (Blueprint $table) {
$table->dropColumn([
'bg_type', 'bg_color', 'bg_gradient_from', 'bg_gradient_to',
'bg_gradient_angle', 'bg_image_path', 'bg_overlay_opacity', 'bg_video_url',
'animation', 'padding_y', 'hidden_on_mobile',
]);
});
}
};
...@@ -2,13 +2,35 @@ ...@@ -2,13 +2,35 @@
@section('content') @section('content')
@foreach($sections as $section) @foreach($sections as $section)
<section id="section-{{ $section->section_key->value }}"> @php
@include('website.sections.' . $section->section_key->value, [ $paddingClass = match($section->padding_y) {
'section' => $section, 'none' => 'py-0',
'academy' => $academy, 'small' => 'py-8',
'settings' => $settings, 'medium' => 'py-16',
'data' => $data, 'large' => 'py-24',
]) 'xlarge' => 'py-40',
default => '',
};
$mobileHidden = $section->hidden_on_mobile ? 'hidden md:block' : '';
$animAttr = ($settings->animations_enabled ?? true) ? $section->animation ?? 'fade-up' : 'none';
@endphp
<section id="section-{{ $section->section_key->value }}"
class="{{ $mobileHidden }} {{ $paddingClass }}"
@if($animAttr !== 'none') data-animation="{{ $animAttr }}" @endif>
@if($section->bg_type !== 'inherit')
@include('website.partials.section-bg', ['section' => $section])
@else
@include('website.sections.' . $section->section_key->value, [
'section' => $section,
'academy' => $academy,
'settings' => $settings,
'data' => $data,
])
@endif
</section> </section>
@if(($settings->section_divider ?? 'none') !== 'none' && !$loop->last)
@include('website.partials.divider', ['settings' => $settings])
@endif
@endforeach @endforeach
@endsection @endsection
...@@ -27,6 +27,26 @@ ...@@ -27,6 +27,26 @@
@livewireStyles @livewireStyles
{{-- Dynamic CSS Variables from Academy Branding --}} {{-- Dynamic CSS Variables from Academy Branding --}}
@php
$fontSizeMap = ['small' => '14px', 'medium' => '16px', 'large' => '18px', 'xlarge' => '20px'];
$h1Map = ['small' => '2rem', 'medium' => '2.5rem', 'large' => '3rem', 'xlarge' => '3.5rem'];
$h2Map = ['small' => '1.6rem', 'medium' => '2rem', 'large' => '2.4rem', 'xlarge' => '2.8rem'];
$h3Map = ['small' => '1.3rem', 'medium' => '1.5rem', 'large' => '1.75rem', 'xlarge' => '2rem'];
$lineHeightMap = ['tight' => '1.3', 'normal' => '1.6', 'relaxed' => '1.8', 'loose' => '2'];
$letterSpacingMap = ['tight' => '-0.02em', 'normal' => '0', 'wide' => '0.05em'];
$sectionPyMap = ['compact' => '3rem', 'comfortable' => '5rem', 'spacious' => '8rem'];
$cardGapMap = ['compact' => '1rem', 'comfortable' => '1.5rem', 'spacious' => '2.5rem'];
$radiusMap = ['sharp' => '0px', 'subtle' => '4px', 'rounded' => '12px', 'pill' => '9999px'];
$radiusLgMap = ['sharp' => '0px', 'subtle' => '8px', 'rounded' => '20px', 'pill' => '9999px'];
$animDurationMap = ['slow' => '1.2s', 'normal' => '0.8s', 'fast' => '0.4s'];
$fss = $settings->font_size_scale ?? 'medium';
$lh = $settings->line_height ?? 'normal';
$ls = $settings->letter_spacing ?? 'normal';
$dn = $settings->density ?? 'comfortable';
$br = $settings->border_radius_preset ?? 'rounded';
$as = $settings->animation_speed ?? 'normal';
@endphp
<style> <style>
:root { :root {
--site-primary: {{ $settings->primary_color ?? '#1e3a5f' }}; --site-primary: {{ $settings->primary_color ?? '#1e3a5f' }};
...@@ -34,11 +54,57 @@ ...@@ -34,11 +54,57 @@
--site-accent: {{ $settings->accent_color ?? '#2563eb' }}; --site-accent: {{ $settings->accent_color ?? '#2563eb' }};
--site-text: {{ $settings->text_color ?? '#1f2937' }}; --site-text: {{ $settings->text_color ?? '#1f2937' }};
--site-bg: {{ $settings->background_color ?? '#ffffff' }}; --site-bg: {{ $settings->background_color ?? '#ffffff' }};
--site-surface: {{ $settings->surface_color ?? '#f8fafc' }};
--site-border: {{ $settings->border_color ?? '#e2e8f0' }};
--site-muted: {{ $settings->muted_text_color ?? '#64748b' }};
--site-heading-font: '{{ $settings->heading_font ?? 'Cairo' }}', 'Noto Sans Arabic', sans-serif; --site-heading-font: '{{ $settings->heading_font ?? 'Cairo' }}', 'Noto Sans Arabic', sans-serif;
--site-body-font: '{{ $settings->body_font ?? 'Cairo' }}', 'Noto Sans Arabic', sans-serif; --site-body-font: '{{ $settings->body_font ?? 'Cairo' }}', 'Noto Sans Arabic', sans-serif;
--site-text-base: {{ $fontSizeMap[$fss] }};
--site-h1: {{ $h1Map[$fss] }};
--site-h2: {{ $h2Map[$fss] }};
--site-h3: {{ $h3Map[$fss] }};
--site-heading-weight: {{ $settings->heading_weight ?? '700' }};
--site-line-height: {{ $lineHeightMap[$lh] }};
--site-letter-spacing: {{ $letterSpacingMap[$ls] }};
--section-py: {{ $sectionPyMap[$dn] }};
--card-gap: {{ $cardGapMap[$dn] }};
--site-radius: {{ $radiusMap[$br] }};
--site-radius-lg: {{ $radiusLgMap[$br] }};
--anim-duration: {{ $animDurationMap[$as] }};
}
.website-body {
font-family: var(--site-body-font);
font-size: var(--site-text-base);
line-height: var(--site-line-height);
letter-spacing: var(--site-letter-spacing);
background-color: var(--site-bg);
color: var(--site-text);
}
.hero-title, .section-title, .stat-number {
font-family: var(--site-heading-font);
font-weight: var(--site-heading-weight);
}
/* Card Styles */
.site-card {
border-radius: var(--site-radius-lg);
{{ match($settings->card_style ?? 'raised') {
'flat' => 'box-shadow: none; border: none; background: transparent;',
'raised' => 'box-shadow: 0 4px 20px rgba(0,0,0,0.15);',
'bordered' => 'box-shadow: none; border: 1px solid var(--site-border);',
'glass' => 'box-shadow: 0 8px 32px rgba(0,0,0,0.1); border: 1px solid rgba(255,255,255,0.1); backdrop-filter: blur(10px); background: rgba(255,255,255,0.05);',
default => '',
} }}
}
/* Image Shapes */
.site-img {
{{ match($settings->image_shape ?? 'rounded') {
'square' => 'border-radius: 0;',
'rounded' => 'border-radius: var(--site-radius-lg);',
'circle' => 'border-radius: 50%;',
'blob' => 'border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;',
default => '',
} }}
} }
.website-body { font-family: var(--site-body-font); }
.hero-title, .section-title, .stat-number { font-family: var(--site-heading-font); }
</style> </style>
@if($settings->custom_css) @if($settings->custom_css)
...@@ -71,7 +137,7 @@ function gtag(){dataLayer.push(arguments);} ...@@ -71,7 +137,7 @@ function gtag(){dataLayer.push(arguments);}
</script> </script>
@endif @endif
</head> </head>
<body class="website-body"> <body class="website-body" data-template="{{ $settings->template ?? 'bold_athletic' }}" data-density="{{ $settings->density ?? 'comfortable' }}" data-radius="{{ $settings->border_radius_preset ?? 'rounded' }}" data-card="{{ $settings->card_style ?? 'raised' }}" data-animations="{{ ($settings->animations_enabled ?? true) ? 'true' : 'false' }}" data-anim-speed="{{ $settings->animation_speed ?? 'normal' }}">
{{-- Navbar --}} {{-- Navbar --}}
@include('website.navbar') @include('website.navbar')
......
@switch($settings->section_divider)
@case('line')
<div class="w-full px-8">
<div class="border-t" style="border-color: var(--site-border)"></div>
</div>
@break
@case('wave')
<div class="w-full overflow-hidden" style="color: var(--site-bg)">
<svg viewBox="0 0 1200 60" preserveAspectRatio="none" class="w-full h-10 block">
<path d="M0,30 C200,60 400,0 600,30 C800,60 1000,0 1200,30 L1200,60 L0,60 Z" fill="currentColor"/>
</svg>
</div>
@break
@case('angle')
<div class="w-full overflow-hidden" style="color: var(--site-bg)">
<svg viewBox="0 0 1200 60" preserveAspectRatio="none" class="w-full h-10 block">
<polygon points="0,60 1200,0 1200,60" fill="currentColor"/>
</svg>
</div>
@break
@case('curve')
<div class="w-full overflow-hidden" style="color: var(--site-bg)">
<svg viewBox="0 0 1200 60" preserveAspectRatio="none" class="w-full h-10 block">
<path d="M0,60 Q600,0 1200,60 Z" fill="currentColor"/>
</svg>
</div>
@break
@endswitch
@php
$bgStyle = match($section->bg_type) {
'solid' => "background-color: {$section->bg_color};",
'gradient' => "background: linear-gradient({$section->bg_gradient_angle}deg, {$section->bg_gradient_from}, {$section->bg_gradient_to});",
'image' => $section->bg_image_path ? "background-image: url('" . Storage::disk('public')->url($section->bg_image_path) . "'); background-size: cover; background-position: center;" : '',
default => '',
};
@endphp
<div class="relative" style="{{ $bgStyle }}">
@if($section->bg_type === 'image' && $section->bg_overlay_opacity > 0)
<div class="absolute inset-0 bg-black" style="opacity: {{ $section->bg_overlay_opacity / 100 }}"></div>
@endif
@if($section->bg_type === 'video' && $section->bg_video_url)
<video autoplay muted loop playsinline class="absolute inset-0 w-full h-full object-cover">
<source src="{{ $section->bg_video_url }}" type="video/mp4">
</video>
<div class="absolute inset-0 bg-black" style="opacity: {{ $section->bg_overlay_opacity / 100 }}"></div>
@endif
<div class="relative z-10">
@include('website.sections.' . $section->section_key->value, [
'section' => $section,
'academy' => $academy,
'settings' => $settings,
'data' => $data,
])
</div>
</div>
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