Enhance user experience with animated loading placeholders across the platform

Implements animated loading skeletons using React and Tailwind CSS for Chat, Finances, and Tasks pages.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: d7e7c4e8-20cb-41c4-9d0e-79f48938fede
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/db413c20-caa8-482a-9f55-d510ff0c04b5.jpg
This commit is contained in:
ghaddaditw
2025-06-08 16:06:07 +00:00
parent d7f612559a
commit 871ae2c733
4 changed files with 254 additions and 14 deletions
@@ -0,0 +1,203 @@
import { cn } from "@/lib/utils"
interface SkeletonProps {
className?: string
}
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
// Card skeleton for general content
function CardSkeleton({ className }: SkeletonProps) {
return (
<div className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}>
<div className="p-6 space-y-4">
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-[180px]" />
</div>
</div>
</div>
)
}
// Task item skeleton
function TaskSkeleton({ className }: SkeletonProps) {
return (
<div className={cn("flex items-center space-x-4 p-4 border-b", className)}>
<Skeleton className="h-4 w-4 rounded" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-[200px]" />
<Skeleton className="h-3 w-[150px]" />
</div>
<div className="flex space-x-2">
<Skeleton className="h-6 w-16 rounded-full" />
<Skeleton className="h-6 w-12 rounded-full" />
</div>
</div>
)
}
// Financial record skeleton
function FinancialSkeleton({ className }: SkeletonProps) {
return (
<div className={cn("flex items-center justify-between p-4 border-b", className)}>
<div className="flex items-center space-x-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-1">
<Skeleton className="h-4 w-[120px]" />
<Skeleton className="h-3 w-[80px]" />
</div>
</div>
<div className="text-right space-y-1">
<Skeleton className="h-4 w-[60px] ml-auto" />
<Skeleton className="h-3 w-[40px] ml-auto" />
</div>
</div>
)
}
// Chat message skeleton
function MessageSkeleton({ className, isOwn = false }: SkeletonProps & { isOwn?: boolean }) {
return (
<div className={cn("flex gap-3 mb-4", isOwn && "flex-row-reverse", className)}>
{!isOwn && <Skeleton className="h-8 w-8 rounded-full" />}
<div className={cn("max-w-[70%] space-y-2", isOwn && "items-end")}>
<div className={cn(
"rounded-lg p-3 space-y-1",
isOwn ? "bg-primary" : "bg-muted"
)}>
<Skeleton className={cn("h-3", isOwn ? "bg-primary-foreground/20" : "bg-foreground/20", "w-[150px]")} />
<Skeleton className={cn("h-3", isOwn ? "bg-primary-foreground/20" : "bg-foreground/20", "w-[100px]")} />
</div>
<Skeleton className="h-2 w-[60px]" />
</div>
</div>
)
}
// Analytics chart skeleton
function ChartSkeleton({ className }: SkeletonProps) {
return (
<div className={cn("space-y-4", className)}>
<div className="flex items-center justify-between">
<Skeleton className="h-6 w-[150px]" />
<Skeleton className="h-4 w-[80px]" />
</div>
<div className="h-[300px] flex items-end justify-between space-x-2">
{Array.from({ length: 7 }).map((_, i) => (
<Skeleton
key={i}
className="w-full"
style={{ height: `${Math.random() * 60 + 40}%` }}
/>
))}
</div>
<div className="flex justify-between">
{Array.from({ length: 7 }).map((_, i) => (
<Skeleton key={i} className="h-3 w-8" />
))}
</div>
</div>
)
}
// Table skeleton
function TableSkeleton({ rows = 5, columns = 4, className }: SkeletonProps & { rows?: number; columns?: number }) {
return (
<div className={cn("space-y-3", className)}>
{/* Header */}
<div className="flex space-x-4">
{Array.from({ length: columns }).map((_, i) => (
<Skeleton key={i} className="h-4 flex-1" />
))}
</div>
{/* Rows */}
{Array.from({ length: rows }).map((_, rowIndex) => (
<div key={rowIndex} className="flex space-x-4">
{Array.from({ length: columns }).map((_, colIndex) => (
<Skeleton key={colIndex} className="h-4 flex-1" />
))}
</div>
))}
</div>
)
}
// Profile skeleton
function ProfileSkeleton({ className }: SkeletonProps) {
return (
<div className={cn("flex items-center space-x-4", className)}>
<Skeleton className="h-16 w-16 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-5 w-[180px]" />
<Skeleton className="h-4 w-[120px]" />
<Skeleton className="h-3 w-[100px]" />
</div>
</div>
)
}
// List skeleton
function ListSkeleton({ items = 5, className }: SkeletonProps & { items?: number }) {
return (
<div className={cn("space-y-3", className)}>
{Array.from({ length: items }).map((_, i) => (
<div key={i} className="flex items-center space-x-3">
<Skeleton className="h-6 w-6 rounded" />
<div className="space-y-1 flex-1">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-3 w-3/4" />
</div>
</div>
))}
</div>
)
}
// Page skeleton for full page loading
function PageSkeleton({ className }: SkeletonProps) {
return (
<div className={cn("space-y-6 p-6", className)}>
{/* Header */}
<div className="space-y-2">
<Skeleton className="h-8 w-[200px]" />
<Skeleton className="h-4 w-[300px]" />
</div>
{/* Content grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<CardSkeleton key={i} />
))}
</div>
</div>
)
}
export {
Skeleton,
CardSkeleton,
TaskSkeleton,
FinancialSkeleton,
MessageSkeleton,
ChartSkeleton,
TableSkeleton,
ProfileSkeleton,
ListSkeleton,
PageSkeleton
}
+2 -2
View File
@@ -3,13 +3,13 @@ import { useAuth } from '@/hooks/useAuth';
import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { PageHeader } from '@/components/ui/page-header'; import { PageHeader } from '@/components/ui/page-header';
import { MessageSkeleton, ListSkeleton } from '@/components/ui/loading-skeleton';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import Sidebar from '@/components/layout/Sidebar'; import Sidebar from '@/components/layout/Sidebar';
import { import {
+36 -7
View File
@@ -9,8 +9,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { PageHeader } from '@/components/ui/page-header'; import { PageHeader } from '@/components/ui/page-header';
import { FinancialSkeleton, CardSkeleton, ChartSkeleton } from '@/components/ui/loading-skeleton';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { apiRequest } from '@/lib/queryClient';
import Sidebar from '@/components/layout/Sidebar'; import Sidebar from '@/components/layout/Sidebar';
import { Plus, TrendingUp, TrendingDown, DollarSign } from 'lucide-react'; import { Plus, TrendingUp, TrendingDown, DollarSign } from 'lucide-react';
@@ -99,14 +99,43 @@ export default function FinancesPage() {
<div className="flex h-screen bg-gray-50 dark:bg-gray-900"> <div className="flex h-screen bg-gray-50 dark:bg-gray-900">
<Sidebar className="w-64 border-r" /> <Sidebar className="w-64 border-r" />
<div className="flex-1 overflow-auto p-6"> <div className="flex-1 overflow-auto p-6">
<div className="animate-pulse space-y-4"> <div className="space-y-6">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4"></div> <PageHeader
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div> title="Financial Overview"
<div className="grid grid-cols-3 gap-4"> description="Track your income and expenses"
{[...Array(3)].map((_, i) => ( />
<div key={i} className="h-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
{/* Summary cards skeleton */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{Array.from({ length: 3 }).map((_, i) => (
<CardSkeleton key={i} />
))} ))}
</div> </div>
{/* Chart skeleton */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Spending Overview</CardTitle>
</CardHeader>
<CardContent>
<ChartSkeleton />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Recent Transactions</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{Array.from({ length: 6 }).map((_, i) => (
<FinancialSkeleton key={i} />
))}
</div>
</CardContent>
</Card>
</div>
</div> </div>
</div> </div>
</div> </div>
+13 -5
View File
@@ -9,6 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { PageHeader } from '@/components/ui/page-header'; import { PageHeader } from '@/components/ui/page-header';
import { TaskSkeleton, CardSkeleton } from '@/components/ui/loading-skeleton';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { VoiceShortcuts } from '@/components/voice/VoiceShortcuts'; import { VoiceShortcuts } from '@/components/voice/VoiceShortcuts';
import Sidebar from '@/components/layout/Sidebar'; import Sidebar from '@/components/layout/Sidebar';
@@ -170,12 +171,19 @@ export default function TasksPage() {
<div className="flex h-screen bg-gray-50 dark:bg-gray-900"> <div className="flex h-screen bg-gray-50 dark:bg-gray-900">
<Sidebar className="w-64 border-r" /> <Sidebar className="w-64 border-r" />
<div className="flex-1 overflow-auto p-6"> <div className="flex-1 overflow-auto p-6">
<div className="animate-pulse space-y-4"> <div className="space-y-6">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4"></div> <PageHeader
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div> title="Tasks"
description="Manage your tasks and projects efficiently"
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<CardSkeleton key={i} />
))}
</div>
<div className="space-y-3"> <div className="space-y-3">
{[...Array(5)].map((_, i) => ( {Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="h-20 bg-gray-200 dark:bg-gray-700 rounded"></div> <TaskSkeleton key={i} />
))} ))}
</div> </div>
</div> </div>