DocDoeAI / src /components /shared /SharedEmptyState.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
6.54 kB
"use client";
import Link from "next/link";
import { HTMLAttributes } from "react";
import { clsx } from "clsx";
// =============================================================================
// EmptyState Types
// =============================================================================
export interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {
icon?: React.ReactNode;
title: string;
description?: string;
actionText?: string;
actionHref?: string;
actionOnClick?: () => void;
actionVariant?: "primary" | "secondary" | "ghost";
compact?: boolean;
}
// =============================================================================
// SharedEmptyState Component
// =============================================================================
export function SharedEmptyState({
icon,
title,
description,
actionText,
actionHref,
actionOnClick,
actionVariant = "primary",
compact = false,
className,
...props
}: EmptyStateProps) {
const variantClasses = {
primary: "bg-[var(--accent-primary)] text-white hover:bg-[var(--accent-primary-hover)]",
secondary: "bg-[var(--bg-surface)] text-[var(--text-primary)] border border-[var(--border-subtle)] hover:bg-[var(--bg-elevated)]",
ghost: "bg-transparent text-[var(--text-secondary)] hover:bg-[var(--bg-glass)]",
};
return (
<div
className={clsx(
"flex flex-col items-center justify-center",
compact ? "gap-3 p-6" : "gap-4 p-8 sm:p-12",
"rounded-xl",
"border border-[var(--border-subtle)]",
"bg-[var(--bg-glass)]",
"text-center",
className,
)}
{...props}
>
{/* Icon */}
{icon && (
<div
className={clsx(
"grid place-items-center",
compact ? "size-10" : "size-14",
"rounded-full",
"bg-[var(--accent-primary)]/10",
)}
>
{icon}
</div>
)}
{/* Title */}
<h3
className={clsx(
compact ? "text-[16px]" : "text-[18px]",
"font-bold",
"text-[var(--text-primary)]",
)}
>
{title}
</h3>
{/* Description */}
{description && (
<p
className={clsx(
compact ? "text-[13px]" : "text-[14px]",
"leading-relaxed",
"text-[var(--text-secondary)]",
"max-w-[400px]",
)}
>
{description}
</p>
)}
{/* Action */}
{(actionText || actionHref || actionOnClick) && (
<div className="mt-2">
{actionHref ? (
<Link
href={actionHref}
className={clsx(
"inline-flex items-center justify-center gap-2",
"h-[40px] px-4",
"rounded-lg",
"text-[13px] font-semibold",
"transition",
variantClasses[actionVariant],
)}
>
{actionText}
</Link>
) : actionOnClick ? (
<button
type="button"
onClick={actionOnClick}
className={clsx(
"inline-flex items-center justify-center gap-2",
"h-[40px] px-4",
"rounded-lg",
"text-[13px] font-semibold",
"transition",
variantClasses[actionVariant],
)}
>
{actionText}
</button>
) : null}
</div>
)}
</div>
);
}
// =============================================================================
// Convenience Empty State Components
// =============================================================================
// No results empty state
export function NoResultsEmptyState({
searchQuery,
onClear,
className,
...props
}: { searchQuery?: string; onClear?: () => void } & Omit<EmptyStateProps, "title" | "description">) {
return (
<SharedEmptyState
title="No results found"
description={
searchQuery
? `No results match "${searchQuery}". Try different keywords.`
: "No items found. Check back later."
}
actionText={searchQuery ? "Clear search" : undefined}
actionOnClick={onClear}
actionVariant="ghost"
className={className}
{...props}
/>
);
}
// No content empty state
export function NoContentEmptyState({
type,
onCreate,
className,
...props
}: { type: string; onCreate?: () => void } & Omit<EmptyStateProps, "title" | "description">) {
const messages: Record<string, { title: string; description: string }> = {
documents: {
title: "No documents uploaded",
description: "Upload your PDFs or notes to start studying with DocDoe.",
},
chapters: {
title: "No chapters found",
description: "Select a subject to see available chapters.",
},
progress: {
title: "No study progress yet",
description: "Complete lessons and quizzes to track your progress.",
},
bookmarks: {
title: "No bookmarks saved",
description: "Save important topics to revisit later.",
},
default: {
title: "No content available",
description: "Check back later for updates.",
},
};
const message = messages[type] || messages.default;
return (
<SharedEmptyState
title={message.title}
description={message.description}
actionText={onCreate ? `Add ${type}` : undefined}
actionOnClick={onCreate}
className={className}
{...props}
/>
);
}
// Error empty state
export function ErrorEmptyState({
errorMessage,
onRetry,
className,
...props
}: { errorMessage?: string; onRetry?: () => void } & Omit<EmptyStateProps, "title" | "description">) {
return (
<SharedEmptyState
title="Could not complete this"
description={errorMessage || "We couldn't load this content. Please try again."}
actionText="Try again"
actionOnClick={onRetry}
actionVariant="primary"
className={className}
{...props}
/>
);
}
// Loading placeholder (not a skeleton, but a full empty state)
export function LoadingEmptyState({
message = "Loading...",
className,
...props
}: { message?: string } & Omit<EmptyStateProps, "title" | "description">) {
return (
<SharedEmptyState
title=""
description={message}
compact
className={clsx("min-h-[120px]", className)}
{...props}
/>
);
}