feat: unify site theming via CSS custom properties (GH #91) #100
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useEffect, useState, useCallback } from "react";
|
||||
import { createContext, useContext, useEffect, useRef, useState, useCallback } from "react";
|
||||
|
||||
export interface Branding {
|
||||
businessName: string;
|
||||
@@ -27,6 +27,7 @@ export function useBranding() {
|
||||
|
||||
export function BrandingProvider({ children }: { children: React.ReactNode }) {
|
||||
const [branding, setBranding] = useState<Branding>(DEFAULT_BRANDING);
|
||||
const metaThemeColorRef = useRef<HTMLMetaElement | null>(null);
|
||||
|
||||
const fetchBranding = useCallback(() => {
|
||||
fetch("/api/branding")
|
||||
@@ -45,6 +46,16 @@ export function BrandingProvider({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty("--color-primary", branding.primaryColor);
|
||||
document.documentElement.style.setProperty("--color-accent", branding.accentColor);
|
||||
// Keep PWA theme-color meta tag in sync with primary color
|
||||
if (!metaThemeColorRef.current) {
|
||||
metaThemeColorRef.current = document.querySelector<HTMLMetaElement>("meta[name='theme-color']");
|
||||
if (!metaThemeColorRef.current) {
|
||||
metaThemeColorRef.current = document.createElement("meta");
|
||||
metaThemeColorRef.current.name = "theme-color";
|
||||
document.head.appendChild(metaThemeColorRef.current);
|
||||
}
|
||||
}
|
||||
metaThemeColorRef.current.content = branding.primaryColor;
|
||||
}, [branding.primaryColor, branding.accentColor]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { BrandingProvider, useBranding } from "../BrandingContext.js";
|
||||
|
||||
function BrandingConsumer() {
|
||||
const { branding } = useBranding();
|
||||
return (
|
||||
<div data-testid="branding">
|
||||
<span data-testid="primary">{branding.primaryColor}</span>
|
||||
<span data-testid="accent">{branding.accentColor}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
document.documentElement.style.removeProperty("--color-primary");
|
||||
document.documentElement.style.removeProperty("--color-accent");
|
||||
// Remove any theme-color meta tags
|
||||
document.querySelectorAll("meta[name='theme-color']").forEach((el) => el.remove());
|
||||
});
|
||||
|
||||
describe("BrandingProvider", () => {
|
||||
it("applies CSS vars to document root when branding loads", async () => {
|
||||
const branding = {
|
||||
businessName: "Test Salon",
|
||||
primaryColor: "#123456",
|
||||
accentColor: "#654321",
|
||||
logoBase64: null,
|
||||
logoMimeType: null,
|
||||
};
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve({ ok: true, json: async () => branding } as Response)
|
||||
) as unknown as typeof fetch;
|
||||
|
||||
render(
|
||||
<BrandingProvider>
|
||||
<BrandingConsumer />
|
||||
</BrandingProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.documentElement.style.getPropertyValue("--color-primary")).toBe("#123456");
|
||||
expect(document.documentElement.style.getPropertyValue("--color-accent")).toBe("#654321");
|
||||
});
|
||||
});
|
||||
|
||||
it("creates and updates meta[name=theme-color]", async () => {
|
||||
const branding = {
|
||||
businessName: "Test Salon",
|
||||
primaryColor: "#abcdef",
|
||||
accentColor: "#fedcba",
|
||||
logoBase64: null,
|
||||
logoMimeType: null,
|
||||
};
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve({ ok: true, json: async () => branding } as Response)
|
||||
) as unknown as typeof fetch;
|
||||
|
||||
render(
|
||||
<BrandingProvider>
|
||||
<BrandingConsumer />
|
||||
</BrandingProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const meta = document.querySelector<HTMLMetaElement>("meta[name='theme-color']");
|
||||
expect(meta).not.toBeNull();
|
||||
expect(meta!.content).toBe("#abcdef");
|
||||
});
|
||||
});
|
||||
|
||||
it("does not create duplicate meta[name=theme-color] tags on rerender", async () => {
|
||||
const branding = {
|
||||
businessName: "Test Salon",
|
||||
primaryColor: "#111111",
|
||||
accentColor: "#222222",
|
||||
logoBase64: null,
|
||||
logoMimeType: null,
|
||||
};
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve({ ok: true, json: async () => branding } as Response)
|
||||
) as unknown as typeof fetch;
|
||||
|
||||
const { rerender } = render(
|
||||
<BrandingProvider>
|
||||
<BrandingConsumer />
|
||||
</BrandingProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector("meta[name='theme-color']")).not.toBeNull();
|
||||
});
|
||||
|
||||
rerender(
|
||||
<BrandingProvider>
|
||||
<BrandingConsumer />
|
||||
</BrandingProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const metas = document.querySelectorAll("meta[name='theme-color']");
|
||||
expect(metas.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,7 @@ export function DevSessionIndicator() {
|
||||
<Link
|
||||
to="/login"
|
||||
style={{
|
||||
color: "#4f8a6f",
|
||||
color: "var(--color-primary)",
|
||||
textDecoration: "underline",
|
||||
fontSize: 12,
|
||||
}}
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
:root {
|
||||
--color-primary: #4f8a6f;
|
||||
--color-primary-dark: color-mix(in srgb, var(--color-primary) 80%, #000);
|
||||
--color-accent: #8b7355;
|
||||
--color-accent-hover: color-mix(in srgb, var(--color-accent) 88%, #000);
|
||||
--color-accent-dark: color-mix(in srgb, var(--color-accent) 78%, #000);
|
||||
--color-accent-light: color-mix(in srgb, var(--color-accent) 18%, #fff);
|
||||
--color-accent-lighter: color-mix(in srgb, var(--color-accent) 9%, #fff);
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
@@ -51,8 +56,8 @@ button:active:not(:disabled) {
|
||||
/* ─── Admin input / select focus states ─── */
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: #4f8a6f;
|
||||
box-shadow: 0 0 0 3px rgba(79, 138, 111, 0.12);
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||
}
|
||||
|
||||
/* ─── Admin card-like containers (borders get subtle shadow) ─── */
|
||||
|
||||
@@ -291,7 +291,7 @@ export function AppointmentsPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openNewForm()}
|
||||
style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", marginLeft: "auto", borderColor: "#4f8a6f" }}
|
||||
style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", marginLeft: "auto", borderColor: "var(--color-primary)" }}
|
||||
>
|
||||
+ New Appointment
|
||||
</button>
|
||||
@@ -374,7 +374,7 @@ export function AppointmentsPage() {
|
||||
<div
|
||||
style={{
|
||||
padding: "0.4rem 0.6rem",
|
||||
background: isToday ? "linear-gradient(135deg, #4f8a6f, #3d7a5f)" : "#f8fafc",
|
||||
background: isToday ? "linear-gradient(135deg, var(--color-primary), var(--color-primary-dark))" : "#f8fafc",
|
||||
color: isToday ? "#fff" : "#374151",
|
||||
fontWeight: 600,
|
||||
fontSize: 12,
|
||||
@@ -594,7 +594,7 @@ export function AppointmentsPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f" }}
|
||||
style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)" }}
|
||||
>
|
||||
{saving
|
||||
? "Saving…"
|
||||
|
||||
@@ -66,8 +66,8 @@ function StepIndicator({ step }: { step: number }) {
|
||||
padding: "0.5rem 0.25rem",
|
||||
fontSize: 12,
|
||||
fontWeight: active ? 700 : 400,
|
||||
color: active ? "#4f8a6f" : done ? "#4f8a6f" : "#9ca3af",
|
||||
borderBottom: `3px solid ${active ? "#4f8a6f" : done ? "#4f8a6f" : "#e5e7eb"}`,
|
||||
color: active ? "var(--color-primary)" : done ? "var(--color-primary)" : "#9ca3af",
|
||||
borderBottom: `3px solid ${active ? "var(--color-primary)" : done ? "var(--color-primary)" : "#e5e7eb"}`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
@@ -78,7 +78,7 @@ function StepIndicator({ step }: { step: number }) {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: "50%",
|
||||
background: active ? "#4f8a6f" : done ? "#4f8a6f" : "#e5e7eb",
|
||||
background: active ? "var(--color-primary)" : done ? "var(--color-primary)" : "#e5e7eb",
|
||||
color: active || done ? "#fff" : "#6b7280",
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
@@ -218,7 +218,7 @@ export function BookPage() {
|
||||
|
||||
const selectedCard: React.CSSProperties = {
|
||||
...card,
|
||||
border: "2px solid #4f8a6f",
|
||||
border: "2px solid var(--color-primary)",
|
||||
background: "#f0faf5",
|
||||
};
|
||||
|
||||
@@ -250,7 +250,7 @@ export function BookPage() {
|
||||
|
||||
const primaryBtn: React.CSSProperties = {
|
||||
...btn,
|
||||
background: "#4f8a6f",
|
||||
background: "var(--color-primary)",
|
||||
color: "#fff",
|
||||
};
|
||||
|
||||
@@ -301,7 +301,7 @@ export function BookPage() {
|
||||
)}
|
||||
</div>
|
||||
<div style={{ textAlign: "right", flexShrink: 0, marginLeft: "1rem" }}>
|
||||
<div style={{ fontWeight: 700, color: "#4f8a6f", fontSize: 15 }}>
|
||||
<div style={{ fontWeight: 700, color: "var(--color-primary)", fontSize: 15 }}>
|
||||
{fmtPrice(svc.basePriceCents)}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#9ca3af" }}>{fmtDuration(svc.durationMinutes)}</div>
|
||||
@@ -349,8 +349,8 @@ export function BookPage() {
|
||||
style={{
|
||||
padding: "0.4rem 0.85rem",
|
||||
borderRadius: 6,
|
||||
border: `2px solid ${selectedSlot === slot ? "#4f8a6f" : "#d1d5db"}`,
|
||||
background: selectedSlot === slot ? "#4f8a6f" : "#fff",
|
||||
border: `2px solid ${selectedSlot === slot ? "var(--color-primary)" : "#d1d5db"}`,
|
||||
background: selectedSlot === slot ? "var(--color-primary)" : "#fff",
|
||||
color: selectedSlot === slot ? "#fff" : "#374151",
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
|
||||
@@ -367,7 +367,7 @@ export function ClientsPage() {
|
||||
<h1 style={{ margin: 0, fontSize: 20 }}>Clients</h1>
|
||||
<button
|
||||
onClick={openNewClient}
|
||||
style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f", marginLeft: "auto", padding: "0.3rem 0.7rem" }}
|
||||
style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)", marginLeft: "auto", padding: "0.3rem 0.7rem" }}
|
||||
>
|
||||
+ New
|
||||
</button>
|
||||
@@ -622,7 +622,7 @@ export function ClientsPage() {
|
||||
</Field>
|
||||
{clientFormError && <p style={{ color: "red", margin: "0.5rem 0 0" }}>{clientFormError}</p>}
|
||||
<div style={{ display: "flex", gap: "0.5rem", marginTop: "1rem" }}>
|
||||
<button type="submit" disabled={savingClient} style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f" }}>
|
||||
<button type="submit" disabled={savingClient} style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)" }}>
|
||||
{savingClient ? "Saving…" : editingClient ? "Save Changes" : "Create Client"}
|
||||
</button>
|
||||
<button type="button" onClick={() => setShowClientForm(false)} style={btnStyle}>Cancel</button>
|
||||
@@ -761,7 +761,7 @@ export function ClientsPage() {
|
||||
</Field>
|
||||
{logFormError && <p style={{ color: "red", margin: "0.5rem 0 0" }}>{logFormError}</p>}
|
||||
<div style={{ display: "flex", gap: "0.5rem", marginTop: "1rem" }}>
|
||||
<button type="submit" disabled={savingLog} style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f" }}>
|
||||
<button type="submit" disabled={savingLog} style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)" }}>
|
||||
{savingLog ? "Saving…" : "Save Visit Log"}
|
||||
</button>
|
||||
<button type="button" onClick={() => setShowLogForm(false)} style={btnStyle}>Cancel</button>
|
||||
|
||||
@@ -287,7 +287,7 @@ function NewGroupBookingForm({
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f" }}
|
||||
style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)" }}
|
||||
>
|
||||
{saving ? "Booking…" : "Create Group Booking"}
|
||||
</button>
|
||||
@@ -471,7 +471,7 @@ export function GroupBookingPage() {
|
||||
</select>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
style={{ ...btnStyle, marginLeft: "auto", backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f" }}
|
||||
style={{ ...btnStyle, marginLeft: "auto", backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)" }}
|
||||
>
|
||||
+ New Group Booking
|
||||
</button>
|
||||
|
||||
@@ -129,7 +129,7 @@ function CreateFromAppointmentForm({
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving || !selectedApptId}
|
||||
style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f" }}
|
||||
style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)" }}
|
||||
>
|
||||
{saving ? "Creating…" : "Create Invoice"}
|
||||
</button>
|
||||
@@ -540,7 +540,7 @@ export function InvoicesPage() {
|
||||
</select>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f", marginLeft: "auto" }}
|
||||
style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)", marginLeft: "auto" }}
|
||||
>
|
||||
+ Create Invoice
|
||||
</button>
|
||||
|
||||
@@ -270,7 +270,7 @@ export function ReportsPage() {
|
||||
<option value="month">Month</option>
|
||||
</select>
|
||||
</label>
|
||||
<button onClick={loadAll} style={{ ...btnStyle, background: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f" }}>
|
||||
<button onClick={loadAll} style={{ ...btnStyle, background: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)" }}>
|
||||
{loading ? "Loading…" : "Refresh"}
|
||||
</button>
|
||||
<div style={{ marginLeft: "auto", display: "flex", gap: "0.5rem" }}>
|
||||
|
||||
@@ -119,7 +119,7 @@ export function ServicesPage() {
|
||||
<h1 style={{ margin: 0 }}>Services</h1>
|
||||
<button
|
||||
onClick={openNew}
|
||||
style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f", marginLeft: "auto" }}
|
||||
style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)", marginLeft: "auto" }}
|
||||
>
|
||||
+ Add Service
|
||||
</button>
|
||||
@@ -232,7 +232,7 @@ export function ServicesPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f" }}
|
||||
style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)" }}
|
||||
>
|
||||
{saving ? "Saving…" : editing ? "Save Changes" : "Create Service"}
|
||||
</button>
|
||||
|
||||
@@ -78,7 +78,7 @@ export function StaffPage() {
|
||||
<div style={{ fontFamily: "system-ui, sans-serif" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem", marginBottom: "1rem" }}>
|
||||
<h1 style={{ margin: 0 }}>Staff</h1>
|
||||
<button onClick={openNew} style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f", marginLeft: "auto" }}>
|
||||
<button onClick={openNew} style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)", marginLeft: "auto" }}>
|
||||
+ Add Staff
|
||||
</button>
|
||||
</div>
|
||||
@@ -145,7 +145,7 @@ export function StaffPage() {
|
||||
</div>
|
||||
{formError && <p style={{ color: "red", margin: "0.5rem 0 0" }}>{formError}</p>}
|
||||
<div style={{ display: "flex", gap: "0.5rem", marginTop: "1rem" }}>
|
||||
<button type="submit" disabled={saving} style={{ ...btnStyle, backgroundColor: "#4f8a6f", color: "#fff", borderColor: "#4f8a6f" }}>
|
||||
<button type="submit" disabled={saving} style={{ ...btnStyle, backgroundColor: "var(--color-primary)", color: "#fff", borderColor: "var(--color-primary)" }}>
|
||||
{saving ? "Saving…" : editing ? "Save Changes" : "Add Staff"}
|
||||
</button>
|
||||
<button type="button" onClick={() => setShowForm(false)} style={btnStyle}>Cancel</button>
|
||||
|
||||
@@ -22,7 +22,7 @@ export function AccountSettings({ readOnly }: Props) {
|
||||
key={id}
|
||||
onClick={() => setTab(id)}
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium ${
|
||||
tab === id ? "bg-[#f0ebe4] text-[#6b5a42]" : "text-stone-500 hover:bg-stone-50"
|
||||
tab === id ? "bg-(--color-accent-light) text-(--color-accent-dark)" : "text-stone-500 hover:bg-stone-50"
|
||||
}`}
|
||||
>
|
||||
<Icon size={14} />
|
||||
@@ -69,7 +69,7 @@ function PersonalInfo({ readOnly }: { readOnly: boolean }) {
|
||||
</div>
|
||||
))}
|
||||
{!readOnly && (
|
||||
<button className="px-4 py-2 bg-[#8b7355] text-white rounded-lg text-sm font-medium hover:bg-[#7a6549]">
|
||||
<button className="px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium hover:bg-(--color-accent-hover)">
|
||||
Save Changes
|
||||
</button>
|
||||
)}
|
||||
@@ -103,7 +103,7 @@ function PasswordChange({ readOnly }: { readOnly: boolean }) {
|
||||
<label className="block text-sm font-medium text-stone-700 mb-1">Confirm New Password</label>
|
||||
<input type="password" className="w-full border border-stone-300 rounded-lg px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-[#8b7355] text-white rounded-lg text-sm font-medium hover:bg-[#7a6549]">
|
||||
<button className="px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium hover:bg-(--color-accent-hover)">
|
||||
Update Password
|
||||
</button>
|
||||
</div>
|
||||
@@ -116,7 +116,7 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
|
||||
<div className="space-y-4">
|
||||
{PETS.map(pet => (
|
||||
<div key={pet.id} className="bg-white rounded-2xl border border-stone-200 p-4 shadow-sm flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-xl bg-[#f0ebe4] flex items-center justify-center text-3xl">
|
||||
<div className="w-14 h-14 rounded-xl bg-(--color-accent-light) flex items-center justify-center text-3xl">
|
||||
{pet.photo}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
@@ -136,7 +136,7 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
|
||||
</div>
|
||||
))}
|
||||
{!readOnly && (
|
||||
<button className="w-full flex items-center justify-center gap-2 py-3 border-2 border-dashed border-stone-300 rounded-2xl text-sm text-stone-500 hover:border-[#8b7355] hover:text-[#6b5a42] transition-colors">
|
||||
<button className="w-full flex items-center justify-center gap-2 py-3 border-2 border-dashed border-stone-300 rounded-2xl text-sm text-stone-500 hover:border-(--color-accent) hover:text-(--color-accent-dark) transition-colors">
|
||||
<Plus size={16} />
|
||||
Add New Pet
|
||||
</button>
|
||||
@@ -165,7 +165,7 @@ function Agreements() {
|
||||
{new Date(agr.dateSigned).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||
</td>
|
||||
<td className="px-5 py-3">
|
||||
<button className="text-sm text-[#6b5a42] font-medium hover:underline">View</button>
|
||||
<button className="text-sm text-(--color-accent-dark) font-medium hover:underline">View</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -30,13 +30,13 @@ export function AppointmentsSection({ readOnly }: Props) {
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setTab("upcoming")}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium ${tab === "upcoming" ? "bg-[#f0ebe4] text-[#6b5a42]" : "text-stone-500 hover:bg-stone-50"}`}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium ${tab === "upcoming" ? "bg-(--color-accent-light) text-(--color-accent-dark)" : "text-stone-500 hover:bg-stone-50"}`}
|
||||
>
|
||||
Upcoming ({UPCOMING_APPOINTMENTS.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab("past")}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium ${tab === "past" ? "bg-[#f0ebe4] text-[#6b5a42]" : "text-stone-500 hover:bg-stone-50"}`}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium ${tab === "past" ? "bg-(--color-accent-light) text-(--color-accent-dark)" : "text-stone-500 hover:bg-stone-50"}`}
|
||||
>
|
||||
Past ({PAST_APPOINTMENTS.length})
|
||||
</button>
|
||||
@@ -44,7 +44,7 @@ export function AppointmentsSection({ readOnly }: Props) {
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={() => setShowBooking(true)}
|
||||
className="flex items-center gap-1.5 px-4 py-2 bg-[#8b7355] text-white rounded-lg text-sm font-medium hover:bg-[#7a6549]"
|
||||
className="flex items-center gap-1.5 px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium hover:bg-(--color-accent-hover)"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Book New
|
||||
@@ -101,7 +101,7 @@ function AppointmentCard({
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-stone-200 shadow-sm overflow-hidden">
|
||||
<button onClick={onToggle} className="w-full flex items-center gap-4 p-4 text-left hover:bg-stone-50">
|
||||
<div className="w-10 h-10 rounded-lg bg-[#f0ebe4] flex items-center justify-center text-lg shrink-0">
|
||||
<div className="w-10 h-10 rounded-lg bg-(--color-accent-light) flex items-center justify-center text-lg shrink-0">
|
||||
{PETS.find(p => p.id === appt.petId)?.photo || "🐾"}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -150,7 +150,7 @@ function AppointmentCard({
|
||||
)}
|
||||
{appt.reportCardId && (
|
||||
<div className="mt-2">
|
||||
<span className="text-xs text-[#6b5a42] font-medium cursor-pointer hover:underline">
|
||||
<span className="text-xs text-(--color-accent-dark) font-medium cursor-pointer hover:underline">
|
||||
View Report Card →
|
||||
</span>
|
||||
</div>
|
||||
@@ -190,7 +190,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo
|
||||
{/* Step Indicator */}
|
||||
<div className="flex items-center gap-1 px-5 pt-4">
|
||||
{[1, 2, 3, 4, 5].map(s => (
|
||||
<div key={s} className={`flex-1 h-1.5 rounded-full ${s <= step ? "bg-[#8b7355]" : "bg-stone-200"}`} />
|
||||
<div key={s} className={`flex-1 h-1.5 rounded-full ${s <= step ? "bg-(--color-accent)" : "bg-stone-200"}`} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -202,7 +202,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo
|
||||
<p className="text-sm text-stone-500 mb-4">
|
||||
{selectedPet?.name} with {selectedGroomer?.name || "First Available"} on {formatDate(selectedDate)} at {selectedTime}
|
||||
</p>
|
||||
<button onClick={onClose} className="px-4 py-2 bg-[#8b7355] text-white rounded-lg text-sm font-medium">
|
||||
<button onClick={onClose} className="px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium">
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
@@ -218,7 +218,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo
|
||||
key={pet.id}
|
||||
onClick={() => { setSelectedPet(pet); setStep(2); }}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-xl border text-left transition-colors ${
|
||||
selectedPet?.id === pet.id ? "border-[#8b7355] bg-[#faf5ef]" : "border-stone-200 hover:border-stone-300"
|
||||
selectedPet?.id === pet.id ? "border-(--color-accent) bg-(--color-accent-lighter)" : "border-stone-200 hover:border-stone-300"
|
||||
}`}
|
||||
>
|
||||
<span className="text-2xl">{pet.photo}</span>
|
||||
@@ -246,7 +246,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo
|
||||
);
|
||||
}}
|
||||
className={`w-full flex items-center justify-between p-3 rounded-xl border text-left ${
|
||||
selectedServices.find(s => s.id === svc.id) ? "border-[#8b7355] bg-[#faf5ef]" : "border-stone-200 hover:border-stone-300"
|
||||
selectedServices.find(s => s.id === svc.id) ? "border-(--color-accent) bg-(--color-accent-lighter)" : "border-stone-200 hover:border-stone-300"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
@@ -273,7 +273,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo
|
||||
);
|
||||
}}
|
||||
className={`w-full flex items-center justify-between p-2.5 rounded-lg border text-left text-sm ${
|
||||
selectedAddOns.find(s => s.id === svc.id) ? "border-[#8b7355] bg-[#faf5ef]" : "border-stone-200 hover:border-stone-300"
|
||||
selectedAddOns.find(s => s.id === svc.id) ? "border-(--color-accent) bg-(--color-accent-lighter)" : "border-stone-200 hover:border-stone-300"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
@@ -291,7 +291,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo
|
||||
<button
|
||||
onClick={() => setStep(3)}
|
||||
disabled={selectedServices.length === 0}
|
||||
className="flex-1 px-4 py-2 bg-[#8b7355] text-white rounded-lg text-sm font-medium disabled:opacity-50"
|
||||
className="flex-1 px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium disabled:opacity-50"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
@@ -307,7 +307,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo
|
||||
<button
|
||||
onClick={() => { setSelectedGroomer(null); setStep(4); }}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-xl border text-left ${
|
||||
selectedGroomer === null ? "border-[#8b7355] bg-[#faf5ef]" : "border-stone-200 hover:border-stone-300"
|
||||
selectedGroomer === null ? "border-(--color-accent) bg-(--color-accent-lighter)" : "border-stone-200 hover:border-stone-300"
|
||||
}`}
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-stone-100 flex items-center justify-center">
|
||||
@@ -323,10 +323,10 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo
|
||||
key={g.id}
|
||||
onClick={() => { setSelectedGroomer(g); setStep(4); }}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-xl border text-left ${
|
||||
selectedGroomer?.id === g.id ? "border-[#8b7355] bg-[#faf5ef]" : "border-stone-200 hover:border-stone-300"
|
||||
selectedGroomer?.id === g.id ? "border-(--color-accent) bg-(--color-accent-lighter)" : "border-stone-200 hover:border-stone-300"
|
||||
}`}
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-[#f0ebe4] flex items-center justify-center text-xl">
|
||||
<div className="w-10 h-10 rounded-full bg-(--color-accent-light) flex items-center justify-center text-xl">
|
||||
{g.avatar}
|
||||
</div>
|
||||
<div>
|
||||
@@ -358,7 +358,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo
|
||||
key={time}
|
||||
onClick={() => setSelectedTime(time)}
|
||||
className={`px-3 py-2 rounded-lg text-sm border ${
|
||||
selectedTime === time ? "border-[#8b7355] bg-[#faf5ef] font-medium" : "border-stone-200 hover:border-stone-300"
|
||||
selectedTime === time ? "border-(--color-accent) bg-(--color-accent-lighter) font-medium" : "border-stone-200 hover:border-stone-300"
|
||||
}`}
|
||||
>
|
||||
{time}
|
||||
@@ -387,7 +387,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo
|
||||
<button
|
||||
onClick={() => setStep(5)}
|
||||
disabled={!selectedDate || !selectedTime}
|
||||
className="flex-1 px-4 py-2 bg-[#8b7355] text-white rounded-lg text-sm font-medium disabled:opacity-50"
|
||||
className="flex-1 px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium disabled:opacity-50"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
@@ -426,7 +426,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo
|
||||
<button onClick={() => setStep(4)} className="flex-1 px-4 py-2 border border-stone-200 rounded-lg text-sm">Back</button>
|
||||
<button
|
||||
onClick={() => setConfirmed(true)}
|
||||
className="flex-1 px-4 py-2 bg-[#8b7355] text-white rounded-lg text-sm font-medium hover:bg-[#7a6549]"
|
||||
className="flex-1 px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium hover:bg-(--color-accent-hover)"
|
||||
>
|
||||
Confirm Booking
|
||||
</button>
|
||||
|
||||
@@ -38,7 +38,7 @@ export function BillingPayments({ readOnly }: Props) {
|
||||
>
|
||||
Add Tip
|
||||
</button>
|
||||
<button className="px-6 py-2 bg-[#8b7355] text-white rounded-lg text-sm font-medium hover:bg-[#7a6549]">
|
||||
<button className="px-6 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium hover:bg-(--color-accent-hover)">
|
||||
Pay Now
|
||||
</button>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ export function BillingPayments({ readOnly }: Props) {
|
||||
key={id}
|
||||
onClick={() => setTab(id)}
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium ${
|
||||
tab === id ? "bg-[#f0ebe4] text-[#6b5a42]" : "text-stone-500 hover:bg-stone-50"
|
||||
tab === id ? "bg-(--color-accent-light) text-(--color-accent-dark)" : "text-stone-500 hover:bg-stone-50"
|
||||
}`}
|
||||
>
|
||||
<Icon size={14} />
|
||||
@@ -134,7 +134,7 @@ export function BillingPayments({ readOnly }: Props) {
|
||||
</div>
|
||||
))}
|
||||
{!readOnly && (
|
||||
<button className="flex items-center gap-2 text-sm text-[#6b5a42] font-medium hover:underline mt-2">
|
||||
<button className="flex items-center gap-2 text-sm text-(--color-accent-dark) font-medium hover:underline mt-2">
|
||||
<Plus size={14} />
|
||||
Add Payment Method
|
||||
</button>
|
||||
@@ -145,8 +145,8 @@ export function BillingPayments({ readOnly }: Props) {
|
||||
<div className="bg-white rounded-2xl border border-stone-200 p-5 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-[#f0ebe4] flex items-center justify-center">
|
||||
<Zap size={18} className="text-[#8b7355]" />
|
||||
<div className="w-10 h-10 rounded-lg bg-(--color-accent-light) flex items-center justify-center">
|
||||
<Zap size={18} className="text-(--color-accent)" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-800">Autopay</p>
|
||||
@@ -156,7 +156,7 @@ export function BillingPayments({ readOnly }: Props) {
|
||||
{!readOnly ? (
|
||||
<button
|
||||
onClick={() => setAutopay(!autopay)}
|
||||
className={`w-12 h-6 rounded-full transition-colors ${autopay ? "bg-[#8b7355]" : "bg-stone-300"}`}
|
||||
className={`w-12 h-6 rounded-full transition-colors ${autopay ? "bg-(--color-accent)" : "bg-stone-300"}`}
|
||||
>
|
||||
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ${autopay ? "translate-x-6" : "translate-x-0.5"}`} />
|
||||
</button>
|
||||
@@ -174,7 +174,7 @@ export function BillingPayments({ readOnly }: Props) {
|
||||
{PREPAID_PACKAGES.map(pkg => (
|
||||
<div key={pkg.id} className="bg-white rounded-2xl border border-stone-200 p-5 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Package size={20} className="text-[#8b7355]" />
|
||||
<Package size={20} className="text-(--color-accent)" />
|
||||
<h3 className="font-medium text-stone-800">{pkg.name}</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mb-3">
|
||||
@@ -184,7 +184,7 @@ export function BillingPayments({ readOnly }: Props) {
|
||||
</div>
|
||||
<div className="flex-1 bg-stone-100 rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
className="bg-[#8b7355] h-full rounded-full"
|
||||
className="bg-(--color-accent) h-full rounded-full"
|
||||
style={{ width: `${((pkg.totalCredits - pkg.usedCredits) / pkg.totalCredits) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -218,7 +218,7 @@ function TipModal({ onClose }: { onClose: () => void }) {
|
||||
key={pct}
|
||||
onClick={() => { setTipPercent(pct); setCustomTip(""); }}
|
||||
className={`flex-1 py-2 rounded-lg text-sm font-medium border ${
|
||||
tipPercent === pct ? "border-[#8b7355] bg-[#faf5ef] text-[#6b5a42]" : "border-stone-200 text-stone-600"
|
||||
tipPercent === pct ? "border-(--color-accent) bg-(--color-accent-lighter) text-(--color-accent-dark)" : "border-stone-200 text-stone-600"
|
||||
}`}
|
||||
>
|
||||
{pct}%
|
||||
@@ -227,7 +227,7 @@ function TipModal({ onClose }: { onClose: () => void }) {
|
||||
<button
|
||||
onClick={() => { setTipPercent(null); }}
|
||||
className={`flex-1 py-2 rounded-lg text-sm font-medium border ${
|
||||
tipPercent === null ? "border-[#8b7355] bg-[#faf5ef] text-[#6b5a42]" : "border-stone-200 text-stone-600"
|
||||
tipPercent === null ? "border-(--color-accent) bg-(--color-accent-lighter) text-(--color-accent-dark)" : "border-stone-200 text-stone-600"
|
||||
}`}
|
||||
>
|
||||
Custom
|
||||
@@ -244,7 +244,7 @@ function TipModal({ onClose }: { onClose: () => void }) {
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<button onClick={onClose} className="flex-1 px-4 py-2 border border-stone-200 rounded-lg text-sm">Cancel</button>
|
||||
<button onClick={onClose} className="flex-1 px-4 py-2 bg-[#8b7355] text-white rounded-lg text-sm font-medium">Add Tip</button>
|
||||
<button onClick={onClose} className="flex-1 px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium">Add Tip</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function Communication({ readOnly }: Props) {
|
||||
<button
|
||||
onClick={() => setTab("messages")}
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium ${
|
||||
tab === "messages" ? "bg-[#f0ebe4] text-[#6b5a42]" : "text-stone-500 hover:bg-stone-50"
|
||||
tab === "messages" ? "bg-(--color-accent-light) text-(--color-accent-dark)" : "text-stone-500 hover:bg-stone-50"
|
||||
}`}
|
||||
>
|
||||
Messages
|
||||
@@ -24,7 +24,7 @@ export function Communication({ readOnly }: Props) {
|
||||
<button
|
||||
onClick={() => setTab("notifications")}
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium ${
|
||||
tab === "notifications" ? "bg-[#f0ebe4] text-[#6b5a42]" : "text-stone-500 hover:bg-stone-50"
|
||||
tab === "notifications" ? "bg-(--color-accent-light) text-(--color-accent-dark)" : "text-stone-500 hover:bg-stone-50"
|
||||
}`}
|
||||
>
|
||||
<Bell size={14} />
|
||||
@@ -68,7 +68,7 @@ function MessageThread({ readOnly }: { readOnly: boolean }) {
|
||||
<div key={msg.id} className={`flex ${msg.sender === "customer" ? "justify-end" : "justify-start"}`}>
|
||||
<div className={`max-w-[80%] rounded-2xl px-4 py-2.5 ${
|
||||
msg.sender === "customer"
|
||||
? "bg-[#8b7355] text-white rounded-br-md"
|
||||
? "bg-(--color-accent) text-white rounded-br-md"
|
||||
: "bg-stone-100 text-stone-800 rounded-bl-md"
|
||||
}`}>
|
||||
<p className="text-sm">{msg.text}</p>
|
||||
@@ -95,12 +95,12 @@ function MessageThread({ readOnly }: { readOnly: boolean }) {
|
||||
onChange={e => setNewMessage(e.target.value)}
|
||||
onKeyDown={e => e.key === "Enter" && handleSend()}
|
||||
placeholder="Type a message..."
|
||||
className="flex-1 border border-stone-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b7355]/30 focus:border-[#8b7355]"
|
||||
className="flex-1 border border-stone-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-(--color-accent)/30 focus:border-(--color-accent)"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!newMessage.trim()}
|
||||
className="px-4 py-2 bg-[#8b7355] text-white rounded-lg hover:bg-[#7a6549] disabled:opacity-50"
|
||||
className="px-4 py-2 bg-(--color-accent) text-white rounded-lg hover:bg-(--color-accent-hover) disabled:opacity-50"
|
||||
>
|
||||
<Send size={16} />
|
||||
</button>
|
||||
@@ -177,7 +177,7 @@ function NotificationPreferences({ readOnly }: { readOnly: boolean }) {
|
||||
onClick={() => toggle(cat.key, ch.key)}
|
||||
disabled={readOnly}
|
||||
className={`w-10 h-5 rounded-full transition-colors inline-block ${
|
||||
prefs[cat.key][ch.key] ? "bg-[#8b7355]" : "bg-stone-300"
|
||||
prefs[cat.key][ch.key] ? "bg-(--color-accent)" : "bg-stone-300"
|
||||
} ${readOnly ? "cursor-not-allowed opacity-60" : ""}`}
|
||||
>
|
||||
<div className={`w-4 h-4 bg-white rounded-full shadow transition-transform ${
|
||||
|
||||
@@ -42,7 +42,7 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
|
||||
{nextAppt && (
|
||||
<div className="bg-white rounded-2xl border border-stone-200 p-5 shadow-sm">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-[#6b5a42]">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-(--color-accent-dark)">
|
||||
<Calendar size={16} />
|
||||
Next Appointment
|
||||
</div>
|
||||
@@ -71,7 +71,7 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center sm:text-right">
|
||||
<div className="text-3xl font-bold text-[#6b5a42]">{daysUntil(nextAppt.date)}</div>
|
||||
<div className="text-3xl font-bold text-(--color-accent-dark)">{daysUntil(nextAppt.date)}</div>
|
||||
<div className="text-xs text-stone-500">days away</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
|
||||
className="bg-white rounded-2xl border border-stone-200 p-4 shadow-sm text-left hover:border-stone-300 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-12 h-12 rounded-full bg-[#f0ebe4] flex items-center justify-center text-2xl">
|
||||
<div className="w-12 h-12 rounded-full bg-(--color-accent-light) flex items-center justify-center text-2xl">
|
||||
{pet.photo}
|
||||
</div>
|
||||
<div>
|
||||
@@ -128,14 +128,14 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
|
||||
|
||||
{/* Loyalty Card */}
|
||||
<div className="bg-white rounded-2xl border border-stone-200 p-4 shadow-sm">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-[#6b5a42] mb-3">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-(--color-accent-dark) mb-3">
|
||||
<Star size={16} />
|
||||
Loyalty Rewards
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-stone-800">{LOYALTY.points} <span className="text-sm font-normal text-stone-500">pts</span></p>
|
||||
<div className="mt-2 bg-stone-100 rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className="bg-[#8b7355] h-full rounded-full transition-all"
|
||||
className="bg-(--color-accent) h-full rounded-full transition-all"
|
||||
style={{ width: `${(LOYALTY.points / LOYALTY.nextRewardAt) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -161,7 +161,7 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={() => onNavigate("billing")}
|
||||
className="px-4 py-2 bg-[#8b7355] text-white rounded-lg text-sm font-medium hover:bg-[#7a6549]"
|
||||
className="px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium hover:bg-(--color-accent-hover)"
|
||||
>
|
||||
Pay Now
|
||||
</button>
|
||||
@@ -176,7 +176,7 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
|
||||
<div className="space-y-2.5">
|
||||
{recentEvents.map(evt => (
|
||||
<div key={evt.id} className="flex items-center gap-3 text-sm">
|
||||
<div className={`w-2 h-2 rounded-full shrink-0 ${evt.type === "payment" ? "bg-green-400" : "bg-[#8b7355]"}`} />
|
||||
<div className={`w-2 h-2 rounded-full shrink-0 ${evt.type === "payment" ? "bg-green-400" : "bg-(--color-accent)"}`} />
|
||||
<span className="text-stone-600 flex-1">{evt.text}</span>
|
||||
<span className="text-xs text-stone-400">{formatDate(evt.date)}</span>
|
||||
</div>
|
||||
@@ -184,7 +184,7 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onNavigate("appointments")}
|
||||
className="flex items-center gap-1 text-sm text-[#6b5a42] font-medium mt-3 hover:text-[#8b7355]"
|
||||
className="flex items-center gap-1 text-sm text-(--color-accent-dark) font-medium mt-3 hover:text-(--color-accent)"
|
||||
>
|
||||
View all <ChevronRight size={14} />
|
||||
</button>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function PetProfiles({ readOnly }: Props) {
|
||||
key={p.id}
|
||||
onClick={() => { setSelectedPetId(p.id); setActiveTab("info"); }}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-xl border transition-colors ${
|
||||
p.id === selectedPetId ? "border-[#8b7355] bg-[#faf5ef]" : "border-stone-200 bg-white hover:border-stone-300"
|
||||
p.id === selectedPetId ? "border-(--color-accent) bg-(--color-accent-lighter)" : "border-stone-200 bg-white hover:border-stone-300"
|
||||
}`}
|
||||
>
|
||||
<span className="text-2xl">{p.photo}</span>
|
||||
@@ -45,7 +45,7 @@ export function PetProfiles({ readOnly }: Props) {
|
||||
{/* Profile Header */}
|
||||
<div className="bg-white rounded-2xl border border-stone-200 p-5 shadow-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-20 h-20 rounded-2xl bg-[#f0ebe4] flex items-center justify-center text-4xl">
|
||||
<div className="w-20 h-20 rounded-2xl bg-(--color-accent-light) flex items-center justify-center text-4xl">
|
||||
{pet.photo}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
@@ -74,7 +74,7 @@ export function PetProfiles({ readOnly }: Props) {
|
||||
key={id}
|
||||
onClick={() => setActiveTab(id)}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap ${
|
||||
activeTab === id ? "bg-[#f0ebe4] text-[#6b5a42]" : "text-stone-500 hover:text-stone-700"
|
||||
activeTab === id ? "bg-(--color-accent-light) text-(--color-accent-dark)" : "text-stone-500 hover:text-stone-700"
|
||||
}`}
|
||||
>
|
||||
<Icon size={14} />
|
||||
@@ -114,7 +114,7 @@ function BasicInfoTab({ pet, readOnly }: { pet: Pet; readOnly: boolean }) {
|
||||
<InfoRow label="Sex" value={pet.sex === "male" ? "Male" : "Female"} />
|
||||
<InfoRow label="Spayed/Neutered" value={pet.spayedNeutered ? "Yes" : "No"} />
|
||||
{!readOnly && (
|
||||
<button className="mt-4 text-sm text-[#6b5a42] font-medium hover:underline">
|
||||
<button className="mt-4 text-sm text-(--color-accent-dark) font-medium hover:underline">
|
||||
Upload Photo
|
||||
</button>
|
||||
)}
|
||||
@@ -148,7 +148,7 @@ function GroomingTab({ pet, readOnly }: { pet: Pet; readOnly: boolean }) {
|
||||
<InfoRow label="Sensitive Areas" value={pet.sensitiveAreas} />
|
||||
<InfoRow label="Standing Instructions" value={pet.standingInstructions} />
|
||||
{!readOnly && (
|
||||
<button className="mt-4 text-sm text-[#6b5a42] font-medium hover:underline">
|
||||
<button className="mt-4 text-sm text-(--color-accent-dark) font-medium hover:underline">
|
||||
Upload Reference Photo
|
||||
</button>
|
||||
)}
|
||||
@@ -189,7 +189,7 @@ function VaccinationsTab({ pet, readOnly }: { pet: Pet; readOnly: boolean }) {
|
||||
{vax.documentUploaded ? (
|
||||
<span className="text-green-600 text-xs">Uploaded</span>
|
||||
) : !readOnly ? (
|
||||
<button className="flex items-center gap-1 text-xs text-[#6b5a42] hover:underline">
|
||||
<button className="flex items-center gap-1 text-xs text-(--color-accent-dark) hover:underline">
|
||||
<Upload size={12} />
|
||||
Upload
|
||||
</button>
|
||||
@@ -226,7 +226,7 @@ function HistoryTab({ petHistory }: { petHistory: typeof PAST_APPOINTMENTS }) {
|
||||
{new Date(appt.date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||
</span>
|
||||
{appt.reportCardId && (
|
||||
<span className="text-xs text-[#6b5a42] font-medium">Report →</span>
|
||||
<span className="text-xs text-(--color-accent-dark) font-medium">Report →</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -33,7 +33,7 @@ export function ReportCards() {
|
||||
className="w-full bg-white rounded-2xl border border-stone-200 p-5 shadow-sm text-left hover:border-stone-300 transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-14 h-14 rounded-xl bg-[#f0ebe4] flex items-center justify-center text-[#8b7355]">
|
||||
<div className="w-14 h-14 rounded-xl bg-(--color-accent-light) flex items-center justify-center text-(--color-accent)">
|
||||
<FileText size={24} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
@@ -70,13 +70,13 @@ function ReportCardDetail({ card, onBack }: { card: ReportCard; onBack: () => vo
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<button onClick={onBack} className="text-sm text-[#6b5a42] font-medium hover:underline">
|
||||
<button onClick={onBack} className="text-sm text-(--color-accent-dark) font-medium hover:underline">
|
||||
← Back to Report Cards
|
||||
</button>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-stone-200 shadow-sm overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-[#f0ebe4] to-[#e8e0d5] p-6">
|
||||
<div className="bg-gradient-to-r from-(--color-accent-lighter) to-(--color-accent-light) p-6">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h2 className="text-xl font-semibold text-stone-800">{card.petName}'s Grooming Report</h2>
|
||||
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-white/80 text-stone-700 rounded-lg text-sm font-medium hover:bg-white">
|
||||
@@ -101,9 +101,9 @@ function ReportCardDetail({ card, onBack }: { card: ReportCard; onBack: () => vo
|
||||
</div>
|
||||
<p className="text-sm text-stone-600">{card.beforeDescription}</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-[#faf5ef] p-4">
|
||||
<p className="text-xs font-medium text-[#8b7355] uppercase mb-2">After</p>
|
||||
<div className="w-full h-32 bg-[#f0ebe4] rounded-lg flex items-center justify-center text-[#8b7355] text-sm mb-2">
|
||||
<div className="rounded-xl bg-(--color-accent-lighter) p-4">
|
||||
<p className="text-xs font-medium text-(--color-accent) uppercase mb-2">After</p>
|
||||
<div className="w-full h-32 bg-(--color-accent-light) rounded-lg flex items-center justify-center text-(--color-accent) text-sm mb-2">
|
||||
Photo placeholder
|
||||
</div>
|
||||
<p className="text-sm text-stone-700">{card.afterDescription}</p>
|
||||
@@ -148,7 +148,7 @@ function ReportCardDetail({ card, onBack }: { card: ReportCard; onBack: () => vo
|
||||
)}
|
||||
|
||||
{/* Groomer's Note */}
|
||||
<div className="bg-[#faf5ef] rounded-xl p-4">
|
||||
<div className="bg-(--color-accent-lighter) rounded-xl p-4">
|
||||
<h3 className="font-medium text-stone-800 mb-2">A Note from {card.groomerName}</h3>
|
||||
<p className="text-sm text-stone-700 italic leading-relaxed">"{card.groomerNote}"</p>
|
||||
</div>
|
||||
@@ -161,7 +161,7 @@ function ReportCardDetail({ card, onBack }: { card: ReportCard; onBack: () => vo
|
||||
{new Date(card.nextRecommendedDate).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}
|
||||
</p>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-[#8b7355] text-white rounded-lg text-sm font-medium hover:bg-[#7a6549]">
|
||||
<button className="px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium hover:bg-(--color-accent-hover)">
|
||||
Rebook Now
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user