From 48ddb0399569cd9c08b82efe00a59341cc19af3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Sat, 7 Feb 2026 15:15:02 +0300 Subject: [PATCH] feat: Add loading skeletons for improved UX - Implement loading states for dashboard, project, and video views - Introduce skeleton UIs for settings pages - Add skeletons for Navbar and charts - Enhance `Skeleton` component with a text variant - Improve perceived performance during data fetching - Update `.gitignore` with `Optimization.md` --- .gitignore | 3 +- app/(dashboard)/dashboard/loading.tsx | 48 +++++++++++ .../projects/[projectId]/loading.tsx | 53 ++++++++++++ .../projects/[projectId]/settings/loading.tsx | 67 +++++++++++++++ .../videos/[videoId]/compare/loading.tsx | 39 +++++++++ .../videos/[videoId]/compare/page.tsx | 28 ++++++- .../[projectId]/videos/[videoId]/loading.tsx | 84 +++++++++++++++++++ .../[projectId]/videos/[videoId]/page.tsx | 68 ++++++++++++++- app/(dashboard)/settings/loading.tsx | 72 ++++++++++++++++ app/(dashboard)/settings/page.tsx | 30 ++++++- app/(dashboard)/workspaces/loading.tsx | 44 ++++++++++ app/watch/[videoId]/loading.tsx | 81 ++++++++++++++++++ app/watch/[videoId]/page.tsx | 65 +++++++++++++- components/ui/skeleton.tsx | 13 +++ 14 files changed, 686 insertions(+), 9 deletions(-) create mode 100644 app/(dashboard)/dashboard/loading.tsx create mode 100644 app/(dashboard)/projects/[projectId]/loading.tsx create mode 100644 app/(dashboard)/projects/[projectId]/settings/loading.tsx create mode 100644 app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/loading.tsx create mode 100644 app/(dashboard)/projects/[projectId]/videos/[videoId]/loading.tsx create mode 100644 app/(dashboard)/settings/loading.tsx create mode 100644 app/(dashboard)/workspaces/loading.tsx create mode 100644 app/watch/[videoId]/loading.tsx create mode 100644 components/ui/skeleton.tsx diff --git a/.gitignore b/.gitignore index d7a0bd5..dd85413 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ next-env.d.ts /lib/generated/prisma # Progress (Internal Tracking) -PROGRESS.md \ No newline at end of file +PROGRESS.md +Optimization.md \ No newline at end of file diff --git a/app/(dashboard)/dashboard/loading.tsx b/app/(dashboard)/dashboard/loading.tsx new file mode 100644 index 0000000..c395cae --- /dev/null +++ b/app/(dashboard)/dashboard/loading.tsx @@ -0,0 +1,48 @@ +import { Skeleton } from "@/components/ui/skeleton" +import { Card, CardHeader, CardContent } from "@/components/ui/card" + +function ProjectCardSkeleton() { + return ( + + +
+
+ + +
+ +
+ + + +
+ +
+ + + +
+
+
+ ) +} + +export default function DashboardLoading() { + return ( +
+
+
+ + +
+ +
+ +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+ ) +} diff --git a/app/(dashboard)/projects/[projectId]/loading.tsx b/app/(dashboard)/projects/[projectId]/loading.tsx new file mode 100644 index 0000000..1313c16 --- /dev/null +++ b/app/(dashboard)/projects/[projectId]/loading.tsx @@ -0,0 +1,53 @@ +import { Skeleton } from "@/components/ui/skeleton" +import { Card, CardContent } from "@/components/ui/card" + +function VideoCardSkeleton() { + return ( + + + + +
+ + + +
+
+
+ ) +} + +export default function ProjectLoading() { + return ( +
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ + + + +
+
+ +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+ ) +} diff --git a/app/(dashboard)/projects/[projectId]/settings/loading.tsx b/app/(dashboard)/projects/[projectId]/settings/loading.tsx new file mode 100644 index 0000000..da924ee --- /dev/null +++ b/app/(dashboard)/projects/[projectId]/settings/loading.tsx @@ -0,0 +1,67 @@ +import { Skeleton } from "@/components/ui/skeleton" +import { Card, CardHeader, CardContent } from "@/components/ui/card" + +export default function ProjectSettingsLoading() { + return ( +
+
+ + +
+ + +
+ + + + + + + +
+ + +
+
+ + +
+
+ + +
+ +
+
+ + + + + + + +
+ + +
+
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+
+ + + + + + + + + + +
+
+ ) +} diff --git a/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/loading.tsx b/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/loading.tsx new file mode 100644 index 0000000..c2c37b3 --- /dev/null +++ b/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/loading.tsx @@ -0,0 +1,39 @@ +import { Skeleton } from "@/components/ui/skeleton" +import { Separator } from "@/components/ui/separator" + +function PlayerPanelSkeleton() { + return ( +
+
+ +
+
+
+ +
+
+ ) +} + +export default function CompareLoading() { + return ( +
+
+
+ + +
+ + + +
+
+
+ +
+ + +
+
+ ) +} diff --git a/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/page.tsx b/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/page.tsx index 13974a6..10d01d3 100644 --- a/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/page.tsx +++ b/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/page.tsx @@ -13,6 +13,7 @@ import { import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; +import { Skeleton } from '@/components/ui/skeleton'; import { DropdownMenu, DropdownMenuContent, @@ -118,8 +119,31 @@ export default function CompareVersionsPage() { if (loading) { return ( -
- +
+
+
+ + +
+ + + +
+
+
+
+ {Array.from({ length: 2 }).map((_, i) => ( +
+
+ +
+
+
+ +
+
+ ))} +
); } diff --git a/app/(dashboard)/projects/[projectId]/videos/[videoId]/loading.tsx b/app/(dashboard)/projects/[projectId]/videos/[videoId]/loading.tsx new file mode 100644 index 0000000..0210704 --- /dev/null +++ b/app/(dashboard)/projects/[projectId]/videos/[videoId]/loading.tsx @@ -0,0 +1,84 @@ +import { Skeleton } from "@/components/ui/skeleton" +import { Separator } from "@/components/ui/separator" + +function CommentSkeleton() { + return ( +
+
+
+ + +
+ +
+ + +
+ ) +} + +export default function VideoPlayerLoading() { + return ( +
+
+
+
+
+ + +
+ + +
+
+
+ + +
+
+ +
+ +
+
+ + + + + +
+ +
+
+ +
+
+ +
+
+
+ + + +
+ +
+ +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ +
+ +
+ + +
+
+
+
+
+ ) +} diff --git a/app/(dashboard)/projects/[projectId]/videos/[videoId]/page.tsx b/app/(dashboard)/projects/[projectId]/videos/[videoId]/page.tsx index 57e40cb..7db08d2 100644 --- a/app/(dashboard)/projects/[projectId]/videos/[videoId]/page.tsx +++ b/app/(dashboard)/projects/[projectId]/videos/[videoId]/page.tsx @@ -39,6 +39,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Separator } from '@/components/ui/separator'; +import { Skeleton } from '@/components/ui/skeleton'; import { Dialog, DialogContent, @@ -1060,8 +1061,71 @@ export default function VideoPage() { if (loading) { return ( -
- +
+
+
+
+
+ + +
+ + +
+
+
+ + +
+
+
+
+
+ + + + + +
+ +
+
+ +
+
+
+
+
+ + + +
+ +
+
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+
+ + +
+ +
+ + +
+ ))} +
+
+ +
+ + +
+
+
+
); } diff --git a/app/(dashboard)/settings/loading.tsx b/app/(dashboard)/settings/loading.tsx new file mode 100644 index 0000000..50eab41 --- /dev/null +++ b/app/(dashboard)/settings/loading.tsx @@ -0,0 +1,72 @@ +import { Skeleton } from "@/components/ui/skeleton" +import { Card, CardHeader, CardContent } from "@/components/ui/card" + +function SettingsCardSkeleton({ rows }: { rows: number }) { + return ( + + + + + + + {Array.from({ length: rows }).map((_, i) => ( +
+
+ + +
+ +
+ ))} +
+
+ ) +} + +export default function SettingsLoading() { + return ( +
+
+
+ + +
+ + + + + + + + + +
+ + +
+
+ + +
+
+
+ + + + + + + + + + + + + +
+ +
+
+
+ ) +} diff --git a/app/(dashboard)/settings/page.tsx b/app/(dashboard)/settings/page.tsx index 791df16..6c1082b 100644 --- a/app/(dashboard)/settings/page.tsx +++ b/app/(dashboard)/settings/page.tsx @@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; +import { Skeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; import { Select, @@ -177,8 +178,33 @@ export default function SettingsPage() { if (loading) { return ( -
- +
+
+ + +
+ {Array.from({ length: 4 }).map((_, i) => ( + + + + + + + {Array.from({ length: 3 }).map((_, j) => ( +
+
+ + +
+ +
+ ))} +
+
+ ))} +
+ +
); } diff --git a/app/(dashboard)/workspaces/loading.tsx b/app/(dashboard)/workspaces/loading.tsx new file mode 100644 index 0000000..6e85092 --- /dev/null +++ b/app/(dashboard)/workspaces/loading.tsx @@ -0,0 +1,44 @@ +import { Skeleton } from "@/components/ui/skeleton" +import { Card, CardHeader, CardContent } from "@/components/ui/card" + +function WorkspaceCardSkeleton() { + return ( + + +
+ + +
+ + +
+ +
+ + + +
+
+
+ ) +} + +export default function WorkspacesLoading() { + return ( +
+
+
+ + +
+ +
+ +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+ ) +} diff --git a/app/watch/[videoId]/loading.tsx b/app/watch/[videoId]/loading.tsx new file mode 100644 index 0000000..ccdcb4b --- /dev/null +++ b/app/watch/[videoId]/loading.tsx @@ -0,0 +1,81 @@ +import { Skeleton } from "@/components/ui/skeleton" +import { Separator } from "@/components/ui/separator" + +function CommentSkeleton() { + return ( +
+
+
+ + +
+ +
+ + +
+ ) +} + +export default function WatchLoading() { + return ( +
+
+
+
+
+ + +
+ + +
+
+ +
+ +
+ +
+
+ + + + + +
+ +
+
+ +
+
+ +
+
+
+ + + +
+ +
+ +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ +
+ +
+ + +
+
+
+
+
+ ) +} diff --git a/app/watch/[videoId]/page.tsx b/app/watch/[videoId]/page.tsx index ab94e66..251439d 100644 --- a/app/watch/[videoId]/page.tsx +++ b/app/watch/[videoId]/page.tsx @@ -34,6 +34,7 @@ import { Textarea } from '@/components/ui/textarea'; import { Input } from '@/components/ui/input'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Separator } from '@/components/ui/separator'; +import { Skeleton } from '@/components/ui/skeleton'; import { DropdownMenu, DropdownMenuContent, @@ -957,8 +958,68 @@ export default function WatchPage() { if (loading) { return ( -
- +
+
+
+
+
+ + +
+ + +
+
+ +
+
+
+
+ + + + + +
+ +
+
+ +
+
+
+
+
+ + + +
+ +
+
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+
+ + +
+ +
+ + +
+ ))} +
+
+ +
+ + +
+
+
+
); } diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx new file mode 100644 index 0000000..7fd3cc6 --- /dev/null +++ b/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton }