Add ExpertiseSelect component for onboarding
Replace database-backed TagInput with a cleaner ExpertiseSelect component that has predefined ocean conservation expertise areas. Features a checkbox grid UI that's more user-friendly for onboarding. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
39f7bc207b
commit
4f0531d2ee
|
|
@ -22,7 +22,7 @@ import {
|
|||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { toast } from 'sonner'
|
||||
import { TagInput } from '@/components/shared/tag-input'
|
||||
import { ExpertiseSelect } from '@/components/shared/expertise-select'
|
||||
import {
|
||||
User,
|
||||
Phone,
|
||||
|
|
@ -202,15 +202,11 @@ export default function OnboardingPage() {
|
|||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Expertise Tags</Label>
|
||||
<TagInput
|
||||
value={expertiseTags}
|
||||
onChange={setExpertiseTags}
|
||||
placeholder="Select your expertise areas..."
|
||||
maxTags={10}
|
||||
/>
|
||||
</div>
|
||||
<ExpertiseSelect
|
||||
value={expertiseTags}
|
||||
onChange={setExpertiseTags}
|
||||
maxTags={5}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Check, X } from 'lucide-react'
|
||||
|
||||
// Predefined expertise areas for ocean conservation
|
||||
const EXPERTISE_OPTIONS = [
|
||||
{ id: 'marine-biology', name: 'Marine Biology', color: '#0ea5e9' },
|
||||
{ id: 'ocean-conservation', name: 'Ocean Conservation', color: '#06b6d4' },
|
||||
{ id: 'climate-science', name: 'Climate Science', color: '#14b8a6' },
|
||||
{ id: 'sustainable-fishing', name: 'Sustainable Fishing', color: '#22c55e' },
|
||||
{ id: 'plastic-pollution', name: 'Plastic Pollution', color: '#84cc16' },
|
||||
{ id: 'coral-reef', name: 'Coral Reef Restoration', color: '#f97316' },
|
||||
{ id: 'blue-economy', name: 'Blue Economy', color: '#3b82f6' },
|
||||
{ id: 'marine-technology', name: 'Marine Technology', color: '#8b5cf6' },
|
||||
{ id: 'environmental-policy', name: 'Environmental Policy', color: '#a855f7' },
|
||||
{ id: 'oceanography', name: 'Oceanography', color: '#0284c7' },
|
||||
{ id: 'renewable-energy', name: 'Renewable Energy', color: '#16a34a' },
|
||||
{ id: 'waste-management', name: 'Waste Management', color: '#65a30d' },
|
||||
{ id: 'biodiversity', name: 'Biodiversity', color: '#059669' },
|
||||
{ id: 'shipping-maritime', name: 'Shipping & Maritime', color: '#6366f1' },
|
||||
{ id: 'education-outreach', name: 'Education & Outreach', color: '#ec4899' },
|
||||
{ id: 'entrepreneurship', name: 'Entrepreneurship', color: '#f43f5e' },
|
||||
{ id: 'investment-finance', name: 'Investment & Finance', color: '#eab308' },
|
||||
{ id: 'research-academia', name: 'Research & Academia', color: '#7c3aed' },
|
||||
]
|
||||
|
||||
interface ExpertiseSelectProps {
|
||||
value: string[]
|
||||
onChange: (tags: string[]) => void
|
||||
maxTags?: number
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ExpertiseSelect({
|
||||
value,
|
||||
onChange,
|
||||
maxTags = 10,
|
||||
disabled = false,
|
||||
className,
|
||||
}: ExpertiseSelectProps) {
|
||||
const handleToggle = (name: string) => {
|
||||
if (disabled) return
|
||||
|
||||
if (value.includes(name)) {
|
||||
onChange(value.filter((t) => t !== name))
|
||||
} else {
|
||||
if (maxTags && value.length >= maxTags) return
|
||||
onChange([...value, name])
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemove = (name: string) => {
|
||||
if (disabled) return
|
||||
onChange(value.filter((t) => t !== name))
|
||||
}
|
||||
|
||||
const getOption = (name: string) =>
|
||||
EXPERTISE_OPTIONS.find((o) => o.name === name)
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
{/* Selected tags at the top */}
|
||||
{value.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{value.map((name) => {
|
||||
const option = getOption(name)
|
||||
return (
|
||||
<Badge
|
||||
key={name}
|
||||
variant="secondary"
|
||||
className="gap-1.5 py-1 px-2 text-sm"
|
||||
style={{
|
||||
backgroundColor: option?.color ? `${option.color}15` : undefined,
|
||||
borderColor: option?.color || undefined,
|
||||
color: option?.color || undefined,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemove(name)}
|
||||
className="ml-0.5 rounded-full p-0.5 hover:bg-black/10 transition-colors"
|
||||
aria-label={`Remove ${name}`}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</Badge>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grid of options */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{EXPERTISE_OPTIONS.map((option) => {
|
||||
const isSelected = value.includes(option.name)
|
||||
const isDisabled = disabled || (!isSelected && value.length >= maxTags)
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={option.id}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleToggle(option.name)}
|
||||
className={cn(
|
||||
'justify-start h-auto py-2 px-3 text-left font-normal transition-all',
|
||||
isSelected && 'ring-2 ring-offset-1',
|
||||
isDisabled && !isSelected && 'opacity-50'
|
||||
)}
|
||||
style={{
|
||||
borderColor: isSelected ? option.color : undefined,
|
||||
ringColor: option.color,
|
||||
backgroundColor: isSelected ? `${option.color}10` : undefined,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'h-4 w-4 rounded border-2 mr-2 flex items-center justify-center transition-colors',
|
||||
isSelected ? 'border-current bg-current' : 'border-muted-foreground/30'
|
||||
)}
|
||||
style={{
|
||||
borderColor: isSelected ? option.color : undefined,
|
||||
backgroundColor: isSelected ? option.color : undefined,
|
||||
}}
|
||||
>
|
||||
{isSelected && <Check className="h-3 w-3 text-white" />}
|
||||
</div>
|
||||
<span className="text-sm">{option.name}</span>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Counter */}
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
{value.length} of {maxTags} selected
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue