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 { 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<boolean>(false);
const toggleSidebar = (): void => {
@ -48,7 +53,11 @@ export const Layout = ({ children, currentPage, breadcrumbs, pageHeader }: Layou
{/* 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">
{/* Top Header */}
<Header currentPage={currentPage} breadcrumbs={breadcrumbs} onMenuClick={toggleSidebar} />
<Header
currentPage={currentPage}
breadcrumbs={breadcrumbs}
onMenuClick={toggleSidebar}
/>
{/* Main Content Area */}
<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="flex gap-3 items-center px-2">
{!isSuperAdmin && logoUrl ? (
<img
<AuthenticatedImage
src={logoUrl}
alt="Logo"
className="h-9 w-auto max-w-[180px] object-contain"
onError={(e) => {
e.currentTarget.style.display = "none";
const fallback = e.currentTarget
.nextElementSibling as HTMLElement;
if (fallback) fallback.style.display = "flex";
}}
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>}
/>
) : null}
<div

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks';
import { fetchThemeAsync } from '@/store/themeSlice';
import { fetchThemeAsync, fetchAuthThemeAsync } from '@/store/themeSlice';
import apiClient from '@/services/api-client';
/**
@ -10,13 +10,31 @@ import apiClient from '@/services/api-client';
export const useTenantTheme = (): void => {
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(() => {

View File

@ -18,4 +18,8 @@ export const themeService = {
const response = await apiClient.get<ThemeResponse>(`/tenants/theme?domain=${encodeURIComponent(domain)}`);
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({
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<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
);
});
},
});