Initial commit - Route Commerce platform

This commit is contained in:
2026-06-01 19:40:55 +00:00
commit 53a9671461
617 changed files with 106132 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
"use client";
type EmptyStateProps = {
title: string;
description?: string;
icon?: string;
action?: React.ReactNode;
className?: string;
};
export default function EmptyState({ title, description, icon, action, className = "" }: EmptyStateProps) {
return (
<div className={`flex flex-col items-center justify-center py-16 px-6 text-center ${className}`}>
{icon && <span className="mb-4 text-4xl">{icon}</span>}
{!icon && (
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-slate-100">
<svg className="h-6 w-6 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
</div>
)}
<h3 className="text-base font-semibold text-slate-900">{title}</h3>
{description && <p className="mt-1 text-sm text-slate-500 max-w-sm">{description}</p>}
{action && <div className="mt-4">{action}</div>}
</div>
);
}
+117
View File
@@ -0,0 +1,117 @@
"use client";
type SkeletonVariant = "text" | "card" | "table" | "avatar" | "button";
type SkeletonProps = {
variant?: SkeletonVariant;
width?: string;
height?: string;
className?: string;
count?: number;
};
function SkeletonLine({ className = "" }: { className?: string }) {
return <div className={`animate-pulse rounded bg-slate-200 ${className}`} />;
}
export function SkeletonText({ lines = 3, className = "" }: { lines?: number; className?: string }) {
return (
<div className={`space-y-2 ${className}`}>
{Array.from({ length: lines }).map((_, i) => (
<SkeletonLine key={i} className={i === lines - 1 ? "w-3/4" : "w-full"} />
))}
</div>
);
}
export function SkeletonCard({ className = "" }: SkeletonProps) {
return (
<div className={`rounded-2xl border border-slate-200 bg-white p-6 ${className}`}>
<div className="flex items-center gap-4">
<SkeletonLine className="h-10 w-10 rounded-lg" />
<div className="flex-1 space-y-2">
<SkeletonLine className="h-4 w-1/3" />
<SkeletonLine className="h-3 w-1/2" />
</div>
</div>
</div>
);
}
export function SkeletonTable({ rows = 5, cols = 4, className = "" }: { rows?: number; cols?: number; className?: string }) {
return (
<div className={`overflow-hidden rounded-2xl border border-slate-200 ${className}`}>
<div className="border-b border-slate-100 bg-slate-50 px-4 py-3">
<div className="flex gap-4">
{Array.from({ length: cols }).map((_, i) => (
<SkeletonLine key={i} className="h-3 flex-1" />
))}
</div>
</div>
<div className="divide-y divide-slate-50">
{Array.from({ length: rows }).map((_, rowIdx) => (
<div key={rowIdx} className="flex items-center gap-4 px-4 py-3">
{Array.from({ length: cols }).map((_, colIdx) => (
<SkeletonLine key={colIdx} className="h-3 flex-1" />
))}
</div>
))}
</div>
</div>
);
}
export function SkeletonAvatar({ className = "" }: SkeletonProps) {
return <SkeletonLine className={`h-10 w-10 rounded-full ${className}`} />;
}
export function SkeletonButton({ className = "" }: SkeletonProps) {
return <SkeletonLine className={`h-9 w-24 rounded-lg ${className}`} />;
}
export default function LoadingSkeleton({ variant = "text", width, height, className = "", count = 1 }: SkeletonProps) {
const style: React.CSSProperties = {};
if (width) style.width = width;
if (height) style.height = height;
const items = Array.from({ length: count });
if (variant === "card") {
return (
<div className="space-y-3">
{items.map((_, i) => (
<SkeletonCard key={i} />
))}
</div>
);
}
if (variant === "table") {
return <SkeletonTable className={className} />;
}
if (variant === "avatar") {
return (
<div className="flex items-center gap-3">
{items.map((_, i) => (
<SkeletonAvatar key={i} />
))}
</div>
);
}
if (variant === "button") {
return (
<div className="flex gap-3">
{items.map((_, i) => (
<SkeletonButton key={i} />
))}
</div>
);
}
// default: text
return (
<div className="space-y-2">
{items.map((_, i) => (
<SkeletonText key={i} className={className} />
))}
</div>
);
}
+26
View File
@@ -0,0 +1,26 @@
type SectionHeaderProps = {
title: string;
description?: string;
titleClassName?: string;
descClassName?: string;
};
export default function SectionHeader({
title,
description,
titleClassName,
descClassName,
}: SectionHeaderProps) {
return (
<div className="mb-8">
<h2 className={`text-3xl font-bold tracking-tight text-stone-950 ${titleClassName ?? ""}`}>
{title}
</h2>
{description && (
<p className={`mt-2 max-w-2xl text-stone-700 leading-relaxed ${descClassName ?? ""}`}>
{description}
</p>
)}
</div>
);
}
+37
View File
@@ -0,0 +1,37 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export default function ThemeToggle({ className }: { className?: string }) {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) {
return <div className={`h-5 w-5 ${className ?? ""}`} />;
}
const isDark = resolvedTheme === "dark";
return (
<button
onClick={() => setTheme(isDark ? "light" : "dark")}
className={`flex h-9 w-9 items-center justify-center rounded-lg text-zinc-400 hover:text-white hover:bg-white/5 transition-all border border-transparent hover:border-white/10 ${className ?? ""}`}
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
>
{isDark ? (
// Sun icon
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
) : (
// Moon icon
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
)}
</button>
);
}