From a4b8a0ca2f58d9a48b3ce2ab6f169d1f121869b7 Mon Sep 17 00:00:00 2001 From: Remi <73385395+itsrubberduck@users.noreply.github.com> Date: Wed, 17 Sep 2025 19:22:56 +0200 Subject: [PATCH] Align public messaging and add news banners --- app/components/NuxtImg.vue | 86 +++++++++++++++++++++++++++ app/pages/agb.vue | 2 +- app/pages/api-docs.vue | 2 +- app/pages/learn.vue | 2 +- app/pages/login.vue | 2 +- app/pages/logout.vue | 39 ++++++++++++ app/pages/news/[slug].vue | 10 ++++ app/pages/news/index.vue | 2 +- app/pages/pm.vue | 2 +- nuxt.config.ts | 2 +- package.json | 1 + public/img/news/alpha-prototype.svg | 56 ++++++++++++++++++ server/utils/notifications.ts | 92 ++++++++++------------------- shared/utils/news.ts | 12 ++++ 14 files changed, 242 insertions(+), 68 deletions(-) create mode 100644 app/components/NuxtImg.vue create mode 100644 app/pages/logout.vue create mode 100644 public/img/news/alpha-prototype.svg diff --git a/app/components/NuxtImg.vue b/app/components/NuxtImg.vue new file mode 100644 index 0000000..d449bdb --- /dev/null +++ b/app/components/NuxtImg.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/app/pages/agb.vue b/app/pages/agb.vue index 5a2571f..73ba29e 100644 --- a/app/pages/agb.vue +++ b/app/pages/agb.vue @@ -87,7 +87,7 @@ const sections = [ { title: '8. Gewährleistung & Haftung', paragraphs: [ - 'OpenSquawk stellt einen Beta-Dienst bereit. Wir übernehmen keine Gewähr für unterbrechungsfreie Verfügbarkeit oder absolute Richtigkeit der generierten Inhalte. Haftung besteht nur bei Vorsatz, grober Fahrlässigkeit sowie bei Verletzung von Leben, Körper oder Gesundheit.', + 'OpenSquawk stellt einen Alpha-Dienst bereit. Wir übernehmen keine Gewähr für unterbrechungsfreie Verfügbarkeit oder absolute Richtigkeit der generierten Inhalte. Haftung besteht nur bei Vorsatz, grober Fahrlässigkeit sowie bei Verletzung von Leben, Körper oder Gesundheit.', ], }, { diff --git a/app/pages/api-docs.vue b/app/pages/api-docs.vue index aac5ad5..1a9f22a 100644 --- a/app/pages/api-docs.vue +++ b/app/pages/api-docs.vue @@ -7,7 +7,7 @@

Developer

OpenSquawk API – Dokumentation

-

Alle Endpunkte sind JSON-basiert und laufen über HTTPS. Standard-Response enthält success oder ein Fehlerobjekt mit statusMessage.

+

Alpha Build – Endpunkte können sich ändern. Alle Routen sind JSON-basiert und laufen über HTTPS. Standard-Response enthält success oder ein Fehlerobjekt mit statusMessage.

diff --git a/app/pages/learn.vue b/app/pages/learn.vue index 39fad7b..23d5a05 100644 --- a/app/pages/learn.vue +++ b/app/pages/learn.vue @@ -48,7 +48,7 @@
-
TRAINING
+
ALPHA BUILD · TRAINING

ATC Lernwelt

Geführte Pfade · Missionen · XP · Abzeichen. Lokal gespeichert.

diff --git a/app/pages/login.vue b/app/pages/login.vue index 850b65f..4b1b525 100644 --- a/app/pages/login.vue +++ b/app/pages/login.vue @@ -7,7 +7,7 @@ Zurück zur Landingpage
-

OpenSquawk Access

+

OpenSquawk Alpha Access

Melde dich an oder sichere dir deinen Invite

Registrierungen sind nur mit Einladungscode möglich. Codes werden nach Eintrag auf die Warteliste vergeben oder von bestehenden Mitgliedern nach zwei Wochen Nutzungsdauer ausgestellt. diff --git a/app/pages/logout.vue b/app/pages/logout.vue new file mode 100644 index 0000000..5692322 --- /dev/null +++ b/app/pages/logout.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/app/pages/news/[slug].vue b/app/pages/news/[slug].vue index 78c548a..edfc268 100644 --- a/app/pages/news/[slug].vue +++ b/app/pages/news/[slug].vue @@ -6,6 +6,14 @@

+
{{ formatNewsDate(post.publishedAt) }}

{{ post.title }}

@@ -31,6 +39,7 @@ import { getNewsBySlug } from '~~/shared/utils/news' const route = useRoute() const slug = computed(() => String(route.params.slug ?? '')) const post = computed(() => getNewsBySlug(slug.value)) +const bannerSrc = computed(() => (post.value ? `/img/news/${post.value.bannerFile}` : '')) useHead(() => { if (!post.value) { @@ -63,6 +72,7 @@ const formatNewsDate = (iso: string) => new Date(iso).toLocaleDateString('de-DE' .btn { @apply inline-flex items-center justify-center gap-2 rounded-xl px-5 py-3 font-medium transition; } .btn-primary { @apply bg-cyan-500 text-white hover:bg-cyan-400 shadow-[0_0_40px_rgba(34,211,238,.25)]; } .chip { @apply inline-flex items-center gap-2 rounded-full bg-white/10 border border-white/15 text-white px-3 py-1 text-xs; } +.news-banner { @apply w-full rounded-xl object-cover; max-height: 420px; } .news-body :deep(h2) { @apply text-2xl md:text-3xl font-semibold mt-6 mb-3 text-white; } .news-body :deep(h3) { @apply text-xl font-semibold mt-5 mb-2 text-white; } .news-body :deep(p) { @apply text-white/80 leading-relaxed mb-4; } diff --git a/app/pages/news/index.vue b/app/pages/news/index.vue index 67e34f5..ce3acf9 100644 --- a/app/pages/news/index.vue +++ b/app/pages/news/index.vue @@ -11,7 +11,7 @@ Content-getriebene Updates zu Alpha-Builds, Simulator-Support und Community-Entscheidungen.

- Zum aktuellen Feed + Zum aktuellen Feed
diff --git a/app/pages/pm.vue b/app/pages/pm.vue index 3e38d48..28f29bf 100644 --- a/app/pages/pm.vue +++ b/app/pages/pm.vue @@ -6,7 +6,7 @@

OpenSquawk

Pilot Monitoring

-

Decision Tree • Enhanced LLM • VATSIM

+

Alpha Build • Decision Tree • VATSIM

diff --git a/nuxt.config.ts b/nuxt.config.ts index b60d601..436ce82 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -54,4 +54,4 @@ export default defineNuxtConfig({ css: [ '~/assets/css/global.css', '~/assets/css/opensquawk-glass.css' ], -}) \ No newline at end of file +}) diff --git a/package.json b/package.json index 8f79d34..0d7597b 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "nuxt": "^4.1.1", "nuxt-aos": "1.2.5", "nuxt-mongoose": "1.0.6", + "nodemailer": "^6.9.13", "openai": "^4.66.0", "pinia": "^3.0.3", "vue": "^3.5.21", diff --git a/public/img/news/alpha-prototype.svg b/public/img/news/alpha-prototype.svg new file mode 100644 index 0000000..5fc8cf7 --- /dev/null +++ b/public/img/news/alpha-prototype.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Alpha Prototype + + + OPEN + SQUAWK + + + + + + + + + + diff --git a/server/utils/notifications.ts b/server/utils/notifications.ts index 6115421..9689e78 100644 --- a/server/utils/notifications.ts +++ b/server/utils/notifications.ts @@ -1,7 +1,5 @@ const ADMIN_EMAIL_FALLBACK = 'info@opensquawk.de' -const RESEND_ENDPOINT = 'https://api.resend.com/emails' - interface MailOptions { to: string subject: string @@ -13,72 +11,58 @@ interface MailPayload extends MailOptions { from: string } +interface SmtpConfig { + host: string + port: number + secure: boolean + user: string + pass: string +} + function resolveFrom(from?: string) { return from || process.env.NOTIFY_EMAIL_FROM || 'OpenSquawk ' } -async function sendViaResend(payload: MailPayload) { - const apiKey = process.env.NOTIFY_RESEND_API_KEY - if (!apiKey) { - return false +function resolveSmtpConfig(): SmtpConfig | null { + const host = process.env.NOTIFY_SMTP_HOST?.trim() + const user = process.env.NOTIFY_SMTP_USER?.trim() + const pass = process.env.NOTIFY_SMTP_PASS?.trim() + + if (!host || !user || !pass) { + console.warn('SMTP notification is not fully configured. Please set NOTIFY_SMTP_HOST, NOTIFY_SMTP_USER and NOTIFY_SMTP_PASS.') + return null } - try { - const response = await fetch(RESEND_ENDPOINT, { - method: 'POST', - headers: { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }) + const secure = (process.env.NOTIFY_SMTP_SECURE || '').toLowerCase() === 'true' + const parsedPort = Number.parseInt(process.env.NOTIFY_SMTP_PORT || '', 10) + const port = Number.isNaN(parsedPort) ? (secure ? 465 : 587) : parsedPort - if (!response.ok) { - const errorText = await response.text().catch(() => '') - console.error('Failed to send email via Resend API', response.status, errorText) - return false - } - - return true - } catch (error) { - console.error('Error while sending email via Resend API', error) - return false - } + return { host, user, pass, secure, port } } async function sendViaSmtp(payload: MailPayload) { - const host = process.env.NOTIFY_SMTP_HOST - if (!host) { + const config = resolveSmtpConfig() + if (!config) { return false } - const user = process.env.NOTIFY_SMTP_USER - const pass = process.env.NOTIFY_SMTP_PASS - if (!user || !pass) { - console.warn('SMTP notification is configured without credentials – skipping send.') - return false - } - - const port = Number.parseInt(process.env.NOTIFY_SMTP_PORT || '', 10) - const secure = process.env.NOTIFY_SMTP_SECURE === 'true' - let nodemailer: any = null try { const module = await import('nodemailer') nodemailer = module.default ?? module } catch (error) { - console.warn('nodemailer is not available. Skipping SMTP notification.', error) + console.error('nodemailer is not available. Install the dependency to send SMTP emails.', error) return false } try { const transporter = nodemailer.createTransport({ - host, - port: Number.isNaN(port) ? (secure ? 465 : 587) : port, - secure, + host: config.host, + port: config.port, + secure: config.secure, auth: { - user, - pass, + user: config.user, + pass: config.pass, }, }) @@ -95,27 +79,13 @@ async function sendViaSmtp(payload: MailPayload) { } } -async function sendMailInternal(options: MailOptions) { +export async function sendMail(options: MailOptions) { const payload: MailPayload = { ...options, from: resolveFrom(options.from), } - const sentViaResend = await sendViaResend(payload) - if (sentViaResend) { - return true - } - - const sentViaSmtp = await sendViaSmtp(payload) - if (sentViaSmtp) { - return true - } - - return false -} - -export async function sendMail(options: MailOptions) { - const success = await sendMailInternal(options) + const success = await sendViaSmtp(payload) if (!success) { console.info(`[mail:fallback] ${options.subject}\nEmpfänger: ${options.to}\n${options.text}`) } @@ -124,7 +94,7 @@ export async function sendMail(options: MailOptions) { export async function sendAdminNotification(subject: string, text: string) { const to = process.env.NOTIFY_EMAIL_TO || ADMIN_EMAIL_FALLBACK - const success = await sendMailInternal({ to, subject, text }) + const success = await sendMail({ to, subject, text }) if (!success) { console.info(`[notify:fallback] ${subject}\nEmpfänger: ${to}\n${text}`) } diff --git a/shared/utils/news.ts b/shared/utils/news.ts index a4da2bb..554860c 100644 --- a/shared/utils/news.ts +++ b/shared/utils/news.ts @@ -6,6 +6,7 @@ export interface NewsPost { body: string html: string readingTime: string + bannerFile: string } interface FrontMatter { @@ -15,6 +16,7 @@ interface FrontMatter { excerpt?: string description?: string readingTime?: string + banner?: string } const rawNewsModules = import.meta.glob('~~/content/news/*.md', { @@ -51,6 +53,7 @@ function parseNewsFile(path: string, raw: string): NewsPost | null { const excerpt = truncateText(excerptSource || firstParagraph(cleanBody), 220) const html = markdownToHtml(cleanBody) const readingTime = attributes.readingTime || formatReadingTime(cleanBody) + const bannerFile = normalizeBannerFile(attributes.banner, slug) return { slug, @@ -60,6 +63,7 @@ function parseNewsFile(path: string, raw: string): NewsPost | null { body: cleanBody, html, readingTime, + bannerFile, } } @@ -146,6 +150,14 @@ function formatReadingTime(content: string): string { return `${minutes} Min Lesezeit` } +function normalizeBannerFile(banner: string | undefined, slug: string): string { + const value = (banner || '').trim() + if (!value) { + return `${slug}.svg` + } + return value.includes('.') ? value : `${value}.svg` +} + function markdownToHtml(markdown: string): string { const lines = markdown.split(/\r?\n/) let html = ''