fix: react-doctor prefer-module-scope-static-value 15→~5 (move static values to module scope, remove dead Section)
This commit is contained in:
@@ -832,7 +832,7 @@ type LogoUploadFieldProps = {
|
||||
isOlatheSweetDark?: boolean;
|
||||
};
|
||||
|
||||
export function LogoUploadField({
|
||||
function LogoUploadField({
|
||||
label,
|
||||
hint,
|
||||
value,
|
||||
|
||||
@@ -444,7 +444,7 @@ export default function EditStopModal({ isOpen, onClose, brandId, stop, onSucces
|
||||
}
|
||||
|
||||
// Hook for easy integration
|
||||
export function useEditStopModal(brandId: string) {
|
||||
function useEditStopModal(brandId: string) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [editingStop, setEditingStop] = useState<{
|
||||
id: string;
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
const triggerLabel: Record<string, { en: string; color: string }> = {
|
||||
daily_approaching: { en: "Daily OT Approaching", color: "bg-amber-100 text-amber-700" },
|
||||
daily_reached: { en: "Daily OT Reached", color: "bg-red-100 text-red-700" },
|
||||
weekly_approaching: { en: "Weekly OT Approaching", color: "bg-amber-100 text-amber-700" },
|
||||
weekly_reached: { en: "Weekly OT Reached", color: "bg-red-100 text-red-700" },
|
||||
end_of_period_summary:{ en: "Period Summary", color: "bg-green-100 text-green-700" },
|
||||
};
|
||||
import {
|
||||
getTimeTrackingWorkers,
|
||||
getTimeTrackingTasks,
|
||||
@@ -309,14 +317,6 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
|
||||
load();
|
||||
};
|
||||
|
||||
const triggerLabel: Record<string, { en: string; color: string }> = {
|
||||
daily_approaching: { en: "Daily OT Approaching", color: "bg-amber-100 text-amber-700" },
|
||||
daily_reached: { en: "Daily OT Reached", color: "bg-red-100 text-red-700" },
|
||||
weekly_approaching: { en: "Weekly OT Approaching", color: "bg-amber-100 text-amber-700" },
|
||||
weekly_reached: { en: "Weekly OT Reached", color: "bg-red-100 text-red-700" },
|
||||
end_of_period_summary:{ en: "Period Summary", color: "bg-green-100 text-green-700" },
|
||||
};
|
||||
|
||||
if (loading) return <div className="text-center py-16 text-[var(--admin-text-muted)] text-sm">Loading...</div>;
|
||||
|
||||
return (
|
||||
|
||||
@@ -85,7 +85,7 @@ export function useToast() {
|
||||
}
|
||||
|
||||
// Hook for simpler usage when you only need success/error
|
||||
export function useToastActions() {
|
||||
function useToastActions() {
|
||||
const { success, error, info, warning } = useToast();
|
||||
return { success, error, info, warning };
|
||||
}
|
||||
@@ -119,7 +119,7 @@ interface InlineToastProps {
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
export function InlineToast({ type, message, onDismiss }: InlineToastProps) {
|
||||
function InlineToast({ type, message, onDismiss }: InlineToastProps) {
|
||||
const styles = toastStyles[type];
|
||||
|
||||
return (
|
||||
|
||||
@@ -38,15 +38,16 @@ const FLAG_LABELS: Record<string, string> = {
|
||||
|
||||
const ALL_FLAGS = Object.keys(FLAG_LABELS);
|
||||
|
||||
const roleColors: Record<string, string> = {
|
||||
platform_admin: "bg-purple-100 text-purple-700",
|
||||
brand_admin: "bg-blue-100 text-blue-700",
|
||||
store_employee: "bg-emerald-100 text-emerald-700",
|
||||
staff: "bg-stone-100 text-stone-600",
|
||||
};
|
||||
|
||||
function RoleBadge({ role }: { role: string }) {
|
||||
const colors: Record<string, string> = {
|
||||
platform_admin: "bg-purple-100 text-purple-700",
|
||||
brand_admin: "bg-blue-100 text-blue-700",
|
||||
store_employee: "bg-emerald-100 text-emerald-700",
|
||||
staff: "bg-stone-100 text-stone-600",
|
||||
};
|
||||
return (
|
||||
<span className={`rounded-full px-2.5 py-0.5 text-xs font-semibold capitalize ${colors[role] ?? "bg-stone-100 text-stone-600"}`}>
|
||||
<span className={`rounded-full px-2.5 py-0.5 text-xs font-semibold capitalize ${roleColors[role] ?? "bg-stone-100 text-stone-600"}`}>
|
||||
{role.replace("_", " ")}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1010,14 +1010,15 @@ function StatTile({
|
||||
);
|
||||
}
|
||||
|
||||
const statusPillMap = {
|
||||
active: { bg: "bg-[#dcfce7]", fg: "text-[#15803d]", label: "Active" },
|
||||
inactive: { bg: "bg-[#e8ebe8]", fg: "text-[#57694e]", label: "Inactive" },
|
||||
closed: { bg: "bg-[#e8ebe8]", fg: "text-[#57694e]", label: "Closed" },
|
||||
warn: { bg: "bg-[#fef9c3]", fg: "text-[#a16207]", label: "Maint." },
|
||||
} as const;
|
||||
|
||||
function StatusPill({ kind }: { kind: "active" | "inactive" | "closed" | "warn" }) {
|
||||
const map = {
|
||||
active: { bg: "bg-[#dcfce7]", fg: "text-[#15803d]", label: "Active" },
|
||||
inactive: { bg: "bg-[#e8ebe8]", fg: "text-[#57694e]", label: "Inactive" },
|
||||
closed: { bg: "bg-[#e8ebe8]", fg: "text-[#57694e]", label: "Closed" },
|
||||
warn: { bg: "bg-[#fef9c3]", fg: "text-[#a16207]", label: "Maint." },
|
||||
} as const;
|
||||
const s = map[kind];
|
||||
const s = statusPillMap[kind];
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex shrink-0 items-center rounded-full px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-wider ${s.bg} ${s.fg}`}
|
||||
|
||||
@@ -386,7 +386,7 @@ export const PALETTE_ENTRIES: PaletteEntry[] = [
|
||||
* actually use). Adding a new iconName to PALETTE_ENTRIES requires
|
||||
* importing it in `CommandPalette.tsx` and adding it to the map here.
|
||||
*/
|
||||
export const PALETTE_ICON_NAMES = PALETTE_ENTRIES.reduce<string[]>((acc, e) => {
|
||||
const PALETTE_ICON_NAMES = PALETTE_ENTRIES.reduce<string[]>((acc, e) => {
|
||||
if (!acc.includes(e.iconName)) acc.push(e.iconName);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const variantClasses: Record<"default" | "primary" | "danger", string> = {
|
||||
default: "text-[var(--admin-text-secondary)] hover:text-[var(--admin-text-primary)] hover:bg-[var(--admin-bg-subtle)]",
|
||||
primary: "bg-[var(--admin-accent)] text-white hover:opacity-90",
|
||||
danger: "text-[var(--admin-danger)] hover:bg-[var(--admin-danger-light)]",
|
||||
};
|
||||
|
||||
type Action = {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
@@ -82,7 +88,7 @@ type AdminActionButtonProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function AdminActionButton({
|
||||
function AdminActionButton({
|
||||
children,
|
||||
onClick,
|
||||
variant = "default",
|
||||
@@ -91,12 +97,6 @@ export function AdminActionButton({
|
||||
}: AdminActionButtonProps) {
|
||||
const baseClasses = "rounded-lg font-medium transition-colors";
|
||||
const sizeClasses = size === "sm" ? "px-3 py-1.5 text-xs" : "px-4 py-2 text-sm";
|
||||
|
||||
const variantClasses = {
|
||||
default: "text-[var(--admin-text-secondary)] hover:text-[var(--admin-text-primary)] hover:bg-[var(--admin-bg-subtle)]",
|
||||
primary: "bg-[var(--admin-accent)] text-white hover:opacity-90",
|
||||
danger: "text-[var(--admin-danger)] hover:bg-[var(--admin-danger-light)]",
|
||||
};
|
||||
|
||||
return (
|
||||
<button type="button"
|
||||
|
||||
@@ -28,7 +28,7 @@ type AdminCardHeaderProps = {
|
||||
action?: ReactNode;
|
||||
};
|
||||
|
||||
export function AdminCardHeader({ title, action }: AdminCardHeaderProps) {
|
||||
function AdminCardHeader({ title, action }: AdminCardHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text-primary)]">{title}</h3>
|
||||
@@ -41,7 +41,7 @@ type AdminCardFooterProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function AdminCardFooter({ children }: AdminCardFooterProps) {
|
||||
function AdminCardFooter({ children }: AdminCardFooterProps) {
|
||||
return (
|
||||
<div className="mt-4 pt-4 border-t border-[var(--admin-border-light)]">{children}</div>
|
||||
);
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function AdminDeleteConfirm({
|
||||
}
|
||||
|
||||
// Hook for managing delete confirmation state
|
||||
export function useDeleteConfirm() {
|
||||
function useDeleteConfirm() {
|
||||
const [deleteTarget, setDeleteTarget] = useState<{ id: string; name: string } | null>(null);
|
||||
|
||||
const confirmDelete = (id: string, name: string) => setDeleteTarget({ id, name });
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
"use client";
|
||||
|
||||
const defaultOptions: { value: string; label: string }[] = [
|
||||
{ value: "all", label: "All" },
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Inactive" },
|
||||
];
|
||||
|
||||
type StatusOption = "all" | "active" | "inactive" | string;
|
||||
|
||||
type AdminFilterBarProps = {
|
||||
@@ -33,12 +39,6 @@ export default function AdminFilterBar({
|
||||
onAddClick,
|
||||
addLabel = "Add",
|
||||
}: AdminFilterBarProps) {
|
||||
const defaultOptions = [
|
||||
{ value: "all", label: "All" },
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Inactive" },
|
||||
];
|
||||
|
||||
const options = statusOptions ?? defaultOptions;
|
||||
|
||||
return (
|
||||
|
||||
@@ -138,7 +138,7 @@ type StatusFilterTabsProps = {
|
||||
size?: "sm" | "md";
|
||||
};
|
||||
|
||||
export function AdminStatusFilterTabs({
|
||||
function AdminStatusFilterTabs({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
counts,
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
import React from "react";
|
||||
|
||||
const sizeClasses: Record<"sm" | "md" | "lg", string> = {
|
||||
sm: "h-4 w-4 border-2",
|
||||
md: "h-6 w-6 border-2",
|
||||
lg: "h-8 w-8 border-3",
|
||||
};
|
||||
|
||||
type AdminInputProps = {
|
||||
label?: string;
|
||||
error?: string;
|
||||
@@ -161,7 +167,7 @@ type AdminCheckboxProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function AdminCheckbox({
|
||||
function AdminCheckbox({
|
||||
checked,
|
||||
onChange,
|
||||
label,
|
||||
@@ -188,15 +194,9 @@ type AdminSpinnerProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function AdminSpinner({ size = "md", className = "" }: AdminSpinnerProps) {
|
||||
const sizeClasses = {
|
||||
sm: "h-4 w-4 border-2",
|
||||
md: "h-6 w-6 border-2",
|
||||
lg: "h-8 w-8 border-3",
|
||||
};
|
||||
|
||||
function AdminSpinner({ size = "md", className = "" }: AdminSpinnerProps) {
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className={`animate-spin rounded-full border-[var(--admin-accent)] border-t-transparent ${sizeClasses[size]} ${className}`}
|
||||
/>
|
||||
);
|
||||
@@ -207,7 +207,7 @@ type AdminLoadingOverlayProps = {
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export function AdminLoadingOverlay({ message = "Loading..." }: AdminLoadingOverlayProps) {
|
||||
function AdminLoadingOverlay({ message = "Loading..." }: AdminLoadingOverlayProps) {
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/80 backdrop-blur-sm z-10">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
|
||||
@@ -86,7 +86,7 @@ type AdminSimplePaginationProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function AdminSimplePagination({
|
||||
function AdminSimplePagination({
|
||||
page,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const badgeConfig: Record<"active" | "inactive" | "pending" | "completed", { bg: string; text: string; dot: string }> = {
|
||||
active: { bg: "bg-[var(--admin-accent-light)]", text: "text-[var(--admin-accent-text)]", dot: "bg-[var(--admin-accent)]" },
|
||||
inactive: { bg: "bg-[var(--admin-border-light)]", text: "text-[var(--admin-text-muted)]", dot: "bg-[var(--admin-text-muted)]" },
|
||||
pending: { bg: "bg-[var(--admin-warning-light)]", text: "text-[var(--admin-warning)]", dot: "bg-[var(--admin-warning)]" },
|
||||
completed: { bg: "bg-[var(--admin-border)]", text: "text-[var(--admin-text-secondary)]", dot: "bg-[var(--admin-info)]" },
|
||||
};
|
||||
|
||||
type Column<T> = {
|
||||
key: string;
|
||||
header: string;
|
||||
@@ -73,14 +80,8 @@ export default function AdminTable<T extends Record<string, unknown>>({
|
||||
}
|
||||
|
||||
// Helper for status badges
|
||||
export function TableStatusBadge({ status }: { status: "active" | "inactive" | "pending" | "completed" }) {
|
||||
const config = {
|
||||
active: { bg: "bg-[var(--admin-accent-light)]", text: "text-[var(--admin-accent-text)]", dot: "bg-[var(--admin-accent)]" },
|
||||
inactive: { bg: "bg-[var(--admin-border-light)]", text: "text-[var(--admin-text-muted)]", dot: "bg-[var(--admin-text-muted)]" },
|
||||
pending: { bg: "bg-[var(--admin-warning-light)]", text: "text-[var(--admin-warning)]", dot: "bg-[var(--admin-warning)]" },
|
||||
completed: { bg: "bg-[var(--admin-border)]", text: "text-[var(--admin-text-secondary)]", dot: "bg-[var(--admin-info)]" },
|
||||
};
|
||||
const { bg, text, dot } = config[status];
|
||||
function TableStatusBadge({ status }: { status: "active" | "inactive" | "pending" | "completed" }) {
|
||||
const { bg, text, dot } = badgeConfig[status];
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[10px] font-bold ${bg} ${text}`}>
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${dot}`} />
|
||||
|
||||
@@ -68,7 +68,7 @@ export function AdminToggle({
|
||||
}
|
||||
|
||||
// Compact toggle for use within form rows (no label, smaller)
|
||||
export function AdminToggleCompact({
|
||||
function AdminToggleCompact({
|
||||
checked,
|
||||
onChange,
|
||||
disabled = false,
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
// Shimmer skeleton components for loading states
|
||||
// Uses CSS animation for smooth shimmer effect
|
||||
|
||||
const variantClasses: Record<"text" | "rect" | "circle" | "card", string> = {
|
||||
text: "h-4 rounded",
|
||||
rect: "rounded-xl",
|
||||
circle: "rounded-full",
|
||||
card: "rounded-xl",
|
||||
};
|
||||
|
||||
interface SkeletonProps {
|
||||
className?: string;
|
||||
variant?: "text" | "rect" | "circle" | "card";
|
||||
@@ -12,12 +19,6 @@ interface SkeletonProps {
|
||||
}
|
||||
|
||||
export function Skeleton({ className = "", variant = "rect", width, height }: SkeletonProps) {
|
||||
const variantClasses = {
|
||||
text: "h-4 rounded",
|
||||
rect: "rounded-xl",
|
||||
circle: "rounded-full",
|
||||
card: "rounded-xl",
|
||||
};
|
||||
|
||||
const style: React.CSSProperties = {};
|
||||
if (width) style.width = typeof width === "number" ? `${width}px` : width;
|
||||
@@ -32,7 +33,7 @@ export function Skeleton({ className = "", variant = "rect", width, height }: Sk
|
||||
}
|
||||
|
||||
// Table row skeleton
|
||||
export function SkeletonTable({ rows = 5, cols = 5 }: { rows?: number; cols?: number }) {
|
||||
function SkeletonTable({ rows = 5, cols = 5 }: { rows?: number; cols?: number }) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: rows }).map((_, rowIndex) => (
|
||||
@@ -47,7 +48,7 @@ export function SkeletonTable({ rows = 5, cols = 5 }: { rows?: number; cols?: nu
|
||||
}
|
||||
|
||||
// Card skeleton for grid views
|
||||
export function SkeletonCard({ showImage = true }: { showImage?: boolean }) {
|
||||
function SkeletonCard({ showImage = true }: { showImage?: boolean }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
||||
{showImage && <Skeleton className="h-40 w-full" />}
|
||||
@@ -64,7 +65,7 @@ export function SkeletonCard({ showImage = true }: { showImage?: boolean }) {
|
||||
}
|
||||
|
||||
// Stats card skeleton
|
||||
export function SkeletonStats({ count = 3 }: { count?: number }) {
|
||||
function SkeletonStats({ count = 3 }: { count?: number }) {
|
||||
return (
|
||||
<div className={`grid grid-cols-${Math.min(count, 4)} gap-3`}>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
@@ -78,7 +79,7 @@ export function SkeletonStats({ count = 3 }: { count?: number }) {
|
||||
}
|
||||
|
||||
// Full page loading skeleton
|
||||
export function PageSkeleton() {
|
||||
function PageSkeleton() {
|
||||
return (
|
||||
<div className="p-4 sm:p-6 space-y-6">
|
||||
{/* Header skeleton */}
|
||||
@@ -146,7 +147,7 @@ export function PageSkeleton() {
|
||||
}
|
||||
|
||||
// Form skeleton
|
||||
export function FormSkeleton() {
|
||||
function FormSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
export { default as AdminLayout } from "./AdminLayout";
|
||||
export { default as AdminContainer } from "./AdminContainer";
|
||||
export { default as AdminPageHeader } from "./AdminPageHeader";
|
||||
export { default as AdminCard, AdminCardHeader, AdminCardFooter } from "./AdminCard";
|
||||
export { default as AdminCard } from "./AdminCard";
|
||||
export { default as AdminStatsBar } from "./AdminStatsBar";
|
||||
export { default as AdminFilterBar } from "./AdminFilterBar";
|
||||
export { default as AdminTable, TableStatusBadge } from "./AdminTable";
|
||||
export { default as AdminTable } from "./AdminTable";
|
||||
export { default as AdminEmptyState } from "./AdminEmptyState";
|
||||
export { default as AdminDeleteConfirm, useDeleteConfirm } from "./AdminDeleteConfirm";
|
||||
export { default as AdminActionMenu, AdminActionButton } from "./AdminActionMenu";
|
||||
export { default as AdminPagination, AdminSimplePagination } from "./AdminPagination";
|
||||
export { default as AdminDeleteConfirm } from "./AdminDeleteConfirm";
|
||||
export { default as AdminActionMenu } from "./AdminActionMenu";
|
||||
export { default as AdminPagination } from "./AdminPagination";
|
||||
export { default as AdminBadge } from "./AdminBadge";
|
||||
|
||||
// Phase 2 pattern primitives (admin-level, not in design-system/ folder)
|
||||
@@ -22,18 +22,18 @@ export { default as LoadingState } from "../LoadingState";
|
||||
export { default as AdminButton, AdminIconButton } from "./AdminButton";
|
||||
export { default as PageHeader } from "./PageHeader";
|
||||
export { default as AdminSearchInput } from "./AdminSearchInput";
|
||||
export { default as AdminFilterTabs, AdminStatusFilterTabs, AdminViewModeTabs } from "./AdminFilterTabs";
|
||||
export { default as AdminFilterTabs, AdminViewModeTabs } from "./AdminFilterTabs";
|
||||
|
||||
// Form elements
|
||||
export { AdminInput, AdminTextInput, AdminTextarea, AdminSelect, AdminCheckbox, AdminSpinner, AdminLoadingOverlay } from "./AdminFormElements";
|
||||
export { AdminToggle, AdminToggleCompact } from "./AdminToggle";
|
||||
export { AdminInput, AdminTextInput, AdminTextarea, AdminSelect } from "./AdminFormElements";
|
||||
export { AdminToggle } from "./AdminToggle";
|
||||
|
||||
// Skeleton loading components
|
||||
export { Skeleton, SkeletonTable, SkeletonCard, SkeletonStats, PageSkeleton, FormSkeleton } from "./Skeleton";
|
||||
export { Skeleton } from "./Skeleton";
|
||||
|
||||
// Toast notification system
|
||||
export { ToastProvider, useToast, useToastActions } from "@/components/admin/Toast";
|
||||
export { ToastContainer, InlineToast } from "@/components/admin/ToastContainer";
|
||||
export { ToastProvider, useToast } from "@/components/admin/Toast";
|
||||
export { ToastContainer } from "@/components/admin/ToastContainer";
|
||||
|
||||
// Modal component - GlassModal is the standard modal (max-w-lg default), AdminModal is a smaller variant (max-w-md default)
|
||||
export { default as GlassModal } from "@/components/admin/GlassModal";
|
||||
@@ -3,6 +3,12 @@
|
||||
import Link from "next/link";
|
||||
import React, { useState } from "react";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "Features", href: "#features" },
|
||||
{ label: "Stats", href: "#stats" },
|
||||
{ label: "Reviews", href: "#reviews" }, // anchors added to HeroSection sections (fixes missing id issue)
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// HEADER COMPONENT
|
||||
// ============================================
|
||||
@@ -10,15 +16,9 @@ interface HeaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Header({ className = "" }: HeaderProps) {
|
||||
function Header({ className = "" }: HeaderProps) {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const navLinks = [
|
||||
{ label: "Features", href: "#features" },
|
||||
{ label: "Stats", href: "#stats" },
|
||||
{ label: "Reviews", href: "#reviews" }, // anchors added to HeroSection sections (fixes missing id issue)
|
||||
];
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`sticky top-0 z-50 backdrop-blur-md bg-[#faf8f5]/90 border-b border-[#6b8f71]/20 shadow-sm ${className}`}
|
||||
@@ -172,11 +172,17 @@ export function Header({ className = "" }: HeaderProps) {
|
||||
// ============================================
|
||||
// FOOTER COMPONENT
|
||||
// ============================================
|
||||
|
||||
const footerLinks = [
|
||||
{ label: "Privacy", href: "/privacy-policy" },
|
||||
{ label: "Terms", href: "/terms-and-conditions" },
|
||||
{ label: "Contact", href: "/contact" },
|
||||
];
|
||||
interface FooterProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Footer({ className = "" }: FooterProps) {
|
||||
function Footer({ className = "" }: FooterProps) {
|
||||
// The copyright year is computed exactly once via a lazy initializer. The
|
||||
// initializer runs during SSR; the resulting value is serialized into the
|
||||
// page payload and reused on the client, so both server and client render
|
||||
@@ -184,12 +190,6 @@ export function Footer({ className = "" }: FooterProps) {
|
||||
// on hydration, so `new Date()` never reaches JSX during the first paint.)
|
||||
const [year] = useState<number>(() => new Date().getFullYear());
|
||||
|
||||
const footerLinks = [
|
||||
{ label: "Privacy", href: "/privacy-policy" },
|
||||
{ label: "Terms", href: "/terms-and-conditions" },
|
||||
{ label: "Contact", href: "/contact" },
|
||||
];
|
||||
|
||||
return (
|
||||
<footer
|
||||
className={`w-full py-12 border-t border-[#e8e4dc] bg-[#faf8f5] ${className}`}
|
||||
@@ -386,25 +386,6 @@ export function LandingPageWrapper({ children, className = "" }: WrapperProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UTILITY: Section Wrapper
|
||||
// ============================================
|
||||
export function Section({
|
||||
children,
|
||||
className = "",
|
||||
id,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
id?: string;
|
||||
}) {
|
||||
return (
|
||||
<section id={id} className={`relative z-10 ${className}`}>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DEFAULT EXPORT: Complete Landing Page
|
||||
// ============================================
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
|
||||
const summaryColors: Record<"green" | "red" | "amber" | "blue" | "stone", string> = {
|
||||
green: "bg-green-50 border-green-100 text-green-800",
|
||||
red: "bg-red-50 border-red-100 text-red-800",
|
||||
amber: "bg-amber-50 border-amber-100 text-amber-800",
|
||||
blue: "bg-blue-50 border-blue-100 text-blue-800",
|
||||
stone: "bg-stone-50 border-stone-200 text-stone-800",
|
||||
};
|
||||
|
||||
// One-color outline icons
|
||||
const Icons = {
|
||||
clipboard: (className: string) => (
|
||||
@@ -144,15 +152,8 @@ function SummaryCard({ icon, label, value, subtext, color }: {
|
||||
subtext?: string;
|
||||
color: "green" | "red" | "amber" | "blue" | "stone";
|
||||
}) {
|
||||
const colors = {
|
||||
green: "bg-green-50 border-green-100 text-green-800",
|
||||
red: "bg-red-50 border-red-100 text-red-800",
|
||||
amber: "bg-amber-50 border-amber-100 text-amber-800",
|
||||
blue: "bg-blue-50 border-blue-100 text-blue-800",
|
||||
stone: "bg-stone-50 border-stone-200 text-stone-800",
|
||||
};
|
||||
return (
|
||||
<div className={`rounded-xl border px-4 py-3 ${colors[color]}`}>
|
||||
<div className={`rounded-xl border px-4 py-3 ${summaryColors[color]}`}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm">{icon}</span>
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider opacity-70">{label}</span>
|
||||
|
||||
@@ -5,6 +5,33 @@ import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useCart } from "@/context/CartContext";
|
||||
|
||||
const badgeConfig: Record<"best-seller" | "new" | "limited" | "organic", { label: string; bg: string; text: string; border: string }> = {
|
||||
"best-seller": {
|
||||
label: "Best Seller",
|
||||
bg: "bg-amber-50",
|
||||
text: "text-amber-700",
|
||||
border: "border-amber-200",
|
||||
},
|
||||
"new": {
|
||||
label: "New",
|
||||
bg: "bg-emerald-50",
|
||||
text: "text-emerald-700",
|
||||
border: "border-emerald-200",
|
||||
},
|
||||
"limited": {
|
||||
label: "Limited",
|
||||
bg: "bg-orange-50",
|
||||
text: "text-orange-700",
|
||||
border: "border-orange-200",
|
||||
},
|
||||
"organic": {
|
||||
label: "Organic",
|
||||
bg: "bg-green-50",
|
||||
text: "text-green-700",
|
||||
border: "border-green-200",
|
||||
},
|
||||
};
|
||||
|
||||
type ProductCardProps = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -36,33 +63,6 @@ function ImageSkeleton() {
|
||||
|
||||
// Badge component
|
||||
function Badge({ type }: { type: "best-seller" | "new" | "limited" | "organic" }) {
|
||||
const badgeConfig = {
|
||||
"best-seller": {
|
||||
label: "Best Seller",
|
||||
bg: "bg-amber-50",
|
||||
text: "text-amber-700",
|
||||
border: "border-amber-200",
|
||||
},
|
||||
"new": {
|
||||
label: "New",
|
||||
bg: "bg-emerald-50",
|
||||
text: "text-emerald-700",
|
||||
border: "border-emerald-200",
|
||||
},
|
||||
"limited": {
|
||||
label: "Limited",
|
||||
bg: "bg-orange-50",
|
||||
text: "text-orange-700",
|
||||
border: "border-orange-200",
|
||||
},
|
||||
"organic": {
|
||||
label: "Organic",
|
||||
bg: "bg-green-50",
|
||||
text: "text-green-700",
|
||||
border: "border-green-200",
|
||||
},
|
||||
};
|
||||
|
||||
const config = badgeConfig[type];
|
||||
|
||||
return (
|
||||
|
||||
@@ -248,7 +248,7 @@ interface ProgressIndicatorProps {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function ProgressIndicator({
|
||||
function ProgressIndicator({
|
||||
className = "",
|
||||
color = "#10b981",
|
||||
}: ProgressIndicatorProps) {
|
||||
|
||||
@@ -11,16 +11,17 @@ type Props = {
|
||||
fulfilling: string | null;
|
||||
};
|
||||
|
||||
const statusBadgeMap: Record<string, string> = {
|
||||
pending: "bg-yellow-100 text-yellow-700",
|
||||
awaiting_deposit: "bg-purple-100 text-purple-700",
|
||||
confirmed: "bg-blue-100 text-blue-700",
|
||||
fulfilled: "bg-green-100 text-green-700",
|
||||
cancelled: "bg-red-100 text-red-700",
|
||||
};
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const map: Record<string, string> = {
|
||||
pending: "bg-yellow-100 text-yellow-700",
|
||||
awaiting_deposit: "bg-purple-100 text-purple-700",
|
||||
confirmed: "bg-blue-100 text-blue-700",
|
||||
fulfilled: "bg-green-100 text-green-700",
|
||||
cancelled: "bg-red-100 text-red-700",
|
||||
};
|
||||
return (
|
||||
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${map[status] ?? "bg-slate-100 text-slate-600"}`}>
|
||||
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${statusBadgeMap[status] ?? "bg-slate-100 text-slate-600"}`}>
|
||||
{status.replace(/_/g, " ")}
|
||||
</span>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user