feat: configureerbare itemnamen per vragenlijst
Voeg item_label en item_label_plural toe aan questionnaires (migratie), beheerformulier en dynamische teksten in UI en API-fouten. Standaard blijft Activiteit/Activiteiten. Negeer tsbuildinfo en geëmitteerde vite.config.js/.d.ts in .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,6 +11,11 @@ interface Questionnaire {
|
||||
creator_name: string
|
||||
created_at: string
|
||||
activity_count: number
|
||||
item_label_plural: string | null
|
||||
}
|
||||
|
||||
function pluralLabel(q: Questionnaire) {
|
||||
return (q.item_label_plural?.trim() || 'Activiteiten').slice(0, 60)
|
||||
}
|
||||
|
||||
export function Dashboard() {
|
||||
@@ -72,7 +77,7 @@ export function Dashboard() {
|
||||
<span className="px-2 py-0.5 bg-accent/20 text-accent text-xs font-semibold rounded">Privé</span>
|
||||
)}
|
||||
<span className="px-2 py-0.5 bg-bg-input rounded text-xs font-semibold text-text-muted">
|
||||
{q.activity_count} activiteiten
|
||||
{q.activity_count} {pluralLabel(q).toLowerCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,7 +116,7 @@ export function Dashboard() {
|
||||
<div className="text-center py-16 bg-bg-card border border-dashed border-border rounded-xl">
|
||||
<div className="text-4xl mb-4 opacity-50">📋</div>
|
||||
<h2 className="text-lg font-semibold text-text mb-2">Nog geen vragenlijsten</h2>
|
||||
<p className="text-text-muted mb-6">Maak je eerste vragenlijst om activiteiten en stemmen te verzamelen.</p>
|
||||
<p className="text-text-muted mb-6">Maak je eerste vragenlijst om voorstellen te inventariseren en stemmen te verzamelen.</p>
|
||||
<Link
|
||||
to="/admin/questionnaires/new"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-accent hover:bg-accent-hover text-white font-semibold rounded-lg transition-colors"
|
||||
|
||||
@@ -27,6 +27,15 @@ interface Questionnaire {
|
||||
title: string
|
||||
description: string | null
|
||||
is_private: boolean
|
||||
item_label: string | null
|
||||
item_label_plural: string | null
|
||||
}
|
||||
|
||||
function itemLabels(q: Questionnaire) {
|
||||
return {
|
||||
singular: (q.item_label?.trim() || 'Activiteit').slice(0, 60),
|
||||
plural: (q.item_label_plural?.trim() || 'Activiteiten').slice(0, 60),
|
||||
}
|
||||
}
|
||||
|
||||
export function PublicQuestionnaire({ accessToken }: { accessToken?: string } = {}) {
|
||||
@@ -318,6 +327,8 @@ export function PublicQuestionnaire({ accessToken }: { accessToken?: string } =
|
||||
)
|
||||
}
|
||||
|
||||
const labels = questionnaire ? itemLabels(questionnaire) : { singular: 'Activiteit', plural: 'Activiteiten' }
|
||||
|
||||
if (error || !questionnaire) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-bg">
|
||||
@@ -396,14 +407,14 @@ export function PublicQuestionnaire({ accessToken }: { accessToken?: string } =
|
||||
{/* Add Activity */}
|
||||
{canWrite && (
|
||||
<div className="bg-bg-card border border-border rounded-xl p-5 mb-8">
|
||||
<h3 className="font-semibold text-text mb-3">Activiteit Toevoegen</h3>
|
||||
<h3 className="font-semibold text-text mb-3">{labels.singular} toevoegen</h3>
|
||||
<form onSubmit={handleAddActivity} className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={activityInput}
|
||||
onChange={(e) => setActivityInput(e.target.value)}
|
||||
placeholder="Naam activiteit"
|
||||
placeholder={`Naam ${labels.singular.toLowerCase()}`}
|
||||
className="flex-1 px-4 py-2 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"
|
||||
required
|
||||
/>
|
||||
@@ -448,7 +459,7 @@ export function PublicQuestionnaire({ accessToken }: { accessToken?: string } =
|
||||
)}
|
||||
|
||||
{/* Activities List */}
|
||||
<h2 className="font-semibold text-text mb-4">Activiteiten</h2>
|
||||
<h2 className="font-semibold text-text mb-4">{labels.plural}</h2>
|
||||
|
||||
{activities.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
@@ -529,7 +540,7 @@ export function PublicQuestionnaire({ accessToken }: { accessToken?: string } =
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 bg-bg-card border border-dashed border-border rounded-xl">
|
||||
<p className="text-text-muted">Nog geen activiteiten. Wees de eerste om er één toe te voegen!</p>
|
||||
<p className="text-text-muted">Nog geen {labels.plural.toLowerCase()}. Wees de eerste om er één toe te voegen!</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -619,7 +630,7 @@ export function PublicQuestionnaire({ accessToken }: { accessToken?: string } =
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<h3 className="font-semibold text-text">Activiteit Bewerken</h3>
|
||||
<h3 className="font-semibold text-text">{labels.singular} bewerken</h3>
|
||||
<button
|
||||
onClick={() => setShowEditModal(false)}
|
||||
className="text-2xl text-text-muted hover:text-text leading-none"
|
||||
|
||||
@@ -28,6 +28,15 @@ interface Questionnaire {
|
||||
description: string | null
|
||||
is_private: boolean
|
||||
created_at: string
|
||||
item_label: string | null
|
||||
item_label_plural: string | null
|
||||
}
|
||||
|
||||
function itemLabels(q: Questionnaire) {
|
||||
return {
|
||||
singular: (q.item_label?.trim() || 'Activiteit').slice(0, 60),
|
||||
plural: (q.item_label_plural?.trim() || 'Activiteiten').slice(0, 60),
|
||||
}
|
||||
}
|
||||
|
||||
export function QuestionnaireDetail() {
|
||||
@@ -126,7 +135,11 @@ export function QuestionnaireDetail() {
|
||||
function getInvitationMessage(participant: Participant, url: string): string {
|
||||
const firstName = getFirstName(participant.name)
|
||||
const title = questionnaire?.title || 'de vragenlijst'
|
||||
return `Hoi ${firstName}, we zijn benieuwd naar je ideeën voor "${title}". Je kunt nieuwe ideeën aandragen en/of stemmen op reeds toegevoegde ideeën via deze link: ${url}. Alvast bedankt voor je hulp!`
|
||||
if (!questionnaire) {
|
||||
return `Hoi ${firstName}, we zijn benieuwd naar je ideeën voor "${title}". Je kunt nieuwe ideeën aandragen en/of stemmen op reeds toegevoegde ideeën via deze link: ${url}. Alvast bedankt voor je hulp!`
|
||||
}
|
||||
const { plural } = itemLabels(questionnaire)
|
||||
return `Hoi ${firstName}, we zijn benieuwd naar je input voor "${title}". Je kunt ${plural.toLowerCase()} toevoegen en stemmen op reeds toegevoegde ${plural.toLowerCase()} via deze link: ${url}. Alvast bedankt voor je hulp!`
|
||||
}
|
||||
|
||||
// Copy invitation message to clipboard
|
||||
@@ -199,7 +212,9 @@ export function QuestionnaireDetail() {
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('Weet je zeker dat je deze vragenlijst wilt verwijderen? Dit verwijdert ook alle activiteiten en stemmen.')) {
|
||||
if (!questionnaire) return
|
||||
const pl = itemLabels(questionnaire).plural.toLowerCase()
|
||||
if (!confirm(`Weet je zeker dat je deze vragenlijst wilt verwijderen? Dit verwijdert ook alle ${pl} en stemmen.`)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -215,7 +230,8 @@ export function QuestionnaireDetail() {
|
||||
}
|
||||
|
||||
async function handleDeleteActivity(activityId: number) {
|
||||
if (!confirm('Deze activiteit verwijderen?')) return
|
||||
if (!questionnaire) return
|
||||
if (!confirm(`Deze ${itemLabels(questionnaire).singular.toLowerCase()} verwijderen?`)) return
|
||||
|
||||
try {
|
||||
await fetch(`/api/admin/activities/${activityId}`, {
|
||||
@@ -284,6 +300,7 @@ export function QuestionnaireDetail() {
|
||||
return <div className="text-text-muted">Vragenlijst niet gevonden</div>
|
||||
}
|
||||
|
||||
const labels = itemLabels(questionnaire)
|
||||
const shareUrl = `${window.location.origin}/q/${questionnaire.slug}`
|
||||
|
||||
return (
|
||||
@@ -323,8 +340,8 @@ export function QuestionnaireDetail() {
|
||||
</div>
|
||||
<p className="text-xs text-text-faint">
|
||||
{questionnaire.is_private
|
||||
? 'Iedereen met deze link kan activiteiten en stemmen bekijken, maar niet deelnemen.'
|
||||
: 'Deel deze URL met mensen die activiteiten moeten toevoegen en stemmen.'}
|
||||
? `Iedereen met deze link kan ${labels.plural.toLowerCase()} en stemmen bekijken, maar niet deelnemen.`
|
||||
: `Deel deze URL met mensen die ${labels.plural.toLowerCase()} moeten toevoegen en stemmen.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -429,14 +446,14 @@ export function QuestionnaireDetail() {
|
||||
)}
|
||||
|
||||
{/* Activities */}
|
||||
<h2 className="font-semibold text-text mb-4">Activiteiten ({activities.length})</h2>
|
||||
<h2 className="font-semibold text-text mb-4">{labels.plural} ({activities.length})</h2>
|
||||
|
||||
{activities.length > 0 ? (
|
||||
<div className="bg-bg-card border border-border rounded-xl overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated">
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-text-muted">Activiteit</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-text-muted">{labels.singular}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-text-muted">Toegevoegd door</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-semibold uppercase tracking-wider text-text-muted">Voor</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-semibold uppercase tracking-wider text-text-muted">Tegen</th>
|
||||
@@ -480,7 +497,7 @@ export function QuestionnaireDetail() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 bg-bg-card border border-dashed border-border rounded-xl">
|
||||
<p className="text-text-muted">Er zijn nog geen activiteiten toegevoegd. Deel de vragenlijst URL zodat mensen activiteiten kunnen toevoegen.</p>
|
||||
<p className="text-text-muted">Er zijn nog geen {labels.plural.toLowerCase()} toegevoegd. Deel de vragenlijst-URL zodat mensen {labels.plural.toLowerCase()} kunnen toevoegen.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -506,7 +523,7 @@ export function QuestionnaireDetail() {
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<h3 className="font-semibold text-text">Activiteit Bewerken</h3>
|
||||
<h3 className="font-semibold text-text">{labels.singular} bewerken</h3>
|
||||
<button
|
||||
onClick={() => setShowEditModal(false)}
|
||||
className="text-2xl text-text-muted hover:text-text leading-none"
|
||||
|
||||
@@ -13,6 +13,8 @@ export function QuestionnaireForm() {
|
||||
const [ogImage, setOgImage] = useState('')
|
||||
const [ogImageMode, setOgImageMode] = useState<'upload' | 'url'>('upload')
|
||||
const [isPrivate, setIsPrivate] = useState(false)
|
||||
const [itemLabel, setItemLabel] = useState('Activiteit')
|
||||
const [itemLabelPlural, setItemLabelPlural] = useState('Activiteiten')
|
||||
const [error, setError] = useState('')
|
||||
const [slugError, setSlugError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -34,6 +36,8 @@ export function QuestionnaireForm() {
|
||||
setDescription(data.questionnaire.description || '')
|
||||
setOgImage(data.questionnaire.og_image || '')
|
||||
setIsPrivate(!!data.questionnaire.is_private)
|
||||
setItemLabel(data.questionnaire.item_label?.trim() || 'Activiteit')
|
||||
setItemLabelPlural(data.questionnaire.item_label_plural?.trim() || 'Activiteiten')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch questionnaire:', error)
|
||||
@@ -142,7 +146,15 @@ export function QuestionnaireForm() {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ title, slug, description, ogImage, isPrivate }),
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
slug,
|
||||
description,
|
||||
ogImage,
|
||||
isPrivate,
|
||||
itemLabel: itemLabel.trim() || 'Activiteit',
|
||||
itemLabelPlural: itemLabelPlural.trim() || 'Activiteiten',
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
@@ -236,6 +248,43 @@ export function QuestionnaireForm() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<label htmlFor="itemLabel" className="block text-sm font-medium text-text mb-2">
|
||||
Naam per item (enkelvoud)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="itemLabel"
|
||||
value={itemLabel}
|
||||
onChange={(e) => setItemLabel(e.target.value)}
|
||||
maxLength={60}
|
||||
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. Activiteit, Muzieknummer"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-text-faint">
|
||||
Zo wordt één voorstel getoond in formulieren en knoppen.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="itemLabelPlural" className="block text-sm font-medium text-text mb-2">
|
||||
Meervoud
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="itemLabelPlural"
|
||||
value={itemLabelPlural}
|
||||
onChange={(e) => setItemLabelPlural(e.target.value)}
|
||||
maxLength={60}
|
||||
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. Activiteiten, Muzieknummers"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-text-faint">
|
||||
Voor lijsten en tellers (bijv. "12 muzieknummers").
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">
|
||||
Preview Afbeelding (voor WhatsApp/Social Media)
|
||||
|
||||
Reference in New Issue
Block a user