Skip to content
Merged
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
25 changes: 14 additions & 11 deletions src/app/components/dashboard/DashboardGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -524,17 +525,19 @@ export const DashboardGrid: React.FC<DashboardGridProps> = ({

{/* Empty State */}
{widgets.length === 0 && (
<div className="text-center py-12">
<Grid3X3 size={64} className="mx-auto text-gray-300 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No widgets yet</h3>
<p className="text-gray-600 mb-4">Add your first widget to get started</p>
<button
onClick={() => setShowAddWidget(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Add Widget
</button>
</div>
<EmptyState
icon={Grid3X3}
title="No widgets yet"
description="Add your first widget to get started"
action={
<button
onClick={() => setShowAddWidget(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Add Widget
</button>
}
/>
)}
</div>
</div>
Expand Down
19 changes: 6 additions & 13 deletions src/app/components/messaging/ConversationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -95,19 +96,11 @@ export default function ConversationList({
))}
</div>
) : conversations.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 px-6 text-center">
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-violet-100 to-purple-100 dark:from-violet-900/30 dark:to-purple-900/30 flex items-center justify-center mb-4">
<FiMessageCircle className="w-7 h-7 text-violet-500" />
</div>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{searchQuery ? 'No conversations found' : 'No conversations yet'}
</p>
<p className="text-xs text-gray-400">
{searchQuery
? 'Try a different search term'
: 'Start a new conversation to begin messaging'}
</p>
</div>
<EmptyState
icon={FiMessageCircle as any}
title={searchQuery ? 'No conversations found' : 'No conversations yet'}
description={searchQuery ? 'Try a different search term' : 'Start a new conversation to begin messaging'}
/>
) : (
<div className="space-y-0.5 px-2">
{conversations.map((conversation) => {
Expand Down
29 changes: 15 additions & 14 deletions src/app/components/notifications/NotificationCenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
truncateMessage,
groupNotificationsByDate,
} from '@/utils/notificationUtils';
import { EmptyState } from '@/components';

interface NotificationCenterProps {
userId?: string;
Expand Down Expand Up @@ -322,20 +323,20 @@ export default function NotificationCenter({
{/* Notifications List */}
<div className="overflow-y-auto" style={{ maxHeight }}>
{processedNotifications.length === 0 ? (
<div className="p-8 text-center">
<Bell size={48} className="mx-auto text-gray-300 mb-3" />
<p className="text-gray-500">
{hasActiveFilters ? 'No notifications match your filters' : "You're all caught up!"}
</p>
{hasActiveFilters && (
<button
onClick={clearFilters}
className="mt-2 text-sm text-blue-600 hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
>
Clear filters
</button>
)}
</div>
<EmptyState
icon={Bell}
title={hasActiveFilters ? 'No notifications match your filters' : "You're all caught up!"}
action={
hasActiveFilters ? (
<button
onClick={clearFilters}
className="text-sm text-blue-600 hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
>
Clear filters
</button>
) : undefined
}
/>
) : (
Array.from(groupedNotifications.entries()).map(([date, dateNotifications]) => (
<div key={date}>
Expand Down
11 changes: 3 additions & 8 deletions src/app/components/search/SearchResults.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -44,13 +45,7 @@ export const SearchResults: React.FC<SearchResultsProps> = ({
}

if (!results || results.length === 0) {
return (
<div className="text-center py-12">
<p className="text-gray-500 dark:text-gray-400 text-lg">
No courses found matching your criteria
</p>
</div>
);
return <EmptyState icon={SearchX} title="No courses found" description="Try adjusting your search or filters" />;
}

const getPriceDisplay = (price: number, originalPrice?: number | null) => {
Expand Down
17 changes: 11 additions & 6 deletions src/app/components/social/SharedResourceLibrary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -209,9 +210,11 @@ export default function SharedResourceLibrary({ resources, onAdd }: SharedResour
</motion.a>
))}
{filteredResources.filter((r) => r.type === 'link').length === 0 && (
<div className="text-sm text-gray-500 dark:text-gray-400 p-4 text-center border border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
{searchQuery || filterType !== 'all' ? 'No matching links found.' : 'No links yet.'}
</div>
<EmptyState
icon={LinkIcon}
title={searchQuery || filterType !== 'all' ? 'No matching links found' : 'No links yet'}
className="border border-dashed border-gray-300 dark:border-gray-600 rounded-lg py-6"
/>
)}
</div>
</div>
Expand Down Expand Up @@ -255,9 +258,11 @@ export default function SharedResourceLibrary({ resources, onAdd }: SharedResour
</motion.a>
))}
{filteredResources.filter((r) => r.type === 'file').length === 0 && (
<div className="text-sm text-gray-500 dark:text-gray-400 p-4 text-center border border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
{searchQuery || filterType !== 'all' ? 'No matching files found.' : 'No files yet.'}
</div>
<EmptyState
icon={FolderOpen}
title={searchQuery || filterType !== 'all' ? 'No matching files found' : 'No files yet'}
className="border border-dashed border-gray-300 dark:border-gray-600 rounded-lg py-6"
/>
)}
</div>
</div>
Expand Down
10 changes: 7 additions & 3 deletions src/components/drag-drop/SortableList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -157,9 +159,11 @@ export const SortableList = ({
}: SortableListProps) => {
if (items.length === 0) {
return (
<div className="rounded-md border border-dashed border-slate-300 bg-slate-50 p-4 text-center text-sm text-slate-500">
{emptyText}
</div>
<EmptyState
icon={GripVertical}
title={emptyText}
className="border border-dashed border-slate-300 dark:border-slate-600 rounded-md bg-slate-50 dark:bg-slate-900/30 py-8"
/>
);
}

Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
*/

export * from './ui/Toast';
export * from './ui/EmptyState';
export * from './shared/EnvGuard';
export * from './errors/ErrorBoundarySystem';
7 changes: 3 additions & 4 deletions src/components/notificationcenter.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -165,10 +167,7 @@ export function NotificationCenter() {

<div className="notification-panel__list">
{notifications.length === 0 ? (
<div className="notification-empty">
<span aria-hidden="true">🔕</span>
<p>You&rsquo;re all caught up!</p>
</div>
<EmptyState icon={BellOff} title="You're all caught up!" className="py-8" />
) : (
notifications.map((n) => (
<NotificationItem
Expand Down
39 changes: 39 additions & 0 deletions src/components/ui/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';

import React from 'react';
import { LucideIcon } from 'lucide-react';

interface EmptyStateProps {
icon?: LucideIcon;
title: string;
description?: string;
action?: React.ReactNode;
className?: string;
}

export const EmptyState: React.FC<EmptyStateProps> = ({
icon: Icon,
title,
description,
action,
className = '',
}) => (
<div
className={`flex flex-col items-center justify-center py-12 px-6 text-center ${className}`}
role="status"
aria-label={title}
>
{Icon && (
<Icon
size={48}
className="text-gray-300 dark:text-gray-600 mb-4"
aria-hidden="true"
/>
)}
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{title}</p>
{description && (
<p className="text-xs text-gray-400 dark:text-gray-500 mb-4">{description}</p>
)}
{action}
</div>
);
8 changes: 3 additions & 5 deletions src/components/virtualizedsearchresults.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -110,11 +112,7 @@ const VirtualizedSearchResults: React.FC<VirtualizedSearchResultsProps> = ({
}

if (results.length === 0 && query) {
return (
<div className="search-results-empty">
<p>No results found for &ldquo;{query}&rdquo;</p>
</div>
);
return <EmptyState icon={SearchX} title={`No results for "${query}"`} description="Try a different search term" />;
}

return (
Expand Down
Loading