255 lines
9.2 KiB
TypeScript
255 lines
9.2 KiB
TypeScript
import { useState, useEffect, FormEvent } from 'react'
|
|
import { useNavigate, useParams, Link } from 'react-router-dom'
|
|
|
|
export function QuestionnaireForm() {
|
|
const { id } = useParams()
|
|
const isEditing = !!id
|
|
const navigate = useNavigate()
|
|
|
|
const [title, setTitle] = useState('')
|
|
const [slug, setSlug] = useState('')
|
|
const [description, setDescription] = useState('')
|
|
const [ogImage, setOgImage] = useState('')
|
|
const [isPrivate, setIsPrivate] = useState(false)
|
|
const [error, setError] = useState('')
|
|
const [slugError, setSlugError] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (isEditing) {
|
|
fetchQuestionnaire()
|
|
}
|
|
}, [id])
|
|
|
|
async function fetchQuestionnaire() {
|
|
try {
|
|
const res = await fetch(`/api/admin/questionnaires/${id}`, { credentials: 'include' })
|
|
const data = await res.json()
|
|
if (data.questionnaire) {
|
|
setTitle(data.questionnaire.title)
|
|
setSlug(data.questionnaire.slug)
|
|
setDescription(data.questionnaire.description || '')
|
|
setOgImage(data.questionnaire.og_image || '')
|
|
setIsPrivate(!!data.questionnaire.is_private)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch questionnaire:', error)
|
|
}
|
|
}
|
|
|
|
// Auto-generate slug from title (only when creating new)
|
|
function handleTitleChange(value: string) {
|
|
setTitle(value)
|
|
if (!isEditing && !slug) {
|
|
const autoSlug = value
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\s-]/g, '')
|
|
.replace(/\s+/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.substring(0, 50)
|
|
setSlug(autoSlug)
|
|
}
|
|
}
|
|
|
|
function handleSlugChange(value: string) {
|
|
const cleanSlug = value
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9-]/g, '')
|
|
.substring(0, 50)
|
|
setSlug(cleanSlug)
|
|
setSlugError('')
|
|
}
|
|
|
|
async function checkSlugAvailability() {
|
|
if (!slug || slug.length < 3) return
|
|
|
|
try {
|
|
const excludeParam = isEditing ? `?excludeId=${id}` : ''
|
|
const res = await fetch(`/api/admin/questionnaires/check-slug/${slug}${excludeParam}`, { credentials: 'include' })
|
|
const data = await res.json()
|
|
if (!data.available) {
|
|
setSlugError('Deze slug is al in gebruik')
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to check slug:', err)
|
|
}
|
|
}
|
|
|
|
async function handleSubmit(e: FormEvent) {
|
|
e.preventDefault()
|
|
setError('')
|
|
setLoading(true)
|
|
|
|
try {
|
|
const url = isEditing ? `/api/admin/questionnaires/${id}` : '/api/admin/questionnaires'
|
|
const method = isEditing ? 'PUT' : 'POST'
|
|
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ title, slug, description, ogImage, isPrivate }),
|
|
})
|
|
|
|
const data = await res.json()
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data.error || 'Vragenlijst opslaan mislukt')
|
|
}
|
|
|
|
navigate('/admin/dashboard')
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Opslaan mislukt')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-2xl">
|
|
<Link to="/admin/dashboard" className="inline-block text-text-muted hover:text-accent text-sm mb-4 transition-colors">
|
|
← Terug naar Dashboard
|
|
</Link>
|
|
|
|
<h1 className="text-2xl font-bold text-text mb-8">
|
|
{isEditing ? 'Vragenlijst Bewerken' : 'Vragenlijst Maken'}
|
|
</h1>
|
|
|
|
<div className="bg-bg-card border border-border rounded-xl p-6">
|
|
{error && (
|
|
<div className="mb-6 p-4 bg-danger-muted border border-danger/30 rounded-lg text-danger text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div>
|
|
<label htmlFor="title" className="block text-sm font-medium text-text mb-2">
|
|
Titel <span className="text-danger">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="title"
|
|
value={title}
|
|
onChange={(e) => handleTitleChange(e.target.value)}
|
|
className="w-full px-4 py-3 bg-bg-input border border-border rounded-lg text-text placeholder-text-faint focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 transition-colors"
|
|
placeholder="bijv. Teambuilding Activiteiten"
|
|
required
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="slug" className="block text-sm font-medium text-text mb-2">
|
|
URL Slug <span className="text-danger">*</span>
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-text-muted text-sm">/q/</span>
|
|
<input
|
|
type="text"
|
|
id="slug"
|
|
value={slug}
|
|
onChange={(e) => handleSlugChange(e.target.value)}
|
|
onBlur={checkSlugAvailability}
|
|
className={`flex-1 px-4 py-3 bg-bg-input border rounded-lg text-text placeholder-text-faint focus:outline-none focus:ring-2 transition-colors font-mono ${
|
|
slugError ? 'border-danger focus:border-danger focus:ring-danger/20' : 'border-border focus:border-accent focus:ring-accent/20'
|
|
}`}
|
|
placeholder="teambuilding-2024"
|
|
required
|
|
minLength={3}
|
|
maxLength={50}
|
|
/>
|
|
</div>
|
|
{slugError && (
|
|
<p className="mt-1 text-sm text-danger">{slugError}</p>
|
|
)}
|
|
<p className="mt-1 text-xs text-text-faint">
|
|
Alleen kleine letters, cijfers en koppeltekens. 3-50 tekens.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="description" className="block text-sm font-medium text-text mb-2">
|
|
Beschrijving
|
|
</label>
|
|
<textarea
|
|
id="description"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
rows={3}
|
|
className="w-full px-4 py-3 bg-bg-input border border-border rounded-lg text-text placeholder-text-faint focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 transition-colors resize-y"
|
|
placeholder="Optionele beschrijving voor deelnemers"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="ogImage" className="block text-sm font-medium text-text mb-2">
|
|
Preview Afbeelding (voor WhatsApp/Social Media)
|
|
</label>
|
|
<input
|
|
type="url"
|
|
id="ogImage"
|
|
value={ogImage}
|
|
onChange={(e) => setOgImage(e.target.value)}
|
|
className="w-full px-4 py-3 bg-bg-input border border-border rounded-lg text-text placeholder-text-faint focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 transition-colors"
|
|
placeholder="https://voorbeeld.nl/afbeelding.jpg"
|
|
/>
|
|
<p className="mt-1 text-xs text-text-faint">
|
|
URL naar een afbeelding die getoond wordt bij het delen van de link op WhatsApp, Facebook, etc.
|
|
Aanbevolen formaat: 1200x630 pixels.
|
|
</p>
|
|
{ogImage && (
|
|
<div className="mt-3 p-3 bg-bg-input rounded-lg border border-border">
|
|
<p className="text-xs text-text-muted mb-2">Preview:</p>
|
|
<img
|
|
src={ogImage}
|
|
alt="Preview"
|
|
className="max-h-32 rounded border border-border"
|
|
onError={(e) => {
|
|
(e.target as HTMLImageElement).style.display = 'none'
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 p-4 bg-bg-input rounded-lg border border-border">
|
|
<input
|
|
type="checkbox"
|
|
id="isPrivate"
|
|
checked={isPrivate}
|
|
onChange={(e) => setIsPrivate(e.target.checked)}
|
|
className="w-5 h-5 rounded border-border text-accent focus:ring-accent focus:ring-offset-0 bg-bg-input"
|
|
/>
|
|
<div>
|
|
<label htmlFor="isPrivate" className="block font-medium text-text cursor-pointer">
|
|
Privé Vragenlijst
|
|
</label>
|
|
<p className="text-sm text-text-muted">
|
|
Alleen uitgenodigde deelnemers kunnen items toevoegen en stemmen. Anderen kunnen alleen bekijken.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-3 justify-end pt-4">
|
|
<Link
|
|
to="/admin/dashboard"
|
|
className="px-4 py-2 border border-border rounded-lg text-text-muted hover:text-text hover:border-text-muted transition-colors"
|
|
>
|
|
Annuleren
|
|
</Link>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="px-4 py-2 bg-accent hover:bg-accent-hover text-white font-semibold rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{loading ? 'Opslaan...' : isEditing ? 'Wijzigingen Opslaan' : 'Vragenlijst Maken'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|