Compare commits
3 Commits
a3cc73e49d
...
41a36f72b3
| Author | SHA1 | Date |
|---|---|---|
|
|
41a36f72b3 | |
|
|
4f0531d2ee | |
|
|
39f7bc207b |
|
|
@ -1,11 +1,12 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { trpc } from '@/lib/trpc/client'
|
import { trpc } from '@/lib/trpc/client'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { PhoneInput } from '@/components/ui/phone-input'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
|
@ -21,7 +22,7 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { TagInput } from '@/components/shared/tag-input'
|
import { ExpertiseSelect } from '@/components/shared/expertise-select'
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
Phone,
|
Phone,
|
||||||
|
|
@ -47,10 +48,23 @@ export default function OnboardingPage() {
|
||||||
'EMAIL' | 'WHATSAPP' | 'BOTH' | 'NONE'
|
'EMAIL' | 'WHATSAPP' | 'BOTH' | 'NONE'
|
||||||
>('EMAIL')
|
>('EMAIL')
|
||||||
|
|
||||||
|
// Fetch feature flags
|
||||||
|
const { data: featureFlags } = trpc.settings.getFeatureFlags.useQuery()
|
||||||
|
const whatsappEnabled = featureFlags?.whatsappEnabled ?? false
|
||||||
|
|
||||||
const completeOnboarding = trpc.user.completeOnboarding.useMutation()
|
const completeOnboarding = trpc.user.completeOnboarding.useMutation()
|
||||||
|
|
||||||
const steps: Step[] = ['name', 'phone', 'tags', 'preferences', 'complete']
|
// Dynamic steps based on WhatsApp availability
|
||||||
|
const steps: Step[] = useMemo(() => {
|
||||||
|
if (whatsappEnabled) {
|
||||||
|
return ['name', 'phone', 'tags', 'preferences', 'complete']
|
||||||
|
}
|
||||||
|
// Skip phone step if WhatsApp is disabled
|
||||||
|
return ['name', 'tags', 'preferences', 'complete']
|
||||||
|
}, [whatsappEnabled])
|
||||||
|
|
||||||
const currentIndex = steps.indexOf(step)
|
const currentIndex = steps.indexOf(step)
|
||||||
|
const totalVisibleSteps = steps.length - 1 // Exclude 'complete' from count
|
||||||
|
|
||||||
const goNext = () => {
|
const goNext = () => {
|
||||||
if (step === 'name' && !name.trim()) {
|
if (step === 'name' && !name.trim()) {
|
||||||
|
|
@ -91,8 +105,8 @@ export default function OnboardingPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-[#053d57] to-[#557f8c]">
|
<div className="absolute inset-0 -m-4 flex items-center justify-center p-4 md:p-8 bg-gradient-to-br from-[#053d57] to-[#557f8c]">
|
||||||
<Card className="w-full max-w-lg">
|
<Card className="w-full max-w-lg max-h-[85vh] overflow-y-auto shadow-2xl">
|
||||||
{/* Progress indicator */}
|
{/* Progress indicator */}
|
||||||
<div className="px-6 pt-6">
|
<div className="px-6 pt-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -111,7 +125,7 @@ export default function OnboardingPage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mt-2">
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
Step {currentIndex + 1} of {steps.length - 1}
|
Step {currentIndex + 1} of {totalVisibleSteps}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -147,8 +161,8 @@ export default function OnboardingPage() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 2: Phone */}
|
{/* Step 2: Phone (only if WhatsApp enabled) */}
|
||||||
{step === 'phone' && (
|
{step === 'phone' && whatsappEnabled && (
|
||||||
<>
|
<>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
|
@ -162,15 +176,15 @@ export default function OnboardingPage() {
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="phone">Phone Number (Optional)</Label>
|
<Label htmlFor="phone">Phone Number (Optional)</Label>
|
||||||
<Input
|
<PhoneInput
|
||||||
id="phone"
|
id="phone"
|
||||||
type="tel"
|
|
||||||
value={phoneNumber}
|
value={phoneNumber}
|
||||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
onChange={(value) => setPhoneNumber(value || '')}
|
||||||
placeholder="+377 12 34 56 78"
|
defaultCountry="MC"
|
||||||
|
placeholder="Enter phone number"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Include country code for WhatsApp notifications
|
Select your country and enter your number for WhatsApp notifications
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -201,15 +215,11 @@ export default function OnboardingPage() {
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-2">
|
<ExpertiseSelect
|
||||||
<Label>Expertise Tags</Label>
|
value={expertiseTags}
|
||||||
<TagInput
|
onChange={setExpertiseTags}
|
||||||
value={expertiseTags}
|
maxTags={5}
|
||||||
onChange={setExpertiseTags}
|
/>
|
||||||
placeholder="Select your expertise areas..."
|
|
||||||
maxTags={10}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="outline" onClick={goBack} className="flex-1">
|
<Button variant="outline" onClick={goBack} className="flex-1">
|
||||||
|
|
@ -251,16 +261,20 @@ export default function OnboardingPage() {
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="EMAIL">Email only</SelectItem>
|
<SelectItem value="EMAIL">Email only</SelectItem>
|
||||||
<SelectItem value="WHATSAPP" disabled={!phoneNumber}>
|
{whatsappEnabled && (
|
||||||
WhatsApp only
|
<>
|
||||||
</SelectItem>
|
<SelectItem value="WHATSAPP" disabled={!phoneNumber}>
|
||||||
<SelectItem value="BOTH" disabled={!phoneNumber}>
|
WhatsApp only
|
||||||
Both Email and WhatsApp
|
</SelectItem>
|
||||||
</SelectItem>
|
<SelectItem value="BOTH" disabled={!phoneNumber}>
|
||||||
|
Both Email and WhatsApp
|
||||||
|
</SelectItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<SelectItem value="NONE">No notifications</SelectItem>
|
<SelectItem value="NONE">No notifications</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{!phoneNumber && (
|
{whatsappEnabled && !phoneNumber && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Add a phone number to enable WhatsApp notifications
|
Add a phone number to enable WhatsApp notifications
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -273,7 +287,7 @@ export default function OnboardingPage() {
|
||||||
<p>
|
<p>
|
||||||
<span className="text-muted-foreground">Name:</span> {name}
|
<span className="text-muted-foreground">Name:</span> {name}
|
||||||
</p>
|
</p>
|
||||||
{phoneNumber && (
|
{whatsappEnabled && phoneNumber && (
|
||||||
<p>
|
<p>
|
||||||
<span className="text-muted-foreground">Phone:</span>{' '}
|
<span className="text-muted-foreground">Phone:</span>{' '}
|
||||||
{phoneNumber}
|
{phoneNumber}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,22 @@ function categorizeModel(modelId: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settingsRouter = router({
|
export const settingsRouter = router({
|
||||||
|
/**
|
||||||
|
* Get public feature flags (no auth required)
|
||||||
|
* These are non-sensitive settings that can be exposed to any user
|
||||||
|
*/
|
||||||
|
getFeatureFlags: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
const [whatsappEnabled] = await Promise.all([
|
||||||
|
ctx.prisma.systemSettings.findUnique({
|
||||||
|
where: { key: 'whatsapp_enabled' },
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
whatsappEnabled: whatsappEnabled?.value === 'true',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all settings by category
|
* Get all settings by category
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue