diff --git a/platform/src/app/[tenant]/ai-exposure/loading.tsx b/platform/src/app/[tenant]/ai-exposure/loading.tsx new file mode 100644 index 0000000..417df4a --- /dev/null +++ b/platform/src/app/[tenant]/ai-exposure/loading.tsx @@ -0,0 +1,25 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function AIExposureLoading() { + return ( +
+
+ + +
+ +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ +
+ + {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+ ); +} diff --git a/platform/src/app/[tenant]/audit-log/loading.tsx b/platform/src/app/[tenant]/audit-log/loading.tsx new file mode 100644 index 0000000..eb78dd3 --- /dev/null +++ b/platform/src/app/[tenant]/audit-log/loading.tsx @@ -0,0 +1,18 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function AuditLogLoading() { + return ( +
+
+ + +
+ +
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))} +
+
+ ); +} diff --git a/platform/src/app/[tenant]/compare/loading.tsx b/platform/src/app/[tenant]/compare/loading.tsx new file mode 100644 index 0000000..f184171 --- /dev/null +++ b/platform/src/app/[tenant]/compare/loading.tsx @@ -0,0 +1,22 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function CompareLoading() { + return ( +
+
+
+ + +
+ +
+ +
+ + {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+ ); +} diff --git a/platform/src/app/[tenant]/connect/loading.tsx b/platform/src/app/[tenant]/connect/loading.tsx new file mode 100644 index 0000000..26bafb4 --- /dev/null +++ b/platform/src/app/[tenant]/connect/loading.tsx @@ -0,0 +1,21 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function ConnectLoading() { + return ( +
+ + + {Array.from({ length: 3 }).map((_, i) => ( + + + + + + + + + ))} +
+ ); +} diff --git a/platform/src/app/[tenant]/dashboard/loading.tsx b/platform/src/app/[tenant]/dashboard/loading.tsx new file mode 100644 index 0000000..d171abd --- /dev/null +++ b/platform/src/app/[tenant]/dashboard/loading.tsx @@ -0,0 +1,34 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function DashboardLoading() { + return ( +
+
+
+ + +
+ +
+ +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+ + + +
+ + +
+
+ ); +} diff --git a/platform/src/app/[tenant]/repos/[repoName]/loading.tsx b/platform/src/app/[tenant]/repos/[repoName]/loading.tsx new file mode 100644 index 0000000..e5f8d39 --- /dev/null +++ b/platform/src/app/[tenant]/repos/[repoName]/loading.tsx @@ -0,0 +1,27 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function RepoDetailLoading() { + return ( +
+
+
+ + +
+ +
+ +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+ +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+ ); +} diff --git a/platform/src/app/[tenant]/repos/loading.tsx b/platform/src/app/[tenant]/repos/loading.tsx new file mode 100644 index 0000000..9a35d5b --- /dev/null +++ b/platform/src/app/[tenant]/repos/loading.tsx @@ -0,0 +1,20 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function ReposLoading() { + return ( +
+
+ + +
+ + + +
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))} +
+
+ ); +} diff --git a/platform/src/app/[tenant]/settings/integrations/[provider]/loading.tsx b/platform/src/app/[tenant]/settings/integrations/[provider]/loading.tsx new file mode 100644 index 0000000..41b9ad5 --- /dev/null +++ b/platform/src/app/[tenant]/settings/integrations/[provider]/loading.tsx @@ -0,0 +1,22 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function IntegrationDetailLoading() { + return ( +
+ + + + + + + + + + + + + +
+ ); +} diff --git a/platform/src/app/[tenant]/settings/integrations/loading.tsx b/platform/src/app/[tenant]/settings/integrations/loading.tsx new file mode 100644 index 0000000..c43f308 --- /dev/null +++ b/platform/src/app/[tenant]/settings/integrations/loading.tsx @@ -0,0 +1,15 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function IntegrationsLoading() { + return ( +
+ + +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+ ); +} diff --git a/platform/src/app/[tenant]/settings/loading.tsx b/platform/src/app/[tenant]/settings/loading.tsx new file mode 100644 index 0000000..4e2fd56 --- /dev/null +++ b/platform/src/app/[tenant]/settings/loading.tsx @@ -0,0 +1,22 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function SettingsLoading() { + return ( +
+ + + {Array.from({ length: 3 }).map((_, i) => ( + + + + + + + + + + ))} +
+ ); +} diff --git a/platform/src/app/[tenant]/team/loading.tsx b/platform/src/app/[tenant]/team/loading.tsx new file mode 100644 index 0000000..093d40b --- /dev/null +++ b/platform/src/app/[tenant]/team/loading.tsx @@ -0,0 +1,25 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function TeamLoading() { + return ( +
+
+ + +
+ + + + + + + + {Array.from({ length: 4 }).map((_, i) => ( + + ))} + + +
+ ); +} diff --git a/platform/src/components/WindowSelector.tsx b/platform/src/components/WindowSelector.tsx index 7bf9085..87a0749 100644 --- a/platform/src/components/WindowSelector.tsx +++ b/platform/src/components/WindowSelector.tsx @@ -1,7 +1,11 @@ "use client"; +import { useTransition } from "react"; + import { useRouter, usePathname, useSearchParams } from "next/navigation"; +import { Loader2 } from "lucide-react"; + import { Select, SelectContent, @@ -32,13 +36,21 @@ export function WindowSelector({ const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const [isPending, startTransition] = useTransition(); if (options.length < 2) return null; function handleChange(value: string) { const params = new URLSearchParams(searchParams.toString()); params.set(paramKey, value); - router.replace(`${pathname}?${params.toString()}`, { scroll: false }); + // Wrapping the navigation in a transition keeps this page visible with + // isPending=true instead of unmounting to the route's loading.tsx — the + // right feel for a filter (instant feedback on the control itself) + // rather than a full-page flash. Also blocks a second click from firing + // another replace() before the first one resolves. + startTransition(() => { + router.replace(`${pathname}?${params.toString()}`, { scroll: false }); + }); } return ( @@ -46,9 +58,17 @@ export function WindowSelector({ {t("common.windowSelector.label")} - - + {isPending ? ( + + ) : ( + + )} {options.map((days) => ( diff --git a/platform/src/components/ui/skeleton.tsx b/platform/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..67c3229 --- /dev/null +++ b/platform/src/components/ui/skeleton.tsx @@ -0,0 +1,12 @@ +import { cn } from "@/lib/utils"; + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Skeleton };