UI polish: grouped dropdowns, analytics readability, invite tag picker
Build and Push Docker Image / build (push) Successful in 9m56s
Details
Build and Push Docker Image / build (push) Successful in 9m56s
Details
- 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:
parent
4830c0638c
commit
98fe658c33
|
|
@ -37,6 +37,19 @@ import {
|
|||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
|
|
@ -50,6 +63,8 @@ import {
|
|||
UserPlus,
|
||||
FolderKanban,
|
||||
ChevronDown,
|
||||
Check,
|
||||
Tags,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
|
|
@ -67,7 +82,6 @@ interface MemberRow {
|
|||
email: string
|
||||
role: Role
|
||||
expertiseTags: string[]
|
||||
tagInput: string
|
||||
assignments: Assignment[]
|
||||
}
|
||||
|
||||
|
|
@ -96,30 +110,144 @@ function nextRowId(): string {
|
|||
}
|
||||
|
||||
function createEmptyRow(role: Role = 'JURY_MEMBER'): MemberRow {
|
||||
return { id: nextRowId(), name: '', email: '', role, expertiseTags: [], tagInput: '', assignments: [] }
|
||||
return { id: nextRowId(), name: '', email: '', role, expertiseTags: [], assignments: [] }
|
||||
}
|
||||
|
||||
// Common expertise tags for suggestions
|
||||
const SUGGESTED_TAGS = [
|
||||
'Marine Biology',
|
||||
'Ocean Conservation',
|
||||
'Coral Reef Restoration',
|
||||
'Sustainable Fisheries',
|
||||
'Marine Policy',
|
||||
'Ocean Technology',
|
||||
'Climate Science',
|
||||
'Biodiversity',
|
||||
'Blue Economy',
|
||||
'Coastal Management',
|
||||
'Oceanography',
|
||||
'Marine Pollution',
|
||||
'Plastic Reduction',
|
||||
'Renewable Energy',
|
||||
'Business Development',
|
||||
'Impact Investment',
|
||||
'Social Entrepreneurship',
|
||||
'Startup Mentoring',
|
||||
]
|
||||
/** Inline tag picker with grouped dropdown from database tags */
|
||||
function TagPicker({
|
||||
selectedTags,
|
||||
onAdd,
|
||||
onRemove,
|
||||
}: {
|
||||
selectedTags: string[]
|
||||
onAdd: (tag: string) => void
|
||||
onRemove: (tag: string) => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { data, isLoading } = trpc.tag.list.useQuery({ isActive: true })
|
||||
const tags = data?.tags || []
|
||||
|
||||
const tagsByCategory = useMemo(() => {
|
||||
const grouped: Record<string, typeof tags> = {}
|
||||
for (const tag of tags) {
|
||||
const category = tag.category || 'Other'
|
||||
if (!grouped[category]) grouped[category] = []
|
||||
grouped[category].push(tag)
|
||||
}
|
||||
return grouped
|
||||
}, [tags])
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Expertise Tags (optional)
|
||||
</Label>
|
||||
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between font-normal text-muted-foreground"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Tags className="h-4 w-4" />
|
||||
{selectedTags.length > 0
|
||||
? `${selectedTags.length} tag${selectedTags.length !== 1 ? 's' : ''} selected`
|
||||
: 'Select expertise tags...'}
|
||||
</span>
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[350px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search tags..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{isLoading ? 'Loading tags...' : 'No tags found.'}
|
||||
</CommandEmpty>
|
||||
{Object.entries(tagsByCategory)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([category, categoryTags]) => (
|
||||
<CommandGroup key={category} heading={category}>
|
||||
{categoryTags.map((tag) => {
|
||||
const isSelected = selectedTags.includes(tag.name)
|
||||
return (
|
||||
<CommandItem
|
||||
key={tag.id}
|
||||
value={tag.name}
|
||||
onSelect={() => {
|
||||
if (isSelected) {
|
||||
onRemove(tag.name)
|
||||
} else {
|
||||
onAdd(tag.name)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded border',
|
||||
isSelected
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-muted-foreground/30'
|
||||
)}
|
||||
style={{
|
||||
borderColor: isSelected && tag.color ? tag.color : undefined,
|
||||
backgroundColor: isSelected && tag.color ? tag.color : undefined,
|
||||
}}
|
||||
>
|
||||
{isSelected && <Check className="h-3 w-3" />}
|
||||
</div>
|
||||
<span className="flex-1 truncate">{tag.name}</span>
|
||||
{tag.color && (
|
||||
<span
|
||||
className="h-2.5 w-2.5 rounded-full shrink-0"
|
||||
style={{ backgroundColor: tag.color }}
|
||||
/>
|
||||
)}
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{selectedTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{selectedTags.map((tagName) => {
|
||||
const tagData = tags.find((t) => t.name === tagName)
|
||||
return (
|
||||
<Badge
|
||||
key={tagName}
|
||||
variant="secondary"
|
||||
className="gap-1 pr-1"
|
||||
style={{
|
||||
backgroundColor: tagData?.color ? `${tagData.color}15` : undefined,
|
||||
borderColor: tagData?.color || undefined,
|
||||
color: tagData?.color || undefined,
|
||||
}}
|
||||
>
|
||||
{tagName}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemove(tagName)}
|
||||
className="ml-1 hover:text-destructive rounded-full"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MemberInvitePage() {
|
||||
const [step, setStep] = useState<Step>('input')
|
||||
|
|
@ -198,7 +326,7 @@ export default function MemberInvitePage() {
|
|||
prev.map((r) => {
|
||||
if (r.id !== id) return r
|
||||
if (r.expertiseTags.includes(trimmed)) return r
|
||||
return { ...r, expertiseTags: [...r.expertiseTags, trimmed], tagInput: '' }
|
||||
return { ...r, expertiseTags: [...r.expertiseTags, trimmed] }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
@ -213,15 +341,6 @@ export default function MemberInvitePage() {
|
|||
)
|
||||
}
|
||||
|
||||
// Get suggestions that haven't been added yet for a specific row
|
||||
const getSuggestionsForRow = (row: MemberRow) => {
|
||||
return SUGGESTED_TAGS.filter(
|
||||
(tag) =>
|
||||
!row.expertiseTags.includes(tag) &&
|
||||
tag.toLowerCase().includes(row.tagInput.toLowerCase())
|
||||
).slice(0, 5)
|
||||
}
|
||||
|
||||
// Per-row project assignment management
|
||||
const toggleProjectAssignment = (rowId: string, projectId: string) => {
|
||||
if (!selectedRoundId) return
|
||||
|
|
@ -518,87 +637,11 @@ export default function MemberInvitePage() {
|
|||
</div>
|
||||
|
||||
{/* Per-member expertise tags */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Expertise Tags (optional)
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Type tag and press Enter or comma..."
|
||||
value={row.tagInput}
|
||||
onChange={(e) =>
|
||||
updateRow(row.id, 'tagInput', e.target.value)
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ',') {
|
||||
e.preventDefault()
|
||||
addTagToRow(row.id, row.tagInput)
|
||||
}
|
||||
}}
|
||||
<TagPicker
|
||||
selectedTags={row.expertiseTags}
|
||||
onAdd={(tag) => addTagToRow(row.id, tag)}
|
||||
onRemove={(tag) => removeTagFromRow(row.id, tag)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tag suggestions */}
|
||||
{row.tagInput && getSuggestionsForRow(row).length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{getSuggestionsForRow(row).map((suggestion) => (
|
||||
<Button
|
||||
key={suggestion}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => addTagToRow(row.id, suggestion)}
|
||||
>
|
||||
+ {suggestion}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick suggestions when empty */}
|
||||
{!row.tagInput && row.expertiseTags.length === 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<span className="text-xs text-muted-foreground mr-1">
|
||||
Suggestions:
|
||||
</span>
|
||||
{SUGGESTED_TAGS.slice(0, 5).map((suggestion) => (
|
||||
<Button
|
||||
key={suggestion}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 text-xs px-2"
|
||||
onClick={() => addTagToRow(row.id, suggestion)}
|
||||
>
|
||||
+ {suggestion}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Added tags */}
|
||||
{row.expertiseTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{row.expertiseTags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
className="gap-1 pr-1"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTagFromRow(row.id, tag)}
|
||||
className="ml-1 hover:text-destructive rounded-full"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Per-member project pre-assignment (only for jury members) */}
|
||||
{row.role === 'JURY_MEMBER' && selectedRoundId && (
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ import { Switch } from '@/components/ui/switch'
|
|||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
|
|
@ -321,11 +323,44 @@ export default function MessagesPage() {
|
|||
<SelectValue placeholder="Choose a user..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(users as { users: Array<{ id: string; name: string | null; email: string }> } | undefined)?.users?.map((u) => (
|
||||
{(() => {
|
||||
const userList = (users as { users: Array<{ id: string; name: string | null; email: string; role: string }> } | undefined)?.users
|
||||
if (!userList) return null
|
||||
|
||||
const grouped = userList.reduce<Record<string, typeof userList>>((acc, u) => {
|
||||
const role = u.role || 'OTHER'
|
||||
if (!acc[role]) acc[role] = []
|
||||
acc[role].push(u)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const roleLabels: Record<string, string> = {
|
||||
SUPER_ADMIN: 'Super Admins',
|
||||
PROGRAM_ADMIN: 'Program Admins',
|
||||
JURY_MEMBER: 'Jury Members',
|
||||
MENTOR: 'Mentors',
|
||||
OBSERVER: 'Observers',
|
||||
APPLICANT: 'Applicants',
|
||||
OTHER: 'Other',
|
||||
}
|
||||
|
||||
const roleOrder = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT', 'OTHER']
|
||||
|
||||
return roleOrder
|
||||
.filter((role) => grouped[role]?.length)
|
||||
.map((role) => (
|
||||
<SelectGroup key={role}>
|
||||
<SelectLabel>{roleLabels[role] || role}</SelectLabel>
|
||||
{grouped[role]
|
||||
.sort((a, b) => (a.name || a.email).localeCompare(b.name || b.email))
|
||||
.map((u) => (
|
||||
<SelectItem key={u.id} value={u.id}>
|
||||
{u.name || u.email}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))
|
||||
})()}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
width={110}
|
||||
tick={{ fontSize: 13 }}
|
||||
/>
|
||||
<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})
|
||||
|
|
|
|||
Loading…
Reference in New Issue