From 6cc57156d2be03abcbc01f14977058d9bb3565a3 Mon Sep 17 00:00:00 2001 From: myusername Date: Fri, 22 May 2026 11:56:38 +0530 Subject: [PATCH] feat: EmptyState composite component created. --- .../EmptyState/EmptyState.stories.tsx | 116 ++++++++++++++++++ .../composites/EmptyState/EmptyState.tsx | 68 ++++++++++ src/shared/composites/EmptyState/index.ts | 3 + src/shared/illustrations/FilterEmptyIcon.tsx | 26 ++++ src/shared/illustrations/NoAnalyticsIcon.tsx | 80 ++++++++++++ src/shared/illustrations/NoNotifications.tsx | 34 +++++ .../illustrations/NoResutsFoundIcon.tsx | 29 +++++ src/shared/illustrations/NoUsersIcon.tsx | 21 ++++ src/views/LoginPage.tsx | 49 ++++++-- 9 files changed, 413 insertions(+), 13 deletions(-) create mode 100644 src/shared/composites/EmptyState/EmptyState.stories.tsx create mode 100644 src/shared/composites/EmptyState/EmptyState.tsx create mode 100644 src/shared/composites/EmptyState/index.ts create mode 100644 src/shared/illustrations/FilterEmptyIcon.tsx create mode 100644 src/shared/illustrations/NoAnalyticsIcon.tsx create mode 100644 src/shared/illustrations/NoNotifications.tsx create mode 100644 src/shared/illustrations/NoResutsFoundIcon.tsx create mode 100644 src/shared/illustrations/NoUsersIcon.tsx diff --git a/src/shared/composites/EmptyState/EmptyState.stories.tsx b/src/shared/composites/EmptyState/EmptyState.stories.tsx new file mode 100644 index 0000000..020ed84 --- /dev/null +++ b/src/shared/composites/EmptyState/EmptyState.stories.tsx @@ -0,0 +1,116 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { CheckCircleIcon } from '@heroicons/react/24/outline' + +import { EmptyState } from './EmptyState' +import FilterEmptyIcon from '@/shared/illustrations/FilterEmptyIcon' +import NoUsersIcon from '@/shared/illustrations/NoUsersIcon' +import NoAnalyticsIcon from '@/shared/illustrations/NoAnalyticsIcon' +import { SearchWindowIcon } from '@/shared/illustrations/NoResutsFoundIcon' +import { BellSlashIcon } from '@/shared/illustrations/NoNotifications' + +const meta = { + title: 'Shared/Composites/EmptyState', + + component: EmptyState, + + tags: ['autodocs'], + + parameters: { + layout: 'fullscreen', + }, + + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + icon: , + + title: 'No search results found.', + + description: 'Try again using more general search terms', + }, +} +export const QueueEmpty: Story = { + args: { + icon: , + + title: 'Queue is clear', + + description: 'No reports are awaiting moderation right now.', + }, +} + +export const FilterEmpty: Story = { + args: { + icon: , + + title: 'No reports match this filter', + + description: 'Try adjusting or clearing your filters.', + + action: { + label: 'Clear filters', + + onClick: () => { + console.log('Clear filters clicked') + }, + }, + }, +} + +export const NoUsers: Story = { + args: { + icon: , + + title: 'No users found', + description: 'No such user found.', + }, +} + +export const AnalyticsNoData: Story = { + args: { + icon: , + + title: 'Not enough data yet', + + description: 'Analytics will appear once enough moderation activity has been collected.', + }, +} + +export const WithoutDescription: Story = { + args: { + icon: , + + title: 'No notifications available', + }, +} + +export const AccessibilityPreview: Story = { + args: { + icon: , + + title: 'Queue is clear', + + description: 'No reports are awaiting moderation right now.', + + action: { + label: 'Refresh queue', + + onClick: () => { + console.log('Refresh queue clicked') + }, + }, + }, +} diff --git a/src/shared/composites/EmptyState/EmptyState.tsx b/src/shared/composites/EmptyState/EmptyState.tsx new file mode 100644 index 0000000..d2d7e50 --- /dev/null +++ b/src/shared/composites/EmptyState/EmptyState.tsx @@ -0,0 +1,68 @@ +import React from 'react' + +import { Button } from '@/shared/primitives/Button' + +export interface EmptyStateProps { + icon?: React.ReactNode + + title: string + + description?: string + + action?: { + label: string + + onClick: () => void + } + + className?: string +} + +export function EmptyState({ + icon, + + title, + + description, + + action, + + className = '', +}: EmptyStateProps) { + return ( +
+ {/* Icon */} + {icon && ( +
{icon}
+ )} + + {/* Title */} +

{title}

+ + {/* Description */} + {description && ( +

{description}

+ )} + + {/* Action */} + {action && ( +
+ +
+ )} +
+ ) +} + +export default EmptyState diff --git a/src/shared/composites/EmptyState/index.ts b/src/shared/composites/EmptyState/index.ts new file mode 100644 index 0000000..88a97a1 --- /dev/null +++ b/src/shared/composites/EmptyState/index.ts @@ -0,0 +1,3 @@ +export * from './EmptyState' + +export { default } from './EmptyState' diff --git a/src/shared/illustrations/FilterEmptyIcon.tsx b/src/shared/illustrations/FilterEmptyIcon.tsx new file mode 100644 index 0000000..ce60849 --- /dev/null +++ b/src/shared/illustrations/FilterEmptyIcon.tsx @@ -0,0 +1,26 @@ +export function FilterEmptyIcon({ + size = 100, + color = 'currentColor', + // , ...props +}) { + return ( + + + + + + ) +} + +export default FilterEmptyIcon diff --git a/src/shared/illustrations/NoAnalyticsIcon.tsx b/src/shared/illustrations/NoAnalyticsIcon.tsx new file mode 100644 index 0000000..f1f74f4 --- /dev/null +++ b/src/shared/illustrations/NoAnalyticsIcon.tsx @@ -0,0 +1,80 @@ +export const NoAnalyticsIcon = ({ + size = 100, + color = 'currentColor', + strokeWidth = 3.5, + className = '', + style = {}, + ...props +}) => ( + +) + +export default NoAnalyticsIcon diff --git a/src/shared/illustrations/NoNotifications.tsx b/src/shared/illustrations/NoNotifications.tsx new file mode 100644 index 0000000..acdf4d2 --- /dev/null +++ b/src/shared/illustrations/NoNotifications.tsx @@ -0,0 +1,34 @@ +import React from 'react' + +export interface BellSlashIconProps extends React.SVGProps { + size?: number | string +} + +export const BellSlashIcon: React.FC = ({ + size = 24, + fill = 'currentColor', + ...props +}) => { + return ( + + + + + + + + + + + + + + ) +} diff --git a/src/shared/illustrations/NoResutsFoundIcon.tsx b/src/shared/illustrations/NoResutsFoundIcon.tsx new file mode 100644 index 0000000..2023a54 --- /dev/null +++ b/src/shared/illustrations/NoResutsFoundIcon.tsx @@ -0,0 +1,29 @@ +import React from 'react' + +export interface SearchWindowIconProps extends React.SVGProps { + size?: number | string +} + +export const SearchWindowIcon: React.FC = ({ + size = 24, + fill = 'currentColor', + ...props +}) => { + return ( + + + + ) +} diff --git a/src/shared/illustrations/NoUsersIcon.tsx b/src/shared/illustrations/NoUsersIcon.tsx new file mode 100644 index 0000000..3e36a35 --- /dev/null +++ b/src/shared/illustrations/NoUsersIcon.tsx @@ -0,0 +1,21 @@ +function NoUsersIcon({ size = 100, color = 'currentColor', ...props }) { + return ( + + + + ) +} + +export default NoUsersIcon diff --git a/src/views/LoginPage.tsx b/src/views/LoginPage.tsx index cb08f07..cbb1737 100644 --- a/src/views/LoginPage.tsx +++ b/src/views/LoginPage.tsx @@ -6,7 +6,9 @@ import ReportCard from '@/shared/composites/ReportCard' import { reports } from '@/shared/composites/ReportCard/dummyData' import MetricCard from '@/shared/composites/MetricCard' +import EmptyState from '@/shared/composites/EmptyState' +import { SearchWindowIcon } from '@/shared/illustrations/NoResutsFoundIcon' const LoginPage = () => { const [selected, setSelected] = useState('all') @@ -42,6 +44,9 @@ const LoginPage = () => { }, ] as const + // To test the EmptyState icon interchange the commented filteredReports + // const filteredReports = [] + const filteredReports = reports return (
{/* Header */} @@ -100,20 +105,38 @@ const LoginPage = () => { {/* Report cards */}
- {reports.map((report) => ( - { - setExpandedId(expandedId === report.id ? null : report.id) - }} - onAction={(reportId, action) => { - console.log('Action:', action, 'on', reportId) - }} - className="bg-bg-secondary cursor-pointer" + {filteredReports.length === 0 ? ( + } + title="No results found." + description=" + Try adjusting your filters + or search query. + " + // action={{ + // label: 'Clear filters', + + // onClick: () => { + // setSelected('all') + // }, + // }} /> - ))} + ) : ( + filteredReports.map((report) => ( + { + setExpandedId(expandedId === report.id ? null : report.id) + }} + onAction={(reportId, action) => { + console.log('Action:', action, 'on', reportId) + }} + className="bg-bg-secondary cursor-pointer" + /> + )) + )}
)