fix(app): show loader during protected boot
Some checks are pending
Frontend CI/CD / build (push) Waiting to run
Frontend CI/CD / deploy (push) Blocked by required conditions

This commit is contained in:
2026-06-20 19:55:11 +03:30
parent f7238d1767
commit 1aa45beba4
4 changed files with 80 additions and 70 deletions

View File

@@ -1,13 +1,14 @@
import { createBrowserRouter, RouterProvider, Navigate, Outlet, useLocation } from "react-router-dom"
import { useState } from "react"
import { Command } from "lucide-react"
import { ThemeProvider } from "./components/ThemeProvider"
import { LanguageProvider } from "./components/LanguageProvider"
import { Toaster } from "./components/ui/toaster"
import { Navbar } from "./components/Navbar"
import { Sidebar } from './components/Sidebar';
import { AppProvider } from "./context/AppContext"
import { AppProvider, useAppContext } from "./context/AppContext"
import { NotificationsProvider } from "./context/NotificationsContext"
import { WorkspaceProvider } from "./context/WorkspaceContext"
import { useWorkspace, WorkspaceProvider } from "./context/WorkspaceContext"
import Auth from "./pages/Auth"
import GoogleAuthCallback from "./pages/GoogleAuthCallback"
import Profile from "./pages/Profile"
@@ -39,6 +40,7 @@ import { SignupPasswordPage } from "./pages/auth/SignupPasswordPage"
import { ForgotPasswordMobilePage } from "./pages/auth/ForgotPasswordMobilePage"
import { ForgotPasswordOtpPage } from "./pages/auth/ForgotPasswordOtpPage"
import { ForgotPasswordPasswordPage } from "./pages/auth/ForgotPasswordPasswordPage"
import { useTranslation } from "./hooks/useTranslation"
const MainLayout = () => {
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
@@ -94,12 +96,45 @@ const AuthLayout = () => (
</AuthFlowProvider>
)
const ProtectedAppLayout = () => (
<AppProvider>
<WorkspaceProvider>
const ProtectedBootLoader = () => {
const { t } = useTranslation()
return (
<div className="flex min-h-screen items-center justify-center overflow-hidden bg-slate-50 px-6 text-slate-900 transition-colors dark:bg-slate-950 dark:text-slate-50">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_20%_20%,rgba(59,130,246,0.12),transparent_30%),radial-gradient(circle_at_80%_70%,rgba(14,165,233,0.10),transparent_32%)] dark:bg-[radial-gradient(circle_at_20%_20%,rgba(59,130,246,0.16),transparent_30%),radial-gradient(circle_at_80%_70%,rgba(14,165,233,0.12),transparent_32%)]" />
<div className="relative flex flex-col items-center text-center">
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-2xl border border-blue-200 bg-white shadow-lg shadow-blue-500/10 dark:border-blue-500/20 dark:bg-slate-900">
<Command className="h-8 w-8 text-blue-600 dark:text-blue-400" />
</div>
<div className="mb-4 h-1.5 w-48 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800">
<div className="h-full w-1/2 animate-[pulse_1.2s_ease-in-out_infinite] rounded-full bg-blue-600 dark:bg-blue-400" />
</div>
<p className="text-base font-semibold">{t.title || "Qlockify"}</p>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{t.loading || "Loading..."}</p>
</div>
</div>
)
}
const ProtectedAppGate = () => {
const { isLoadingUser } = useAppContext()
const { isLoading } = useWorkspace()
if (isLoadingUser || isLoading) {
return <ProtectedBootLoader />
}
return (
<NotificationsProvider>
<MainLayout />
</NotificationsProvider>
)
}
const ProtectedAppLayout = () => (
<AppProvider>
<WorkspaceProvider>
<ProtectedAppGate />
</WorkspaceProvider>
</AppProvider>
)

View File

