VIBN Frontend for Coolify deployment
This commit is contained in:
306
components/layout/page-template.tsx
Normal file
306
components/layout/page-template.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
interface PageTemplateProps {
|
||||
children: ReactNode;
|
||||
|
||||
// Sidebar configuration
|
||||
sidebar?: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
items: Array<{
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
href: string;
|
||||
isActive?: boolean;
|
||||
badge?: string | number;
|
||||
}>;
|
||||
footer?: ReactNode;
|
||||
customContent?: ReactNode;
|
||||
};
|
||||
|
||||
// Hero section configuration
|
||||
hero?: {
|
||||
icon?: LucideIcon;
|
||||
iconBgColor?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
actions?: Array<{
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: "default" | "outline" | "ghost" | "secondary";
|
||||
icon?: LucideIcon;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Container width
|
||||
containerWidth?: "default" | "wide" | "full";
|
||||
|
||||
// Custom classes
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
export function PageTemplate({
|
||||
children,
|
||||
sidebar,
|
||||
hero,
|
||||
containerWidth = "default",
|
||||
className,
|
||||
contentClassName,
|
||||
}: PageTemplateProps) {
|
||||
const maxWidthClass = {
|
||||
default: "max-w-5xl",
|
||||
wide: "max-w-7xl",
|
||||
full: "max-w-none",
|
||||
}[containerWidth];
|
||||
|
||||
return (
|
||||
<div className={cn("flex h-full w-full overflow-hidden", className)}>
|
||||
{/* Left Sidebar Navigation (if provided) */}
|
||||
{sidebar && (
|
||||
<div className="w-64 border-r bg-muted/30 py-4">
|
||||
{/* Sidebar Header */}
|
||||
{sidebar.title && (
|
||||
<div className="pb-4 border-b mb-4">
|
||||
<h2 className="text-lg font-semibold">{sidebar.title}</h2>
|
||||
{sidebar.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{sidebar.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top Div: Navigation Items */}
|
||||
<div className="px-3 py-4 space-y-1">
|
||||
{sidebar.items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = item.isActive;
|
||||
|
||||
return (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-2 px-2 py-1.5 rounded-md text-sm transition-all group",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary font-medium"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Icon className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">{item.title}</span>
|
||||
</div>
|
||||
{item.badge && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary shrink-0 font-medium">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t my-4" />
|
||||
|
||||
{/* Bottom Div: Custom Content */}
|
||||
{sidebar.customContent && (
|
||||
<div className="px-3 py-4">
|
||||
{sidebar.customContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Hero Section (if provided) */}
|
||||
{hero && (
|
||||
<div className="border-b bg-gradient-to-br from-primary/5 to-background">
|
||||
<div className={cn(maxWidthClass, "mx-auto px-8 py-12")}>
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="flex items-center gap-4 min-w-0 flex-1">
|
||||
{hero.icon && (
|
||||
<div
|
||||
className={cn(
|
||||
"h-12 w-12 rounded-xl flex items-center justify-center shrink-0",
|
||||
hero.iconBgColor || "bg-primary/10"
|
||||
)}
|
||||
>
|
||||
<hero.icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="text-3xl font-bold truncate">{hero.title}</h1>
|
||||
{hero.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{hero.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Actions */}
|
||||
{hero.actions && hero.actions.length > 0 && (
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{hero.actions.map((action, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onClick={action.onClick}
|
||||
variant={action.variant || "default"}
|
||||
size="sm"
|
||||
>
|
||||
{action.icon && <action.icon className="h-4 w-4 mr-2" />}
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Page Content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className={cn(maxWidthClass, "mx-auto px-6 py-6", contentClassName)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Utility Components for common page patterns
|
||||
|
||||
interface PageSectionProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
headerAction?: ReactNode;
|
||||
}
|
||||
|
||||
export function PageSection({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className,
|
||||
headerAction,
|
||||
}: PageSectionProps) {
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{(title || description || headerAction) && (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
{title && <h2 className="text-lg font-semibold">{title}</h2>}
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{headerAction && <div>{headerAction}</div>}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageCardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
padding?: "sm" | "md" | "lg";
|
||||
hover?: boolean;
|
||||
}
|
||||
|
||||
export function PageCard({
|
||||
children,
|
||||
className,
|
||||
padding = "md",
|
||||
hover = false,
|
||||
}: PageCardProps) {
|
||||
const paddingClass = {
|
||||
sm: "p-4",
|
||||
md: "p-6",
|
||||
lg: "p-8",
|
||||
}[padding];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border rounded-lg bg-card",
|
||||
paddingClass,
|
||||
hover && "hover:border-primary hover:shadow-md transition-all",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageGridProps {
|
||||
children: ReactNode;
|
||||
cols?: 1 | 2 | 3 | 4;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PageGrid({ children, cols = 2, className }: PageGridProps) {
|
||||
const colsClass = {
|
||||
1: "grid-cols-1",
|
||||
2: "md:grid-cols-2",
|
||||
3: "md:grid-cols-3",
|
||||
4: "md:grid-cols-2 lg:grid-cols-4",
|
||||
}[cols];
|
||||
|
||||
return (
|
||||
<div className={cn("grid grid-cols-1 gap-4", colsClass, className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageEmptyStateProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
icon?: LucideIcon;
|
||||
};
|
||||
}
|
||||
|
||||
export function PageEmptyState({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
}: PageEmptyStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
|
||||
<div className="rounded-full bg-muted p-6 mb-4">
|
||||
<Icon className="h-12 w-12 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">{title}</h3>
|
||||
{description && (
|
||||
<p className="text-muted-foreground mb-6 max-w-md">{description}</p>
|
||||
)}
|
||||
{action && (
|
||||
<Button onClick={action.onClick}>
|
||||
{action.icon && <action.icon className="h-4 w-4 mr-2" />}
|
||||
{action.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user