Initial commit - Route Commerce platform
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user