Public Header Mobile Menu: - Added hamburger menu for non-logged-in visitors on mobile - Desktop shows full navigation, mobile shows slide-out menu with icons - Cart icon remains visible on mobile alongside hamburger Tiptap Editor List Formatting: - Added visual styling for bullet lists (disc markers, padding, spacing) - Added visual styling for ordered lists (decimal markers, padding, spacing) - List markers now use primary color for better visibility Product Content HTML Formatting: - Enhanced prose styling with proper heading sizes (h1, h2, h3) - Improved list formatting with proper indentation and markers - Added blockquote styling with left border and italic text - Added code and preformatted text styling - Ensures all formatted content displays properly on product detail pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
314 lines
12 KiB
TypeScript
314 lines
12 KiB
TypeScript
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 { Footer } from '@/components/Footer';
|
|
import { cn } from '@/lib/utils';
|
|
import {
|
|
LayoutDashboard,
|
|
Package,
|
|
BookOpen,
|
|
ShoppingCart,
|
|
Receipt,
|
|
User,
|
|
Settings,
|
|
Users,
|
|
Calendar,
|
|
LogOut,
|
|
Menu,
|
|
Home,
|
|
MoreHorizontal,
|
|
X,
|
|
Video,
|
|
Star,
|
|
} from 'lucide-react';
|
|
|
|
interface NavItem {
|
|
label: string;
|
|
href: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
}
|
|
|
|
const userNavItems: NavItem[] = [
|
|
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
|
{ label: 'Produk', href: '/products', icon: Package },
|
|
{ label: 'Akses', href: '/access', icon: BookOpen },
|
|
{ label: 'Order', href: '/orders', icon: Receipt },
|
|
{ label: 'Profil', href: '/profile', icon: User },
|
|
];
|
|
|
|
const adminNavItems: NavItem[] = [
|
|
{ label: 'Dashboard', href: '/admin', icon: LayoutDashboard },
|
|
{ label: 'Produk', href: '/admin/products', icon: Package },
|
|
{ label: 'Bootcamp', href: '/admin/bootcamp', icon: BookOpen },
|
|
{ label: 'Konsultasi', href: '/admin/consulting', icon: Video },
|
|
{ label: 'Order', href: '/admin/orders', icon: Receipt },
|
|
{ label: 'Member', href: '/admin/members', icon: Users },
|
|
{ label: 'Ulasan', href: '/admin/reviews', icon: Star },
|
|
{ label: 'Kalender', href: '/admin/events', icon: Calendar },
|
|
{ label: 'Pengaturan', href: '/admin/settings', icon: Settings },
|
|
];
|
|
|
|
const mobileUserNav: NavItem[] = [
|
|
{ label: 'Home', href: '/dashboard', icon: Home },
|
|
{ label: 'Kelas', href: '/access', icon: BookOpen },
|
|
{ label: 'Pesanan', href: '/orders', icon: Receipt },
|
|
{ label: 'Profil', href: '/profile', icon: User },
|
|
];
|
|
|
|
const mobileAdminNav: NavItem[] = [
|
|
{ label: 'Dashboard', href: '/admin', icon: LayoutDashboard },
|
|
{ label: 'Produk', href: '/admin/products', icon: Package },
|
|
{ label: 'Pesanan', href: '/admin/orders', icon: Receipt },
|
|
{ label: 'Pengguna', href: '/admin/members', icon: Users },
|
|
];
|
|
|
|
interface AppLayoutProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
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);
|
|
|
|
const navItems = isAdmin ? adminNavItems : userNavItems;
|
|
const mobileNav = isAdmin ? mobileAdminNav : mobileUserNav;
|
|
|
|
const handleSignOut = async () => {
|
|
await signOut();
|
|
navigate('/');
|
|
};
|
|
|
|
const isActive = (href: string) => {
|
|
if (href === '/dashboard' && location.pathname === '/dashboard') return true;
|
|
if (href === '/admin' && location.pathname === '/admin') return true;
|
|
if (href !== '/dashboard' && href !== '/admin') {
|
|
return location.pathname.startsWith(href);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// 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 flex flex-col">
|
|
<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 flex items-center gap-2">
|
|
{logoUrl && (
|
|
<img src={logoUrl} alt={brandName} className="h-8 object-contain" />
|
|
)}
|
|
<span>{brandName}</span>
|
|
</Link>
|
|
|
|
{/* Desktop Navigation */}
|
|
<nav className="hidden md:flex items-center gap-4">
|
|
<Link to="/products" className="hover:underline font-medium">Produk</Link>
|
|
<Link to="/calendar" className="hover:underline font-medium">Kalender</Link>
|
|
<Link to="/auth">
|
|
<Button variant="outline" size="sm" className="border-2">
|
|
<User className="w-4 h-4 mr-2" />
|
|
Login
|
|
</Button>
|
|
</Link>
|
|
<Link to="/checkout">
|
|
<Button variant="outline" size="sm" className="relative border-2">
|
|
<ShoppingCart className="w-4 h-4" />
|
|
{items.length > 0 && (
|
|
<span className="absolute -top-2 -right-2 bg-primary text-primary-foreground text-xs w-5 h-5 flex items-center justify-center">
|
|
{items.length}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
</Link>
|
|
</nav>
|
|
|
|
{/* Mobile Menu Trigger */}
|
|
<div className="md:hidden flex items-center gap-2">
|
|
<Link to="/checkout">
|
|
<Button variant="outline" size="sm" className="relative border-2">
|
|
<ShoppingCart className="w-4 h-4" />
|
|
{items.length > 0 && (
|
|
<span className="absolute -top-2 -right-2 bg-primary text-primary-foreground text-xs w-5 h-5 flex items-center justify-center">
|
|
{items.length}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
</Link>
|
|
<Sheet>
|
|
<SheetTrigger asChild>
|
|
<Button variant="ghost" size="sm">
|
|
<Menu className="w-6 h-6" />
|
|
</Button>
|
|
</SheetTrigger>
|
|
<SheetContent side="right" className="border-l-2 border-border">
|
|
<nav className="flex flex-col space-y-4 mt-8">
|
|
<Link to="/products" className="flex items-center gap-3 text-lg font-medium">
|
|
<Package className="w-5 h-5" />
|
|
Produk
|
|
</Link>
|
|
<Link to="/calendar" className="flex items-center gap-3 text-lg font-medium">
|
|
<Calendar className="w-5 h-5" />
|
|
Kalender
|
|
</Link>
|
|
<Link to="/auth" className="flex items-center gap-3 text-lg font-medium">
|
|
<User className="w-5 h-5" />
|
|
Login
|
|
</Link>
|
|
</nav>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<main className="flex-1">{children}</main>
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background flex">
|
|
{/* 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 flex items-center gap-2">
|
|
{logoUrl && (
|
|
<img src={logoUrl} alt={brandName} className="h-8 object-contain" />
|
|
)}
|
|
<span>{brandName}</span>
|
|
</Link>
|
|
</div>
|
|
|
|
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
|
|
{navItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
to={item.href}
|
|
className={cn(
|
|
"flex items-center gap-3 px-3 py-2 rounded-none text-sm font-medium transition-colors",
|
|
isActive(item.href)
|
|
? "bg-primary text-primary-foreground"
|
|
: "hover:bg-muted"
|
|
)}
|
|
>
|
|
<item.icon className="w-5 h-5" />
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="p-4 border-t-2 border-border space-y-2">
|
|
{!isAdmin && (
|
|
<Link to="/checkout" className="flex items-center gap-3 px-3 py-2 hover:bg-muted text-sm font-medium">
|
|
<ShoppingCart className="w-5 h-5" />
|
|
Keranjang
|
|
{items.length > 0 && (
|
|
<span className="ml-auto bg-primary text-primary-foreground text-xs px-2 py-0.5">
|
|
{items.length}
|
|
</span>
|
|
)}
|
|
</Link>
|
|
)}
|
|
<button
|
|
onClick={handleSignOut}
|
|
className="flex items-center gap-3 px-3 py-2 hover:bg-muted text-sm font-medium w-full text-left"
|
|
>
|
|
<LogOut className="w-5 h-5" />
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main content */}
|
|
<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 flex items-center gap-2">
|
|
{logoUrl && (
|
|
<img src={logoUrl} alt={brandName} className="h-6 object-contain" />
|
|
)}
|
|
<span>{brandName}</span>
|
|
</Link>
|
|
<div className="flex items-center gap-2">
|
|
<Link to="/checkout" className="relative p-2">
|
|
<ShoppingCart className="w-5 h-5" />
|
|
{items.length > 0 && (
|
|
<span className="absolute top-0 right-0 bg-primary text-primary-foreground text-xs w-4 h-4 flex items-center justify-center">
|
|
{items.length}
|
|
</span>
|
|
)}
|
|
</Link>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="pb-20 md:pb-0">{children}</main>
|
|
|
|
{/* Mobile Bottom Navigation */}
|
|
<nav className="md:hidden fixed bottom-0 left-0 right-0 border-t-2 border-border bg-background z-50">
|
|
<div className="flex items-center justify-around py-2">
|
|
{mobileNav.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
to={item.href}
|
|
className={cn(
|
|
"flex flex-col items-center gap-1 px-3 py-1 text-xs font-medium",
|
|
isActive(item.href) ? "text-primary" : "text-muted-foreground"
|
|
)}
|
|
>
|
|
<item.icon className="w-5 h-5" />
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
{moreItems.length > 0 && (
|
|
<Sheet open={moreOpen} onOpenChange={setMoreOpen}>
|
|
<SheetTrigger asChild>
|
|
<button className="flex flex-col items-center gap-1 px-3 py-1 text-xs font-medium text-muted-foreground">
|
|
<MoreHorizontal className="w-5 h-5" />
|
|
Lainnya
|
|
</button>
|
|
</SheetTrigger>
|
|
<SheetContent side="bottom" className="border-t-2 border-border">
|
|
<div className="space-y-2 py-4">
|
|
{moreItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
to={item.href}
|
|
onClick={() => setMoreOpen(false)}
|
|
className="flex items-center gap-3 px-4 py-3 hover:bg-muted text-sm font-medium"
|
|
>
|
|
<item.icon className="w-5 h-5" />
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
<button
|
|
onClick={() => { handleSignOut(); setMoreOpen(false); }}
|
|
className="flex items-center gap-3 px-4 py-3 hover:bg-muted text-sm font-medium w-full text-left text-destructive"
|
|
>
|
|
<LogOut className="w-5 h-5" />
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)}
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|