mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-07 01:55:51 +08:00
Wire feedback form to backend endpoint
This commit is contained in:
107
server/api/service/feedback/index.post.ts
Normal file
107
server/api/service/feedback/index.post.ts
Normal 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 }
|
||||
})
|
||||
70
server/api/service/feedback/issues.post.ts
Normal file
70
server/api/service/feedback/issues.post.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user