feat: proper logo, favicon, and pwa icons

- replace placeholder house svg with clean geometric house mark
  on indigo-700 (#4338CA) — roof triangle, body, door cutout
- add favicon.svg with prefers-color-scheme dark variant
- add icon-16/32 sizes; regenerate all pngs from svg via sharp
- update BrandMark component to use inline svg house instead of 'f'
- wire favicon + sized pngs into layout metadata
- fix middleware to allow icon/manifest/favicon paths without auth
  (android launcher fetches icons in a cookieless system context)
- fix getCurrentSession() to redirect to /login instead of throwing
  when session cookie is stale/invalid
This commit is contained in:
ginnoir
2026-06-03 15:59:34 -05:00
parent 10cad3df17
commit ec2f3930ad
15 changed files with 69 additions and 98 deletions
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<style>
@media (prefers-color-scheme: dark) { .bg { fill: #6366F1; } }
</style>
<rect class="bg" width="512" height="512" rx="96" fill="#4338CA"/>
<rect x="130" y="238" width="252" height="190" fill="white"/>
<polygon points="256,82 68,254 444,254" fill="white"/>
<rect x="216" y="338" width="80" height="90" rx="8" fill="#4338CA"/>
</svg>

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 B

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+4 -10
View File
@@ -1,12 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<!-- Background -->
<rect width="512" height="512" rx="80" fill="#4F46E5"/>
<!-- House body -->
<polygon points="256,108 396,234 364,234 364,392 148,392 148,234 116,234" fill="white"/>
<!-- Door -->
<rect x="212" y="296" width="88" height="96" rx="6" fill="#4F46E5"/>
<!-- Left window -->
<rect x="164" y="254" width="66" height="54" rx="6" fill="#4F46E5" opacity="0.55"/>
<!-- Right window -->
<rect x="282" y="254" width="66" height="54" rx="6" fill="#4F46E5" opacity="0.55"/>
<rect width="512" height="512" rx="96" fill="#4338CA"/>
<rect x="130" y="238" width="252" height="190" fill="white"/>
<polygon points="256,82 68,254 444,254" fill="white"/>
<rect x="216" y="338" width="80" height="90" rx="8" fill="#4338CA"/>
</svg>

Before

Width:  |  Height:  |  Size: 594 B

After

Width:  |  Height:  |  Size: 345 B

+8 -5
View File
@@ -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",
+27 -79
View File
@@ -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)`);
+5
View File
@@ -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",
},
};
+12 -1
View File
@@ -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 (
<span
className={cn("brand-mark", size === "sm" && "brand-mark-sm", className)}
aria-hidden="true"
>
f
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
width={iconSize}
height={iconSize}
fill="currentColor"
>
<polygon points="12,2 1,12 23,12" />
<rect x="3.5" y="11" width="17" height="12" />
<rect x="9.5" y="16" width="5" height="7" rx="1" style={{ fill: "var(--ink)" }} />
</svg>
</span>
);
}
+2 -1
View File
@@ -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<CurrentSession> {
const session = await auth();
if (!session?.user?.id) throw new Error("Not authenticated");
if (!session?.user?.id) redirect("/login");
const [row] = await db
.select({
+2 -2
View File
@@ -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.