Base app architecture:

    Route groups for customer, staff and owner.
* Reusable design system with around 20 UI components.
* Complete Prisma schema.
* Authentication with email/password and Google.
* Role and permission model.
* Protected routes.
* Clean separation between features, services, components and shared utilities.
This commit is contained in:
Stefan Lazic committed 2026-08-02 17:47:23 +01:00
1 parent 468d163299
commit 090646fdfd
201 files changed
+31984

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
import { prisma } from "@/lib/db/prisma";
import type { Permission } from "@/generated/prisma/enums";
/**
* Foundation for Phase 3+ role gating — not exercised by any UI yet.
* Deliberately re-checks the database rather than trusting only the JWT:
* permission changes (e.g. an owner demoting a barber) must take effect
* without waiting for the token to expire. See
* docs/implementation-plan.md §11 ("Permission enforcement happens AGAIN
* at the server-action/service layer — never trust the route group alone").
*/
export async function getStaffPermissions(userId: string): Promise<Set<Permission>> {
const staffProfile = await prisma.staffProfile.findUnique({
where: { userId },
include: { role: { include: { permissions: true } } },
});
if (!staffProfile) return new Set();
return new Set(staffProfile.role.permissions.map((rolePermission) => rolePermission.permission));
}
export async function hasPermission(userId: string, permission: Permission): Promise<boolean> {
const permissions = await getStaffPermissions(userId);
return permissions.has(permission);
}
+34
View File
@@ -0,0 +1,34 @@
import { PrismaClient } from "@/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
/**
* Single Prisma Client instance for the whole app.
*
* Prisma 7 requires a driver adapter instead of a `datasource.url` in the
* schema (see prisma/schema.prisma). This is the ONLY file that imports
* `@prisma/adapter-pg` — switching to MySQL later means swapping this one
* adapter (`@prisma/adapter-mariadb`), updating `datasource.provider` in
* the schema, and re-running `prisma migrate dev`. No other file in the
* app touches the database driver directly.
*/
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
});
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
adapter,
log: process.env.NODE_ENV === "development" ? ["warn", "error"] : ["error"],
});
// Reuse the client across hot-reloads in dev so we don't exhaust the
// connection pool every time a file changes.
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
+23
View File
@@ -0,0 +1,23 @@
import type { BottomNavItem } from "@/components/navigation/bottom-nav";
/**
* Single source of truth for the staff/owner bottom nav. The templates had
* two competing 4-item sets for what is conceptually the same nav
* (Calendar/Current/Clients/Settings on most staff+owner screens vs.
* Dashboard/Staff/Calendar/Admin on the Team Management screen — see
* docs/implementation-plan.md §5/§8). Owner-only screens (team, business
* settings, etc.) are reached from Settings rather than getting their own
* top-level tab, so this one list serves both the (staff) and (owner)
* route groups.
*
* Icons are string keys, not component references — this module is
* imported by Server Component layouts, and a Lucide icon component isn't
* a serializable prop across the Server → Client boundary into `BottomNav`
* (see bottom-nav.tsx).
*/
export const STAFF_NAV_ITEMS: BottomNavItem[] = [
{ href: "/staff", label: "Calendar", icon: "calendar-days" },
{ href: "/staff/current", label: "Current", icon: "scissors" },
{ href: "/staff/clients", label: "Clients", icon: "users" },
{ href: "/staff/settings", label: "Settings", icon: "settings" },
];
+10
View File
@@ -0,0 +1,10 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* Merge Tailwind class lists safely — later classes win over earlier ones
* for the same utility group (e.g. `cn("px-4", condition && "px-6")`).
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}