This commit is contained in:
itsrubberduck
2025-09-17 19:38:37 +02:00
16 changed files with 187 additions and 211 deletions

Binary file not shown.

View File

@@ -21,20 +21,20 @@
</section>
<footer class="rounded-2xl border border-white/10 bg-white/5 p-6 text-sm text-white/70">
<p>Fragen zu diesen AGB beantworten wir unter legal@opensquawk.dev. Für Enterprise- oder Trainingsprogramme erstellen wir individuelle Verträge.</p>
<p>Fragen zu diesen AGB beantworten wir unter info@opensquawk.dev. Für Enterprise- oder Trainingsprogramme erstellen wir individuelle Verträge.</p>
</footer>
</div>
</div>
</template>
<script setup lang="ts">
const lastUpdated = new Date('2025-01-15').toLocaleDateString('de-DE')
const lastUpdated = new Date('2025-09-17').toLocaleDateString('de-DE')
const sections = [
{
title: '1. Geltungsbereich',
paragraphs: [
'Diese AGB regeln die Nutzung der Plattform OpenSquawk (“Dienst”), betrieben von der OpenSquawk UG (haftungsbeschränkt). Abweichende Bedingungen des Nutzers werden nicht anerkannt, es sei denn, wir stimmen ihrer Geltung ausdrücklich schriftlich zu.',
'Diese AGB regeln die Nutzung der Plattform OpenSquawk (“Dienst”), betrieben von der Faktor Mensch MEDIA UG (haftungsbeschränkt). Abweichende Bedingungen des Nutzers werden nicht anerkannt, es sei denn, wir stimmen ihrer Geltung ausdrücklich schriftlich zu.',
],
},
{
@@ -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.',
],
},
{

View File

@@ -7,7 +7,7 @@
</NuxtLink>
<p class="text-xs uppercase tracking-[0.3em] text-cyan-300/80">Developer</p>
<h1 class="text-3xl font-semibold">OpenSquawk API Dokumentation</h1>
<p class="text-white/70">Alle Endpunkte sind JSON-basiert und laufen über HTTPS. Standard-Response enthält <code class="bg-white/10 px-1">success</code> oder ein Fehlerobjekt mit <code class="bg-white/10 px-1">statusMessage</code>.</p>
<p class="text-white/70">Alpha Build Endpunkte können sich ändern. Alle Routen sind JSON-basiert und laufen über HTTPS. Standard-Response enthält <code class="bg-white/10 px-1">success</code> oder ein Fehlerobjekt mit <code class="bg-white/10 px-1">statusMessage</code>.</p>
</header>
<section class="rounded-2xl border border-white/10 bg-white/5 p-6 space-y-4">

View File

@@ -13,7 +13,20 @@
<section class="space-y-4 rounded-2xl border border-white/10 bg-white/5 p-6">
<h2 class="text-xl font-semibold">1. Verantwortlicher</h2>
<p class="text-white/70">
Verantwortlich für die Datenverarbeitung im Sinne der Datenschutz-Grundverordnung (DSGVO) ist die OpenSquawk UG (haftungsbeschränkt), Musterstraße 12, 10115 Berlin, Deutschland. Kontakt: datenschutz@opensquawk.dev.
Verantwortlich für die Datenverarbeitung im Sinne der Datenschutz-Grundverordnung (DSGVO) ist die
Faktor Mensch MEDIA UG (haftungsbeschränkt),
Wilhelm-Holzamer-Straße 8
55129 Mainz,
Vertreten durch,
Geschäftsführer: Dominik Ziegenhagel, Emanuel Leube,
Telefon: +49 251 981 157 912 0
E-Mail: info@opensquawk.de
</p>
</section>
@@ -74,7 +87,7 @@
<section class="space-y-4 rounded-2xl border border-white/10 bg-white/5 p-6">
<h2 class="text-xl font-semibold">7. Kontakt für Datenschutzanfragen</h2>
<p class="text-white/70">
Bitte richten Sie Auskunfts- oder Löschersuchen an datenschutz@opensquawk.dev. Zur eindeutigen Zuordnung benötigen wir die bei OpenSquawk registrierte E-Mail-Adresse sowie ggf. weitere Identifikationsmerkmale (z. B. VATSIM-ID).
Bitte richten Sie Auskunfts- oder Löschersuchen an info@opensquawk.de. Zur eindeutigen Zuordnung benötigen wir die bei OpenSquawk registrierte E-Mail-Adresse sowie ggf. weitere Identifikationsmerkmale (z. B. VATSIM-ID).
</p>
</section>
</div>
@@ -82,7 +95,7 @@
</template>
<script setup lang="ts">
const lastUpdated = new Date('2025-02-20').toLocaleDateString('de-DE')
const lastUpdated = new Date('2025-09-17').toLocaleDateString('de-DE')
</script>
<style scoped>

View File

@@ -48,7 +48,7 @@
<div class="container">
<div class="panel hero-panel" :style="worldTiltStyle" @mousemove="tilt">
<div class="hero-left">
<div class="eyebrow">TRAINING</div>
<div class="eyebrow">ALPHA BUILD · TRAINING</div>
<h1 class="h1">ATC Lernwelt</h1>
<p class="muted">Geführte Pfade · Missionen · XP · Abzeichen. Lokal gespeichert.</p>
<div class="actions">

View File

@@ -7,7 +7,7 @@
<v-icon icon="mdi-arrow-left" size="18" /> Zurück zur Landingpage
</NuxtLink>
<div class="space-y-4">
<p class="text-xs uppercase tracking-[0.3em] text-cyan-300/80">OpenSquawk Access</p>
<p class="text-xs uppercase tracking-[0.3em] text-cyan-300/80">OpenSquawk Alpha Access</p>
<h1 class="text-3xl md:text-4xl font-semibold">Melde dich an oder sichere dir deinen Invite</h1>
<p class="text-white/70">
Registrierungen sind nur mit Einladungscode möglich. Codes werden nach Eintrag auf die Warteliste vergeben oder von bestehenden Mitgliedern nach zwei Wochen Nutzungsdauer ausgestellt.

39
app/pages/logout.vue Normal file
View File

@@ -0,0 +1,39 @@
<template>
<div class="min-h-screen bg-[#0b1020] text-white">
<div class="mx-auto flex min-h-screen max-w-xl flex-col items-center justify-center gap-6 px-6 text-center">
<div class="space-y-3">
<p class="text-xs uppercase tracking-[0.3em] text-cyan-300/80">OpenSquawk</p>
<h1 class="text-3xl font-semibold">Du wirst abgemeldet</h1>
<p class="text-sm text-white/70">
Wir melden dich vom Alpha-Dienst ab und leiten dich gleich weiter.
</p>
</div>
<v-progress-circular indeterminate size="40" width="3" color="cyan" />
<NuxtLink to="/login" class="text-sm text-cyan-300 underline">Zum Login</NuxtLink>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useHead } from '#imports'
import { useAuthStore } from '~/stores/auth'
const router = useRouter()
const auth = useAuthStore()
useHead({
title: 'Logout OpenSquawk',
meta: [
{ name: 'robots', content: 'noindex' },
],
})
onMounted(async () => {
await auth.logout()
router.replace('/login')
})
</script>

