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
36 changes: 36 additions & 0 deletions tenant-dashboard/src/app/chat/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Skeleton } from '@/components/loading/Skeleton';

export default function ChatLoading() {
return (
<div className="flex h-[calc(100vh-4rem)]">
{/* Sidebar skeleton */}
<div className="w-64 border-r p-4 space-y-2">
<Skeleton variant="rounded" height="2.5rem" />
<div className="space-y-1 pt-4">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} variant="rounded" height="2rem" />
))}
</div>
</div>

{/* Chat area skeleton */}
<div className="flex-1 flex flex-col">
{/* Messages area */}
<div className="flex-1 p-4 space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className={`flex ${i % 2 === 0 ? 'justify-start' : 'justify-end'}`}>
<div className={`max-w-[70%] ${i % 2 === 0 ? 'order-2' : 'order-1'}`}>
<Skeleton variant="rounded" height="3rem" width="100%" />
</div>
</div>
))}
</div>

{/* Input area skeleton */}
<div className="border-t p-4">
<Skeleton variant="rounded" height="3rem" />
</div>
</div>
</div>
);
}
40 changes: 40 additions & 0 deletions tenant-dashboard/src/app/cost/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Skeleton } from '@/components/loading/Skeleton';
import { GridSkeleton } from '@/components/loading/DataTable';

export default function CostLoading() {
return (
<div className="space-y-6 p-6">
{/* Header */}
<div>
<Skeleton variant="text" height="2rem" width="200px" />
<Skeleton variant="text" height="1rem" width="350px" className="mt-2" />
</div>

{/* Summary cards */}
<div className="grid gap-4 md:grid-cols-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="rounded-lg border p-6 space-y-2">
<div className="flex items-center justify-between">
<Skeleton variant="text" height="0.875rem" width="80px" />
<Skeleton variant="circular" height="24px" width="24px" />
</div>
<Skeleton variant="text" height="2rem" width="120px" />
<Skeleton variant="text" height="0.875rem" width="150px" />
</div>
))}
</div>

{/* Chart */}
<div className="rounded-lg border p-6">
<Skeleton variant="text" height="1.5rem" width="180px" className="mb-4" />
<Skeleton variant="rectangular" height="350px" />
</div>

{/* Table */}
<div className="rounded-lg border p-6">
<Skeleton variant="text" height="1.5rem" width="150px" className="mb-4" />
<GridSkeleton items={4} columns={2} />
</div>
</div>
);
}
31 changes: 31 additions & 0 deletions tenant-dashboard/src/app/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SectionLoader } from '@/components/loading/PageLoader';
import { Skeleton } from '@/components/loading/Skeleton';

export default function DashboardLoading() {
return (
<div className="space-y-6 p-6">
{/* Header skeleton */}
<div className="flex justify-between items-center">
<Skeleton variant="text" height="2rem" width="200px" />
<Skeleton variant="rounded" height="2.5rem" width="120px" />
</div>

{/* Stats cards skeleton */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="rounded-lg border p-6 space-y-2">
<Skeleton variant="text" height="1rem" width="60%" />
<Skeleton variant="text" height="2rem" width="40%" />
<Skeleton variant="text" height="0.875rem" width="80%" />
</div>
))}
</div>

{/* Chart skeleton */}
<div className="rounded-lg border p-6">
<Skeleton variant="text" height="1.5rem" width="150px" className="mb-4" />
<Skeleton variant="rectangular" height="300px" />
</div>
</div>
);
}
20 changes: 20 additions & 0 deletions tenant-dashboard/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@
}
}

@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}

.animate-fade-in {
animation: fade-in 0.3s ease-out;
}
Expand All @@ -130,6 +139,17 @@
animation: pulse-subtle 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

.animate-shimmer {
background: linear-gradient(
90deg,
transparent 25%,
rgba(255, 255, 255, 0.2) 50%,
transparent 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}

/* Focus styles for accessibility */
.focus-ring {
@apply focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background;
Expand Down
5 changes: 5 additions & 0 deletions tenant-dashboard/src/app/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PageLoader } from '@/components/loading/PageLoader';

export default function Loading() {
return <PageLoader message="Loading PyAirtable..." />;
}
30 changes: 30 additions & 0 deletions tenant-dashboard/src/app/settings/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Skeleton } from '@/components/loading/Skeleton';

