diff --git a/CLAUDE.md b/CLAUDE.md index e04f6b2..214caf6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,18 +51,18 @@ In dev, Vite proxies `/api` to the target set in `vite.config.ts`, so the API mu - All pages use `PageShell.tsx` as the wrapper unless explicitly noted otherwise. - Pages live in `src/pages/`; section-level components go in `src/pages/sections/`. - `src/data/` holds only hardcoded data shared across multiple section files (e.g. `historyDecades.ts`, map grid). Everything else comes from SQLite. -- `navConfig.js` is the single source of truth for navigation, routes, and actions (header, footer, pages). -- `api.js` is the shared caching client used by frontend data hooks. +- `navConfig.ts` is the single source of truth for navigation, routes, and actions (header, footer, pages). +- `api.ts` is the shared caching client used by frontend data hooks. - Logos: org logos in `public/org-logos/` (served at `/org-logos/`), event logos in `public/event-logos/`. The `` component hides itself on load error. ## Rules and gotchas - **Role checks must use ladder comparisons, never equality.** Roles rank viewer → editor → admin → superadmin. Use the minimum-rank helpers from `src/lib/roles.ts` (`canWrite`, `canDelete`, `isSuper`, `atLeast`). Where a local variable shadows the name, import with an alias, e.g. `canWrite as roleCanWrite`. `role === "admin"` silently excludes higher roles and has caused repeated bugs. - **Imports need explicit extensions** (`.ts`, `.tsx`, `.js`) everywhere. - **Vite resolves `.js` before `.ts`**, so a `.js` and `.ts` file with the same base name will import the wrong one. Give new hooks distinct names. -- **Don't use `fallback: EMPTY` in api.js hooks.** It silently returns empty arrays and hides server errors; let the error state surface. +- **Don't use `fallback: EMPTY` in api.ts hooks.** It silently returns empty arrays and hides server errors; let the error state surface. ## Admin CRUD engine -Descriptor-driven: `server/admin-crud.js` and `admin-schema.js` (server) and `adminSchema.js` (client) generate SQL and form fields from declarative entity configs. Adding an entity should mean adding a descriptor, not new CRUD code. +Descriptor-driven: `server/admin-crud.js` and `admin-schema.js` (server) and `adminSchema.ts` (client) generate SQL and form fields from declarative entity configs. Adding an entity should mean adding a descriptor, not new CRUD code. - Child collections are deleted and reinserted wholesale. Unsafe for entities referenced by foreign keys elsewhere. - `reindex: false` prevents cross-entity sort order collisions. - The `OMIT` sentinel distinguishes unsent fields from deliberate clears. diff --git a/server/src/seed.js b/server/src/seed.js index 7aa11a1..e105d3e 100644 --- a/server/src/seed.js +++ b/server/src/seed.js @@ -42,8 +42,8 @@ import { openDatabase, migrate, tx } from "./db.js"; const HERE = dirname(fileURLToPath(import.meta.url)); const DB_PATH = process.env.DB_PATH ?? "./dev.db"; -const EVENTS_MODULE = process.env.EVENTS_MODULE ?? "../../src/data/events.js"; -const CHAPTERS_MODULE = process.env.CHAPTERS_MODULE ?? "../../src/data/chapters.js"; +const EVENTS_MODULE = process.env.EVENTS_MODULE ?? "../../src/data/events.ts"; +const CHAPTERS_MODULE = process.env.CHAPTERS_MODULE ?? "../../src/data/chapters.ts"; // The root organization. Every national retreat hangs off this, and // it's what makes the org_logo fallback work uniformly. diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 7c0f6cc..f713b67 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { Link } from "react-router-dom"; -import { SITE_BANNER } from "../data/bannerConfig.js"; +import { SITE_BANNER } from "../data/bannerConfig.ts"; export default function Banner() { const [visible, setVisible] = useState(false); diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index c590eac..ce2346c 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,5 +1,5 @@ import { Link } from "react-router-dom"; -import { PAGE_LINKS, PAGE_SECTIONS, NAV_ACTIONS } from "../navConfig.js"; +import { PAGE_LINKS, PAGE_SECTIONS, NAV_ACTIONS } from "../navConfig.ts"; import nguLogo from "../assets/NGU_Logo.svg"; const InstagramIcon = ({ id = "ig-gradient" }) => ( diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 2073f0a..6b999a7 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from "react"; import { NavLink, Link, Outlet, useLocation } from "react-router-dom"; -import { PAGE_LINKS, PAGE_SECTIONS, NAV_ACTIONS } from "../navConfig.js"; +import { PAGE_LINKS, PAGE_SECTIONS, NAV_ACTIONS } from "../navConfig.ts"; import Banner from "./Banner.jsx"; import nguLogo from "../assets/NGU_Logo.svg"; import Footer from "./Footer.tsx"; diff --git a/src/components/PageShell.tsx b/src/components/PageShell.tsx index 63ce434..496f926 100644 --- a/src/components/PageShell.tsx +++ b/src/components/PageShell.tsx @@ -29,9 +29,27 @@ /> ═══════════════════════════════════════════════════════════════ */ +import type { ReactNode } from "react"; + const TEAL = "#138ba0"; -export function Section({ section }) { +export type ShellSection = { + id: string; + title: string; + blurb?: string; + accent: string; + background: string; + actions?: ReactNode; + content: ReactNode; +}; + +type PageShellProps = { + title: ReactNode; + intro?: ReactNode; + sections: ShellSection[]; +}; + +export function Section({ section }: { section: ShellSection }) { const { id, title, @@ -70,7 +88,7 @@ export function Section({ section }) { ); } -export default function PageShell({ title, intro, sections }) { +export default function PageShell({ title, intro, sections }: PageShellProps) { return ( <> {/* Page header */} diff --git a/src/components/PeopleTiles.tsx b/src/components/PeopleTiles.tsx index 9ef1043..ab1b12d 100644 --- a/src/components/PeopleTiles.tsx +++ b/src/components/PeopleTiles.tsx @@ -10,7 +10,7 @@ import { import { Link } from "react-router-dom"; -import { get } from "../lib/api.js"; +import { get } from "../lib/api.ts"; import { isBadId, personHref } from "../lib/hrefs.ts"; import "./PeopleTiles.css"; @@ -316,7 +316,7 @@ function resolveAll( const { peopleslug, ...overrides } = entry; const merged: Person = { ...base }; for (const [key, value] of Object.entries(overrides)) { - if (value !== undefined) (merged as Record)[key] = value; + if (value !== undefined) (merged as any)[key] = value; } resolved.push(merged); } diff --git a/src/components/admin/fields.tsx b/src/components/admin/fields.tsx index aba024c..c0fa4d2 100644 --- a/src/components/admin/fields.tsx +++ b/src/components/admin/fields.tsx @@ -71,12 +71,12 @@ export function setPath(object, path, value) { /* ── Field ───────────────────────────────────────────────────── */ -export function Field({ field, value, row, options, error, onChange }) { +export function Field({ field, value, row, options, error, onChange }: any) { const id = `f-${field.path.replace(/\./g, "-")}`; const widget = field.widget ?? "text"; const locked = Boolean(field.readOnly); - let list = null; + let list: any = null; let orphaned = false; if (widget === "select") { @@ -236,9 +236,9 @@ export function Repeater({ spec, rows, options, errors, errorPrefix, onChange }) // Which row is in flight, and which one it's currently over. // Both are per-Repeater, which is what keeps a drag inside a // nested collection from being accepted by the outer one. - const [dragIndex, setDragIndex] = useState(null); - const [overIndex, setOverIndex] = useState(null); - const rowRefs = useRef([]); + const [dragIndex, setDragIndex] = useState(null); + const [overIndex, setOverIndex] = useState(null); + const rowRefs = useRef([]); const update = (index, next) => onChange(list.map((row, i) => (i === index ? next : row))); @@ -400,7 +400,7 @@ export function Repeater({ spec, rows, options, errors, errorPrefix, onChange }) ); } -function IconButton({ label, onClick, danger, disabled, children }) { +function IconButton({ label, onClick, danger = false, disabled = false, children }) { return (