Add dynamic apply wizard customization with admin settings UI
- Create wizard config types, utilities, and defaults (wizard-config.ts) - Add admin apply settings page with drag-and-drop step ordering, dropdown option management, feature toggles, welcome message customization, and custom field builder with select/multiselect options editor - Build dynamic apply wizard component with animated step transitions, mobile-first responsive design, and config-driven form validation - Update step components to accept dynamic config (categories, ocean issues, field visibility, feature flags) - Replace hardcoded enum validation with string-based validation for admin-configurable dropdown values, with safe enum casting at storage layer - Add wizard template system (model, router, admin UI) with built-in MOPC Classic preset - Add program wizard config CRUD procedures to program router - Update application router getConfig to return wizardConfig, submit handler to store custom field data in metadataJson - Add edition-based apply page, project pool page, and supporting routers - Fix CSS (invalid sm:fixed-none), Enter key handler (skip textarea), safe area insets for notched phones, buildStepsArray field visibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,35 +14,22 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import type { ApplicationFormData } from '@/server/routers/application'
|
||||
import { OceanIssue } from '@prisma/client'
|
||||
|
||||
interface OceanIssueOption {
|
||||
value: OceanIssue
|
||||
label: string
|
||||
}
|
||||
|
||||
const oceanIssueOptions: OceanIssueOption[] = [
|
||||
{ value: 'POLLUTION_REDUCTION', label: 'Reduction of pollution (plastics, chemicals, noise, light,...)' },
|
||||
{ value: 'CLIMATE_MITIGATION', label: 'Mitigation of climate change and sea-level rise' },
|
||||
{ value: 'TECHNOLOGY_INNOVATION', label: 'Technology & innovations' },
|
||||
{ value: 'SUSTAINABLE_SHIPPING', label: 'Sustainable shipping & yachting' },
|
||||
{ value: 'BLUE_CARBON', label: 'Blue carbon' },
|
||||
{ value: 'HABITAT_RESTORATION', label: 'Restoration of marine habitats & ecosystems' },
|
||||
{ value: 'COMMUNITY_CAPACITY', label: 'Capacity building for coastal communities' },
|
||||
{ value: 'SUSTAINABLE_FISHING', label: 'Sustainable fishing and aquaculture & blue food' },
|
||||
{ value: 'CONSUMER_AWARENESS', label: 'Consumer awareness and education' },
|
||||
{ value: 'OCEAN_ACIDIFICATION', label: 'Mitigation of ocean acidification' },
|
||||
{ value: 'OTHER', label: 'Other' },
|
||||
]
|
||||
import { type DropdownOption, type WizardConfig, DEFAULT_OCEAN_ISSUES } from '@/types/wizard-config'
|
||||
import { isFieldVisible, getFieldConfig } from '@/lib/wizard-config'
|
||||
|
||||
interface StepProjectProps {
|
||||
form: UseFormReturn<ApplicationFormData>
|
||||
oceanIssues?: DropdownOption[]
|
||||
config?: WizardConfig
|
||||
}
|
||||
|
||||
export function StepProject({ form }: StepProjectProps) {
|
||||
export function StepProject({ form, oceanIssues, config }: StepProjectProps) {
|
||||
const issueOptions = oceanIssues ?? DEFAULT_OCEAN_ISSUES
|
||||
const { register, formState: { errors }, setValue, watch } = form
|
||||
const oceanIssue = watch('oceanIssue')
|
||||
const description = watch('description') || ''
|
||||
const showTeamName = !config || isFieldVisible(config, 'teamName')
|
||||
const descriptionLabel = config ? getFieldConfig(config, 'description').label : undefined
|
||||
|
||||
return (
|
||||
<WizardStepContent
|
||||
@@ -71,17 +58,19 @@ export function StepProject({ form }: StepProjectProps) {
|
||||
</div>
|
||||
|
||||
{/* Team Name (optional) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="teamName">
|
||||
Team Name <span className="text-muted-foreground text-xs">(optional)</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="teamName"
|
||||
placeholder="Blue Innovation Team"
|
||||
{...register('teamName')}
|
||||
className="h-12 text-base"
|
||||
/>
|
||||
</div>
|
||||
{showTeamName && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="teamName">
|
||||
Team Name <span className="text-muted-foreground text-xs">(optional)</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="teamName"
|
||||
placeholder="Blue Innovation Team"
|
||||
{...register('teamName')}
|
||||
className="h-12 text-base"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ocean Issue */}
|
||||
<div className="space-y-2">
|
||||
@@ -90,13 +79,13 @@ export function StepProject({ form }: StepProjectProps) {
|
||||
</Label>
|
||||
<Select
|
||||
value={oceanIssue}
|
||||
onValueChange={(value) => setValue('oceanIssue', value as OceanIssue)}
|
||||
onValueChange={(value) => setValue('oceanIssue', value)}
|
||||
>
|
||||
<SelectTrigger className="h-12 text-base">
|
||||
<SelectValue placeholder="Select an ocean issue" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{oceanIssueOptions.map((option) => (
|
||||
{issueOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
|
||||
Reference in New Issue
Block a user