-
-
- setSearch(event.target.value)}
- placeholder="Payment ID, loan, user name/email/phone, title, ISBN, reference"
- />
-
-
-
-
-
-
-
-
-
-
+
Auditable}
+ filters={(
+ <>
+
+
+ setSearch(event.target.value)}
+ placeholder="Payment ID, loan, user name/email/phone, title, ISBN, reference"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ footer={(
+
+
+ Page {page} · Showing {rows.length} payment record{rows.length === 1 ? "" : "s"}
+
+
+
+
+
-
-
-
+ )}
+ >
+
{rows.map((row) => (
@@ -253,31 +279,8 @@ export default function FinesLedgerPage() {
) : null}
-
-
-
- Page {page} · Showing {rows.length} payment record{rows.length === 1 ? "" : "s"}
-
-
-
-
-
-
-
+
+
);
}
diff --git a/frontend/app/global-error.tsx b/frontend/app/global-error.tsx
new file mode 100644
index 0000000..6a6b568
--- /dev/null
+++ b/frontend/app/global-error.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import "./globals.css";
+
+export default function GlobalError({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ return (
+
+
+
+
+ Critical Error
+ Application failed to load
+
+ A global rendering error occurred.
+
+ {error?.message ? {error.message}
: null}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index 16d8f6e..0e87823 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -181,6 +181,13 @@ a {
padding: 24px;
}
+.app-error-shell {
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ padding: 24px;
+}
+
.auth-card {
width: min(480px, 100%);
background: var(--surface);
@@ -192,6 +199,17 @@ a {
gap: 14px;
}
+.app-error-card {
+ width: min(560px, 100%);
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 18px;
+ box-shadow: var(--shadow);
+ padding: 24px;
+ display: grid;
+ gap: 14px;
+}
+
.badge {
display: inline-flex;
align-items: center;
@@ -277,6 +295,14 @@ p.lede {
transform: translateY(-1px);
}
+.action-tile:disabled,
+.action-tile:disabled:hover {
+ background: linear-gradient(135deg, #fff, #fff8ee);
+ border-color: rgba(14, 107, 78, 0.22);
+ box-shadow: 0 6px 14px rgba(19, 15, 10, 0.08);
+ transform: none;
+}
+
.action-tile:focus-visible {
outline: 2px solid rgba(14, 107, 78, 0.55);
outline-offset: 2px;
@@ -947,6 +973,16 @@ button:hover {
background: var(--accent-dark);
}
+button:disabled {
+ cursor: not-allowed;
+ opacity: 0.62;
+ transform: none;
+}
+
+button:disabled:hover {
+ transform: none;
+}
+
button.secondary {
background: transparent;
color: var(--accent-dark);
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index 90725bf..2c9d7c1 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -1,5 +1,6 @@
import "./globals.css";
import AppShell from "../components/AppShell";
+import AppErrorBoundary from "../components/AppErrorBoundary";
export const metadata = {
title: "Neighborhood Library Service",
@@ -10,7 +11,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
-
{children}
+
+ {children}
+
);
diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx
index 5cc7cf3..81b0963 100644
--- a/frontend/app/login/page.tsx
+++ b/frontend/app/login/page.tsx
@@ -17,10 +17,17 @@ export default function LoginPage() {
const onSubmit = async (event: FormEvent) => {
event.preventDefault();
+ if (submitting || bootstrapping) return;
+ const trimmedEmail = email.trim();
+ const trimmedPassword = password.trim();
+ if (!trimmedEmail || !trimmedPassword) {
+ setError("Enter a valid email and password. Whitespace-only values are not allowed.");
+ return;
+ }
setSubmitting(true);
setError(null);
try {
- const result = await login({ email: email.trim(), password });
+ const result = await login({ email: trimmedEmail, password });
setStoredToken(result.access_token);
setStoredUser(result.user);
router.replace("/");
@@ -32,6 +39,7 @@ export default function LoginPage() {
};
const onBootstrap = async () => {
+ if (submitting || bootstrapping) return;
const trimmedName = name.trim();
const trimmedEmail = email.trim();
const trimmedPassword = password.trim();
@@ -99,14 +107,14 @@ export default function LoginPage() {
required
/>
-