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 @@ + + + + + + KP. + + 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 @@ + + + + + + + + + KETOPATH + + + KP. + + + una keto italiana + 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