diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..bb21eb4
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/icon-16.png b/public/icon-16.png
new file mode 100644
index 0000000..7fe28ed
Binary files /dev/null and b/public/icon-16.png differ
diff --git a/public/icon-180.png b/public/icon-180.png
index c2ddc72..f05509c 100644
Binary files a/public/icon-180.png and b/public/icon-180.png differ
diff --git a/public/icon-192.png b/public/icon-192.png
index e755168..3d956e6 100644
Binary files a/public/icon-192.png and b/public/icon-192.png differ
diff --git a/public/icon-32.png b/public/icon-32.png
new file mode 100644
index 0000000..71d727b
Binary files /dev/null and b/public/icon-32.png differ
diff --git a/public/icon-384.png b/public/icon-384.png
index 1498832..38f4c7b 100644
Binary files a/public/icon-384.png and b/public/icon-384.png differ
diff --git a/public/icon-512-maskable.png b/public/icon-512-maskable.png
index 745c0b9..3dfc3ce 100644
Binary files a/public/icon-512-maskable.png and b/public/icon-512-maskable.png differ
diff --git a/public/icon-512.png b/public/icon-512.png
index 745c0b9..99613a1 100644
Binary files a/public/icon-512.png and b/public/icon-512.png differ
diff --git a/public/icon.svg b/public/icon.svg
index b495eef..0723161 100644
--- a/public/icon.svg
+++ b/public/icon.svg
@@ -1,12 +1,6 @@
diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest
index b470957..c21203b 100644
--- a/public/manifest.webmanifest
+++ b/public/manifest.webmanifest
@@ -4,23 +4,26 @@
"description": "Family coordination app",
"start_url": "/",
"display": "standalone",
- "background_color": "#ffffff",
- "theme_color": "#4F46E5",
+ "background_color": "#4338CA",
+ "theme_color": "#4338CA",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
- "type": "image/png"
+ "type": "image/png",
+ "purpose": "any"
},
{
"src": "/icon-384.png",
"sizes": "384x384",
- "type": "image/png"
+ "type": "image/png",
+ "purpose": "any"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
- "type": "image/png"
+ "type": "image/png",
+ "purpose": "any"
},
{
"src": "/icon-512-maskable.png",
diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs
index bc4e57e..a0eff59 100644
--- a/scripts/generate-icons.mjs
+++ b/scripts/generate-icons.mjs
@@ -1,100 +1,48 @@
/**
- * Generates placeholder PNG icons for the famapp PWA.
+ * Rasterizes public/icon.svg into all required PNG icon sizes.
*
* Usage: node scripts/generate-icons.mjs
*
* Produces:
+ * public/icon-16.png
+ * public/icon-32.png
+ * public/icon-180.png (apple-touch-icon)
* public/icon-192.png
* public/icon-384.png
* public/icon-512.png
- * public/icon-512-maskable.png (same image; OS applies its own mask)
- * public/icon-180.png (apple-touch-icon)
- *
- * All images are solid indigo (#4F46E5) squares — replace with a real
- * branded export from icon.svg when assets are finalised.
+ * public/icon-512-maskable.png (square bg, content in safe zone)
*/
-import { deflateSync } from "zlib";
-import { writeFileSync } from "fs";
-import { resolve, dirname } from "path";
+import sharp from "sharp";
+import { readFileSync } from "fs";
import { fileURLToPath } from "url";
+import { dirname, join } from "path";
-const __dir = dirname(fileURLToPath(import.meta.url));
-const publicDir = resolve(__dir, "../public");
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const pub = join(__dirname, "..", "public");
-// Build CRC-32 lookup table once.
-const CRC_TABLE = new Uint32Array(256);
-for (let i = 0; i < 256; i++) {
- let c = i;
- for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
- CRC_TABLE[i] = c;
-}
+const svg = readFileSync(join(pub, "icon.svg"));
-function crc32(buf) {
- let crc = 0xffffffff;
- for (let i = 0; i < buf.length; i++) crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
- return (crc ^ 0xffffffff) >>> 0;
-}
-
-function u32(n) {
- const b = Buffer.alloc(4);
- b.writeUInt32BE(n, 0);
- return b;
-}
-
-function pngChunk(type, data) {
- const typeBytes = Buffer.from(type, "ascii");
- const crc = u32(crc32(Buffer.concat([typeBytes, data])));
- return Buffer.concat([u32(data.length), typeBytes, data, crc]);
-}
-
-/**
- * Returns a Buffer containing a valid PNG of size×size filled with (r, g, b).
- */
-function solidPNG(size, r, g, b) {
- const PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
-
- const ihdr = pngChunk(
- "IHDR",
- Buffer.concat([
- u32(size),
- u32(size),
- Buffer.from([8, 2, 0, 0, 0]), // 8-bit RGB, no interlace
- ]),
- );
-
- // One filter byte (0 = None) followed by size×3 RGB bytes per row.
- const rowLen = 1 + size * 3;
- const raw = Buffer.alloc(size * rowLen);
- for (let y = 0; y < size; y++) {
- const base = y * rowLen;
- raw[base] = 0;
- for (let x = 0; x < size; x++) {
- raw[base + 1 + x * 3] = r;
- raw[base + 2 + x * 3] = g;
- raw[base + 3 + x * 3] = b;
- }
- }
-
- const idat = pngChunk("IDAT", deflateSync(raw));
- const iend = pngChunk("IEND", Buffer.alloc(0));
-
- return Buffer.concat([PNG_SIG, ihdr, idat, iend]);
-}
-
-// Indigo-600 (#4F46E5)
-const [R, G, B] = [0x4f, 0x46, 0xe5];
-
-const icons = [
+const sizes = [
+ { name: "icon-16.png", size: 16 },
+ { name: "icon-32.png", size: 32 },
+ { name: "icon-180.png", size: 180 },
{ name: "icon-192.png", size: 192 },
{ name: "icon-384.png", size: 384 },
{ name: "icon-512.png", size: 512 },
- { name: "icon-512-maskable.png", size: 512 },
- { name: "icon-180.png", size: 180 },
];
-for (const { name, size } of icons) {
- const out = resolve(publicDir, name);
- writeFileSync(out, solidPNG(size, R, G, B));
+for (const { name, size } of sizes) {
+ await sharp(svg).resize(size, size).png().toFile(join(pub, name));
console.log(` ✓ public/${name} (${size}×${size})`);
}
+
+// Maskable: remove rounded corners so the background fills the full canvas,
+// allowing the OS to apply any mask shape without clipping the icon corners.
+const maskableSvg = readFileSync(join(pub, "icon.svg"), "utf-8").replace('rx="96"', "");
+
+await sharp(Buffer.from(maskableSvg))
+ .resize(512, 512)
+ .png()
+ .toFile(join(pub, "icon-512-maskable.png"));
+console.log(` ✓ public/icon-512-maskable.png (512×512 maskable)`);
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 1fe1246..ba61772 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -54,6 +54,11 @@ export const metadata: Metadata = {
title: "famapp",
},
icons: {
+ icon: [
+ { url: "/favicon.svg", type: "image/svg+xml" },
+ { url: "/icon-32.png", sizes: "32x32", type: "image/png" },
+ { url: "/icon-16.png", sizes: "16x16", type: "image/png" },
+ ],
apple: "/icon-180.png",
},
};
diff --git a/src/components/brand-mark.tsx b/src/components/brand-mark.tsx
index a72aebb..7e07c1f 100644
--- a/src/components/brand-mark.tsx
+++ b/src/components/brand-mark.tsx
@@ -1,12 +1,23 @@
import { cn } from "@/lib/utils";
export function BrandMark({ size = "md", className }: { size?: "sm" | "md"; className?: string }) {
+ const iconSize = size === "sm" ? 14 : 17;
return (
- f
+
);
}
diff --git a/src/lib/session.ts b/src/lib/session.ts
index bebf3ab..cf51637 100644
--- a/src/lib/session.ts
+++ b/src/lib/session.ts
@@ -1,4 +1,5 @@
import { eq } from "drizzle-orm";
+import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { householdMembers, households, users } from "@/modules/_core/schema";
@@ -13,7 +14,7 @@ export interface CurrentSession {
export async function getCurrentSession(): Promise {
const session = await auth();
- if (!session?.user?.id) throw new Error("Not authenticated");
+ if (!session?.user?.id) redirect("/login");
const [row] = await db
.select({
diff --git a/src/middleware.ts b/src/middleware.ts
index 5519687..6fb1281 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,8 +1,8 @@
import { NextResponse, type NextRequest } from "next/server";
import { consume } from "@/lib/rate-limit";
-const PUBLIC_PREFIXES = ["/api/auth/", "/s/"];
-const PUBLIC_PATHS = new Set(["/login"]);
+const PUBLIC_PREFIXES = ["/api/auth/", "/s/", "/icon-", "/favicon"];
+const PUBLIC_PATHS = new Set(["/login", "/manifest.webmanifest", "/offline.html"]);
const SESSION_COOKIE_NAMES = ["authjs.session-token", "__Secure-authjs.session-token"];
// Token-prefix length used as part of the rate-limit bucket key.