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:
@@ -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
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user