Phase 2
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:
201 files changed
+31984
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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" },
|
||||
];
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user