diff --git a/apps/frontend/src/components/layout/Layout.tsx b/apps/frontend/src/components/layout/Layout.tsx
index 7dfbb33..4a8a778 100644
--- a/apps/frontend/src/components/layout/Layout.tsx
+++ b/apps/frontend/src/components/layout/Layout.tsx
@@ -2,12 +2,14 @@ import { useState } from "react";
import { Outlet } from "@tanstack/react-router";
import { Header } from "./Header";
import { Sidebar } from "./Sidebar";
+import { LoadingBar } from "@/components/ui/loading-bar";
export function Layout() {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
+
setSidebarOpen(false)} />
setSidebarOpen(true)} />
diff --git a/apps/frontend/src/components/ui/loading-bar.tsx b/apps/frontend/src/components/ui/loading-bar.tsx
new file mode 100644
index 0000000..dd135bf
--- /dev/null
+++ b/apps/frontend/src/components/ui/loading-bar.tsx
@@ -0,0 +1,37 @@
+import { useState, useEffect, useRef } from "react";
+import { useRouterState } from "@tanstack/react-router";
+import { useIsFetching, useIsMutating } from "@tanstack/react-query";
+import { cn } from "@/lib/utils";
+
+export function LoadingBar() {
+ const isNavigating = useRouterState({ select: (s) => s.status === "pending" });
+ const isFetching = useIsFetching();
+ const isMutating = useIsMutating();
+
+ const [visible, setVisible] = useState(false);
+ const timerRef = useRef>(undefined);
+
+ const loading = isNavigating || isFetching > 0 || isMutating > 0;
+
+ useEffect(() => {
+ if (loading) {
+ timerRef.current = setTimeout(() => setVisible(true), 200);
+ } else {
+ clearTimeout(timerRef.current);
+ setVisible(false);
+ }
+ return () => clearTimeout(timerRef.current);
+ }, [loading]);
+
+ return (
+
+ );
+}
diff --git a/apps/frontend/src/index.css b/apps/frontend/src/index.css
index e7600c6..cbba481 100644
--- a/apps/frontend/src/index.css
+++ b/apps/frontend/src/index.css
@@ -114,6 +114,28 @@
--sidebar-ring: oklch(0.556 0 0);
}
+@keyframes loading-bar {
+ 0% {
+ transform: scaleX(0);
+ transform-origin: left;
+ }
+ 50% {
+ transform: scaleX(1);
+ transform-origin: left;
+ }
+ 51% {
+ transform-origin: right;
+ }
+ 100% {
+ transform: scaleX(0);
+ transform-origin: right;
+ }
+}
+
+.loading-bar {
+ animation: loading-bar 1.5s ease-in-out infinite;
+}
+
* {
border-color: var(--border);
}