export default function SettingsLoading() {
return (
<div className="space-y-6 p-6">
<div>
<Skeleton variant="text" height="2rem" width="150px" />
<Skeleton variant="text" height="1rem" width="300px" className="mt-2" />
</div>

<div className="grid gap-6">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="rounded-lg border p-6 space-y-4">
<div className="flex items-center space-x-3">
<Skeleton variant="circular" height="40px" width="40px" />
<div className="space-y-1">
<Skeleton variant="text" height="1.25rem" width="120px" />
<Skeleton variant="text" height="0.875rem" width="200px" />
</div>
</div>
<div className="space-y-2">
<Skeleton variant="rounded" height="2.5rem" />
<Skeleton variant="rounded" height="2.5rem" />
</div>
</div>
))}
</div>
</div>
);
}
93 changes: 93 additions & 0 deletions tenant-dashboard/src/components/loading/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client';

import React from 'react';
import { Skeleton } from './Skeleton';
import { cn } from '@/lib/utils';

interface TableSkeletonProps {
rows?: number;
columns?: number;
showHeader?: boolean;
className?: string;
}

export function TableSkeleton({
rows = 5,
columns = 4,
showHeader = true,
className,
}: TableSkeletonProps) {
return (
<div className={cn('w-full', className)}>
<div className="rounded-md border">
<div className="overflow-x-auto">
<table className="w-full">
{showHeader && (
<thead className="border-b bg-muted/50">
<tr>
{Array.from({ length: columns }).map((_, i) => (
<th key={i} className="px-4 py-3">
<Skeleton
variant="text"
height="1rem"
width={`${60 + Math.random() * 40}%`}
/>
</th>
))}
</tr>
</thead>
)}
<tbody>
{Array.from({ length: rows }).map((_, rowIndex) => (
<tr key={rowIndex} className="border-b">
{Array.from({ length: columns }).map((_, colIndex) => (
<td key={colIndex} className="px-4 py-3">
<Skeleton
variant="text"
height="1rem"
width={`${40 + Math.random() * 60}%`}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

export function GridSkeleton({
items = 6,
columns = 3,
className,
}: {
items?: number;
columns?: number;
className?: string;
}) {
return (
<div
className={cn(
'grid gap-4',
columns === 2 && 'grid-cols-1 sm:grid-cols-2',
columns === 3 && 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
columns === 4 && 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
className
)}
>
{Array.from({ length: items }).map((_, i) => (
<div key={i} className="rounded-lg border p-4 space-y-3">
<Skeleton variant="rounded" height="8rem" />
<Skeleton variant="text" height="1.25rem" width="70%" />
<div className="space-y-1">
<Skeleton variant="text" height="0.875rem" />
<Skeleton variant="text" height="0.875rem" width="80%" />
</div>
</div>
))}
</div>
);
}
47 changes: 47 additions & 0 deletions tenant-dashboard/src/components/loading/LoadingButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client';

import React from 'react';
import { Button, ButtonProps } from '@/components/ui/button';
import { LoadingSpinner } from './LoadingSpinner';
import { cn } from '@/lib/utils';

interface LoadingButtonProps extends ButtonProps {
loading?: boolean;
loadingText?: string;
}

export function LoadingButton({
children,
loading = false,
loadingText,
disabled,
className,
...props
}: LoadingButtonProps) {
return (
<Button
disabled={disabled || loading}
className={cn(
'relative',
loading && 'cursor-not-allowed',
className
)}
{...props}
>
{loading && (
<LoadingSpinner
size="sm"
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
/>
)}
<span className={cn(loading && 'opacity-0')}>
{children}
</span>
{loading && loadingText && (
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 pl-6">
{loadingText}
</span>
)}
</Button>
);
}
37 changes: 37 additions & 0 deletions tenant-dashboard/src/components/loading/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import React from 'react';
import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';

interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg' | 'xl';
className?: string;
label?: string;
}

const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-6 w-6',
lg: 'h-8 w-8',
xl: 'h-12 w-12',
};

export function LoadingSpinner({
size = 'md',
className,
label = 'Loading...'
}: LoadingSpinnerProps) {
return (
<div className={cn('flex items-center justify-center', className)}>
<Loader2
className={cn(
'animate-spin text-primary',
sizeClasses[size]
)}
aria-label={label}
/>
<span className="sr-only">{label}</span>
</div>
);
}
Loading
Loading