From a6ef7e6beef3e138dac87a049549b29bce3289ce Mon Sep 17 00:00:00 2001 From: Yashwin Date: Thu, 19 Mar 2026 11:49:49 +0530 Subject: [PATCH] feat: Implement authenticated tenant theme fetching and replace logo image with `AuthenticatedImage` component. --- src/components/layout/Layout.tsx | 23 ++++++++++----- src/components/layout/Sidebar.tsx | 9 ++---- src/hooks/useTenantTheme.ts | 24 ++++++++++++++-- src/services/theme-service.ts | 4 +++ src/store/themeSlice.ts | 48 +++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 17 deletions(-) diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 7e9be39..1637e90 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -1,8 +1,8 @@ -import { useState } from 'react'; -import { Sidebar } from '@/components/layout/Sidebar'; -import { Header } from '@/components/layout/Header'; -import { PageHeader, type TabItem } from '@/components/shared'; -import type { ReactNode, ReactElement } from 'react'; +import { useState } from "react"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { Header } from "@/components/layout/Header"; +import { PageHeader, type TabItem } from "@/components/shared"; +import type { ReactNode, ReactElement } from "react"; interface LayoutProps { children: ReactNode; @@ -15,7 +15,12 @@ interface LayoutProps { }; } -export const Layout = ({ children, currentPage, breadcrumbs, pageHeader }: LayoutProps): ReactElement => { +export const Layout = ({ + children, + currentPage, + breadcrumbs, + pageHeader, +}: LayoutProps): ReactElement => { const [isSidebarOpen, setIsSidebarOpen] = useState(false); const toggleSidebar = (): void => { @@ -48,7 +53,11 @@ export const Layout = ({ children, currentPage, breadcrumbs, pageHeader }: Layou {/* Main Content */}
{/* Top Header */} -
+
{/* Main Content Area */}
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 4d66fd2..fc89efe 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -370,16 +370,11 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
{!isSuperAdmin && logoUrl ? ( - Logo { - e.currentTarget.style.display = "none"; - const fallback = e.currentTarget - .nextElementSibling as HTMLElement; - if (fallback) fallback.style.display = "flex"; - }} + fallback={
} /> ) : null}
{ const dispatch = useAppDispatch(); const { faviconUrl, isInitialized, isLoading } = useAppSelector((state) => state.theme); + const { isAuthenticated, roles } = useAppSelector((state) => state.auth); useEffect(() => { // Only fetch if not already initialized if (!isInitialized && !isLoading) { - dispatch(fetchThemeAsync()); + let isSuperAdmin = false; + let rolesArray: string[] = []; + if (Array.isArray(roles)) { + rolesArray = roles; + } else if (typeof roles === 'string') { + try { + rolesArray = JSON.parse(roles); + } catch { + rolesArray = []; + } + } + isSuperAdmin = rolesArray.includes('super_admin'); + + if (isAuthenticated && !isSuperAdmin) { + dispatch(fetchAuthThemeAsync()); + } else { + dispatch(fetchThemeAsync()); + } } - }, [dispatch, isInitialized, isLoading]); + }, [dispatch, isInitialized, isLoading, isAuthenticated, roles]); // Apply favicon useEffect(() => { diff --git a/src/services/theme-service.ts b/src/services/theme-service.ts index ce434db..5043c40 100644 --- a/src/services/theme-service.ts +++ b/src/services/theme-service.ts @@ -18,4 +18,8 @@ export const themeService = { const response = await apiClient.get(`/tenants/theme?domain=${encodeURIComponent(domain)}`); return response.data; }, + getAuthTenantTheme: async (): Promise => { + const response = await apiClient.get(`/tenants/tenant-theme`); + return response.data; + }, }; diff --git a/src/store/themeSlice.ts b/src/store/themeSlice.ts index 80e9fa8..20681ed 100644 --- a/src/store/themeSlice.ts +++ b/src/store/themeSlice.ts @@ -65,6 +65,25 @@ export const fetchThemeAsync = createAsyncThunk< } }); +// Async thunk to fetch authenticated theme +export const fetchAuthThemeAsync = createAsyncThunk< + ThemeData, + void, + { rejectValue: { message: string } } +>('theme/fetchAuth', async (_, { rejectWithValue }) => { + try { + const response = await themeService.getAuthTenantTheme(); + if (response.success && response.data) { + return response.data; + } + return rejectWithValue({ message: 'Failed to fetch authenticated theme' }); + } catch (error: any) { + return rejectWithValue({ + message: error?.response?.data?.error?.message || error?.message || 'Failed to fetch authenticated theme', + }); + } +}); + const themeSlice = createSlice({ name: 'theme', initialState, @@ -131,6 +150,35 @@ const themeSlice = createSlice({ state.faviconUrl = null; state.isInitialized = true; applyThemeColors(defaultTheme.primary_color, defaultTheme.secondary_color, defaultTheme.accent_color); + }) + .addCase(fetchAuthThemeAsync.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchAuthThemeAsync.fulfilled, (state, action: PayloadAction) => { + state.isLoading = false; + state.theme = action.payload; + state.logoUrl = action.payload.logo_file_path; + state.faviconUrl = action.payload.favicon_file_path; + state.error = null; + state.isInitialized = true; + + applyThemeColors( + action.payload.primary_color, + action.payload.secondary_color, + action.payload.accent_color + ); + }) + .addCase(fetchAuthThemeAsync.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload?.message || 'Failed to fetch authenticated theme'; + if (!state.theme) state.theme = defaultTheme; + state.isInitialized = true; + applyThemeColors( + state.theme.primary_color || defaultTheme.primary_color, + state.theme.secondary_color || defaultTheme.secondary_color, + state.theme.accent_color || defaultTheme.accent_color + ); }); }, });