From bd6d9a06fad6a8621bce08abfb60267e42a000ce Mon Sep 17 00:00:00 2001 From: Marvelous Felix Date: Mon, 27 Apr 2026 16:03:08 +0000 Subject: [PATCH] feat(ui): add EmptyState component and apply consistently across lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create reusable EmptyState component (src/components/ui/EmptyState.tsx) with icon, title, description, action, and className props - Export EmptyState from the components barrel (src/components/index.ts) - Replace inconsistent inline empty state patterns in: - SearchResults: plain text → EmptyState with SearchX icon - VirtualizedSearchResults: CSS-class div → EmptyState with SearchX icon - NotificationCenter (providers): emoji div → EmptyState with BellOff icon - NotificationCenter (app): custom markup → EmptyState with action support - ConversationList: gradient icon div → EmptyState with dynamic messaging - DashboardGrid: heading/paragraph/button → EmptyState with action prop - SortableList: dashed border div → EmptyState with className override - SharedResourceLibrary: two inline divs → two EmptyState instances Closes # --- .../components/dashboard/DashboardGrid.tsx | 25 ++++++------ .../components/messaging/ConversationList.tsx | 19 +++------ .../notifications/NotificationCenter.tsx | 29 +++++++------- src/app/components/search/SearchResults.tsx | 11 ++---- .../social/SharedResourceLibrary.tsx | 17 +++++--- src/components/drag-drop/SortableList.tsx | 10 +++-- src/components/index.ts | 1 + src/components/notificationcenter.tsx | 7 ++-- src/components/ui/EmptyState.tsx | 39 +++++++++++++++++++ src/components/virtualizedsearchresults.tsx | 8 ++-- 10 files changed, 102 insertions(+), 64 deletions(-) create mode 100644 src/components/ui/EmptyState.tsx diff --git a/src/app/components/dashboard/DashboardGrid.tsx b/src/app/components/dashboard/DashboardGrid.tsx index 6c063689..1c97fba6 100644 --- a/src/app/components/dashboard/DashboardGrid.tsx +++ b/src/app/components/dashboard/DashboardGrid.tsx @@ -22,6 +22,7 @@ import { CSS } from '@dnd-kit/utilities'; import { Settings, Plus, Grid3X3, Calendar } from 'lucide-react'; import dynamic from 'next/dynamic'; import { useDashboardWidgets } from '../../hooks/useDashboardWidgets'; +import { EmptyState } from '@/components'; const ProgressSummaryWidget = dynamic( () => import('./widgets/ProgressSummaryWidget').then((mod) => mod.ProgressSummaryWidget), @@ -524,17 +525,19 @@ export const DashboardGrid: React.FC = ({ {/* Empty State */} {widgets.length === 0 && ( -
- -

No widgets yet

-

Add your first widget to get started

- -
+ setShowAddWidget(true)} + className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors" + > + Add Widget + + } + /> )} diff --git a/src/app/components/messaging/ConversationList.tsx b/src/app/components/messaging/ConversationList.tsx index 48b3cda0..1584b1ee 100644 --- a/src/app/components/messaging/ConversationList.tsx +++ b/src/app/components/messaging/ConversationList.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { formatDistanceToNow } from 'date-fns'; import { FiSearch, FiMessageCircle } from 'react-icons/fi'; import type { Conversation } from '@/app/store/messagingStore'; +import { EmptyState } from '@/components'; interface ConversationListProps { conversations: Conversation[]; @@ -95,19 +96,11 @@ export default function ConversationList({ ))} ) : conversations.length === 0 ? ( -
-
- -
-

- {searchQuery ? 'No conversations found' : 'No conversations yet'} -

-

- {searchQuery - ? 'Try a different search term' - : 'Start a new conversation to begin messaging'} -

