Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions components/ui/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';

export interface EmptyStateAction {
label: string;
onClick: () => void;
}

export interface EmptyStateProps {
illustration?: React.ReactNode;
title: string;
description?: string;
primaryAction?: EmptyStateAction;
secondaryAction?: EmptyStateAction;
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
className?: string;
children?: React.ReactNode;
}

export function EmptyState({
illustration,
title,
description,
primaryAction,
secondaryAction,
headingLevel = 2,
className,
children,
}: EmptyStateProps) {
const HeadingTag = `h${headingLevel}` as keyof JSX.IntrinsicElements;

return (
<div
className={cn(
'flex min-h-[280px] flex-col items-center justify-center px-6 py-12 text-center',
className
)}
role="region"
aria-label={title}
>
<div className="mx-auto flex max-w-sm flex-col items-center gap-4">
{illustration && (
<div aria-hidden="true" className="mb-2">
{illustration}
</div>
)}
<HeadingTag className="text-lg font-semibold text-foreground">
{title}
</HeadingTag>
{description && (
<p className="text-sm text-on-muted">{description}</p>
)}
{(primaryAction || secondaryAction) && (
<div className="mt-2 flex items-center gap-3">
{primaryAction && (
<Button onClick={primaryAction.onClick}>
{primaryAction.label}
</Button>
)}
{secondaryAction && (
<Button variant="outline" onClick={secondaryAction.onClick}>
{secondaryAction.label}
</Button>
)}
</div>
)}
{children}
</div>
</div>
);
}

export default EmptyState;
4 changes: 4 additions & 0 deletions components/ui/EmptyState/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { NoCollectionsIllustration } from './no-collections';
export { NoResultsIllustration } from './no-results';
export { NoRecentlyPlayedIllustration } from './no-recently-played';
export { NoFollowersIllustration } from './no-followers';
26 changes: 26 additions & 0 deletions components/ui/EmptyState/no-collections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';

export function NoCollectionsIllustration(props: React.ComponentProps<'svg'>) {
return (
<svg
width="120"
height="120"
viewBox="0 0 120 120"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
{...props}
>
<rect x="10" y="30" width="100" height="70" rx="8" className="fill-border-muted/30" />
<rect x="10" y="30" width="100" height="70" rx="8" className="stroke-border-muted" strokeWidth="2" fill="none" />
<rect x="20" y="42" width="36" height="36" rx="4" className="fill-border-muted/20" />
<rect x="20" y="42" width="36" height="36" rx="4" className="stroke-border-muted" strokeWidth="1.5" fill="none" />
<rect x="62" y="42" width="36" height="10" rx="2" className="fill-on-muted/30" />
<rect x="62" y="56" width="24" height="6" rx="2" className="fill-on-muted/20" />
<circle cx="38" cy="60" r="6" className="fill-brand/40" />
<path d="M35 60h6M38 57v6" stroke="white" strokeWidth="1.5" strokeLinecap="round" />
<rect x="40" y="80" width="60" height="2" rx="1" className="fill-border-muted/20" />
<rect x="15" y="34" width="20" height="4" rx="2" className="fill-on-muted/40" />
</svg>
);
}
25 changes: 25 additions & 0 deletions components/ui/EmptyState/no-followers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';

export function NoFollowersIllustration(props: React.ComponentProps<'svg'>) {
return (
<svg
width="120"
height="120"
viewBox="0 0 120 120"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
{...props}
>
<circle cx="45" cy="38" r="14" className="stroke-border-muted" strokeWidth="2" fill="none" />
<circle cx="45" cy="38" r="5" className="fill-border-muted/20" />
<path d="M28 72c0-11 7.6-20 17-20s17 9 17 20" className="stroke-border-muted" strokeWidth="2" fill="none" />
<circle cx="80" cy="45" r="10" className="stroke-border-muted" strokeWidth="1.5" fill="none" />
<circle cx="80" cy="45" r="3.5" className="fill-border-muted/15" />
<path d="M68 68c0-8 5.4-14 12-14s12 6 12 14" className="stroke-border-muted" strokeWidth="1.5" fill="none" />
<path d="M10 105l6-6M16 105l-6-6" className="stroke-border-muted" strokeWidth="2" strokeLinecap="round" />
<path d="M108 18l-6 6M114 18l-6-6" className="stroke-border-muted" strokeWidth="2" strokeLinecap="round" />
<path d="M45 58v-6M42 55h6" className="stroke-brand/50" strokeWidth="2" strokeLinecap="round" />
</svg>
);
}
34 changes: 34 additions & 0 deletions components/ui/EmptyState/no-recently-played.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as React from 'react';

export function NoRecentlyPlayedIllustration(props: React.ComponentProps<'svg'>) {
return (
<svg
width="120"
height="120"
viewBox="0 0 120 120"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
{...props}
>
<circle cx="60" cy="55" r="35" className="stroke-border-muted" strokeWidth="2" fill="none" />
<path d="M60 35v20l15 9" className="stroke-brand" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="60" cy="55" r="6" className="fill-brand/30" />
<rect x="48" y="78" width="24" height="3" rx="1.5" className="fill-on-muted/20" />
<rect x="52" y="84" width="16" height="2" rx="1" className="fill-on-muted/15" />
<path
d="M30 105l-6-6M36 105l-6-6"
className="stroke-border-muted"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M90 15l-6 6M96 15l-6-6"
className="stroke-border-muted"
strokeWidth="2"
strokeLinecap="round"
/>
<path d="M75 20l4-4M20 70l4-4" className="stroke-on-muted/30" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
}
24 changes: 24 additions & 0 deletions components/ui/EmptyState/no-results.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';

export function NoResultsIllustration(props: React.ComponentProps<'svg'>) {
return (
<svg
width="120"
height="120"
viewBox="0 0 120 120"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
{...props}
>
<circle cx="50" cy="50" r="30" className="stroke-border-muted" strokeWidth="2" fill="none" />
<circle cx="50" cy="50" r="12" className="fill-border-muted/20" />
<path d="M72 72l18 18" className="stroke-on-muted" strokeWidth="3" strokeLinecap="round" />
<rect x="30" y="40" width="40" height="3" rx="1.5" className="fill-on-muted/20" />
<rect x="35" y="48" width="30" height="2" rx="1" className="fill-on-muted/15" />
<rect x="38" y="54" width="24" height="2" rx="1" className="fill-on-muted/10" />
<path d="M20 95l8-8M28 95l-8-8" className="stroke-border-muted" strokeWidth="2" strokeLinecap="round" />
<path d="M100 25l-8-8M108 25l-8-8" className="stroke-border-muted" strokeWidth="2" strokeLinecap="round" />
</svg>
);
}