Skip to content

Commit 23c123c

Browse files
catomeanclaude
andcommitted
refactor: add governance shared components and utilities
- Create types/governance.ts with centralized type definitions (TaxPayment, CitizenData, AgencyData, etc.) - Add lib/governance/utils.ts with shared utility functions (formatCurrency, getTransparencyScoreClasses, getStatusBadgeClasses) - Create reusable governance components: - ProfileHeader: shared header component for profile pages - ProfileTabs: reusable tab navigation component - InfoBox: configurable info/warning/success/error boxes These shared components and utilities support breaking down large governance components (CitizenProfile.tsx 650 lines, AgencyProfile.tsx 437 lines) into smaller, more maintainable pieces while following DRY principles. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 260ddab commit 23c123c

7 files changed

Lines changed: 407 additions & 0 deletions

File tree

components/governance/InfoBox.tsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use client';
2+
3+
import { type FC, type ReactNode } from 'react';
4+
5+
type InfoBoxVariant = 'info' | 'warning' | 'success' | 'error';
6+
7+
interface InfoBoxProps {
8+
variant: InfoBoxVariant;
9+
title?: string;
10+
children: ReactNode;
11+
action?: {
12+
label: string;
13+
onClick: () => void;
14+
};
15+
}
16+
17+
const variantStyles: Record<InfoBoxVariant, { bg: string; icon: string; text: string; button: string }> = {
18+
info: {
19+
bg: 'bg-blue-50',
20+
icon: 'text-blue-400',
21+
text: 'text-blue-700',
22+
button: 'text-blue-700 hover:text-blue-600',
23+
},
24+
warning: {
25+
bg: 'bg-yellow-50',
26+
icon: 'text-yellow-400',
27+
text: 'text-yellow-700',
28+
button: 'text-yellow-700 hover:text-yellow-600',
29+
},
30+
success: {
31+
bg: 'bg-green-50',
32+
icon: 'text-green-400',
33+
text: 'text-green-700',
34+
button: 'bg-green-100 text-green-800 hover:bg-green-200',
35+
},
36+
error: {
37+
bg: 'bg-red-50',
38+
icon: 'text-red-400',
39+
text: 'text-red-700',
40+
button: 'text-red-700 hover:text-red-600',
41+
},
42+
};
43+
44+
const icons: Record<InfoBoxVariant, JSX.Element> = {
45+
info: (
46+
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
47+
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
48+
</svg>
49+
),
50+
warning: (
51+
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
52+
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
53+
</svg>
54+
),
55+
success: (
56+
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
57+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
58+
</svg>
59+
),
60+
error: (
61+
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
62+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
63+
</svg>
64+
),
65+
};
66+
67+
/**
68+
* Reusable info/warning/success/error box component
69+
*/
70+
export const InfoBox: FC<InfoBoxProps> = ({
71+
variant,
72+
title,
73+
children,
74+
action,
75+
}) => {
76+
const styles = variantStyles[variant];
77+
78+
return (
79+
<div className={`rounded-md ${styles.bg} p-4`}>
80+
<div className="flex">
81+
<div className={`flex-shrink-0 ${styles.icon}`}>
82+
{icons[variant]}
83+
</div>
84+
<div className="ml-3 flex-1">
85+
{title && (
86+
<h3 className={`text-sm font-medium ${styles.text.replace('text-', 'text-').replace('-700', '-800')}`}>
87+
{title}
88+
</h3>
89+
)}
90+
<div className={`${title ? 'mt-2' : ''} text-sm ${styles.text}`}>
91+
{children}
92+
</div>
93+
{action && (
94+
<div className="mt-4">
95+
<button
96+
type="button"
97+
onClick={action.onClick}
98+
className={`px-3 py-1.5 rounded-md text-sm font-medium ${styles.button} focus:outline-none focus:ring-2 focus:ring-offset-2`}
99+
>
100+
{action.label}
101+
</button>
102+
</div>
103+
)}
104+
</div>
105+
</div>
106+
</div>
107+
);
108+
};
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use client';
2+
3+
import { type FC } from 'react';
4+
5+
interface ProfileHeaderProps {
6+
name: string;
7+
id: string;
8+
subtitle?: string;
9+
avatarUrl?: string;
10+
gradientFrom?: string;
11+
gradientTo?: string;
12+
badge?: {
13+
label: string;
14+
value: string | number;
15+
};
16+
}
17+
18+
/**
19+
* Reusable profile header component for governance profiles
20+
* Used by CitizenProfile, AgencyProfile, and EmployeeProfile
21+
*/
22+
export const ProfileHeader: FC<ProfileHeaderProps> = ({
23+
name,
24+
id,
25+
subtitle,
26+
avatarUrl,
27+
gradientFrom = 'blue-600',
28+
gradientTo = 'green-600',
29+
badge,
30+
}) => {
31+
return (
32+
<div className={`relative bg-gradient-to-r from-${gradientFrom} to-${gradientTo} px-4 py-6 sm:px-6 lg:px-8`}>
33+
<div className="relative max-w-7xl mx-auto">
34+
<div className="md:flex md:items-center md:justify-between">
35+
<div className="flex-1 min-w-0 flex items-center">
36+
{avatarUrl ? (
37+
/* eslint-disable-next-line @next/next/no-img-element -- Dynamic external avatar URL from data */
38+
<img
39+
className="h-16 w-16 rounded-full border-4 border-white"
40+
src={avatarUrl}
41+
alt={name}
42+
/>
43+
) : (
44+
<div className="h-16 w-16 rounded-full border-4 border-white bg-gray-200 flex items-center justify-center text-gray-500 text-2xl">
45+
{name.charAt(0)}
46+
</div>
47+
)}
48+
<div className="ml-4">
49+
<h1 className="text-2xl font-bold text-white truncate">{name}</h1>
50+
<p className="text-white opacity-90">ID: {id}</p>
51+
{subtitle && <p className="text-white opacity-80">{subtitle}</p>}
52+
</div>
53+
</div>
54+
{badge && (
55+
<div className="mt-4 flex-shrink-0 flex md:mt-0 md:ml-4">
56+
<span className="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
57+
{badge.label}: {badge.value}
58+
</span>
59+
</div>
60+
)}
61+
</div>
62+
</div>
63+
</div>
64+
);
65+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
3+
import { type FC } from 'react';
4+
5+
interface Tab {
6+
id: string;
7+
label: string;
8+
}
9+
10+
interface ProfileTabsProps {
11+
tabs: Tab[];
12+
activeTab: string;
13+
onTabChange: (tabId: string) => void;
14+
}
15+
16+
/**
17+
* Reusable tab navigation component for governance profiles
18+
*/
19+
export const ProfileTabs: FC<ProfileTabsProps> = ({
20+
tabs,
21+
activeTab,
22+
onTabChange,
23+
}) => {
24+
return (
25+
<div className="border-b border-gray-200">
26+
<nav className="-mb-px flex">
27+
{tabs.map((tab) => (
28+
<button
29+
key={tab.id}
30+
onClick={() => onTabChange(tab.id)}
31+
className={`${
32+
activeTab === tab.id
33+
? 'border-blue-500 text-blue-600'
34+
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
35+
} whitespace-nowrap py-4 px-6 border-b-2 font-medium text-sm`}
36+
>
37+
{tab.label}
38+
</button>
39+
))}
40+
</nav>
41+
</div>
42+
);
43+
};

components/governance/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Governance shared components
2+
export { ProfileHeader } from './ProfileHeader';
3+
export { ProfileTabs } from './ProfileTabs';
4+
export { InfoBox } from './InfoBox';

lib/governance/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Governance utility exports
2+
export * from './utils';

lib/governance/utils.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Governance utility functions
2+
3+
/**
4+
* Format a number as USD currency
5+
*/
6+
export const formatCurrency = (amount: number): string => {
7+
return new Intl.NumberFormat('en-US', {
8+
style: 'currency',
9+
currency: 'USD',
10+
maximumFractionDigits: 0,
11+
}).format(amount);
12+
};
13+
14+
/**
15+
* Get the appropriate Tailwind classes for a transparency score badge
16+
*/
17+
export const getTransparencyScoreClasses = (score: number): string => {
18+
if (score >= 90) return 'bg-green-100 text-green-800';
19+
if (score >= 70) return 'bg-blue-100 text-blue-800';
20+
if (score >= 50) return 'bg-yellow-100 text-yellow-800';
21+
return 'bg-red-100 text-red-800';
22+
};
23+
24+
/**
25+
* Get the appropriate Tailwind classes for a transparency score progress bar
26+
*/
27+
export const getTransparencyBarClasses = (score: number): string => {
28+
if (score >= 90) return 'bg-green-500';
29+
if (score >= 70) return 'bg-blue-500';
30+
if (score >= 50) return 'bg-yellow-500';
31+
return 'bg-red-500';
32+
};
33+
34+
/**
35+
* Get status badge classes based on status type
36+
*/
37+
export const getStatusBadgeClasses = (status: string): string => {
38+
const statusMap: Record<string, string> = {
39+
'Paid': 'bg-green-100 text-green-800',
40+
'Active': 'bg-green-100 text-green-800',
41+
'Completed': 'bg-green-100 text-green-800',
42+
'Pending': 'bg-yellow-100 text-yellow-800',
43+
'Late': 'bg-red-100 text-red-800',
44+
'Flagged': 'bg-red-100 text-red-800',
45+
'Disputed': 'bg-gray-100 text-gray-800',
46+
'Expired': 'bg-gray-100 text-gray-800',
47+
};
48+
return statusMap[status] || 'bg-gray-100 text-gray-800';
49+
};
50+
51+
/**
52+
* Get difference indicator classes (positive/negative/neutral)
53+
*/
54+
export const getDifferenceClasses = (difference: number): string => {
55+
if (difference > 0) return 'text-green-600';
56+
if (difference < 0) return 'text-red-600';
57+
return 'text-gray-600';
58+
};
59+
60+
/**
61+
* Get allocation total indicator classes
62+
*/
63+
export const getAllocationClasses = (total: number): string => {
64+
if (total > 100) return 'text-red-600';
65+
if (total < 100) return 'text-yellow-600';
66+
return 'text-green-600';
67+
};
68+
69+
/**
70+
* Get allocation bar background classes
71+
*/
72+
export const getAllocationBarClasses = (total: number): string => {
73+
if (total > 100) return 'bg-red-500';
74+
if (total < 100) return 'bg-yellow-500';
75+
return 'bg-green-500';
76+
};

0 commit comments

Comments
 (0)