Changes
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user