Wire feedback form to backend endpoint

This commit is contained in:
Remi
2025-10-05 14:23:10 +02:00
committed by itsrubberduck
parent 913a118124
commit 92efc5a6c2
4 changed files with 361 additions and 121 deletions

View File

@@ -0,0 +1,107 @@
import { createError, readBody } from 'h3'
import { sendAdminNotification } from '../../../utils/notifications'
interface FeedbackRequestBody {
name?: string
email?: string
excitement?: number
highlightSelections?: string[]
highlightNotes?: string
frictionSelections?: string[]
frictionNotes?: string
classroomNotes?: string
hostingInterest?: string
otherIdeas?: string
contactConsent?: boolean
}
function ensureRating(value: unknown) {
if (typeof value !== 'number') {
return null
}
if (!Number.isFinite(value)) {
return null
}
const rounded = Math.round(value)
if (rounded < 1 || rounded > 5) {
return null
}
return rounded
}
function normaliseText(value?: string | null) {
return value?.trim() || ''
}
function normaliseArray(value?: string[] | null) {
if (!Array.isArray(value)) {
return []
}
return value
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
.filter((entry) => entry.length > 0)
}
export default defineEventHandler(async (event) => {
const body = await readBody<FeedbackRequestBody>(event)
const excitement = ensureRating(body.excitement)
if (!excitement) {
throw createError({ statusCode: 400, statusMessage: 'Please rate your excitement from 1 to 5.' })
}
const name = normaliseText(body.name)
const email = normaliseText(body.email)
const highlightNotes = normaliseText(body.highlightNotes)
const frictionNotes = normaliseText(body.frictionNotes)
const classroomNotes = normaliseText(body.classroomNotes)
const hostingInterest = normaliseText(body.hostingInterest)
const otherIdeas = normaliseText(body.otherIdeas)
const highlightSelections = normaliseArray(body.highlightSelections)
const frictionSelections = normaliseArray(body.frictionSelections)
const allowContact = Boolean(body.contactConsent)
const details: string[] = []
details.push(`Overall excitement: ${excitement}/5`)
details.push(`Highlights: ${highlightSelections.length ? highlightSelections.join(', ') : '—'}`)
if (highlightNotes) {
details.push('')
details.push('Highlight notes:')
details.push(highlightNotes)
}
details.push('')
details.push(`Friction: ${frictionSelections.length ? frictionSelections.join(', ') : '—'}`)
if (frictionNotes) {
details.push('')
details.push('Friction notes:')
details.push(frictionNotes)
}
details.push('')
details.push('Classroom stories:')
details.push(classroomNotes || '—')
details.push('')
details.push('Hosting & deployment:')
details.push(hostingInterest || '—')
details.push('')
details.push('Big ideas & requests:')
details.push(otherIdeas || '—')
const summaryParts = [`Excitement ${excitement}/5`]
if (name) {
summaryParts.push(`from ${name}`)
}
await sendAdminNotification({
event: 'Feedback submission',
summary: `New feedback ${summaryParts.join(' ')}`,
message: details.join('\n'),
data: [
['Name', name || '—'],
['Email', email || '—'],
['Okay to contact', allowContact ? 'Yes' : 'No'],
],
from: email || undefined,
})
return { success: true }
})

View File

@@ -0,0 +1,70 @@
import { createError, readBody } from 'h3'
import { randomUUID } from 'node:crypto'
import { sendAdminNotification } from '../../../utils/notifications'
interface FeedbackIssueRequest {
title?: string
description?: string
categories?: string[]
contactEmail?: string
}
function sanitize(value?: string | null) {
return value?.trim() || ''
}
function sanitizeCategories(value?: string[]) {
if (!Array.isArray(value)) {
return []
}
return value
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
.filter((entry) => entry.length > 0)
.slice(0, 10)
}
export default defineEventHandler(async (event) => {
const body = await readBody<FeedbackIssueRequest>(event)
const title = sanitize(body.title)
const description = sanitize(body.description)
const categories = sanitizeCategories(body.categories)
const contactEmail = sanitize(body.contactEmail)
if (title.length < 4) {
throw createError({ statusCode: 400, statusMessage: 'Title must be at least 4 characters long.' })
}
if (description.length < 20) {
throw createError({ statusCode: 400, statusMessage: 'Description must be at least 20 characters long.' })
}
if (categories.length === 0) {
throw createError({ statusCode: 400, statusMessage: 'Please select at least one category.' })
}
const reportId = randomUUID()
const lines: string[] = []
lines.push(`Title: ${title}`)
lines.push(`Categories: ${categories.join(', ')}`)
lines.push('')
lines.push(description)
await sendAdminNotification({
event: 'Public feedback report',
summary: `Feedback report: ${title}`,
message: lines.join('\n'),
data: [
['Report ID', reportId],
['Categories', categories.join(', ')],
['Contact email', contactEmail || '—'],
],
from: contactEmail || undefined,
})
return {
success: true,
reportId,
}
})