Revise landing messaging and add news feed

This commit is contained in:
Remi
2025-09-17 15:44:41 +02:00
parent 6882a93417
commit e6755d2042
17 changed files with 1286 additions and 117 deletions

View File

@@ -0,0 +1,60 @@
import { readBody, createError } from 'h3'
import { RoadmapSuggestion } from '../../models/RoadmapSuggestion'
import { sendAdminNotification } from '../../utils/notifications'
interface RoadmapSuggestionBody {
title?: string
details?: string
email?: string
allowContact?: boolean
consentPrivacy?: boolean
}
const MIN_TITLE_LENGTH = 4
const MIN_DETAILS_LENGTH = 20
export default defineEventHandler(async (event) => {
const body = await readBody<RoadmapSuggestionBody>(event)
const title = body.title?.trim()
const details = body.details?.trim()
const email = body.email?.trim().toLowerCase()
const allowContact = Boolean(body.allowContact && email)
if (!title || title.length < MIN_TITLE_LENGTH) {
throw createError({ statusCode: 400, statusMessage: 'Bitte gib einen kurzen Titel (mindestens 4 Zeichen) an.' })
}
if (!details || details.length < MIN_DETAILS_LENGTH) {
throw createError({ statusCode: 400, statusMessage: 'Beschreibe deinen Vorschlag mit mindestens 20 Zeichen.' })
}
if (!body.consentPrivacy) {
throw createError({ statusCode: 400, statusMessage: 'Wir benötigen deine Einwilligung zur Datenschutzerklärung.' })
}
if (body.allowContact && !email) {
throw createError({ statusCode: 400, statusMessage: 'Bitte gib eine E-Mail an, damit wir dich kontaktieren können.' })
}
const suggestion = await RoadmapSuggestion.create({
title,
details,
email,
allowContact,
consentPrivacy: true,
})
const lines = [
`Titel: ${title}`,
`Beschreibung: ${details}`,
`Kontakt: ${email || '—'}`,
allowContact ? 'Kontaktaufnahme erwünscht' : 'Keine Kontaktaufnahme erwünscht',
]
await sendAdminNotification('[OpenSquawk] Neuer Roadmap-Vorschlag', lines.join('\n'))
return {
success: true,
suggestionId: suggestion._id.toString(),
}
})

View File

@@ -0,0 +1,52 @@
import { readBody, createError } from 'h3'
import { registerUpdateSubscriber } from '../../utils/subscribers'
import { sendAdminNotification } from '../../utils/notifications'
interface UpdatesRequestBody {
email?: string
name?: string
consentPrivacy?: boolean
consentMarketing?: boolean
source?: string
}
export default defineEventHandler(async (event) => {
const body = await readBody<UpdatesRequestBody>(event)
const email = body.email?.trim().toLowerCase()
const name = body.name?.trim()
const source = body.source?.trim() || 'landing-updates'
if (!email) {
throw createError({ statusCode: 400, statusMessage: 'E-Mail wird benötigt' })
}
if (!body.consentPrivacy) {
throw createError({ statusCode: 400, statusMessage: 'Bitte bestätige die Datenschutzerklärung' })
}
if (!body.consentMarketing) {
throw createError({ statusCode: 400, statusMessage: 'Wir benötigen deine Einwilligung für Produkt-Updates per E-Mail' })
}
const result = await registerUpdateSubscriber({
email,
name,
source,
consentPrivacy: true,
consentMarketing: true,
})
if (result.created) {
const lines = [
`E-Mail: ${email}`,
name ? `Name: ${name}` : null,
`Quelle: ${source}`,
].filter(Boolean)
await sendAdminNotification('[OpenSquawk] Neue Updates-Liste', lines.join('\n'))
}
return {
success: true,
alreadySubscribed: !result.created,
}
})

View File

@@ -1,5 +1,7 @@
import { readBody, createError } from 'h3'
import { WaitlistEntry } from '../../models/WaitlistEntry'
import { sendAdminNotification } from '../../utils/notifications'
import { registerUpdateSubscriber } from '../../utils/subscribers'
interface WaitlistRequestBody {
email?: string
@@ -8,6 +10,7 @@ interface WaitlistRequestBody {
consentPrivacy?: boolean
consentTerms?: boolean
source?: string
wantsProductUpdates?: boolean
}
export default defineEventHandler(async (event) => {
@@ -16,6 +19,7 @@ export default defineEventHandler(async (event) => {
const name = body.name?.trim()
const notes = body.notes?.trim()
const source = body.source?.trim() || 'landing'
const wantsProductUpdates = Boolean(body.wantsProductUpdates)
if (!email) {
throw createError({ statusCode: 400, statusMessage: 'E-Mail wird benötigt' })
@@ -30,12 +34,41 @@ export default defineEventHandler(async (event) => {
const existing = await WaitlistEntry.findOne({ email })
if (existing) {
const previouslyWantedUpdates = Boolean(existing.wantsProductUpdates)
existing.name = name || existing.name
existing.notes = notes || existing.notes
existing.source = source
existing.consentPrivacy = true
existing.consentTerms = true
if (wantsProductUpdates && !previouslyWantedUpdates) {
existing.wantsProductUpdates = true
existing.updatesOptedInAt = now
}
await existing.save()
if (wantsProductUpdates) {
const updateResult = await registerUpdateSubscriber({
email,
name,
source: `${source}-waitlist`,
consentPrivacy: true,
consentMarketing: true,
})
if (!previouslyWantedUpdates && updateResult.created) {
const lines = [
`E-Mail: ${email}`,
name ? `Name: ${name}` : null,
notes ? `Notizen: ${notes}` : null,
`Quelle: ${source}`,
].filter(Boolean)
await sendAdminNotification(
'[OpenSquawk] Neue Updates-Liste (via Warteliste)',
`${lines.join('\n')}\nOpt-In: Produkt-Updates`,
)
}
}
return {
success: true,
alreadyJoined: true,
@@ -51,8 +84,29 @@ export default defineEventHandler(async (event) => {
consentPrivacy: true,
consentTerms: true,
joinedAt: now,
wantsProductUpdates,
updatesOptedInAt: wantsProductUpdates ? now : undefined,
})
if (wantsProductUpdates) {
await registerUpdateSubscriber({
email,
name,
source: `${source}-waitlist`,
consentPrivacy: true,
consentMarketing: true,
})
}
const lines = [
`E-Mail: ${email}`,
name ? `Name: ${name}` : null,
notes ? `Notizen: ${notes}` : null,
`Quelle: ${source}`,
wantsProductUpdates ? 'Opt-In: Produkt-Updates' : 'Opt-In: nur Warteliste',
].filter(Boolean)
await sendAdminNotification('[OpenSquawk] Neue Wartelisten-Anmeldung', lines.join('\n'))
return {
success: true,
alreadyJoined: false,