UI polish: grouped dropdowns, analytics readability, invite tag picker
- Messages: group users by role in recipient dropdown (SelectGroup) - Analytics: full country names via Intl.DisplayNames, format SNAKE_CASE labels to Title Case, custom tooltips, increased font sizes - Invite: replace free-text tag input with grouped dropdown from DB tags using Command/Popover, showing tags organized by category with colors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,53 @@ const PIE_COLORS = [
|
||||
'#8884d8', '#82ca9d', '#ffc658', '#ff7c7c', '#8dd1e1',
|
||||
]
|
||||
|
||||
/** Convert ISO 3166-1 alpha-2 code to full country name using Intl API */
|
||||
function getCountryName(code: string): string {
|
||||
if (code === 'Others') return 'Others'
|
||||
try {
|
||||
const displayNames = new Intl.DisplayNames(['en'], { type: 'region' })
|
||||
return displayNames.of(code.toUpperCase()) || code
|
||||
} catch {
|
||||
return code
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert SCREAMING_SNAKE_CASE to Title Case */
|
||||
function formatLabel(value: string): string {
|
||||
if (!value) return value
|
||||
return value
|
||||
.replace(/_/g, ' ')
|
||||
.toLowerCase()
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
}
|
||||
|
||||
/** Custom tooltip for the pie chart */
|
||||
function CountryTooltip({ active, payload }: { active?: boolean; payload?: Array<{ payload: { country: string; count: number; percentage: number } }> }) {
|
||||
if (!active || !payload?.length) return null
|
||||
const d = payload[0].payload
|
||||
return (
|
||||
<div className="rounded-md border bg-card px-3 py-2 text-sm shadow-md">
|
||||
<p className="font-medium">{getCountryName(d.country)}</p>
|
||||
<p className="text-muted-foreground">{d.count} projects ({d.percentage.toFixed(1)}%)</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Custom tooltip for bar charts */
|
||||
function BarTooltip({ active, payload, labelFormatter }: { active?: boolean; payload?: Array<{ value: number }>; label?: string; labelFormatter: (val: string) => string }) {
|
||||
if (!active || !payload?.length) return null
|
||||
const entry = payload[0]
|
||||
const rawPayload = entry as unknown as { payload: Record<string, unknown> }
|
||||
const dataPoint = rawPayload.payload
|
||||
const rawLabel = (dataPoint.category || dataPoint.issue || '') as string
|
||||
return (
|
||||
<div className="rounded-md border bg-card px-3 py-2 text-sm shadow-md">
|
||||
<p className="font-medium">{labelFormatter(rawLabel)}</p>
|
||||
<p className="text-muted-foreground">{entry.value} projects</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
if (data.total === 0) {
|
||||
return (
|
||||
@@ -56,6 +103,17 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
}]
|
||||
: topCountries
|
||||
|
||||
// Pre-format category and ocean issue data for display
|
||||
const formattedCategories = data.byCategory.slice(0, 10).map((c) => ({
|
||||
...c,
|
||||
category: formatLabel(c.category),
|
||||
}))
|
||||
|
||||
const formattedOceanIssues = data.byOceanIssue.slice(0, 15).map((o) => ({
|
||||
...o,
|
||||
issue: formatLabel(o.issue),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Summary */}
|
||||
@@ -93,7 +151,7 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
<CardTitle>Geographic Distribution</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[350px]">
|
||||
<div className="h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
@@ -105,18 +163,20 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
paddingAngle={2}
|
||||
dataKey="count"
|
||||
nameKey="country"
|
||||
label
|
||||
label={((props: unknown) => {
|
||||
const p = props as { country: string; percentage: number }
|
||||
return `${getCountryName(p.country)} (${p.percentage.toFixed(0)}%)`
|
||||
}) as unknown as boolean}
|
||||
fontSize={13}
|
||||
>
|
||||
{countryPieData.map((_, index) => (
|
||||
<Cell key={`cell-${index}`} fill={PIE_COLORS[index % PIE_COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
<Tooltip content={<CountryTooltip />} />
|
||||
<Legend
|
||||
formatter={(value: string) => getCountryName(value)}
|
||||
wrapperStyle={{ fontSize: '13px' }}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -130,29 +190,23 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
<CardTitle>Competition Categories</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data.byCategory.length > 0 ? (
|
||||
<div className="h-[350px]">
|
||||
{formattedCategories.length > 0 ? (
|
||||
<div className="h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={data.byCategory.slice(0, 10)}
|
||||
data={formattedCategories}
|
||||
layout="vertical"
|
||||
margin={{ top: 5, right: 30, bottom: 5, left: 100 }}
|
||||
margin={{ top: 5, right: 30, bottom: 5, left: 120 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis type="number" />
|
||||
<XAxis type="number" tick={{ fontSize: 13 }} />
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="category"
|
||||
width={90}
|
||||
tick={{ fontSize: 12 }}
|
||||
width={110}
|
||||
tick={{ fontSize: 13 }}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
/>
|
||||
<Tooltip content={<BarTooltip labelFormatter={(v) => v} />} />
|
||||
<Bar dataKey="count" fill="#053d57" radius={[0, 4, 4, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -165,34 +219,29 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
</div>
|
||||
|
||||
{/* Ocean Issues */}
|
||||
{data.byOceanIssue.length > 0 && (
|
||||
{formattedOceanIssues.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ocean Issues Addressed</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[350px]">
|
||||
<div className="h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={data.byOceanIssue.slice(0, 15)}
|
||||
margin={{ top: 20, right: 30, bottom: 60, left: 20 }}
|
||||
data={formattedOceanIssues}
|
||||
margin={{ top: 20, right: 30, bottom: 80, left: 20 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis
|
||||
dataKey="issue"
|
||||
angle={-35}
|
||||
textAnchor="end"
|
||||
height={80}
|
||||
tick={{ fontSize: 11 }}
|
||||
/>
|
||||
<YAxis />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
height={100}
|
||||
tick={{ fontSize: 12 }}
|
||||
interval={0}
|
||||
/>
|
||||
<YAxis tick={{ fontSize: 13 }} />
|
||||
<Tooltip content={<BarTooltip labelFormatter={(v) => v} />} />
|
||||
<Bar dataKey="count" fill="#557f8c" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -215,7 +264,7 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
variant="secondary"
|
||||
className="text-sm"
|
||||
style={{
|
||||
fontSize: `${Math.max(0.7, Math.min(1.4, 0.7 + tag.percentage / 20))}rem`,
|
||||
fontSize: `${Math.max(0.75, Math.min(1.4, 0.75 + tag.percentage / 20))}rem`,
|
||||
}}
|
||||
>
|
||||
{tag.tag} ({tag.count})
|
||||
|
||||
Reference in New Issue
Block a user