Initial commit: Activiteiten Inventaris applicatie

This commit is contained in:
2026-01-06 01:23:45 +01:00
commit 6d26aea0cf
38 changed files with 9818 additions and 0 deletions

View 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>
)
}