We're making the partner custom dashboards in TopNav dynamic. The features section (Real-time Verification, Embed, etc.) will remain hardcoded for now. This document outlines the frontend changes needed to integrate with the new backend API.
- Location: Partner dashboards are hardcoded in
app/components/MinimalHeader.tsx(lines 262-269) - Component:
app/components/TopNav/components/TopNav.tsx - Icons: Currently stored as React components in
app/components/TopNav/components/Icons.tsx
The backend will provide partner dashboards through the existing customization API:
interface Customization {
// ... existing fields ...
partnerDashboards?: PartnerDashboard[];
}
interface PartnerDashboard {
id: string; // Used for routing: /#/{id}/dashboard
name: string; // Display name in TopNav
logo: string; // SVG string (complete SVG code)
showInTopNav: boolean; // Whether to show in TopNav
}Important Notes:
- Array order = display order (no separate order field)
isCurrentis NOT from backend - determine this on frontend based on current route- SVG comes as a complete string, already sanitized by our existing DOMPurify setup
Add the new types to the CustomizationResponse interface:
// Add to CustomizationResponse interface
type CustomizationResponse = {
// ... existing fields ...
partnerDashboards?: PartnerDashboard[];
};
// Add new interface
interface PartnerDashboard {
id: string;
name: string;
logo: string;
showInTopNav: boolean;
}Update the requestCustomizationConfig function to include the new field:
export const requestCustomizationConfig = async (customizationKey: string): Promise<Customization | undefined> => {
const response = await axios.get(`${CUSTOMIZATION_ENDPOINT}/${customizationKey}`);
const customizationResponse: CustomizationResponse = response.data;
return {
// ... existing fields ...
partnerDashboards: customizationResponse.partnerDashboards,
};
};Replace the hardcoded partner dashboards with dynamic data:
import { useCustomization } from "../hooks/useCustomization";
import { useParams } from "react-router-dom";
const MinimalHeader = ({ className }: MinimalHeaderProps): JSX.Element => {
const { key } = useParams();
const customization = useCustomization();
// Filter for TopNav-visible dashboards and add isCurrent flag
const partnerDashboards = useMemo(() => {
const visibleDashboards = customization.partnerDashboards
?.filter(dashboard => dashboard.showInTopNav)
?.map(dashboard => ({
...dashboard,
isCurrent: dashboard.id === key // Mark current based on URL
})) || [];
return visibleDashboards;
}, [customization.partnerDashboards, key]);
// Keep features hardcoded as before
const navFeatures: NavFeature[] = [
{
icon: "user-check",
title: "Real-time Verification",
description: "Protect programs in real-time with Stamps and Models",
url: "https://passport.human.tech/verification",
},
// ... rest of features remain the same
];
return (
<>
{/* ... existing JSX ... */}
{showTopNav ? (
<TopNav
features={navFeatures}
partners={partnerDashboards} // Now using dynamic data
onClose={() => setShowTopNav(false)}
buttonRef={navButtonRef}
/>
) : (
// ... partner with us section
)}
</>
);
};Update the PartnerLink interface in app/components/TopNav/mocks/navData.ts:
export interface PartnerLink {
id: string;
name: string;
logo: string; // Now a string (SVG) instead of icon name
isCurrent?: boolean;
}We already have the perfect component for this! Just import and use it.
Modify app/components/TopNav/components/TopNav.tsx:
// Import the existing sanitization component
import { SanitizedHTMLComponent } from "../../utils/customizationUtils";
export const TopNav: React.FC<TopNavProps> = ({
features = [],
partners = [],
onPartnerClick,
onClose,
buttonRef = null,
}) => {
// ... existing code ...
return (
<div /* ... existing props ... */>
{/* ... features section remains the same ... */}
{/* Partner Custom Dashboards Section */}
{partners.length > 0 && (
<div className="bg-background box-border flex flex-col gap-4 items-start p-4 rounded-lg w-full">
{/* ... header remains the same ... */}
<div className="flex items-stretch gap-2 w-full">
{partners.map((partner) => (
<button
key={partner.id}
onClick={() => handlePartnerClick(partner.id)}
className={
partner.isCurrent
? "flex-1 bg-foreground brightness-[.83] shadow-md cursor-default box-border flex gap-2 items-center justify-center p-2 rounded-lg transition-all duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-focus focus:ring-opacity-50"
: "flex-1 bg-foreground box-border flex gap-2 items-center justify-center p-2 rounded-lg transition-all duration-200 ease-in-out hover:brightness-[.83] hover:shadow-md focus:outline-none focus:ring-2 focus:ring-focus focus:ring-opacity-50"
}
>
<div className="w-[23px] h-[23px] flex-shrink-0">
<SanitizedHTMLComponent html={partner.logo} />
</div>
<span className="font-medium text-sm leading-5 text-color-4 whitespace-nowrap">
{partner.name}
</span>
</button>
))}
</div>
</div>
)}
</div>
);
};Once the dynamic implementation is working:
- Remove the hardcoded
partnerDashboardsarray fromMinimalHeader.tsx - Remove
getPartnerLogofunction and individual logo components fromIcons.tsx - Update or remove mock data in
app/components/TopNav/mocks/navData.tsas needed for Storybook
// The component already handles empty arrays gracefully
{partners.length > 0 && (
// ... partner section only renders if there are partners
)}All Dashboards Hidden
If all dashboards have showInTopNav: false, the filtered array will be empty and the section won't render.
// Use optional chaining and provide default empty array
const visibleDashboards = customization.partnerDashboards
?.filter(dashboard => dashboard.showInTopNav) || [];- Empty State: Test with no partner dashboards from backend
- Mixed Visibility: Test with some dashboards having
showInTopNav: trueand othersfalse - Current Dashboard Highlighting: Navigate to different dashboard URLs and verify
isCurrentstyling - SVG Rendering: Verify all partner logos render correctly
- Fallback: Test behavior when
partnerDashboardsis undefined - Click Navigation: Test that clicking partners navigates to
/#/{id}/dashboard
Update the stories to use SVG strings for testing:
// app/components/TopNav/components/TopNav.stories.tsx
export const mockPartners: PartnerLink[] = [
{
name: "Lido",
id: "lido",
logo: '<svg width="23" height="23">...</svg>' // Use actual SVG strings
},
// ... etc
];-
Phase 1 - Prepare:
- Add new types to customizationUtils
- Update TopNav component to handle both old (icon name) and new (SVG string) formats
-
Phase 2 - Deploy Backend:
- Backend deploys new API with
partnerDashboardsfield
- Backend deploys new API with
-
Phase 3 - Switch to Dynamic:
- Update MinimalHeader to use dynamic data
- Remove hardcoded arrays
- Clean up unused icon components
- SVG Sanitization: Use our existing pattern with DOMPurify + html-react-parser (NO dangerouslySetInnerHTML!)
- No Caching Needed: The customization data is already being fetched and managed
- Extensible: The
PartnerDashboardinterface can be extended with more fields in the future without breaking existing code - Performance: Since we're already fetching customization data, there's no additional API call
- Safety First: The SanitizedSVG component follows the same safe pattern we use for other HTML content
{
"partnerName": "Example Partner",
"partnerDashboards": [
{
"id": "lido",
"name": "Lido",
"logo": "<svg width=\"23\" height=\"23\" viewBox=\"0 0 23 23\">...</svg>",
"showInTopNav": true
},
{
"id": "verax",
"name": "Verax",
"logo": "<svg width=\"23\" height=\"23\" viewBox=\"0 0 23 23\">...</svg>",
"showInTopNav": true
}
// ... more dashboards
]
}If you encounter any issues or have questions during implementation, the key things to remember:
- Features stay hardcoded (for now)
- Partner dashboards become dynamic
- SVG comes as strings, not component references
isCurrentis frontend logic based on URL params