Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { useState } from "react";
import { AuthProvider, useAuth, type AuthUser } from "./state/AuthContext";
import { ShopProvider } from "./state/ShopContext";
import { ThemeProvider } from "./state/ThemeContext";
import { ToastProvider } from "./state/ToastContext";
import { CounterPage } from "./pages/CounterPage";
import { HeroPage } from "./pages/HeroPage";
import { ProductsPage } from "./pages/ProductsPage";
import { CheckoutPage } from "./pages/CheckoutPage";
import { StaffPage } from "./pages/StaffPage";
import { DashboardPage } from "./pages/DashboardPage";
import { CounterPicker } from "./components/CounterPicker";
import { ThemeToggle } from "./components/ThemeToggle";
import { ToastContainer } from "./components/ToastContainer";

function useQueryParam(name: string, fallback: string): string {
return new URLSearchParams(window.location.search).get(name) ?? fallback;
Expand Down Expand Up @@ -65,7 +69,11 @@ function AuthedApp({ shopId, role }: AuthedAppProps) {
<a
key={link.page}
href={`?page=${link.page}&counter_id=${counterId}`}
className={page === link.page ? "font-semibold text-slate-900" : "text-slate-500 hover:text-slate-700"}
className={
page === link.page
? "font-semibold text-slate-900 dark:text-slate-100"
: "text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
}
>
{link.label}
</a>
Expand All @@ -84,9 +92,10 @@ function TopBar({ user, onSignOut }: { user: AuthUser; onSignOut: () => void })
return (
<div className="mx-auto flex max-w-3xl items-center justify-end gap-3 px-4 pt-3 text-xs text-slate-400 sm:px-6">
<span>{user.email}</span>
<button type="button" onClick={onSignOut} className="underline decoration-dotted hover:text-slate-600">
<button type="button" onClick={onSignOut} className="underline decoration-dotted hover:text-slate-600 dark:hover:text-slate-200">
Sign out
</button>
<ThemeToggle />
</div>
);
}
Expand All @@ -95,7 +104,9 @@ function AppShell() {
const { status, user, signOut } = useAuth();

if (status === "loading") {
return <div className="flex min-h-screen items-center justify-center text-sm text-slate-500">Loading…</div>;
return (
<div className="flex min-h-screen items-center justify-center text-sm text-slate-500 dark:text-slate-400">Loading…</div>
);
}

if (status === "signed_out" || !user?.shopId) {
Expand All @@ -112,8 +123,13 @@ function AppShell() {

export function App() {
return (
<AuthProvider>
<AppShell />
</AuthProvider>
<ThemeProvider>
<ToastProvider>
<AuthProvider>
<AppShell />
</AuthProvider>
<ToastContainer />
</ToastProvider>
</ThemeProvider>
);
}
10 changes: 5 additions & 5 deletions apps/web/src/components/AttributionBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const COLORS = [
"bg-sky-100 text-sky-800",
"bg-emerald-100 text-emerald-800",
"bg-amber-100 text-amber-800",
"bg-violet-100 text-violet-800",
"bg-rose-100 text-rose-800",
"bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300",
"bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300",
"bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300",
"bg-violet-100 text-violet-800 dark:bg-violet-950 dark:text-violet-300",
"bg-rose-100 text-rose-800 dark:bg-rose-950 dark:text-rose-300",
];

/** Deterministic so the same counter always renders the same color, without a shared color registry. */
Expand Down
33 changes: 21 additions & 12 deletions apps/web/src/components/AuditLogView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import type { AuditHistoryEntry } from "@stocksync/core";
import { getAudit } from "../api/client";
import { VectorClockExplainer } from "./VectorClockExplainer";
import { Skeleton } from "./Skeleton";
import { AlertTriangleIcon } from "./icons";

export interface AuditLogViewProps {
Expand Down Expand Up @@ -38,32 +39,40 @@ export function AuditLogView({ itemId, shopId }: AuditLogViewProps) {

if (error)
return (
<p className="flex items-center gap-1.5 text-sm text-rose-600">
<p className="flex items-center gap-1.5 text-sm text-rose-600 dark:text-rose-400">
<AlertTriangleIcon className="h-3.5 w-3.5 shrink-0" />
Couldn't load history for {itemId}.
</p>
);
if (!history)
return (
<p className="flex items-center gap-2 text-sm text-slate-400">
<span className="h-3 w-3 animate-spin rounded-full border-2 border-slate-300 border-t-indigo-500" />
Loading history…
</p>
<div className="space-y-2 border-l-2 border-slate-200 pl-4 dark:border-slate-800">
{[0, 1, 2].map((i) => (
<div key={i} className="space-y-1.5">
<Skeleton className="h-3.5 w-1/3" />
<Skeleton className="h-3 w-2/3" />
</div>
))}
</div>
);
if (history.length === 0) return <p className="text-sm text-slate-400">No history yet for {itemId}.</p>;
if (history.length === 0) return <p className="text-sm text-slate-400 dark:text-slate-500">No history yet for {itemId}.</p>;

return (
<ol className="space-y-2 border-l-2 border-slate-200 pl-4">
<ol className="space-y-2 border-l-2 border-slate-200 pl-4 dark:border-slate-800">
{history.map((entry, index) => (
<li key={`${entry.timestamp}-${index}`} className="text-sm">
<div className="flex items-baseline gap-2">
<span className="font-medium text-slate-900">{entry.action}</span>
<span className="rounded-full bg-slate-100 px-1.5 py-0.5 text-xs text-slate-500">{entry.counter_id}</span>
<span className="text-xs text-slate-400">{new Date(entry.timestamp).toLocaleTimeString()}</span>
<span className="font-medium text-slate-900 dark:text-slate-100">{entry.action}</span>
<span className="rounded-full bg-slate-100 px-1.5 py-0.5 text-xs text-slate-500 dark:bg-slate-800 dark:text-slate-400">
{entry.counter_id}
</span>
<span className="text-xs text-slate-400 dark:text-slate-500">{new Date(entry.timestamp).toLocaleTimeString()}</span>
</div>
<p className="text-slate-600">{formatDetails(entry.details)}</p>
<p className="text-slate-600 dark:text-slate-400">{formatDetails(entry.details)}</p>
<details>
<summary className="cursor-pointer text-xs text-slate-400 hover:text-slate-600">Why?</summary>
<summary className="cursor-pointer text-xs text-slate-400 hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300">
Why?
</summary>
<VectorClockExplainer entry={entry} />
</details>
</li>
Expand Down
42 changes: 24 additions & 18 deletions apps/web/src/components/AuthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { ArrowRightIcon } from "./icons";

type Mode = "login" | "signup" | "confirm" | "new_password";

const INPUT_CLASS = "w-full rounded-md border border-slate-300 px-3 py-2 text-sm";
const INPUT_CLASS =
"w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:placeholder:text-slate-500";
const SUBMIT_CLASS =
"flex w-full items-center justify-center gap-1.5 rounded-md bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-indigo-700 disabled:opacity-50";
const LINK_CLASS = "mt-4 text-sm text-slate-500 underline decoration-dotted hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200";

/**
* The app's real front door for identity — replaces the old shop_id/
Expand Down Expand Up @@ -89,12 +91,16 @@ export function AuthPanel() {
}

return (
<div id="try-it" className="mx-auto max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
<div id="try-it" className="mx-auto max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-8">
{mode === "login" && (
<>
<h2 className="text-xl font-bold text-slate-900">Sign in</h2>
<p className="mt-1 text-sm text-slate-600">Enter your shop's account to get to your counter.</p>
{info && <p className="mt-3 rounded-md bg-emerald-50 px-3 py-2 text-xs text-emerald-700">{info}</p>}
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">Sign in</h2>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">Enter your shop's account to get to your counter.</p>
{info && (
<p className="mt-3 rounded-md bg-emerald-50 px-3 py-2 text-xs text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300">
{info}
</p>
)}
<form className="mt-4 flex flex-col gap-3" onSubmit={handleLogin}>
<input
type="email"
Expand All @@ -114,7 +120,7 @@ export function AuthPanel() {
aria-label="Password"
className={INPUT_CLASS}
/>
{error && <p className="text-xs text-red-600">{error}</p>}
{error && <p className="text-xs text-red-600 dark:text-red-400">{error}</p>}
<button type="submit" disabled={busy} className={SUBMIT_CLASS}>
Sign in <ArrowRightIcon className="h-4 w-4" />
</button>
Expand All @@ -125,7 +131,7 @@ export function AuthPanel() {
setMode("signup");
setError("");
}}
className="mt-4 text-sm text-slate-500 underline decoration-dotted hover:text-slate-700"
className={LINK_CLASS}
>
New shop? Create an account instead
</button>
Expand All @@ -134,8 +140,8 @@ export function AuthPanel() {

{mode === "signup" && (
<>
<h2 className="text-xl font-bold text-slate-900">Set up your shop</h2>
<p className="mt-1 text-sm text-slate-600">You'll be the owner — invite your staff once you're in.</p>
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">Set up your shop</h2>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">You'll be the owner — invite your staff once you're in.</p>
<form className="mt-4 flex flex-col gap-3" onSubmit={handleSignup}>
<input
value={shopName}
Expand Down Expand Up @@ -163,7 +169,7 @@ export function AuthPanel() {
aria-label="Password"
className={INPUT_CLASS}
/>
{error && <p className="text-xs text-red-600">{error}</p>}
{error && <p className="text-xs text-red-600 dark:text-red-400">{error}</p>}
<button type="submit" disabled={busy} className={SUBMIT_CLASS}>
Create account <ArrowRightIcon className="h-4 w-4" />
</button>
Expand All @@ -174,7 +180,7 @@ export function AuthPanel() {
setMode("login");
setError("");
}}
className="mt-4 text-sm text-slate-500 underline decoration-dotted hover:text-slate-700"
className={LINK_CLASS}
>
Already have an account? Sign in
</button>
Expand All @@ -183,8 +189,8 @@ export function AuthPanel() {

{mode === "confirm" && (
<>
<h2 className="text-xl font-bold text-slate-900">Check your email</h2>
<p className="mt-1 text-sm text-slate-600">Enter the confirmation code we sent to {email}.</p>
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">Check your email</h2>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">Enter the confirmation code we sent to {email}.</p>
<form className="mt-4 flex flex-col gap-3" onSubmit={handleConfirm}>
<input
value={code}
Expand All @@ -194,15 +200,15 @@ export function AuthPanel() {
aria-label="Confirmation code"
className={INPUT_CLASS}
/>
{error && <p className="text-xs text-red-600">{error}</p>}
{error && <p className="text-xs text-red-600 dark:text-red-400">{error}</p>}
<button type="submit" disabled={busy} className={SUBMIT_CLASS}>
Confirm <ArrowRightIcon className="h-4 w-4" />
</button>
</form>
<button
type="button"
onClick={() => void resendConfirmationCode(email)}
className="mt-4 text-sm text-slate-500 underline decoration-dotted hover:text-slate-700"
className={LINK_CLASS}
>
Resend code
</button>
Expand All @@ -211,8 +217,8 @@ export function AuthPanel() {

{mode === "new_password" && (
<>
<h2 className="text-xl font-bold text-slate-900">Choose a password</h2>
<p className="mt-1 text-sm text-slate-600">First sign-in on an invited account — set your own password to continue.</p>
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">Choose a password</h2>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">First sign-in on an invited account — set your own password to continue.</p>
<form className="mt-4 flex flex-col gap-3" onSubmit={handleNewPassword}>
<input
type="password"
Expand All @@ -223,7 +229,7 @@ export function AuthPanel() {
aria-label="New password"
className={INPUT_CLASS}
/>
{error && <p className="text-xs text-red-600">{error}</p>}
{error && <p className="text-xs text-red-600 dark:text-red-400">{error}</p>}
<button type="submit" disabled={busy} className={SUBMIT_CLASS}>
Set password and continue <ArrowRightIcon className="h-4 w-4" />
</button>
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/components/BarcodeScanButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ export function BarcodeScanButton({ items, onResolved }: BarcodeScanButtonProps)

if (state === "scanning") {
return (
<div className="rounded-lg border border-indigo-200 bg-indigo-50/50 p-2">
<div className="rounded-lg border border-indigo-200 bg-indigo-50/50 p-2 dark:border-indigo-900 dark:bg-indigo-950/30">
<video ref={videoRef} data-testid="scan-video" className="w-full rounded-md" muted playsInline />
<button
type="button"
onClick={() => setState("idle")}
className="mt-2 text-xs font-medium text-slate-500 hover:text-slate-700"
className="mt-2 text-xs font-medium text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
>
Cancel
</button>
Expand All @@ -84,19 +84,19 @@ export function BarcodeScanButton({ items, onResolved }: BarcodeScanButtonProps)
<button
type="button"
onClick={() => setState("scanning")}
className="flex items-center gap-1.5 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 transition-colors hover:border-indigo-300 hover:bg-indigo-50 hover:text-indigo-700"
className="flex items-center gap-1.5 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 transition-colors hover:border-indigo-300 hover:bg-indigo-50 hover:text-indigo-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-indigo-700 dark:hover:bg-indigo-950/50 dark:hover:text-indigo-300"
>
<ScanIcon className="h-4 w-4" />
Scan barcode/QR
</button>
{state === "not_recognized" && (
<p data-testid="scan-not-recognized" className="mt-1.5 flex items-center gap-1 text-xs text-amber-700">
<p data-testid="scan-not-recognized" className="mt-1.5 flex items-center gap-1 text-xs text-amber-700 dark:text-amber-400">
<AlertTriangleIcon className="h-3 w-3 shrink-0" />
Item not recognized — pick it from the list below instead.
</p>
)}
{state === "permission_denied" && (
<p data-testid="scan-permission-denied" className="mt-1.5 flex items-center gap-1 text-xs text-rose-700">
<p data-testid="scan-permission-denied" className="mt-1.5 flex items-center gap-1 text-xs text-rose-700 dark:text-rose-400">
<AlertTriangleIcon className="h-3 w-3 shrink-0" />
Camera access denied — pick the item from the list below instead.
</p>
Expand Down
17 changes: 12 additions & 5 deletions apps/web/src/components/CartDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,34 @@ export function CartDrawer({ lineItems, productName, onRemove }: CartDrawerProps
const total = lineItems.reduce((sum, item) => sum + item.quantity * item.unit_price, 0);

if (lineItems.length === 0) {
return <p className="text-sm text-slate-400">Cart is empty — add items from the list.</p>;
return <p className="text-sm text-slate-400 dark:text-slate-500">Cart is empty — add items from the list.</p>;
}

return (
<div className="rounded-lg border border-slate-200 bg-white p-4">
<ul className="space-y-1 text-sm">
<div className="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<ul className="space-y-1 text-sm text-slate-700 dark:text-slate-300">
{lineItems.map((item) => (
<li key={item.product_id} className="flex items-center justify-between" data-testid={`cart-line-${item.product_id}`}>
<span>
{productName(item.product_id)} × {item.quantity}
</span>
<span className="flex items-center gap-2">
₹{item.quantity * item.unit_price}
<button type="button" onClick={() => onRemove(item.product_id)} className="text-xs text-rose-600 hover:underline">
<button
type="button"
onClick={() => onRemove(item.product_id)}
className="text-xs text-rose-600 hover:underline dark:text-rose-400"
>
Remove
</button>
</span>
</li>
))}
</ul>
<div className="mt-2 flex justify-between border-t border-slate-200 pt-2 text-sm font-semibold text-slate-900" data-testid="cart-total">
<div
className="mt-2 flex justify-between border-t border-slate-200 pt-2 text-sm font-semibold text-slate-900 dark:border-slate-800 dark:text-slate-100"
data-testid="cart-total"
>
<span>Total</span>
<span>₹{total}</span>
</div>
Expand Down
Loading
Loading