feat: add customizable business branding (name, logo, colors)

Add admin settings for business branding with name, logo upload, and
color scheme via CSS custom properties. Includes database migration,
API endpoints, admin settings page, and dynamic branding in both
admin nav and customer portal.

Closes #61

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
GroomBook CTO
2026-03-19 03:36:33 +00:00
parent 3388895912
commit 5d424d263c
10 changed files with 556 additions and 31 deletions
+59
View File
@@ -0,0 +1,59 @@
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { eq, getDb, businessSettings } from "@groombook/db";
export const settingsRouter = new Hono();
// GET /api/admin/settings — return current business settings
settingsRouter.get("/", async (c) => {
const db = getDb();
const [row] = await db.select().from(businessSettings).limit(1);
if (!row) {
// Auto-create default settings if none exist
const [created] = await db.insert(businessSettings).values({}).returning();
return c.json(created);
}
return c.json(row);
});
const hexColorRegex = /^#[0-9a-fA-F]{6}$/;
const updateSettingsSchema = z.object({
businessName: z.string().min(1).max(200).optional(),
primaryColor: z.string().regex(hexColorRegex, "Must be a hex color like #4f8a6f").optional(),
accentColor: z.string().regex(hexColorRegex, "Must be a hex color like #8b7355").optional(),
logoBase64: z.string().max(700_000).nullable().optional(), // ~512KB base64
logoMimeType: z
.enum(["image/png", "image/svg+xml", "image/jpeg", "image/webp"])
.nullable()
.optional(),
});
// PATCH /api/admin/settings — update business settings
settingsRouter.patch(
"/",
zValidator("json", updateSettingsSchema),
async (c) => {
const db = getDb();
const body = c.req.valid("json");
// Get or create the settings row
const rows = await db.select().from(businessSettings).limit(1);
let settingsId: string;
if (rows[0]) {
settingsId = rows[0].id;
} else {
const inserted = await db.insert(businessSettings).values({}).returning();
settingsId = inserted[0]!.id;
}
const [updated] = await db
.update(businessSettings)
.set({ ...body, updatedAt: new Date() })
.where(eq(businessSettings.id, settingsId))
.returning();
return c.json(updated);
}
);