View File

@@ -6,6 +6,14 @@
</NuxtLink>
<article v-if="post" class="card space-y-6">
<NuxtImg
v-if="bannerSrc"
:src="bannerSrc"
:alt="`${post.title} Beitragsbild`"
class="news-banner"
sizes="sm:100vw md:900px lg:1100px"
loading="lazy"
/>
<header class="space-y-2">
<span class="chip text-[10px] uppercase tracking-[0.3em]">{{ formatNewsDate(post.publishedAt) }}</span>
<h1 class="text-3xl md:text-4xl font-semibold">{{ post.title }}</h1>
@@ -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; }

View File

@@ -11,7 +11,7 @@
Content-getriebene Updates zu Alpha-Builds, Simulator-Support und Community-Entscheidungen.
</p>
</div>
<NuxtLink to="#feed" class="btn btn-primary w-fit">Zum aktuellen Feed</NuxtLink>
<NuxtLink to="/#contributing" class="btn btn-primary w-fit">Zum aktuellen Feed</NuxtLink>
</div>
</header>

View File

@@ -6,7 +6,7 @@
<div>
<p class="text-xs uppercase tracking-[0.35em] text-cyan-400/80">OpenSquawk</p>
<h1 class="text-2xl font-semibold">Pilot Monitoring</h1>
<p class="mt-1 text-sm text-white/70">Decision Tree Enhanced LLM VATSIM</p>
<p class="mt-1 text-sm text-white/70">Alpha Build Decision Tree VATSIM</p>
</div>
<div class="text-right">
<v-chip size="small" :color="currentState?.phase === 'Interrupt' ? 'red' : 'cyan'" variant="flat" class="mb-1">

