Rework app bar, notes layout, Settings/Admin/login restyle
Build and Push Docker Image / build-and-push (push) Failing after 20s

App bar: brand, Studies / Read Bible / Reading Plans, and one account menu
(theme, Settings, Admin, Log out). Study stage: notes get their own box above
the resources in the right column. Settings, Admin and the login page use the
reader themes; add a theme-aware danger colour.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-09-29 10:27:02 -04:00
1 parent 31abee0695
commit ecb1c701fa
22 files changed
+351 -289

No files matched your search

+39 -58
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { useApp } from '../context/AppContext.js';
import AuthStatus from '../components/AuthStatus.jsx';
import PageShell from '../components/study/PageShell.jsx';
import { buildExportHtml } from '../utils/bibleUtils.jsx';
import {
adminListUsers,
@@ -68,46 +68,27 @@ export default function AdminPage() {
};
return (
<div className="min-h-screen bg-slate-50 text-slate-900">
<header className="border-b border-slate-200 bg-slate-900 text-white shadow-sm">
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-4 px-4 py-5 sm:px-6 lg:px-8">
<div>
<p className="text-sm uppercase tracking-[0.24em] text-slate-300">Bible Study Project</p>
<h1 className="mt-2 text-2xl font-semibold">🛡 Admin</h1>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={goHome}
className="rounded-xl border border-slate-500 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-slate-700"
>
← Back
</button>
<AuthStatus />
</div>
</div>
</header>
<main className="mx-auto max-w-5xl px-4 py-8 sm:px-6 lg:px-8 space-y-6">
<PageShell title="Admin" onBack={goHome} maxWidth="max-w-5xl">
<main className="mx-auto max-w-5xl space-y-5 px-4 py-6">
<div className="flex gap-2">
<button type="button" onClick={() => setAdminTab('users')}
className={`rounded-lg px-4 py-2 text-sm font-semibold transition ${adminTab === 'users' ? 'bg-slate-900 text-white' : 'border border-slate-300 bg-white text-slate-600 hover:bg-slate-50'}`}>
className={`rounded-full px-4 py-1.5 text-sm transition ${adminTab === 'users' ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'border border-[var(--r-line)] text-[var(--r-sub)] hover:bg-black/5'}`}>
Users ({adminUsers.length})
</button>
<button type="button" onClick={() => setAdminTab('projects')}
className={`rounded-lg px-4 py-2 text-sm font-semibold transition ${adminTab === 'projects' ? 'bg-slate-900 text-white' : 'border border-slate-300 bg-white text-slate-600 hover:bg-slate-50'}`}>
className={`rounded-full px-4 py-1.5 text-sm transition ${adminTab === 'projects' ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'border border-[var(--r-line)] text-[var(--r-sub)] hover:bg-black/5'}`}>
Projects ({adminProjects.length})
</button>
</div>
{adminLoading && <p className="text-sm text-slate-500">Loading…</p>}
{adminError && <p className="text-sm text-rose-600">{adminError}</p>}
{adminLoading && <p className="text-sm text-[var(--r-sub)]">Loading…</p>}
{adminError && <p className="text-sm text-[var(--r-danger,#b42318)]">{adminError}</p>}
{!adminLoading && !adminError && adminTab === 'users' && (
<div className="overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-panel">
<div className="overflow-hidden rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)]">
<div className="overflow-x-auto">
<table className="w-full min-w-[560px] text-sm">
<thead className="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
<thead className="bg-[var(--r-bg)] text-left text-xs font-medium uppercase tracking-wide text-[var(--r-sub)]">
<tr>
<th className="px-4 py-3">Email</th>
<th className="px-4 py-3">Joined</th>
@@ -118,22 +99,22 @@ export default function AdminPage() {
</thead>
<tbody>
{adminUsers.map((u) => (
<tr key={u.id} className="border-t border-slate-100">
<td className="px-4 py-3 font-medium text-slate-800">
{u.email}{u.id === authUser.id && <span className="ml-2 text-xs font-normal text-slate-400">(you)</span>}
<tr key={u.id} className="border-t border-[var(--r-line)]">
<td className="px-4 py-3 font-medium text-[var(--r-text)]">
{u.email}{u.id === authUser.id && <span className="ml-2 text-xs font-normal text-[var(--r-sub)]">(you)</span>}
</td>
<td className="px-4 py-3 text-slate-500">{new Date(u.createdAt).toLocaleDateString()}</td>
<td className="px-4 py-3 text-slate-500">{u.totpEnabled ? '✓' : '—'}</td>
<td className="px-4 py-3 text-slate-500">{u.projectCount}</td>
<td className="px-4 py-3 text-[var(--r-sub)]">{new Date(u.createdAt).toLocaleDateString()}</td>
<td className="px-4 py-3 text-[var(--r-sub)]">{u.totpEnabled ? '✓' : '—'}</td>
<td className="px-4 py-3 text-[var(--r-sub)]">{u.projectCount}</td>
<td className="px-4 py-3 text-right">
{u.id !== authUser.id && (
<div className="flex justify-end gap-2">
<button type="button" onClick={() => handleResetPasswordAdmin(u.id, u.email)}
className="rounded-lg border border-slate-300 px-3 py-1 text-xs font-semibold text-slate-600 hover:bg-slate-50">
className="rounded-lg border border-[var(--r-line)] px-3 py-1 text-xs hover:bg-black/5">
Reset Password
</button>
<button type="button" onClick={() => handleDeleteUserAdmin(u.id, u.email)}
className="rounded-lg border border-rose-200 px-3 py-1 text-xs font-semibold text-rose-600 hover:bg-rose-50">
className="rounded-lg border border-[var(--r-line)] px-3 py-1 text-xs text-[var(--r-danger,#b42318)] hover:bg-black/5">
Delete
</button>
</div>
@@ -148,10 +129,10 @@ export default function AdminPage() {
)}
{!adminLoading && !adminError && adminTab === 'projects' && (
<div className="overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-panel">
<div className="overflow-hidden rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)]">
<div className="overflow-x-auto">
<table className="w-full min-w-[640px] text-sm">
<thead className="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
<thead className="bg-[var(--r-bg)] text-left text-xs font-medium uppercase tracking-wide text-[var(--r-sub)]">
<tr>
<th className="px-4 py-3">Title</th>
<th className="px-4 py-3">Owner</th>
@@ -163,22 +144,22 @@ export default function AdminPage() {
</thead>
<tbody>
{adminProjects.map((p) => (
<tr key={p.id} className="border-t border-slate-100">
<td className="px-4 py-3 font-medium text-slate-800">{p.title}</td>
<td className="px-4 py-3 text-slate-500">
{p.ownerEmail ?? <span className="italic text-slate-400">orphaned</span>}
<tr key={p.id} className="border-t border-[var(--r-line)]">
<td className="px-4 py-3 font-medium text-[var(--r-text)]">{p.title}</td>
<td className="px-4 py-3 text-[var(--r-sub)]">
{p.ownerEmail ?? <span className="italic text-[var(--r-sub)]">orphaned</span>}
</td>
<td className="px-4 py-3 text-slate-500">{p.chapterSummary}</td>
<td className="px-4 py-3 text-slate-500">{new Date(p.lastEdited).toLocaleDateString()}</td>
<td className="px-4 py-3 text-slate-500">{p.shareToken ? '🔗' : '—'}</td>
<td className="px-4 py-3 text-[var(--r-sub)]">{p.chapterSummary}</td>
<td className="px-4 py-3 text-[var(--r-sub)]">{new Date(p.lastEdited).toLocaleDateString()}</td>
<td className="px-4 py-3 text-[var(--r-sub)]">{p.shareToken ? '🔗' : '—'}</td>
<td className="px-4 py-3 text-right">
<div className="flex justify-end gap-2">
<button type="button" onClick={() => handleViewProjectAdmin(p.id)}
className="rounded-lg border border-slate-300 px-3 py-1 text-xs font-semibold text-slate-600 hover:bg-slate-50">
className="rounded-lg border border-[var(--r-line)] px-3 py-1 text-xs hover:bg-black/5">
View
</button>
<button type="button" onClick={() => handleDeleteProjectAdmin(p.id, p.title)}
className="rounded-lg border border-rose-200 px-3 py-1 text-xs font-semibold text-rose-600 hover:bg-rose-50">
className="rounded-lg border border-[var(--r-line)] px-3 py-1 text-xs text-[var(--r-danger,#b42318)] hover:bg-black/5">
Delete
</button>
</div>
@@ -194,11 +175,11 @@ export default function AdminPage() {
{adminViewProject && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" onClick={() => setAdminViewProject(null)}>
<div className="h-full w-full max-w-4xl overflow-hidden rounded-2xl bg-white shadow-2xl" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between border-b border-slate-200 px-4 py-3">
<p className="text-sm font-semibold text-slate-700">{adminViewProject.title}</p>
<div className="h-full w-full max-w-4xl overflow-hidden rounded-2xl bg-[var(--r-bar)] shadow-2xl" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between border-b border-[var(--r-line)] px-4 py-3">
<p className="text-sm font-medium text-[var(--r-text)]">{adminViewProject.title}</p>
<button type="button" onClick={() => setAdminViewProject(null)}
className="rounded-lg border border-slate-300 px-3 py-1 text-xs text-slate-600 hover:bg-slate-50">
className="rounded-lg border border-[var(--r-line)] px-3 py-1 text-xs hover:bg-black/5">
Close
</button>
</div>
@@ -214,22 +195,22 @@ export default function AdminPage() {
{adminResetResult && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-2xl">
<h3 className="text-sm font-semibold text-slate-900">Password reset</h3>
<p className="mt-1 text-xs text-slate-500">
Relay this to <span className="font-medium text-slate-700">{adminResetResult.email}</span> yourself
<div className="w-full max-w-sm rounded-2xl bg-[var(--r-bar)] p-6 shadow-2xl">
<h3 className="text-sm font-medium text-[var(--r-text)]">Password reset</h3>
<p className="mt-1 text-xs text-[var(--r-sub)]">
Relay this to <span className="font-medium text-[var(--r-text)]">{adminResetResult.email}</span> yourself
(text, call, in person) — it won't be shown again. They're signed out everywhere until they log in with it.
</p>
<p className="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-center font-mono text-lg tracking-wider text-amber-800">
<p className="mt-4 rounded-xl border border-[var(--r-accent)] bg-[var(--r-bg)] px-3 py-2 text-center font-mono text-lg tracking-wider text-[var(--r-accent)]">
{adminResetResult.temporaryPassword}
</p>
<button type="button" onClick={() => setAdminResetResult(null)}
className="mt-4 w-full rounded-md bg-sky-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-sky-400">
className="mt-4 w-full rounded-full bg-[var(--r-accent)] px-5 py-2 text-sm font-medium text-[var(--r-on)] transition hover:opacity-90">
Done
</button>
</div>
</div>
)}
</div>
</PageShell>
);
}