feat: Implement authenticated tenant theme fetching and replace logo image with AuthenticatedImage component.
This commit is contained in:
parent
4e83f55800
commit
a6ef7e6bee
@ -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">
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@ -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
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user