View File

@@ -10,6 +10,7 @@ export default defineNuxtConfig({
'@pinia/nuxt',
'nuxt-mongoose',
'nuxt-module-hotjar',
'@nuxt/image',
],
aos: {once: true, duration: 600, easing: 'ease-out'},
app: {head: {link: [{rel: 'icon', type: 'image/jpeg', href: '/img/logo.jpeg'}]}},
@@ -55,4 +56,4 @@ export default defineNuxtConfig({
css: [
'~/assets/css/global.css', '~/assets/css/opensquawk-glass.css'
],
})
})

View File

@@ -12,10 +12,12 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/image": "1.11.0",
"@nuxtjs/tailwindcss": "6.14.0",
"@pinia/nuxt": "0.11.2",
"dotenv": "^17.2.2",
"fluent-ffmpeg": "^2.1.3",
"nodemailer": "^6.9.13",
"nuxt": "^4.1.1",
"nuxt-aos": "1.2.5",
"nuxt-module-hotjar": "1.3.4",

View File

@@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="720" viewBox="0 0 1600 720" preserveAspectRatio="xMidYMid slice">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#041026" />
<stop offset="50%" stop-color="#0a223f" />
<stop offset="100%" stop-color="#134b5f" />
</linearGradient>
<radialGradient id="glow" cx="75%" cy="25%" r="70%">
<stop offset="0%" stop-color="#2dd4bf" stop-opacity="0.85" />
<stop offset="55%" stop-color="#22d3ee" stop-opacity="0.25" />
<stop offset="100%" stop-color="#0b1020" stop-opacity="0" />
</radialGradient>
<radialGradient id="glow2" cx="20%" cy="80%" r="65%">
<stop offset="0%" stop-color="#38bdf8" stop-opacity="0.75" />
<stop offset="60%" stop-color="#0ea5e9" stop-opacity="0.18" />
<stop offset="100%" stop-color="#0a0f1c" stop-opacity="0" />
</radialGradient>
<linearGradient id="grid" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#22d3ee" stop-opacity="0.1" />
<stop offset="100%" stop-color="#22d3ee" stop-opacity="0" />
</linearGradient>
<filter id="soft" x="-10%" y="-10%" width="120%" height="120%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" />
</filter>
</defs>
<rect width="1600" height="720" fill="url(#bg)" />
<circle cx="1150" cy="180" r="520" fill="url(#glow)" />
<circle cx="360" cy="620" r="460" fill="url(#glow2)" />
<g stroke="url(#grid)" stroke-width="1" fill="none" opacity="0.5">
<path d="M-200 640 L1800 120" />
<path d="M-120 700 L1760 180" />
<path d="M-60 760 L1700 240" />
<path d="M40 760 L1620 320" />
</g>
<g opacity="0.45" filter="url(#soft)">
<circle cx="230" cy="210" r="90" fill="#38bdf8" />
<circle cx="290" cy="210" r="60" fill="#22d3ee" />
<circle cx="350" cy="210" r="30" fill="#0ea5e9" />
</g>
<g transform="translate(180 500) rotate(-6)" opacity="0.3">
<rect x="0" y="0" width="460" height="110" rx="28" ry="28" fill="#0f172a" stroke="#1f2937" stroke-width="2" />
<text x="230" y="68" font-family="'Space Grotesk', 'Segoe UI', sans-serif" font-size="48" fill="#e0f2fe" text-anchor="middle">Alpha Prototype</text>
</g>
<g opacity="0.65">
<text x="150" y="180" font-family="'Space Grotesk', 'Segoe UI', sans-serif" font-size="120" fill="#e0f2fe" letter-spacing="8">OPEN</text>
<text x="150" y="300" font-family="'Space Grotesk', 'Segoe UI', sans-serif" font-size="140" fill="#22d3ee" letter-spacing="10">SQUAWK</text>
</g>
<g transform="translate(980 470)" opacity="0.55">
<circle cx="0" cy="0" r="8" fill="#22d3ee" />
<circle cx="60" cy="-40" r="6" fill="#38bdf8" />
<circle cx="120" cy="-10" r="5" fill="#0ea5e9" />
<circle cx="180" cy="-60" r="4" fill="#38bdf8" />
<circle cx="240" cy="-20" r="6" fill="#22d3ee" />
<polyline points="0,0 60,-40 120,-10 180,-60 240,-20" fill="none" stroke="#bae6fd" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,24 +1,5 @@
const ADMIN_EMAIL_FALLBACK = 'info@opensquawk.de'
const RESEND_ENDPOINT = 'https://api.resend.com/emails'
const ADMIN_NOTIFICATION_SUBJECT_PREFIX = process.env.NOTIFY_SUBJECT_PREFIX || '[OpenSquawk Web]'
type NotificationPrimitive = string | number | boolean | Date | null | undefined
type NotificationValue =
| NotificationPrimitive
| NotificationPrimitive[]
| Record<string, NotificationPrimitive>
type NotificationData = Record<string, NotificationValue> | Array<[string, NotificationValue]>
export interface AdminNotificationOptions {
event: string
summary?: string
data?: NotificationData
to?: string
from?: string
}
interface MailOptions {
to: string
subject: string
@@ -30,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 <no-reply@opensquawk.dev>'
}
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,
},
})
@@ -112,143 +79,24 @@ 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}`)
}
return success
}
function resolveNotificationSubject(event: string) {
const trimmedEvent = event.trim()
if (!trimmedEvent) {
return ADMIN_NOTIFICATION_SUBJECT_PREFIX
}
if (trimmedEvent.toLowerCase().startsWith(ADMIN_NOTIFICATION_SUBJECT_PREFIX.toLowerCase())) {
return trimmedEvent
}
return `${ADMIN_NOTIFICATION_SUBJECT_PREFIX} ${trimmedEvent}`
}
function toNotificationEntries(data?: NotificationData): Array<[string, NotificationValue]> {
if (!data) {
return []
}
if (Array.isArray(data)) {
return data
}
return Object.entries(data)
}
function formatNotificationValue(value: NotificationValue): string {
if (value === null || value === undefined) {
return '—'
}
if (Array.isArray(value)) {
const formatted = value
.map((entry) => formatNotificationValue(entry))
.filter((entry) => entry && entry !== '—')
return formatted.length ? formatted.join(', ') : '—'
}
if (value instanceof Date) {
return value.toISOString()
}
if (typeof value === 'object') {
return JSON.stringify(value, null, 2)
}
if (typeof value === 'boolean') {
return value ? 'Ja' : 'Nein'
}
if (typeof value === 'string') {
const trimmed = value.trim()
return trimmed ? trimmed : '—'
}
return String(value)
}
function formatDataEntries(entries: Array<[string, NotificationValue]>) {
return entries.map(([label, rawValue]) => {
const formattedValue = formatNotificationValue(rawValue)
if (formattedValue.includes('\n')) {
const indented = formattedValue
.split('\n')
.map((line) => ` ${line}`)
.join('\n')
return `${label}:\n${indented}`
}
return `${label}: ${formattedValue}`
})
}
function buildAdminNotificationText(options: AdminNotificationOptions) {
const lines: string[] = []
const summary = options.summary?.trim()
if (summary) {
lines.push(summary)
} else {
lines.push(`Neue Aktivität auf der Website: ${options.event}`)
}
const dataEntries = formatDataEntries([
['Ereignis', options.event],
...toNotificationEntries(options.data),
])
if (dataEntries.length) {
lines.push('')
lines.push('Details:')
lines.push(...dataEntries)
}
return lines.join('\n')
}
export async function sendAdminNotification(options: AdminNotificationOptions) {
const to = options.to || process.env.NOTIFY_EMAIL_TO || ADMIN_EMAIL_FALLBACK
const subject = resolveNotificationSubject(options.event)
const text = buildAdminNotificationText(options)
const success = await sendMailInternal({
to,
subject,
text,
from: options.from,
})
export async function sendAdminNotification(subject: string, text: string) {
const to = process.env.NOTIFY_EMAIL_TO || ADMIN_EMAIL_FALLBACK
const success = await sendMail({ to, subject, text })
if (!success) {
console.info(`[notify:fallback] ${subject}\nEmpfänger: ${to}\n${text}`)
}
return success
}

View File

@@ -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 = ''

View File

@@ -772,6 +772,13 @@ __metadata:
languageName: node
linkType: hard
"@fastify/accept-negotiator@npm:^1.1.0":
version: 1.1.0
resolution: "@fastify/accept-negotiator@npm:1.1.0"
checksum: 10c0/1cb9a298c992b812869158ddc6093557a877b30e5f77618a7afea985a0667c50bc7113593bf0f7f9dc9b82b94c16e8ab127a0afc3efde6677fd645539f6d08e5
languageName: node
linkType: hard
"@iconify-json/carbon@npm:^1.1.27":
version: 1.2.13
resolution: "@iconify-json/carbon@npm:1.2.13"
@@ -1208,7 +1215,7 @@ __metadata:
languageName: node
linkType: hard
"@nuxt/kit@npm:4.1.2, @nuxt/kit@npm:^4.1.1":
"@nuxt/kit@npm:4.1.2":
version: 4.1.2
resolution: "@nuxt/kit@npm:4.1.2"
dependencies:
@@ -6793,17 +6800,6 @@ __metadata:
languageName: node
linkType: hard
"nuxt-module-hotjar@npm:1.3.4":
version: 1.3.4
resolution: "nuxt-module-hotjar@npm:1.3.4"
dependencies:
"@hotjar/browser": "npm:^1.0.9"
"@nuxt/kit": "npm:^4.1.1"
defu: "npm:^6.1.4"
checksum: 10c0/cd4bb0a8b321d96611315681a82eb0e9d896b837540b8cc508cbe4cebd4821641cbcb752ad791675cec5346225431907181165a717e5175d6fa55d4772ced128
languageName: node
linkType: hard
"nuxt-mongoose@npm:1.0.6":
version: 1.0.6
resolution: "nuxt-mongoose@npm:1.0.6"
@@ -7062,7 +7058,6 @@ __metadata:
fluent-ffmpeg: "npm:^2.1.3"
nuxt: "npm:^4.1.1"
nuxt-aos: "npm:1.2.5"
nuxt-module-hotjar: "npm:1.3.4"
nuxt-mongoose: "npm:1.0.6"
openai: "npm:^4.66.0"
pinia: "npm:^3.0.3"