fix(app): show loader during protected boot
This commit is contained in:
45
src/App.tsx
45
src/App.tsx
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user