Initial commit: Activiteiten Inventaris applicatie
This commit is contained in:
221
src/pages/QuestionnaireForm.tsx
Normal file
221
src/pages/QuestionnaireForm.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
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 [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 || '')
|
||||
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, 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 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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user