-
+ ) : (
{conversations.map((conversation) => { diff --git a/src/app/components/notifications/NotificationCenter.tsx b/src/app/components/notifications/NotificationCenter.tsx index e9350a25..2cbf758a 100644 --- a/src/app/components/notifications/NotificationCenter.tsx +++ b/src/app/components/notifications/NotificationCenter.tsx @@ -24,6 +24,7 @@ import { truncateMessage, groupNotificationsByDate, } from '@/utils/notificationUtils'; +import { EmptyState } from '@/components'; interface NotificationCenterProps { userId?: string; @@ -322,20 +323,20 @@ export default function NotificationCenter({ {/* Notifications List */}
{processedNotifications.length === 0 ? ( -
- -

- {hasActiveFilters ? 'No notifications match your filters' : "You're all caught up!"} -

- {hasActiveFilters && ( - - )} -
+ + Clear filters + + ) : undefined + } + /> ) : ( Array.from(groupedNotifications.entries()).map(([date, dateNotifications]) => (
diff --git a/src/app/components/search/SearchResults.tsx b/src/app/components/search/SearchResults.tsx index 03b448d2..301a210a 100644 --- a/src/app/components/search/SearchResults.tsx +++ b/src/app/components/search/SearchResults.tsx @@ -1,9 +1,10 @@ 'use client'; import React from 'react'; -import { Star, Clock, User, ArrowRight } from 'lucide-react'; +import { Star, Clock, User, ArrowRight, SearchX } from 'lucide-react'; import Link from 'next/link'; import clsx from 'clsx'; +import { EmptyState } from '@/components'; export interface CourseResult { id: string; @@ -44,13 +45,7 @@ export const SearchResults: React.FC = ({ } if (!results || results.length === 0) { - return ( -
-

- No courses found matching your criteria -

-
- ); + return ; } const getPriceDisplay = (price: number, originalPrice?: number | null) => { diff --git a/src/app/components/social/SharedResourceLibrary.tsx b/src/app/components/social/SharedResourceLibrary.tsx index 0ff2c667..03cef8ec 100644 --- a/src/app/components/social/SharedResourceLibrary.tsx +++ b/src/app/components/social/SharedResourceLibrary.tsx @@ -4,6 +4,7 @@ import React, { useMemo, useState } from 'react'; import { motion } from 'framer-motion'; import { FolderOpen, Link as LinkIcon, Upload, Search, X, ExternalLink } from 'lucide-react'; import type { GroupResource } from '@/app/hooks/useStudyGroups'; +import { EmptyState } from '@/components'; interface SharedResourceLibraryProps { resources: GroupResource[]; @@ -209,9 +210,11 @@ export default function SharedResourceLibrary({ resources, onAdd }: SharedResour ))} {filteredResources.filter((r) => r.type === 'link').length === 0 && ( -
- {searchQuery || filterType !== 'all' ? 'No matching links found.' : 'No links yet.'} -
+ )}
@@ -255,9 +258,11 @@ export default function SharedResourceLibrary({ resources, onAdd }: SharedResour ))} {filteredResources.filter((r) => r.type === 'file').length === 0 && ( -
- {searchQuery || filterType !== 'all' ? 'No matching files found.' : 'No files yet.'} -
+ )}
diff --git a/src/components/drag-drop/SortableList.tsx b/src/components/drag-drop/SortableList.tsx index 7ae9e7a3..0b109d14 100644 --- a/src/components/drag-drop/SortableList.tsx +++ b/src/components/drag-drop/SortableList.tsx @@ -2,7 +2,9 @@ import React, { useRef, useState } from 'react'; import { useDrag, useDrop } from 'react-dnd'; +import { GripVertical } from 'lucide-react'; import { DragDropItem } from '../../utils/dragDropUtils'; +import { EmptyState } from '@/components'; export const DRAG_ITEM_TYPE = 'COURSE_CONTENT_ITEM'; @@ -157,9 +159,11 @@ export const SortableList = ({ }: SortableListProps) => { if (items.length === 0) { return ( -
- {emptyText} -
+ ); } diff --git a/src/components/index.ts b/src/components/index.ts index 1430e5b1..c51b3812 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,5 +5,6 @@ */ export * from './ui/Toast'; +export * from './ui/EmptyState'; export * from './shared/EnvGuard'; export * from './errors/ErrorBoundarySystem'; diff --git a/src/components/notificationcenter.tsx b/src/components/notificationcenter.tsx index 27310a07..4398b8ad 100644 --- a/src/components/notificationcenter.tsx +++ b/src/components/notificationcenter.tsx @@ -1,9 +1,11 @@ import React, { useState, useRef, useEffect } from "react"; +import { BellOff } from "lucide-react"; import { Notification, NotificationType, useNotifications, } from "@/providers/Notificationprovider"; +import { EmptyState } from "@/components"; // ────────────────────────────────────────────────────────────────────────────── // Icon helpers @@ -165,10 +167,7 @@ export function NotificationCenter() {
{notifications.length === 0 ? ( -
- -

You’re all caught up!

-
+ ) : ( notifications.map((n) => ( = ({ + icon: Icon, + title, + description, + action, + className = '', +}) => ( +
+ {Icon && ( +
+); diff --git a/src/components/virtualizedsearchresults.tsx b/src/components/virtualizedsearchresults.tsx index a26c1595..bc1c98b1 100644 --- a/src/components/virtualizedsearchresults.tsx +++ b/src/components/virtualizedsearchresults.tsx @@ -1,6 +1,8 @@ import React, { useCallback, memo, useMemo } from "react"; import { VariableSizeList as List, ListChildComponentProps } from "react-window"; import AutoSizer from "react-virtualized-auto-sizer"; +import { SearchX } from "lucide-react"; +import { EmptyState } from "@/components"; export type SearchResultType = "course" | "user" | "post" | "file"; @@ -110,11 +112,7 @@ const VirtualizedSearchResults: React.FC = ({ } if (results.length === 0 && query) { - return ( -
-

No results found for “{query}”

-
- ); + return ; } return (