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 { createBrowserRouter, RouterProvider, Navigate, Outlet, useLocation } from "react-router-dom"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
import { Command } from "lucide-react"
|
||||||
import { ThemeProvider } from "./components/ThemeProvider"
|
import { ThemeProvider } from "./components/ThemeProvider"
|
||||||
import { LanguageProvider } from "./components/LanguageProvider"
|
import { LanguageProvider } from "./components/LanguageProvider"
|
||||||
import { Toaster } from "./components/ui/toaster"
|
import { Toaster } from "./components/ui/toaster"
|
||||||
import { Navbar } from "./components/Navbar"
|
import { Navbar } from "./components/Navbar"
|
||||||
import { Sidebar } from './components/Sidebar';
|
import { Sidebar } from './components/Sidebar';
|
||||||
import { AppProvider } from "./context/AppContext"
|
import { AppProvider, useAppContext } from "./context/AppContext"
|
||||||
import { NotificationsProvider } from "./context/NotificationsContext"
|
import { NotificationsProvider } from "./context/NotificationsContext"
|
||||||
import { WorkspaceProvider } from "./context/WorkspaceContext"
|
import { useWorkspace, WorkspaceProvider } from "./context/WorkspaceContext"
|
||||||
import Auth from "./pages/Auth"
|
import Auth from "./pages/Auth"
|
||||||
import GoogleAuthCallback from "./pages/GoogleAuthCallback"
|
import GoogleAuthCallback from "./pages/GoogleAuthCallback"
|
||||||
import Profile from "./pages/Profile"
|
import Profile from "./pages/Profile"
|
||||||
@@ -39,6 +40,7 @@ import { SignupPasswordPage } from "./pages/auth/SignupPasswordPage"
|
|||||||
import { ForgotPasswordMobilePage } from "./pages/auth/ForgotPasswordMobilePage"
|
import { ForgotPasswordMobilePage } from "./pages/auth/ForgotPasswordMobilePage"
|
||||||
import { ForgotPasswordOtpPage } from "./pages/auth/ForgotPasswordOtpPage"
|
import { ForgotPasswordOtpPage } from "./pages/auth/ForgotPasswordOtpPage"
|
||||||
import { ForgotPasswordPasswordPage } from "./pages/auth/ForgotPasswordPasswordPage"
|
import { ForgotPasswordPasswordPage } from "./pages/auth/ForgotPasswordPasswordPage"
|
||||||
|
import { useTranslation } from "./hooks/useTranslation"
|
||||||
|
|
||||||
const MainLayout = () => {
|
const MainLayout = () => {
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
|
||||||
@@ -94,12 +96,45 @@ const AuthLayout = () => (
|
|||||||
</AuthFlowProvider>
|
</AuthFlowProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
const ProtectedAppLayout = () => (
|
const ProtectedBootLoader = () => {
|
||||||
<AppProvider>
|
const { t } = useTranslation()
|
||||||
<WorkspaceProvider>
|
|
||||||
|
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>
|
<NotificationsProvider>
|
||||||
<MainLayout />
|
<MainLayout />
|
||||||
</NotificationsProvider>
|
</NotificationsProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProtectedAppLayout = () => (
|
||||||
|
<AppProvider>
|
||||||
|
<WorkspaceProvider>
|
||||||
|
<ProtectedAppGate />
|
||||||
</WorkspaceProvider>
|
</WorkspaceProvider>
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { Button } from "./ui/button"
|
|||||||
import { SettingsMenu } from "./SettingsMenu"
|
import { SettingsMenu } from "./SettingsMenu"
|
||||||
import { FlaskConical, LogOut, User, Moon, Sun, Globe, Command, Menu, RefreshCcw } from "lucide-react"
|
import { FlaskConical, LogOut, User, Moon, Sun, Globe, Command, Menu, RefreshCcw } from "lucide-react"
|
||||||
import { useTheme } from "./ThemeProvider"
|
import { useTheme } from "./ThemeProvider"
|
||||||
import { logoutUser, getUserProfile } from "../api/users"
|
import { logoutUser } from "../api/users"
|
||||||
import { WorkspaceSelector } from "./WorkspaceSelector"
|
import { WorkspaceSelector } from "./WorkspaceSelector"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { NotificationBell } from "./notifications/NotificationBell"
|
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 { startDemo } from "../api/demo"
|
||||||
|
import { useAppContext } from "../context/AppContext"
|
||||||
|
|
||||||
type NavbarProps = {
|
type NavbarProps = {
|
||||||
onOpenSidebar?: () => void
|
onOpenSidebar?: () => void
|
||||||
@@ -19,12 +20,12 @@ type NavbarProps = {
|
|||||||
export function Navbar({ onOpenSidebar }: NavbarProps) {
|
export function Navbar({ onOpenSidebar }: NavbarProps) {
|
||||||
const { t, lang, setLanguage } = useTranslation()
|
const { t, lang, setLanguage } = useTranslation()
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
|
const { user, isLoadingUser, setUser } = useAppContext()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const [showLogoutModal, setShowLogoutModal] = useState(false)
|
const [showLogoutModal, setShowLogoutModal] = useState(false)
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
||||||
const [isResettingDemo, setIsResettingDemo] = useState(false)
|
const [isResettingDemo, setIsResettingDemo] = useState(false)
|
||||||
const [user, setUser] = useState<any>(null)
|
|
||||||
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@@ -40,33 +41,6 @@ export function Navbar({ onOpenSidebar }: NavbarProps) {
|
|||||||
}).format(new Date(user.demo_expires_at))
|
}).format(new Date(user.demo_expires_at))
|
||||||
: null
|
: 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(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
@@ -279,7 +253,7 @@ export function Navbar({ onOpenSidebar }: NavbarProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : isLoadingUser ? null : (
|
||||||
<>
|
<>
|
||||||
<SettingsMenu />
|
<SettingsMenu />
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { useState } from "react"
|
||||||
import { NavLink, useNavigate } from "react-router-dom"
|
import { NavLink, useNavigate } from "react-router-dom"
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
@@ -28,9 +28,10 @@ import { canWorkspace, WORKSPACE_LOGS_VIEW } from "../lib/permissions"
|
|||||||
import { WorkspaceSelector } from "./WorkspaceSelector"
|
import { WorkspaceSelector } from "./WorkspaceSelector"
|
||||||
import { SettingsMenu } from "./SettingsMenu"
|
import { SettingsMenu } from "./SettingsMenu"
|
||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
import { getUserProfile, logoutUser } from "../api/users"
|
import { logoutUser } from "../api/users"
|
||||||
import { clearSessionTokens, getAccessToken, getRefreshToken, setDemoSessionMeta, setSessionTokens } from "../lib/session"
|
import { clearSessionTokens, getRefreshToken, setDemoSessionMeta, setSessionTokens } from "../lib/session"
|
||||||
import { startDemo } from "../api/demo"
|
import { startDemo } from "../api/demo"
|
||||||
|
import { useAppContext } from "../context/AppContext"
|
||||||
|
|
||||||
type SidebarProps = {
|
type SidebarProps = {
|
||||||
mobileOpen?: boolean
|
mobileOpen?: boolean
|
||||||
@@ -41,7 +42,7 @@ export const Sidebar = ({ mobileOpen = false, onMobileClose }: SidebarProps) =>
|
|||||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||||
const [showLogoutModal, setShowLogoutModal] = useState(false)
|
const [showLogoutModal, setShowLogoutModal] = useState(false)
|
||||||
const [isResettingDemo, setIsResettingDemo] = useState(false)
|
const [isResettingDemo, setIsResettingDemo] = useState(false)
|
||||||
const [user, setUser] = useState<any>(null)
|
const { user, isLoadingUser, setUser } = useAppContext()
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { t, lang, setLanguage } = useTranslation()
|
const { t, lang, setLanguage } = useTranslation()
|
||||||
@@ -67,33 +68,6 @@ export const Sidebar = ({ mobileOpen = false, onMobileClose }: SidebarProps) =>
|
|||||||
? PanelLeftOpen
|
? PanelLeftOpen
|
||||||
: PanelLeftClose
|
: 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 () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
const refreshToken = getRefreshToken()
|
const refreshToken = getRefreshToken()
|
||||||
@@ -245,7 +219,7 @@ export const Sidebar = ({ mobileOpen = false, onMobileClose }: SidebarProps) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderMobileFooterSection = () => {
|
const renderMobileFooterSection = () => {
|
||||||
if (!user) {
|
if (!user && !isLoadingUser) {
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-slate-200 p-4 dark:border-slate-800">
|
<div className="border-t border-slate-200 p-4 dark:border-slate-800">
|
||||||
<button
|
<button
|
||||||
@@ -272,6 +246,10 @@ export const Sidebar = ({ mobileOpen = false, onMobileClose }: SidebarProps) =>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1 p-4">
|
<div className="space-y-1 p-4">
|
||||||
<button
|
<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';
|
import { getUserProfile } from '../api/users';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@@ -14,6 +14,8 @@ interface User {
|
|||||||
|
|
||||||
interface AppContextType {
|
interface AppContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
|
isLoadingUser: boolean;
|
||||||
|
setUser: Dispatch<SetStateAction<User | null>>;
|
||||||
refreshUser: () => Promise<void>;
|
refreshUser: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,14 +23,24 @@ const AppContext = createContext<AppContextType | null>(null);
|
|||||||
|
|
||||||
export const AppProvider = ({ children }: { children: ReactNode }) => {
|
export const AppProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [isLoadingUser, setIsLoadingUser] = useState(() => Boolean(localStorage.getItem('accessToken')));
|
||||||
|
|
||||||
const refreshUser = async () => {
|
const refreshUser = async () => {
|
||||||
|
if (!localStorage.getItem('accessToken')) {
|
||||||
|
setUser(null);
|
||||||
|
setIsLoadingUser(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoadingUser(true);
|
||||||
try {
|
try {
|
||||||
const userData = await getUserProfile();
|
const userData = await getUserProfile();
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch user context data:", error);
|
console.error("Failed to fetch user context data:", error);
|
||||||
setUser(null);
|
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 (
|
return (
|
||||||
<AppContext.Provider value={{ user, refreshUser }}>
|
<AppContext.Provider value={{ user, isLoadingUser, setUser, refreshUser }}>
|
||||||
{children}
|
{children}
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user