diff --git a/apps/web/.eslintrc.cjs b/apps/web/.eslintrc.cjs
index a9910da..d4596a4 100644
--- a/apps/web/.eslintrc.cjs
+++ b/apps/web/.eslintrc.cjs
@@ -5,5 +5,7 @@ module.exports = {
ignorePatterns: [
// Service worker servito al browser: out-of-project per il TS parser.
'public/**/*.js',
+ // Script Node ESM: out-of-project per il TS parser.
+ 'scripts/**/*.mjs',
],
};
diff --git a/apps/web/package.json b/apps/web/package.json
index 29f11ef..683e6df 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -35,13 +35,14 @@
"devDependencies": {
"@ketopath/eslint-config": "workspace:*",
"@ketopath/tsconfig": "workspace:*",
+ "@playwright/test": "^1.45.3",
"@types/node": "^20.12.7",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"eslint-config-next": "^14.2.5",
"postcss": "^8.4.39",
- "tailwindcss": "^3.4.4",
- "@playwright/test": "^1.45.3"
+ "sharp": "^0.34.5",
+ "tailwindcss": "^3.4.4"
}
}
diff --git a/apps/web/public/icons/apple-touch-icon.png b/apps/web/public/icons/apple-touch-icon.png
new file mode 100644
index 0000000..af39111
Binary files /dev/null and b/apps/web/public/icons/apple-touch-icon.png differ
diff --git a/apps/web/public/icons/badge-72.png b/apps/web/public/icons/badge-72.png
new file mode 100644
index 0000000..f059992
Binary files /dev/null and b/apps/web/public/icons/badge-72.png differ
diff --git a/apps/web/public/icons/icon-192.png b/apps/web/public/icons/icon-192.png
new file mode 100644
index 0000000..cba17b6
Binary files /dev/null and b/apps/web/public/icons/icon-192.png differ
diff --git a/apps/web/public/icons/icon-512.png b/apps/web/public/icons/icon-512.png
new file mode 100644
index 0000000..26a5c7a
Binary files /dev/null and b/apps/web/public/icons/icon-512.png differ
diff --git a/apps/web/public/icons/icon-maskable-512.png b/apps/web/public/icons/icon-maskable-512.png
new file mode 100644
index 0000000..aeb43a6
Binary files /dev/null and b/apps/web/public/icons/icon-maskable-512.png differ
diff --git a/apps/web/public/icons/icon-maskable.svg b/apps/web/public/icons/icon-maskable.svg
new file mode 100644
index 0000000..7881962
--- /dev/null
+++ b/apps/web/public/icons/icon-maskable.svg
@@ -0,0 +1,8 @@
+
diff --git a/apps/web/public/icons/icon.svg b/apps/web/public/icons/icon.svg
new file mode 100644
index 0000000..7a08148
--- /dev/null
+++ b/apps/web/public/icons/icon.svg
@@ -0,0 +1,16 @@
+
diff --git a/apps/web/scripts/generate-icons.mjs b/apps/web/scripts/generate-icons.mjs
new file mode 100644
index 0000000..5a67c66
--- /dev/null
+++ b/apps/web/scripts/generate-icons.mjs
@@ -0,0 +1,28 @@
+// Genera i PNG dell'icona PWA a partire dalle SVG sorgente.
+// Eseguilo dopo aver modificato icon.svg / icon-maskable.svg.
+//
+// pnpm --filter @ketopath/web exec node scripts/generate-icons.mjs
+
+import { readFile, writeFile } from 'node:fs/promises';
+import { fileURLToPath } from 'node:url';
+import path from 'node:path';
+import sharp from 'sharp';
+
+const here = path.dirname(fileURLToPath(import.meta.url));
+const PUBLIC_ICONS = path.join(here, '..', 'public', 'icons');
+
+const targets = [
+ { src: 'icon.svg', out: 'icon-192.png', size: 192 },
+ { src: 'icon.svg', out: 'icon-512.png', size: 512 },
+ { src: 'icon.svg', out: 'apple-touch-icon.png', size: 180 },
+ { src: 'icon-maskable.svg', out: 'icon-maskable-512.png', size: 512 },
+ { src: 'icon-maskable.svg', out: 'badge-72.png', size: 72 },
+];
+
+for (const t of targets) {
+ const svg = await readFile(path.join(PUBLIC_ICONS, t.src));
+ const png = await sharp(svg, { density: 384 }).resize(t.size, t.size).png().toBuffer();
+ await writeFile(path.join(PUBLIC_ICONS, t.out), png);
+ // eslint-disable-next-line no-console
+ console.info(`✓ ${t.out} (${t.size}×${t.size})`);
+}
diff --git a/apps/web/src/app/[locale]/layout.tsx b/apps/web/src/app/[locale]/layout.tsx
index 3d93b9c..61d9b42 100644
--- a/apps/web/src/app/[locale]/layout.tsx
+++ b/apps/web/src/app/[locale]/layout.tsx
@@ -1,4 +1,4 @@
-import type { Metadata } from 'next';
+import type { Metadata, Viewport } from 'next';
import { Fraunces, Inter_Tight, JetBrains_Mono } from 'next/font/google';
import { notFound } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl';
@@ -35,6 +35,16 @@ export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
+export const viewport: Viewport = {
+ themeColor: [
+ { media: '(prefers-color-scheme: light)', color: '#f6f0df' },
+ { media: '(prefers-color-scheme: dark)', color: '#221d18' },
+ ],
+ width: 'device-width',
+ initialScale: 1,
+ // Su iOS standalone mode mantiene il colore della status bar coerente.
+};
+
export async function generateMetadata({
params: { locale },
}: {
@@ -44,6 +54,21 @@ export async function generateMetadata({
return {
title: t('title'),
description: t('description'),
+ manifest: '/manifest.webmanifest',
+ applicationName: 'KetoPath',
+ appleWebApp: {
+ capable: true,
+ title: 'KetoPath',
+ statusBarStyle: 'default',
+ },
+ icons: {
+ icon: [
+ { url: '/icons/icon.svg', type: 'image/svg+xml' },
+ { url: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
+ { url: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
+ ],
+ apple: [{ url: '/icons/apple-touch-icon.png', sizes: '180x180', type: 'image/png' }],
+ },
};
}
diff --git a/apps/web/src/app/manifest.ts b/apps/web/src/app/manifest.ts
new file mode 100644
index 0000000..03f9ea3
--- /dev/null
+++ b/apps/web/src/app/manifest.ts
@@ -0,0 +1,48 @@
+// Web App Manifest (Next.js 14 file convention).
+// Servito a /manifest.webmanifest. PRD §5.6 — installabile su iOS/Android per
+// abilitare le notifiche push sui dispositivi mobili (vedi ADR 0003).
+
+import type { MetadataRoute } from 'next';
+
+export default function manifest(): MetadataRoute.Manifest {
+ return {
+ name: 'KetoPath — Una keto italiana',
+ short_name: 'KetoPath',
+ description:
+ 'Chetogenica e digiuno intermittente personalizzati: piani settimanali, lista spesa, notifiche.',
+ lang: 'it',
+ start_url: '/',
+ scope: '/',
+ display: 'standalone',
+ orientation: 'portrait',
+ background_color: '#f6f0df',
+ theme_color: '#221d18',
+ categories: ['health', 'lifestyle', 'food'],
+ icons: [
+ {
+ src: '/icons/icon.svg',
+ sizes: 'any',
+ type: 'image/svg+xml',
+ purpose: 'any',
+ },
+ {
+ src: '/icons/icon-192.png',
+ sizes: '192x192',
+ type: 'image/png',
+ purpose: 'any',
+ },
+ {
+ src: '/icons/icon-512.png',
+ sizes: '512x512',
+ type: 'image/png',
+ purpose: 'any',
+ },
+ {
+ src: '/icons/icon-maskable-512.png',
+ sizes: '512x512',
+ type: 'image/png',
+ purpose: 'maskable',
+ },
+ ],
+ };
+}
diff --git a/package.json b/package.json
index 2e3d9df..4450ec4 100644
--- a/package.json
+++ b/package.json
@@ -47,11 +47,11 @@
"@vitest/coverage-v8": "^1.6.0"
},
"lint-staged": {
- "*.{ts,tsx,jsx,cjs,mjs}": [
+ "*.{ts,tsx,jsx,cjs}": [
"eslint --fix --max-warnings=0",
"prettier --write"
],
- "*.js": [
+ "*.{js,mjs}": [
"prettier --write"
],
"*.{json,md,yml,yaml,css}": [
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ad52f21..d8afb9e 100644
Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