feat: Implement authenticated tenant theme fetching and replace logo image with AuthenticatedImage component.

This commit is contained in:
Yashwin 2026-03-19 11:49:49 +05:30
parent 4e83f55800
commit a6ef7e6bee
5 changed files with 91 additions and 17 deletions

View File

@ -1,8 +1,8 @@
import { useState } from 'react'; import { useState } from "react";
import { Sidebar } from '@/components/layout/Sidebar'; import { Sidebar } from "@/components/layout/Sidebar";
import { Header } from '@/components/layout/Header'; import { Header } from "@/components/layout/Header";
import { PageHeader, type TabItem } from '@/components/shared'; import { PageHeader, type TabItem } from "@/components/shared";
import type { ReactNode, ReactElement } from 'react'; import type { ReactNode, ReactElement } from "react";
interface LayoutProps { interface LayoutProps {
children: ReactNode; 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<boolean>(false); const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
const toggleSidebar = (): void => { const toggleSidebar = (): void => {
@ -48,7 +53,11 @@ export const Layout = ({ children, currentPage, breadcrumbs, pageHeader }: Layou
{/* Main Content */} {/* Main Content */}
<main className="flex-1 min-w-0 min-h-0 max-w-full bg-white border-0 md:border border-[rgba(0,0,0,0.08)] rounded-none md:rounded-xl shadow-none md:shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] flex flex-col overflow-hidden"> <main className="flex-1 min-w-0 min-h-0 max-w-full bg-white border-0 md:border border-[rgba(0,0,0,0.08)] rounded-none md:rounded-xl shadow-none md:shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] flex flex-col overflow-hidden">
{/* Top Header */} {/* Top Header */}
<Header currentPage={currentPage} breadcrumbs={breadcrumbs} onMenuClick={toggleSidebar} /> <Header
currentPage={currentPage}
breadcrumbs={breadcrumbs}
onMenuClick={toggleSidebar}
/>
{/* Main Content Area */} {/* Main Content Area */}
<div className="flex-1 min-h-0 p-4 md:p-4 lg:p-6 overflow-y-auto relative z-0"> <div className="flex-1 min-h-0 p-4 md:p-4 lg:p-6 overflow-y-auto relative z-0">

View File

@ -370,16 +370,11 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
<div className="w-full md:w-[140px] lg:w-[160px] xl:w-[206px] shrink-0"> <div className="w-full md:w-[140px] lg:w-[160px] xl:w-[206px] shrink-0">
<div className="flex gap-3 items-center px-2"> <div className="flex gap-3 items-center px-2">
{!isSuperAdmin && logoUrl ? ( {!isSuperAdmin && logoUrl ? (
<img <AuthenticatedImage
src={logoUrl} src={logoUrl}
alt="Logo" alt="Logo"
className="h-9 w-auto max-w-[180px] object-contain" className="h-9 w-auto max-w-[180px] object-contain"
onError={(e) => { fallback={<div className="w-9 h-9 rounded-[10px] flex items-center justify-center shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)] shrink-0 bg-[#112868]"><Shield className="w-6 h-6 text-white" strokeWidth={1.67} /></div>}
e.currentTarget.style.display = "none";
const fallback = e.currentTarget
.nextElementSibling as HTMLElement;
if (fallback) fallback.style.display = "flex";
}}
/> />
) : null} ) : null}
<div <div

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks'; import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks';
import { fetchThemeAsync } from '@/store/themeSlice'; import { fetchThemeAsync, fetchAuthThemeAsync } from '@/store/themeSlice';
import apiClient from '@/services/api-client'; import apiClient from '@/services/api-client';
/** /**
@ -10,13 +10,31 @@ import apiClient from '@/services/api-client';
export const useTenantTheme = (): void => { export const useTenantTheme = (): void => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { faviconUrl, isInitialized, isLoading } = useAppSelector((state) => state.theme); const { faviconUrl, isInitialized, isLoading } = useAppSelector((state) => state.theme);
const { isAuthenticated, roles } = useAppSelector((state) => state.auth);
useEffect(() => { useEffect(() => {
// Only fetch if not already initialized // Only fetch if not already initialized
if (!isInitialized && !isLoading) { if (!isInitialized && !isLoading) {
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(fetchThemeAsync());
} }
}, [dispatch, isInitialized, isLoading]); }
}, [dispatch, isInitialized, isLoading, isAuthenticated, roles]);
// Apply favicon // Apply favicon
useEffect(() => { useEffect(() => {

View File

@ -18,4 +18,8 @@ export const themeService = {
const response = await apiClient.get<ThemeResponse>(`/tenants/theme?domain=${encodeURIComponent(domain)}`); const response = await apiClient.get<ThemeResponse>(`/tenants/theme?domain=${encodeURIComponent(domain)}`);
return response.data; return response.data;
}, },
getAuthTenantTheme: async (): Promise<ThemeResponse> => {
const response = await apiClient.get<ThemeResponse>(`/tenants/tenant-theme`);
return response.data;
},
}; };

View File

@ -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({ const themeSlice = createSlice({
name: 'theme', name: 'theme',
initialState, initialState,
@ -131,6 +150,35 @@ const themeSlice = createSlice({
state.faviconUrl = null; state.faviconUrl = null;
state.isInitialized = true; state.isInitialized = true;
applyThemeColors(defaultTheme.primary_color, defaultTheme.secondary_color, defaultTheme.accent_color); 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<ThemeData>) => {
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
);
}); });
}, },
}); });