Frontend Route Registration & Page Architecture
Route Configuration Structure
This is the structure we use to register routes in our App.jsx file:
[
{
"id": "products",
"path": "/Product_All",
"pageKey": "Product_All",
"apiPageName": "Product_All",
"public": false
},
{
"id": "connexions",
"path": "/All_Connexions",
"pageKey": "All_Connexions",
"apiPageName": "Connexions_All",
"public": false
},
{
"id": "ad-accounts",
"path": "/campaign/ad-accounts",
"pageKey": "AdAccount_All",
"apiPageName": "AdAccount_All",
"public": false
},
{
"id": "ads-launcher",
"path": "/campaign/ads-launcher",
"pageKey": "AdProject_All",
"apiPageName": "AdProject_All",
"public": false
},
{
"id": "adsets",
"path": "/AdSet_All",
"pageKey": "AdSet_All",
"apiPageName": "AdSet_All",
"public": false
},
{
"id": "market-ads",
"path": "/MarketAds_All",
"pageKey": "MarketAds_All",
"apiPageName": "MarketAds_All",
"paginationType": "server",
"public": false
},
{
"id": "market-pages",
"path": "/MarketPage_All",
"pageKey": "MarketPage_All",
"apiPageName": "MarketPage_All",
"paginationType": "server",
"public": false
},
{
"id": "market-products",
"path": "/MarketProduct_All",
"pageKey": "MarketProduct_All",
"apiPageName": "MarketProduct_All",
"paginationType": "server",
"public": false
},
{
"id": "product-details",
"path": "/Product_Details",
"pageKey": "Product_Details",
"apiPageName": "Product_All",
"public": false,
"queryParams": {
"glid": ":glid"
}
},
{
"id": "market-product-details",
"path": "/MarketProduct_Details",
"pageKey": "MarketProduct_Details",
"apiPageName": "MarketProduct_Details",
"public": false,
"paginationType": "server",
"queryParams": {
"glid": ":id"
}
}
]Design Considerations
This structure allows us to add all possible configuration information like:
- Route guards
- Layout information
- Query parameters
- Any other metadata we might need
- paginationType helps us determine whether the page is going to server-paginated. Helps us with the initial api call, whether to include page_size, page_number etc in api url query params
Note: The handling of pages with query parameters (like glid or page_name) is still being refined.
Implementation
App.jsx
<ErrorBoundary
FallbackComponent={() => (
<ErrorBoundaryFallback resetErrorBoundary={resetErrorBoundary} />
)}
>
<Routes>
{/* Dynamic routes from backend */}
{dynamicRoutes.map(route => {
const element =
route.layout === 'none' ? (
// unique key is required here so the component remounts on route change
<PageContainer key={route.id} routeConfig={route} />
) : (
<Layout>
<PageContainer key={route.id} routeConfig={route} />
</Layout>
);
return (
<Route
key={route.id || route.path}
path={route.path}
element={element}
/>
);
})}
</Routes>
</ErrorBoundary>The Layout component is a simple wrapper with a Sidebar that's populated from the backend.
PageContainer.jsx
This file contains two components: PageContent and PageContainer. PageContainer is the default export.
import { PageDataProvider, usePageData } from '../../context/PageDataContext';
import { Widget } from '../../widgets/Widget';
const PageContent = () => {
const { data: widgets, apiPageName } = usePageData();
// the data is shaped like this:
// [{...widget1}, {...widget2}, {...widget3}]
return (
<div className='w-full space-y-6'>
{widgets.map((widget, idx) => (
<Widget
key={`${widget?.dgv_id || widget?.widget_name || idx}`}
widget={widget}
apiPageName={apiPageName}
/>
))}
</div>
);
};
export default function PageContainer({ routeConfig }) {
return (
<PageDataProvider routeConfig={routeConfig}>
<PageContent />
</PageDataProvider>
);
}PageDataContext.jsx
The provider fetches page data based on the apiPageName:
import { createContext, useContext, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useGetPageDataQuery } from '../services/page';
import LoadingState from '../components/states/LoadingState';
import ErrorState from '../components/states/ErrorState';
const PageDataContext = createContext(null);
/**
* PageDataProvider
* - Fetches page data based on routeConfig.apiPageName
* - Provides page data, loading/error states, and params to children
*/
export const PageDataProvider = ({ routeConfig, children }) => {
const [searchParams] = useSearchParams();
const paramsObject = Object.fromEntries(searchParams.entries());
const glid = paramsObject['glid'];
const apiPageName = routeConfig?.apiPageName;
const {
data,
isLoading,
isError,
error,
isFetching,
refetch: refetchPageData,
} = useGetPageDataQuery({ page: apiPageName }, { skip: !apiPageName });
const value = useMemo(
() => ({
apiPageName,
data,
glid,
isLoading,
isError,
isFetching,
error,
refetchPageData,
}),
[
apiPageName,
data,
glid,
isLoading,
isError,
isFetching,
error,
refetchPageData,
]
);
if (isLoading) return <LoadingState />;
if (isError) return <ErrorState error={error} />;
if (!Array.isArray(data) || data.length === 0) {
return (
<div className='p-4'>
<div className='text-gray-600'>
No content available for {apiPageName}
</div>
</div>
);
}
return (
<PageDataContext.Provider value={value}>
{children}
</PageDataContext.Provider>
);
};
/**
* Hook to use page data anywhere in the tree
*/
export const usePageData = () => {
const context = useContext(PageDataContext);
if (!context) {
throw new Error('usePageData must be used within a PageDataProvider');
}
return context;
};Key Feature: The context makes all page data automatically available to ANY component down the tree that needs it.
Widget System
Widget.jsx
The main widget renderer that looks up widget types and renders appropriate components:
// widgets/Widget.jsx - Cleaner widget renderer
import { widgetRegistry } from './widgetRegistry';
/**
* Widget Component
* - Looks up widget type in registry
* - Renders appropriate component
* - Shows error state for unknown widgets
*/
export function Widget({ widget, apiPageName, index }) {
const WidgetComponent = widgetRegistry[widget.type];
if (!WidgetComponent) {
console.warn(`Unknown widget type: ${widget.type}`, widget);
return (
<div className='p-4 border-2 border-dashed border-yellow-400 rounded-lg bg-yellow-50'>
<div className='flex items-center gap-2 text-yellow-800'>
<span className='text-lg'>⚠️</span>
<div>
<div className='font-semibold'>Unknown Widget Type</div>
<div className='text-sm'>Type: {widget.type}</div>
<div className='text-xs mt-1 text-gray-600'>
Widget ID: {widget.dgv_id || widget.widget_name || index}
</div>
</div>
</div>
</div>
);
}
return (
<WidgetComponent widget={widget} apiPageName={apiPageName} index={index} />
);
}widgetRegistry.jsx
Maps backend widget types to React components:
// widgets/index.js - Centralized widget exports and registry
import TableWidget from './TableWidget';
import PageHeaderWidget from './PageHeaderWidget';
// ... other widget imports
/**
* Widget Registry
* Maps backend widget types to React components
*
* Each widget receives: { widget, apiPageName, pageData }
*/
export const widgetRegistry = {
'page_header': PageHeaderWidget,
'dgv': TableWidget,
'paginated_dgv': TableWidget,
// Add more as needed
};Note: We could fetch this registry from the backend, but keeping it on the frontend keeps things simple for now.
Widget Implementation
Each widget (like TableWidget) handles its own logic internally and passes data to its template component (like <TableTemplate />).
Please note that the User_All will most likely be excluded from this page system, because it is too different from every other page in the webstie.