Bootstrap monorepo: Hono API, React PWA, Drizzle DB, CI/CD
Sets up the initial project structure for groombook/groombook: - pnpm monorepo with apps/api (Hono + TypeScript), apps/web (React + Vite + PWA), packages/db (Drizzle ORM), packages/types (shared types) - Core DB schema: clients, pets, services, appointments, staff with CNPG-compatible Postgres - REST API routes for clients, pets, services, appointments with Zod validation - OIDC auth middleware for Authentik integration - React PWA with vite-plugin-pwa, service worker, offline caching, installable manifest - GitHub Actions CI: lint, typecheck, test, build, Docker image build (groombook-runners) - Dockerfiles for API (Node.js) and Web (nginx) - docker-compose.yml for local development Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Appointment } from "@groombook/types";
|
||||
|
||||
export function AppointmentsPage() {
|
||||
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/appointments")
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json() as Promise<Appointment[]>;
|
||||
})
|
||||
.then(setAppointments)
|
||||
.catch((e: unknown) =>
|
||||
setError(e instanceof Error ? e.message : "Unknown error")
|
||||
)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
if (loading) return <p>Loading appointments…</p>;
|
||||
if (error) return <p style={{ color: "red" }}>Error: {error}</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Appointments</h1>
|
||||
{appointments.length === 0 ? (
|
||||
<p>No appointments yet.</p>
|
||||
) : (
|
||||
<ul>
|
||||
{appointments.map((a) => (
|
||||
<li key={a.id}>
|
||||
{new Date(a.startTime).toLocaleString()} — {a.status}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Client } from "@groombook/types";
|
||||
|
||||
export function ClientsPage() {
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/clients")
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json() as Promise<Client[]>;
|
||||
})
|
||||
.then(setClients)
|
||||
.catch((e: unknown) =>
|
||||
setError(e instanceof Error ? e.message : "Unknown error")
|
||||
)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
if (loading) return <p>Loading clients…</p>;
|
||||
if (error) return <p style={{ color: "red" }}>Error: {error}</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Clients</h1>
|
||||
{clients.length === 0 ? (
|
||||
<p>No clients yet.</p>
|
||||
) : (
|
||||
<ul>
|
||||
{clients.map((c) => (
|
||||
<li key={c.id}>
|
||||
{c.name} {c.email ? `— ${c.email}` : ""}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Service } from "@groombook/types";
|
||||
|
||||
export function ServicesPage() {
|
||||
const [services, setServices] = useState<Service[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/services")
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json() as Promise<Service[]>;
|
||||
})
|
||||
.then(setServices)
|
||||
.catch((e: unknown) =>
|
||||
setError(e instanceof Error ? e.message : "Unknown error")
|
||||
)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
if (loading) return <p>Loading services…</p>;
|
||||
if (error) return <p style={{ color: "red" }}>Error: {error}</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Services</h1>
|
||||
{services.length === 0 ? (
|
||||
<p>No services configured yet.</p>
|
||||
) : (
|
||||
<ul>
|
||||
{services.map((s) => (
|
||||
<li key={s.id}>
|
||||
{s.name} — ${(s.basePriceCents / 100).toFixed(2)} /{" "}
|
||||
{s.durationMinutes} min
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user