@@ -5,12 +5,13 @@ import { Button } from "./ui/button"
import { SettingsMenu } from "./SettingsMenu"
import { FlaskConical, LogOut, User, Moon, Sun, Globe, Command, Menu, RefreshCcw } from "lucide-react"
import { useTheme } from "./ThemeProvider"
import { logoutUser, getUserProfile } from "../api/users"
import { logoutUser } from "../api/users"
import { WorkspaceSelector } from "./WorkspaceSelector"
import { toast } from "sonner"
import { NotificationBell } from "./notifications/NotificationBell"
import { clearSessionTokens, getAccessToken, getRefreshToken, setDemoSessionMeta, setSessionTokens } from "../lib/session"
import { clearSessionTokens, getRefreshToken, setDemoSessionMeta, setSessionTokens } from "../lib/session"
import { startDemo } from "../api/demo"
import { useAppContext } from "../context/AppContext"
type NavbarProps = {
onOpenSidebar?: () => void
@@ -19,12 +20,12 @@ type NavbarProps = {
export function Navbar({ onOpenSidebar }: NavbarProps) {
const { t, lang, setLanguage } = useTranslation()
const { theme, setTheme } = useTheme()
const { user, isLoadingUser, setUser } = useAppContext()
const navigate = useNavigate()
const [showLogoutModal, setShowLogoutModal] = useState(false)
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
const [isResettingDemo, setIsResettingDemo] = useState(false)
const [user, setUser] = useState<any>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
@@ -40,33 +41,6 @@ export function Navbar({ onOpenSidebar }: NavbarProps) {
}).format(new Date(user.demo_expires_at))
: null
useEffect(() => {
const handleProfileUpdated = ((e: CustomEvent) => {
if (e.detail) {
setUser((prev: any) => (prev ? { ...prev, ...e.detail } : e.detail))
}
}) as EventListener
window.addEventListener("profile_updated", handleProfileUpdated)
return () => window.removeEventListener("profile_updated", handleProfileUpdated)
}, [])
useEffect(() => {
const fetchUser = async () => {
const token = getAccessToken()
if (!token) return
try {
const userData = await getUserProfile()
setUser(userData)
} catch (error) {
console.error("Failed to fetch user profile:", error)
}
}
void fetchUser()
}, [])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
@@ -279,7 +253,7 @@ export function Navbar({ onOpenSidebar }: NavbarProps) {
)}
</div>
</>
) : (
) : isLoadingUser ? null : (
<>
<SettingsMenu />

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"
import { useState } from "react"
import { NavLink, useNavigate } from "react-router-dom"
import {
Users,
@@ -28,9 +28,10 @@ import { canWorkspace, WORKSPACE_LOGS_VIEW } from "../lib/permissions"
import { WorkspaceSelector } from "./WorkspaceSelector"
import { SettingsMenu } from "./SettingsMenu"
import { Button } from "./ui/button"
import { getUserProfile, logoutUser } from "../api/users"
import { clearSessionTokens, getAccessToken, getRefreshToken, setDemoSessionMeta, setSessionTokens } from "../lib/session"
import { logoutUser } from "../api/users"
import { clearSessionTokens, getRefreshToken, setDemoSessionMeta, setSessionTokens } from "../lib/session"
import { startDemo } from "../api/demo"
import { useAppContext } from "../context/AppContext"
type SidebarProps = {
mobileOpen?: boolean
@@ -41,7 +42,7 @@ export const Sidebar = ({ mobileOpen = false, onMobileClose }: SidebarProps) =>
const [isCollapsed, setIsCollapsed] = useState(false)
const [showLogoutModal, setShowLogoutModal] = useState(false)
const [isResettingDemo, setIsResettingDemo] = useState(false)
const [user, setUser] = useState<any>(null)
const { user, isLoadingUser, setUser } = useAppContext()
const navigate = useNavigate()
const { t, lang, setLanguage } = useTranslation()
@@ -67,33 +68,6 @@ export const Sidebar = ({ mobileOpen = false, onMobileClose }: SidebarProps) =>
? PanelLeftOpen
: PanelLeftClose
useEffect(() => {
const handleProfileUpdated = ((e: CustomEvent) => {
if (e.detail) {
setUser((prev: any) => (prev ? { ...prev, ...e.detail } : e.detail))
}
}) as EventListener
window.addEventListener("profile_updated", handleProfileUpdated)
return () => window.removeEventListener("profile_updated", handleProfileUpdated)
}, [])
useEffect(() => {
const fetchUser = async () => {
const token = getAccessToken()
if (!token) return
try {
const userData = await getUserProfile()
setUser(userData)
} catch (error) {
console.error("Failed to fetch user profile:", error)
}
}
void fetchUser()
}, [])
const handleLogout = async () => {
try {
const refreshToken = getRefreshToken()
@@ -245,7 +219,7 @@ export const Sidebar = ({ mobileOpen = false, onMobileClose }: SidebarProps) =>
}
const renderMobileFooterSection = () => {
if (!user) {
if (!user && !isLoadingUser) {
return (
<div className="border-t border-slate-200 p-4 dark:border-slate-800">
<button
@@ -272,6 +246,10 @@ export const Sidebar = ({ mobileOpen = false, onMobileClose }: SidebarProps) =>
)
}
if (!user) {
return null
}
return (
<div className="space-y-1 p-4">
<button

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
import { createContext, useContext, useState, useEffect, type Dispatch, type ReactNode, type SetStateAction } from 'react';
import { getUserProfile } from '../api/users';
interface User {
@@ -14,6 +14,8 @@ interface User {
interface AppContextType {
user: User | null;
isLoadingUser: boolean;
setUser: Dispatch<SetStateAction<User | null>>;
refreshUser: () => Promise<void>;
}
@@ -21,14 +23,24 @@ const AppContext = createContext<AppContextType | null>(null);
export const AppProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoadingUser, setIsLoadingUser] = useState(() => Boolean(localStorage.getItem('accessToken')));
const refreshUser = async () => {
if (!localStorage.getItem('accessToken')) {
setUser(null);
setIsLoadingUser(false);
return;
}
setIsLoadingUser(true);
try {
const userData = await getUserProfile();
setUser(userData);
} catch (error) {
console.error("Failed to fetch user context data:", error);
setUser(null);
} finally {
setIsLoadingUser(false);
}
};
@@ -38,8 +50,19 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
}
}, []);
useEffect(() => {
const handleProfileUpdated = ((event: CustomEvent<Partial<User>>) => {
if (event.detail) {
setUser((current) => (current ? { ...current, ...event.detail } : current));
}
}) as EventListener;
window.addEventListener("profile_updated", handleProfileUpdated);
return () => window.removeEventListener("profile_updated", handleProfileUpdated);
}, []);
return (
<AppContext.Provider value={{ user, refreshUser }}>
<AppContext.Provider value={{ user, isLoadingUser, setUser, refreshUser }}>
{children}
</AppContext.Provider>
);