diff --git a/apps/calendar/apps/backend/src/app.module.ts b/apps/calendar/apps/backend/src/app.module.ts
index 4d159c594..6668f5985 100644
--- a/apps/calendar/apps/backend/src/app.module.ts
+++ b/apps/calendar/apps/backend/src/app.module.ts
@@ -65,7 +65,10 @@ import { HttpExceptionFilter } from './common/http-exception.filter';
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
- // ThrottlerGuard registered via ThrottlerModule — use @UseGuards(ThrottlerGuard) on controllers
+ {
+ provide: APP_GUARD,
+ useClass: ThrottlerGuard,
+ },
],
})
export class AppModule {}
diff --git a/apps/calendar/apps/web/scripts/generate-icons.mjs b/apps/calendar/apps/web/scripts/generate-icons.mjs
new file mode 100644
index 000000000..1af7bd63f
--- /dev/null
+++ b/apps/calendar/apps/web/scripts/generate-icons.mjs
@@ -0,0 +1,45 @@
+#!/usr/bin/env node
+/**
+ * Generate PWA icons from SVG favicon
+ * Run: node scripts/generate-icons.mjs
+ * Requires: sharp (available in workspace)
+ */
+
+import { readFileSync } from 'fs';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const staticDir = join(__dirname, '..', 'static');
+
+const sizes = [
+ { name: 'favicon.png', size: 32 },
+ { name: 'pwa-192x192.png', size: 192 },
+ { name: 'pwa-512x512.png', size: 512 },
+ { name: 'apple-touch-icon.png', size: 180 },
+];
+
+async function generateIcons() {
+ try {
+ const sharp = (await import('sharp')).default;
+ const svgPath = join(staticDir, 'favicon.svg');
+ const svgBuffer = readFileSync(svgPath);
+
+ for (const { name, size } of sizes) {
+ const outputPath = join(staticDir, name);
+ await sharp(svgBuffer).resize(size, size).png().toFile(outputPath);
+ console.log(`Generated: ${name} (${size}x${size})`);
+ }
+
+ console.log('\nAll icons generated successfully!');
+ } catch (error) {
+ if (error.code === 'ERR_MODULE_NOT_FOUND') {
+ console.error('Sharp is not installed. Run: pnpm add -D sharp');
+ } else {
+ console.error('Error generating icons:', error);
+ }
+ process.exit(1);
+ }
+}
+
+generateIcons();
diff --git a/apps/calendar/apps/web/src/app.html b/apps/calendar/apps/web/src/app.html
index cff15d4b6..6cf6c7464 100644
--- a/apps/calendar/apps/web/src/app.html
+++ b/apps/calendar/apps/web/src/app.html
@@ -1,9 +1,25 @@
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Calendar
%sveltekit.head%
diff --git a/apps/calendar/apps/web/src/lib/i18n/locales/de.json b/apps/calendar/apps/web/src/lib/i18n/locales/de.json
index d195ddbac..41c73fd83 100644
--- a/apps/calendar/apps/web/src/lib/i18n/locales/de.json
+++ b/apps/calendar/apps/web/src/lib/i18n/locales/de.json
@@ -129,5 +129,9 @@
"a11y": {
"createEventOn": "Termin erstellen am {date}",
"slotTime": "{day} {time}"
+ },
+ "error": {
+ "notFound": "Seite nicht gefunden",
+ "backToHome": "Zurück zur Startseite"
}
}
diff --git a/apps/calendar/apps/web/src/lib/i18n/locales/en.json b/apps/calendar/apps/web/src/lib/i18n/locales/en.json
index 18a452fa9..ecf11e8ce 100644
--- a/apps/calendar/apps/web/src/lib/i18n/locales/en.json
+++ b/apps/calendar/apps/web/src/lib/i18n/locales/en.json
@@ -129,5 +129,9 @@
"a11y": {
"createEventOn": "Create event on {date}",
"slotTime": "{day} {time}"
+ },
+ "error": {
+ "notFound": "Page not found",
+ "backToHome": "Back to home"
}
}
diff --git a/apps/calendar/apps/web/src/lib/i18n/locales/es.json b/apps/calendar/apps/web/src/lib/i18n/locales/es.json
index c8f3cd1be..803c66120 100644
--- a/apps/calendar/apps/web/src/lib/i18n/locales/es.json
+++ b/apps/calendar/apps/web/src/lib/i18n/locales/es.json
@@ -97,5 +97,9 @@
"search": "Buscar",
"error": "Error",
"success": "Éxito"
+ },
+ "error": {
+ "notFound": "Página no encontrada",
+ "backToHome": "Volver al inicio"
}
}
diff --git a/apps/calendar/apps/web/src/lib/i18n/locales/fr.json b/apps/calendar/apps/web/src/lib/i18n/locales/fr.json
index 7a069d492..dea43a0bc 100644
--- a/apps/calendar/apps/web/src/lib/i18n/locales/fr.json
+++ b/apps/calendar/apps/web/src/lib/i18n/locales/fr.json
@@ -97,5 +97,9 @@
"search": "Rechercher",
"error": "Erreur",
"success": "Succès"
+ },
+ "error": {
+ "notFound": "Page non trouvée",
+ "backToHome": "Retour à l'accueil"
}
}
diff --git a/apps/calendar/apps/web/src/lib/i18n/locales/it.json b/apps/calendar/apps/web/src/lib/i18n/locales/it.json
index 68df0d73b..5fae7ff68 100644
--- a/apps/calendar/apps/web/src/lib/i18n/locales/it.json
+++ b/apps/calendar/apps/web/src/lib/i18n/locales/it.json
@@ -97,5 +97,9 @@
"search": "Cerca",
"error": "Errore",
"success": "Successo"
+ },
+ "error": {
+ "notFound": "Pagina non trovata",
+ "backToHome": "Torna alla home"
}
}
diff --git a/apps/calendar/apps/web/src/routes/+error.svelte b/apps/calendar/apps/web/src/routes/+error.svelte
index d5a57d247..5b9e9cd2a 100644
--- a/apps/calendar/apps/web/src/routes/+error.svelte
+++ b/apps/calendar/apps/web/src/routes/+error.svelte
@@ -1,9 +1,10 @@
{$page.status}
-
{$page.error?.message || 'Seite nicht gefunden'}
-
Zurück zur Startseite
+
{$page.error?.message || $_('error.notFound')}
+
{$_('error.backToHome')}
diff --git a/apps/calendar/apps/web/src/routes/+layout.svelte b/apps/calendar/apps/web/src/routes/+layout.svelte
index 72bb7bc61..f8d969b87 100644
--- a/apps/calendar/apps/web/src/routes/+layout.svelte
+++ b/apps/calendar/apps/web/src/routes/+layout.svelte
@@ -26,6 +26,19 @@
});
+
+
+
+
+
+
+
{#if !appReady}
{:else}
diff --git a/apps/calendar/apps/web/static/apple-touch-icon.png b/apps/calendar/apps/web/static/apple-touch-icon.png
new file mode 100644
index 000000000..4e86574d5
Binary files /dev/null and b/apps/calendar/apps/web/static/apple-touch-icon.png differ
diff --git a/apps/calendar/apps/web/static/favicon.png b/apps/calendar/apps/web/static/favicon.png
new file mode 100644
index 000000000..bd550906b
Binary files /dev/null and b/apps/calendar/apps/web/static/favicon.png differ
diff --git a/apps/calendar/apps/web/static/favicon.svg b/apps/calendar/apps/web/static/favicon.svg
new file mode 100644
index 000000000..bb9d53400
--- /dev/null
+++ b/apps/calendar/apps/web/static/favicon.svg
@@ -0,0 +1,31 @@
+
diff --git a/apps/calendar/apps/web/static/pwa-192x192.png b/apps/calendar/apps/web/static/pwa-192x192.png
new file mode 100644
index 000000000..a0db9cb61
Binary files /dev/null and b/apps/calendar/apps/web/static/pwa-192x192.png differ
diff --git a/apps/calendar/apps/web/static/pwa-512x512.png b/apps/calendar/apps/web/static/pwa-512x512.png
new file mode 100644
index 000000000..d7e2f04b1
Binary files /dev/null and b/apps/calendar/apps/web/static/pwa-512x512.png differ
diff --git a/apps/todo/apps/web/src/lib/i18n/locales/de.json b/apps/todo/apps/web/src/lib/i18n/locales/de.json
index 44f98227e..233f24403 100644
--- a/apps/todo/apps/web/src/lib/i18n/locales/de.json
+++ b/apps/todo/apps/web/src/lib/i18n/locales/de.json
@@ -93,6 +93,10 @@
"loadProjects": "Projekte konnten nicht geladen werden",
"loadLabels": "Labels konnten nicht geladen werden"
},
+ "error": {
+ "notFound": "Seite nicht gefunden",
+ "backToHome": "Zurück zur Startseite"
+ },
"success": {
"taskCreated": "Aufgabe erstellt",
"taskUpdated": "Aufgabe aktualisiert",
diff --git a/apps/todo/apps/web/src/lib/i18n/locales/en.json b/apps/todo/apps/web/src/lib/i18n/locales/en.json
index 3038a61f0..323f6792c 100644
--- a/apps/todo/apps/web/src/lib/i18n/locales/en.json
+++ b/apps/todo/apps/web/src/lib/i18n/locales/en.json
@@ -93,6 +93,10 @@
"loadProjects": "Failed to load projects",
"loadLabels": "Failed to load labels"
},
+ "error": {
+ "notFound": "Page not found",
+ "backToHome": "Back to home"
+ },
"success": {
"taskCreated": "Task created",
"taskUpdated": "Task updated",
diff --git a/apps/todo/apps/web/src/lib/i18n/locales/es.json b/apps/todo/apps/web/src/lib/i18n/locales/es.json
index a65dc8cff..0206d5709 100644
--- a/apps/todo/apps/web/src/lib/i18n/locales/es.json
+++ b/apps/todo/apps/web/src/lib/i18n/locales/es.json
@@ -93,6 +93,10 @@
"loadProjects": "No se pudieron cargar los proyectos",
"loadLabels": "No se pudieron cargar las etiquetas"
},
+ "error": {
+ "notFound": "Página no encontrada",
+ "backToHome": "Volver al inicio"
+ },
"success": {
"taskCreated": "Tarea creada",
"taskUpdated": "Tarea actualizada",
diff --git a/apps/todo/apps/web/src/lib/i18n/locales/fr.json b/apps/todo/apps/web/src/lib/i18n/locales/fr.json
index 41fabbe9e..f2ab22cdf 100644
--- a/apps/todo/apps/web/src/lib/i18n/locales/fr.json
+++ b/apps/todo/apps/web/src/lib/i18n/locales/fr.json
@@ -93,6 +93,10 @@
"loadProjects": "Impossible de charger les projets",
"loadLabels": "Impossible de charger les labels"
},
+ "error": {
+ "notFound": "Page non trouvée",
+ "backToHome": "Retour à l'accueil"
+ },
"success": {
"taskCreated": "Tâche créée",
"taskUpdated": "Tâche mise à jour",
diff --git a/apps/todo/apps/web/src/lib/i18n/locales/it.json b/apps/todo/apps/web/src/lib/i18n/locales/it.json
index 5114f9f95..8101be387 100644
--- a/apps/todo/apps/web/src/lib/i18n/locales/it.json
+++ b/apps/todo/apps/web/src/lib/i18n/locales/it.json
@@ -93,6 +93,10 @@
"loadProjects": "Impossibile caricare i progetti",
"loadLabels": "Impossibile caricare le etichette"
},
+ "error": {
+ "notFound": "Pagina non trovata",
+ "backToHome": "Torna alla home"
+ },
"success": {
"taskCreated": "Attività creata",
"taskUpdated": "Attività aggiornata",
diff --git a/apps/todo/apps/web/src/routes/+error.svelte b/apps/todo/apps/web/src/routes/+error.svelte
index b060de2f4..1b638d893 100644
--- a/apps/todo/apps/web/src/routes/+error.svelte
+++ b/apps/todo/apps/web/src/routes/+error.svelte
@@ -1,9 +1,10 @@
{$page.status}
-
{$page.error?.message || 'Seite nicht gefunden'}
-
Zurück zur Startseite
+
{$page.error?.message || $_('error.notFound')}
+
{$_('error.backToHome')}
diff --git a/apps/todo/apps/web/src/routes/+layout.svelte b/apps/todo/apps/web/src/routes/+layout.svelte
index 2af09115b..8d50185a1 100644
--- a/apps/todo/apps/web/src/routes/+layout.svelte
+++ b/apps/todo/apps/web/src/routes/+layout.svelte
@@ -10,6 +10,7 @@
let { children } = $props();
let loading = $state(true);
+
let appReady = $derived(!loading && !$i18nLoading);
onMount(async () => {
@@ -23,6 +24,19 @@
});
+
+
+
+
+
+
+
{#if !appReady}
{:else}