This commit is contained in:
gpt-engineer-app[bot]
2025-12-19 15:17:47 +00:00
parent f57bba6f9c
commit 7fc10126df
11 changed files with 979 additions and 88 deletions

View File

@@ -2,6 +2,7 @@ import { ReactNode, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { useCart } from '@/contexts/CartContext';
import { useBranding } from '@/hooks/useBranding';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { cn } from '@/lib/utils';
@@ -69,6 +70,7 @@ interface AppLayoutProps {
export function AppLayout({ children }: AppLayoutProps) {
const { user, isAdmin, signOut } = useAuth();
const { items } = useCart();
const branding = useBranding();
const location = useLocation();
const navigate = useNavigate();
const [moreOpen, setMoreOpen] = useState(false);
@@ -93,13 +95,22 @@ export function AppLayout({ children }: AppLayoutProps) {
// Get additional items for "More" menu
const moreItems = navItems.filter(item => !mobileNav.some(m => m.href === item.href));
const brandName = branding.brand_name || 'LearnHub';
const logoUrl = branding.brand_logo_url;
if (!user) {
// Public layout for non-authenticated pages
return (
<div className="min-h-screen bg-background">
<header className="border-b-2 border-border bg-background sticky top-0 z-50">
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
<Link to="/" className="text-2xl font-bold">LearnHub</Link>
<Link to="/" className="text-2xl font-bold flex items-center gap-2">
{logoUrl ? (
<img src={logoUrl} alt={brandName} className="h-8 object-contain" />
) : (
brandName
)}
</Link>
<nav className="flex items-center gap-4">
<Link to="/products" className="hover:underline font-medium">Produk</Link>
<Link to="/events" className="hover:underline font-medium">Kalender</Link>
@@ -132,7 +143,13 @@ export function AppLayout({ children }: AppLayoutProps) {
{/* Desktop Sidebar */}
<aside className="hidden md:flex flex-col w-64 border-r-2 border-border bg-sidebar fixed h-screen">
<div className="p-4 border-b-2 border-border">
<Link to="/" className="text-xl font-bold">LearnHub</Link>
<Link to="/" className="text-xl font-bold flex items-center gap-2">
{logoUrl ? (
<img src={logoUrl} alt={brandName} className="h-8 object-contain" />
) : (
brandName
)}
</Link>
</div>
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
@@ -179,7 +196,13 @@ export function AppLayout({ children }: AppLayoutProps) {
<div className="flex-1 md:ml-64">
{/* Mobile Header */}
<header className="md:hidden sticky top-0 z-50 border-b-2 border-border bg-background px-4 py-3 flex items-center justify-between">
<Link to="/" className="text-xl font-bold">LearnHub</Link>
<Link to="/" className="text-xl font-bold flex items-center gap-2">
{logoUrl ? (
<img src={logoUrl} alt={brandName} className="h-6 object-contain" />
) : (
brandName
)}
</Link>
<div className="flex items-center gap-2">
<Link to="/checkout" className="relative p-2">
<ShoppingCart className="w-5 h-5" />

View File

@@ -4,8 +4,15 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { toast } from '@/hooks/use-toast';
import { Palette, Image, Mail } from 'lucide-react';
import { Palette, Image, Mail, Home, Plus, Trash2 } from 'lucide-react';
interface HomepageFeature {
icon: string;
title: string;
description: string;
}
interface PlatformSettings {
id?: string;
@@ -16,8 +23,17 @@ interface PlatformSettings {
brand_primary_color: string;
brand_accent_color: string;
brand_email_from_name: string;
homepage_headline: string;
homepage_description: string;
homepage_features: HomepageFeature[];
}
const defaultFeatures: HomepageFeature[] = [
{ icon: 'Users', title: 'Consulting', description: 'One-on-one sessions with industry experts to solve your specific challenges.' },
{ icon: 'Video', title: 'Webinars', description: 'Live and recorded sessions covering the latest trends and techniques.' },
{ icon: 'BookOpen', title: 'Bootcamps', description: 'Intensive programs to master new skills in weeks, not months.' },
];
const emptySettings: PlatformSettings = {
brand_name: '',
brand_tagline: '',
@@ -26,8 +42,13 @@ const emptySettings: PlatformSettings = {
brand_primary_color: '#111827',
brand_accent_color: '#0F766E',
brand_email_from_name: '',
homepage_headline: 'Learn. Grow. Succeed.',
homepage_description: 'Access premium consulting, live webinars, and intensive bootcamps to accelerate your career.',
homepage_features: defaultFeatures,
};
const iconOptions = ['Users', 'Video', 'BookOpen', 'Star', 'Award', 'Target', 'Zap', 'Heart', 'Shield', 'Rocket'];
export function BrandingTab() {
const [settings, setSettings] = useState<PlatformSettings>(emptySettings);
const [loading, setLoading] = useState(true);
@@ -44,6 +65,17 @@ export function BrandingTab() {
.single();
if (data) {
let features = defaultFeatures;
if (data.homepage_features) {
try {
features = typeof data.homepage_features === 'string'
? JSON.parse(data.homepage_features)
: data.homepage_features;
} catch (e) {
console.error('Error parsing features:', e);
}
}
setSettings({
id: data.id,
brand_name: data.brand_name || '',
@@ -53,6 +85,9 @@ export function BrandingTab() {
brand_primary_color: data.brand_primary_color || '#111827',
brand_accent_color: data.brand_accent_color || '#0F766E',
brand_email_from_name: data.brand_email_from_name || '',
homepage_headline: data.homepage_headline || emptySettings.homepage_headline,
homepage_description: data.homepage_description || emptySettings.homepage_description,
homepage_features: features,
});
}
setLoading(false);
@@ -60,8 +95,18 @@ export function BrandingTab() {
const saveSettings = async () => {
setSaving(true);
const payload = { ...settings };
delete payload.id;
const payload = {
brand_name: settings.brand_name,
brand_tagline: settings.brand_tagline,
brand_logo_url: settings.brand_logo_url,
brand_favicon_url: settings.brand_favicon_url,
brand_primary_color: settings.brand_primary_color,
brand_accent_color: settings.brand_accent_color,
brand_email_from_name: settings.brand_email_from_name,
homepage_headline: settings.homepage_headline,
homepage_description: settings.homepage_description,
homepage_features: settings.homepage_features,
};
if (settings.id) {
const { error } = await supabase
@@ -87,10 +132,33 @@ export function BrandingTab() {
setSaving(false);
};
const updateFeature = (index: number, field: keyof HomepageFeature, value: string) => {
const newFeatures = [...settings.homepage_features];
newFeatures[index] = { ...newFeatures[index], [field]: value };
setSettings({ ...settings, homepage_features: newFeatures });
};
const addFeature = () => {
if (settings.homepage_features.length >= 6) {
toast({ title: 'Maksimum', description: 'Maksimum 6 fitur', variant: 'destructive' });
return;
}
setSettings({
...settings,
homepage_features: [...settings.homepage_features, { icon: 'Star', title: '', description: '' }],
});
};
const removeFeature = (index: number) => {
const newFeatures = settings.homepage_features.filter((_, i) => i !== index);
setSettings({ ...settings, homepage_features: newFeatures });
};
if (loading) return <div className="animate-pulse h-64 bg-muted rounded-md" />;
return (
<div className="space-y-6">
{/* Brand Identity */}
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
@@ -228,12 +296,119 @@ export function BrandingTab() {
Digunakan jika SMTP from_name kosong
</p>
</div>
<Button onClick={saveSettings} disabled={saving} className="shadow-sm">
{saving ? 'Menyimpan...' : 'Simpan Pengaturan'}
</Button>
</CardContent>
</Card>
{/* Homepage Settings */}
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Home className="w-5 h-5" />
Konten Homepage
</CardTitle>
<CardDescription>
Konfigurasi teks dan fitur yang ditampilkan di halaman utama
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label>Headline</Label>
<Input
value={settings.homepage_headline}
onChange={(e) => setSettings({ ...settings, homepage_headline: e.target.value })}
placeholder="Learn. Grow. Succeed."
className="border-2"
/>
</div>
<div className="space-y-2">
<Label>Deskripsi</Label>
<Textarea
value={settings.homepage_description}
onChange={(e) => setSettings({ ...settings, homepage_description: e.target.value })}
placeholder="Access premium consulting, live webinars..."
className="border-2"
rows={3}
/>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label>Fitur Cards ({settings.homepage_features.length}/6)</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={addFeature}
disabled={settings.homepage_features.length >= 6}
className="border-2"
>
<Plus className="w-4 h-4 mr-1" />
Tambah
</Button>
</div>
{settings.homepage_features.map((feature, index) => (
<div key={index} className="p-4 border-2 border-border rounded-lg space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Fitur {index + 1}</span>
{settings.homepage_features.length > 1 && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeFeature(index)}
className="text-destructive hover:text-destructive"
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="space-y-1">
<Label className="text-xs">Icon</Label>
<select
value={feature.icon}
onChange={(e) => updateFeature(index, 'icon', e.target.value)}
className="w-full h-10 px-3 border-2 border-input rounded-md bg-background"
>
{iconOptions.map((icon) => (
<option key={icon} value={icon}>{icon}</option>
))}
</select>
</div>
<div className="space-y-1 md:col-span-2">
<Label className="text-xs">Judul</Label>
<Input
value={feature.title}
onChange={(e) => updateFeature(index, 'title', e.target.value)}
placeholder="Consulting"
className="border-2"
/>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs">Deskripsi</Label>
<Textarea
value={feature.description}
onChange={(e) => updateFeature(index, 'description', e.target.value)}
placeholder="One-on-one sessions with..."
className="border-2"
rows={2}
/>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Button onClick={saveSettings} disabled={saving} className="shadow-sm">
{saving ? 'Menyimpan...' : 'Simpan Pengaturan'}
</Button>
</div>
);
}