Revert implicit-any typing and convert src/ JavaScript to TypeScript #8
43 changed files with 195 additions and 135 deletions
|
|
@ -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 `<Logo>` 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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" }) => (
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>)[key] = value;
|
||||
if (value !== undefined) (merged as any)[key] = value;
|
||||
}
|
||||
resolved.push(merged);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<any>(null);
|
||||
const [overIndex, setOverIndex] = useState<any>(null);
|
||||
const rowRefs = useRef<any[]>([]);
|
||||
|
||||
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 (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@
|
|||
|
||||
Two things live elsewhere:
|
||||
|
||||
the tile grid src/data/mapGrid.js. Where a state sits never
|
||||
the tile grid src/data/mapGrid.ts. Where a state sits never
|
||||
changes, so it isn't worth a round trip.
|
||||
|
||||
the fetch src/data/organizations.js. One endpoint for
|
||||
the fetch src/data/organizations.ts. One endpoint for
|
||||
every kind, so a section listing regions and
|
||||
a section listing partners read alike.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
|
@ -25,12 +25,12 @@ import {
|
|||
areasSentence,
|
||||
initialsFor,
|
||||
useOrganizations,
|
||||
} from "./organizations.js";
|
||||
} from "./organizations.ts";
|
||||
import {
|
||||
areaForChapter,
|
||||
buildAreaSlices,
|
||||
countChaptersByArea,
|
||||
} from "./mapGrid.js";
|
||||
} from "./mapGrid.ts";
|
||||
|
||||
const byName = (a, b) => a.name.localeCompare(b.name);
|
||||
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
One request, filtered per section. The Retreats page has three
|
||||
bands of events, and all three call this hook — the cache in
|
||||
api.js keys on the path, so they share a single fetch and each
|
||||
api.ts keys on the path, so they share a single fetch and each
|
||||
narrows the result to what it shows.
|
||||
|
||||
useEvents({ section: "national" }) one band
|
||||
|
|
@ -33,13 +33,13 @@
|
|||
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useResource } from "../lib/useResource.js";
|
||||
import { useResource } from "../lib/useResource.ts";
|
||||
|
||||
/* One shared empty list, so a memo keyed on `sections` doesn't
|
||||
restart on every render before the data arrives. */
|
||||
const NO_SECTIONS = [];
|
||||
const NO_SECTIONS: any[] = [];
|
||||
|
||||
export function useEvents({ section, host, status, type } = {}) {
|
||||
export function useEvents({ section, host, status, type }: any = {}) {
|
||||
const { data, error, loading } = useResource("/events");
|
||||
|
||||
const all = data?.events;
|
||||
|
|
@ -71,9 +71,9 @@ export function useEvents({ section, host, status, type } = {}) {
|
|||
/* Past and upcoming, split. `status` arrives already resolved — the
|
||||
explicit value when there is one, otherwise derived from ends_on
|
||||
— so nothing here needs to know which of the two it got. */
|
||||
export function splitByStatus(events = []) {
|
||||
const upcoming = [];
|
||||
const past = [];
|
||||
export function splitByStatus(events: any[] = []) {
|
||||
const upcoming: any[] = [];
|
||||
const past: any[] = [];
|
||||
|
||||
for (const event of events) {
|
||||
(event.status === "past" ? past : upcoming).push(event);
|
||||
|
|
@ -86,7 +86,7 @@ export function splitByStatus(events = []) {
|
|||
EVENT_TYPES order rather than whatever order the rows arrived
|
||||
in. A section with one type has nothing to filter, which is what
|
||||
lets the chip bar hide itself. */
|
||||
export function typesPresent(events = [], declared = []) {
|
||||
export function typesPresent(events: any[] = [], declared: any[] = []) {
|
||||
const seen = new Set(events.map(e => e.event_type));
|
||||
return declared.filter(entry => seen.has(entry.id));
|
||||
}
|
||||
|
|
@ -121,9 +121,9 @@ export function areaForChapter(chapter) {
|
|||
regionAreas [{ region_id, area_code, share, edge, note }]
|
||||
regions [{ id, name, color, ... }]
|
||||
───────────────────────────────────────────────────────────── */
|
||||
export function buildAreaSlices(regionAreas = [], regions = []) {
|
||||
export function buildAreaSlices(regionAreas: any[] = [], regions: any[] = []) {
|
||||
const regionById = Object.fromEntries(regions.map((r) => [r.id, r]));
|
||||
const byArea = {};
|
||||
const byArea: Record<string, any[]> = {};
|
||||
|
||||
for (const row of regionAreas) {
|
||||
const area = AREA_BY_CODE[row.area_code];
|
||||
|
|
@ -151,7 +151,7 @@ export function buildAreaSlices(regionAreas = [], regions = []) {
|
|||
/* ── Chapter counts per tile ───────────────────────────────────
|
||||
{ WA: 2, MO: 1, CANADA: 1 }
|
||||
───────────────────────────────────────────────────────────── */
|
||||
export function countChaptersByArea(chapters = []) {
|
||||
export function countChaptersByArea(chapters: any[] = []) {
|
||||
const counts = {};
|
||||
for (const chapter of chapters) {
|
||||
const code = areaForChapter(chapter);
|
||||
|
|
@ -165,7 +165,7 @@ export function countChaptersByArea(chapters = []) {
|
|||
from the row rather than being looked up in a splits table and
|
||||
branched on whether this region is the primary.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
export function areasLabel(regionId, regionAreas = []) {
|
||||
export function areasLabel(regionId, regionAreas: any[] = []) {
|
||||
return regionAreas
|
||||
.filter((row) => row.region_id === regionId && row.area_code !== "CANADA")
|
||||
.map((row) => (row.note ? `${row.area_code} (${row.note})` : row.area_code))
|
||||
|
|
@ -18,12 +18,12 @@
|
|||
chapter { region_id, region_name, region_color, meets, started }
|
||||
partner {}
|
||||
|
||||
Each kind is a separate request path, so the cache in api.js
|
||||
Each kind is a separate request path, so the cache in api.ts
|
||||
keys them apart and two sections asking for regions share one
|
||||
fetch.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
import { useResource } from "../lib/useResource.js";
|
||||
import { useResource } from "../lib/useResource.ts";
|
||||
|
||||
const EMPTY = { organizations: [] };
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ export function orgPath(org) {
|
|||
any more.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
export function areasSentence(areas = []) {
|
||||
export function areasSentence(areas: any[] = []) {
|
||||
return areas
|
||||
.filter((area) => area.area_code !== "CANADA")
|
||||
.map((area) => (area.note ? `${area.area_code} (${area.note})` : area.area_code))
|
||||
|
|
@ -1128,7 +1128,7 @@ const frontPage = {
|
|||
],
|
||||
};
|
||||
|
||||
export const ADMIN_ENTITIES = {
|
||||
export const ADMIN_ENTITIES: Record<string, any> = {
|
||||
organizations,
|
||||
events,
|
||||
people,
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import { createContext, useContext, useEffect } from "react";
|
||||
|
||||
export const AdminTitleContext = createContext(null);
|
||||
export const AdminTitleContext = createContext<any>(null);
|
||||
|
||||
/* Publish the name of whatever this page is showing. Clears on
|
||||
unmount, so navigating away can't leave a stale record name in
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ const DEFAULT_TTL = 60_000;
|
|||
const cache = new Map(); // path → { at, promise }
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(message, { status, fields } = {}) {
|
||||
status;
|
||||
fields;
|
||||
|
||||
constructor(message, { status, fields }: { status?: number; fields?: any } = {}) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
|
|
@ -33,7 +36,7 @@ export class ApiError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
async function request(path, options = {}) {
|
||||
async function request(path, options: RequestInit = {}) {
|
||||
const response = await fetch(`${BASE}${path}`, {
|
||||
headers: { Accept: "application/json", ...options.headers },
|
||||
...options,
|
||||
|
|
@ -68,7 +71,7 @@ async function request(path, options = {}) {
|
|||
get("/events", { fallback }) → fallback on any failure
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
export function get(path, { ttl = DEFAULT_TTL, fallback } = {}) {
|
||||
export function get(path, { ttl = DEFAULT_TTL, fallback }: { ttl?: number; fallback?: any } = {}) {
|
||||
const hit = cache.get(path);
|
||||
|
||||
if (hit && Date.now() - hit.at < ttl) return hit.promise;
|
||||
|
|
@ -16,9 +16,9 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
||||
|
||||
import { get, post, ApiError } from "./api.js";
|
||||
import { get, post, ApiError } from "./api.ts";
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
const AuthContext = createContext<any>(null);
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [user, setUser] = useState(null);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
table, so there is one answer to "where does an event live" and
|
||||
changing it is changing EVENT_BASE below.
|
||||
|
||||
navConfig.js stays the source of truth for the *nav*: these are
|
||||
navConfig.ts stays the source of truth for the *nav*: these are
|
||||
record routes, which never appear in it.
|
||||
|
||||
── On missing ids ──
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
* and the public/ layout stay the frontend's business.
|
||||
*/
|
||||
|
||||
import type { OrgKind } from './hrefs.ts'
|
||||
|
||||
export type DatePrecision = 'year' | 'month' | 'day'
|
||||
|
||||
/** What an entry is about. Drives the marker and the body layout. */
|
||||
|
|
@ -41,6 +43,8 @@ export type TimelineRef = {
|
|||
kind: RefKind
|
||||
/** The row's TEXT primary key — an event id, org slug, team slug. */
|
||||
id: string
|
||||
/** Only on organizations: history.js copies v_timeline.org_kind. */
|
||||
orgKind?: OrgKind
|
||||
}
|
||||
|
||||
/** Filename plus the table it came from; the directory is derived
|
||||
|
|
|
|||
|
|
@ -106,12 +106,22 @@ export type EventRecord = {
|
|||
hosts: EventHost[]
|
||||
description: string[]
|
||||
links: Link[]
|
||||
instagram?: Link | null
|
||||
/** The instagram link's label, the handle. See splitLinks in shape.js. */
|
||||
instagram?: string | null
|
||||
blocks: ContentBlock[]
|
||||
people: EventPerson[]
|
||||
awards: EventAward[]
|
||||
}
|
||||
|
||||
/** One row of GET /events: shapeEvent without the detail-only
|
||||
* blocks, people and awards. */
|
||||
export type EventListItem = Omit<EventRecord, 'blocks' | 'people' | 'awards'>
|
||||
|
||||
/** An event_sections row, as GET /events sends it beside the list. */
|
||||
export type EventSection = { id: string; name: string; sort_order: number }
|
||||
|
||||
export type EventsResponse = { sections: EventSection[]; events: EventListItem[] }
|
||||
|
||||
export const useEvent = (id?: string): Resource<EventRecord> =>
|
||||
useRecord<EventRecord>(detailPath('/events', id), 'event')
|
||||
|
||||
|
|
@ -157,6 +167,17 @@ export type OrgEvent = {
|
|||
color?: string | null
|
||||
}
|
||||
|
||||
/** regions.scope's CHECK values. */
|
||||
export type RegionScope = 'domestic' | 'international' | 'virtual'
|
||||
|
||||
/** A region_areas row as attachRegionDetails sends it. */
|
||||
export type RegionArea = {
|
||||
area_code: string
|
||||
share: number
|
||||
edge: 'top' | 'bottom' | null
|
||||
note: string | null
|
||||
}
|
||||
|
||||
export type OrganizationRecord = {
|
||||
id: string
|
||||
kind: 'national' | 'region' | 'chapter' | 'partner'
|
||||
|
|
@ -176,14 +197,16 @@ export type OrganizationRecord = {
|
|||
blocks: ContentBlock[]
|
||||
links: Link[]
|
||||
socials: Link[]
|
||||
website?: Link | null
|
||||
email?: Link | null
|
||||
instagram?: Link | null
|
||||
/* splitLinks in shape.js lifts these out as bare strings: the
|
||||
website's url, the email's label, the instagram handle. */
|
||||
website?: string | null
|
||||
email?: string | null
|
||||
instagram?: string | null
|
||||
/** Shape depends on `kind`; empty object for national and partner. */
|
||||
details: {
|
||||
scope?: string | null
|
||||
scope?: RegionScope | null
|
||||
map_note?: string | null
|
||||
areas?: Array<{ area_code: string; share?: number | null; edge?: string | null; note?: string | null }>
|
||||
areas?: RegionArea[]
|
||||
chapters?: Array<{ id: string; name: string; location_label?: string | null; logo?: string | null }>
|
||||
region_id?: string | null
|
||||
region_name?: string | null
|
||||
|
|
@ -197,6 +220,14 @@ export type OrganizationRecord = {
|
|||
events: OrgEvent[]
|
||||
}
|
||||
|
||||
/** One row of GET /organizations: the card surface and details,
|
||||
* without the sections only the org's own page loads. */
|
||||
export type OrganizationListItem = Omit<OrganizationRecord, 'teams' | 'awards' | 'events'> & {
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export type OrganizationsResponse = { organizations: OrganizationListItem[] }
|
||||
|
||||
export const useOrganization = (id?: string): Resource<OrganizationRecord> =>
|
||||
useRecord<OrganizationRecord>(detailPath('/organizations', id), 'organization')
|
||||
|
||||
|
|
@ -214,7 +245,8 @@ export type TeamRecord = {
|
|||
description: string[]
|
||||
links: Link[]
|
||||
socials: Link[]
|
||||
instagram?: Link | null
|
||||
/** The instagram handle. See splitLinks in shape.js. */
|
||||
instagram?: string | null
|
||||
blocks: ContentBlock[]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { get, invalidate, ApiError } from './api.js'
|
||||
import { get, invalidate, ApiError } from './api.ts'
|
||||
import type { TimelineItem } from './timeline'
|
||||
|
||||
const PATH = '/history'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* pattern.
|
||||
*
|
||||
* Named for what it returns, and deliberately not useResource:
|
||||
* src/lib/useResource.js is a different hook — it hands back the
|
||||
* src/lib/useResource.ts is a different hook — it hands back the
|
||||
* whole response body and takes an options object — and a .ts file
|
||||
* of the same name would sit one extension away from it. An import
|
||||
* whose target goes missing then resolves to the other file without
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
* · `reload` invalidates before it refetches. Without that, the
|
||||
* retry button inside the 60s TTL hands back the same settled
|
||||
* promise and looks like it did nothing. A *failed* request is
|
||||
* already evicted by api.js, so this matters for the refresh
|
||||
* already evicted by api.ts, so this matters for the refresh
|
||||
* case rather than the error case.
|
||||
*
|
||||
* · no AbortController. `get` shares one promise between callers,
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { get, invalidate, ApiError } from './api.js'
|
||||
import { get, invalidate, ApiError } from './api.ts'
|
||||
|
||||
export type Resource<T> = {
|
||||
data: T | null
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@
|
|||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { get } from "./api.js";
|
||||
import { get } from "./api.ts";
|
||||
|
||||
export function useResource(path, { ttl, fallback } = {}) {
|
||||
export function useResource(path, { ttl, fallback }: { ttl?: number; fallback?: any } = {}) {
|
||||
// Seed with the fallback so the first paint has content when one
|
||||
// is available, rather than flashing a spinner and then the same
|
||||
// data a moment later.
|
||||
const [state, setState] = useState(() => ({
|
||||
const [state, setState] = useState<any>(() => ({
|
||||
data: fallback,
|
||||
error: null,
|
||||
loading: true,
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
|
||||
import PageShell from '../components/PageShell.tsx'
|
||||
import PageShell, { type ShellSection } from '../components/PageShell.tsx'
|
||||
import PageState from '../components/PageState.tsx'
|
||||
import ContentBlocks from '../components/ContentBlocks.tsx'
|
||||
import PeopleTiles, { type PeopleGroupInput } from '../components/PeopleTiles.tsx'
|
||||
|
|
@ -86,7 +86,7 @@ export default function EventDetail() {
|
|||
const accent = event.color || TEAL
|
||||
const groups = peopleGroups(event.people, accent)
|
||||
|
||||
const sections = [
|
||||
const sections: ShellSection[] = [
|
||||
{
|
||||
id: 'about',
|
||||
title: event.theme || 'About',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
The roster is not in this page's data. PeopleTiles fetches
|
||||
/teams/:id/people itself, off v_org_leadership, which already
|
||||
decides who counts as current and public. Two requests, both
|
||||
cached for 60s by api.js, and one set of visibility rules.
|
||||
cached for 60s by api.ts, and one set of visibility rules.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { del, get, patch, ApiError } from "../../lib/api.js";
|
||||
import { del, get, patch, ApiError } from "../../lib/api.ts";
|
||||
import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
|
||||
import { canWrite as roleCanWrite, canDelete } from "../../lib/roles.ts";
|
||||
import { feedbackTypeLabel } from "../../data/feedbackTypes.js";
|
||||
import { feedbackTypeLabel } from "../../data/feedbackTypes.ts";
|
||||
|
||||
const STATUSES = ["new", "read", "actioned", "archived", "spam"];
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ function locationOf(row) {
|
|||
function FeedbackCard({ row, onChange, onRemove, canWrite, canRemove }) {
|
||||
const [note, setNote] = useState(row.admin_note ?? "");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [error, setError] = useState<any>(null);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
|
||||
const noteDirty = note !== (row.admin_note ?? "");
|
||||
|
|
@ -232,11 +232,11 @@ export default function AdminFeedback() {
|
|||
const [query, setQuery] = useState("");
|
||||
const [search, setSearch] = useState(""); // applied, not typed
|
||||
|
||||
const [rows, setRows] = useState([]);
|
||||
const [counts, setCounts] = useState({});
|
||||
const [cursor, setCursor] = useState(null);
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [counts, setCounts] = useState<any>({});
|
||||
const [cursor, setCursor] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [error, setError] = useState<any>(null);
|
||||
|
||||
// Minimums, not equality — see lib/roles.ts.
|
||||
const canWrite = roleCanWrite(user);
|
||||
|
|
@ -298,7 +298,7 @@ export default function AdminFeedback() {
|
|||
}));
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
const tabs: any[] = [
|
||||
{ id: "all", label: "All" },
|
||||
...STATUSES.map((s) => ({ id: s, label: s, count: counts[s] })),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
the header deliberately drops its tab row here so the same links
|
||||
aren't drawn twice — and a standing info panel on the right.
|
||||
|
||||
The cards come from adminNav.js, the same list the CMS header
|
||||
The cards come from adminNav.ts, the same list the CMS header
|
||||
reads, so a new entity shows up here the moment it's registered.
|
||||
Forms sit in their own block below: they're submissions coming
|
||||
in rather than content going out, and there'll be more of them
|
||||
|
|
@ -22,7 +22,7 @@ import { Link } from "react-router-dom";
|
|||
import { useAuth } from "../../lib/auth.tsx";
|
||||
import { SITE_VERSION } from "../../lib/version.ts";
|
||||
import { ROLE_LABELS, isSuper } from "../../lib/roles.ts";
|
||||
import { CMS_NAV, FORMS_NAV, PANEL_NAV, target } from "./adminNav.js";
|
||||
import { CMS_NAV, FORMS_NAV, PANEL_NAV, target } from "./adminNav.ts";
|
||||
|
||||
/* One card. The title link is stretched over the whole card with
|
||||
`after:absolute`, which makes the card clickable without nesting
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import {
|
|||
matches,
|
||||
navFor,
|
||||
target,
|
||||
} from "./adminNav.js";
|
||||
} from "./adminNav.ts";
|
||||
|
||||
export default function AdminLayout() {
|
||||
const { user, logout } = useAuth();
|
||||
|
|
@ -56,7 +56,7 @@ export default function AdminLayout() {
|
|||
// What the page below has published about itself — a record
|
||||
// name, or null on a list. setDetail is stable so publishing
|
||||
// can't loop.
|
||||
const [detail, setDetail] = useState(null);
|
||||
const [detail, setDetail] = useState<any>(null);
|
||||
const stableSet = useCallback((value) => setDetail(value), []);
|
||||
const titleContext = useMemo(() => ({ setDetail: stableSet }), [stableSet]);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { useState } from "react";
|
|||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import { useAuth } from "../../lib/auth.tsx";
|
||||
import { ApiError } from "../../lib/api.js";
|
||||
import { ApiError } from "../../lib/api.ts";
|
||||
|
||||
const GOOGLE_ENABLED = false;
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ export default function AdminLogin() {
|
|||
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState(null);
|
||||
const [error, setError] = useState<any>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const destination = location.state?.from?.pathname ?? "/admin/home";
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { del, get, patch } from "../../lib/api.js";
|
||||
import { del, get, patch } from "../../lib/api.ts";
|
||||
import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ROLES, ROLE_LABELS, ROLE_NOTES } from "../../lib/roles.ts";
|
||||
|
|
@ -50,7 +50,7 @@ function uptime(seconds) {
|
|||
return `${m}m`;
|
||||
}
|
||||
|
||||
function Block({ title, note, children }) {
|
||||
function Block({ title, note, children }: any) {
|
||||
return (
|
||||
<section className="mt-8 first:mt-0">
|
||||
<div className="flex items-baseline gap-3">
|
||||
|
|
@ -77,21 +77,21 @@ export default function AdminPanel() {
|
|||
const { user: me } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [data, setData] = useState(null);
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [error, setError] = useState<any>(null);
|
||||
|
||||
// Which row is mid-request, and what went wrong on it. Scoped to
|
||||
// the row so a failure on one account doesn't blank the table.
|
||||
const [busyId, setBusyId] = useState(null);
|
||||
const [rowError, setRowError] = useState(null);
|
||||
const [busyId, setBusyId] = useState<any>(null);
|
||||
const [rowError, setRowError] = useState<any>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
setData(await get("/admin/panel/overview", { ttl: 0 }));
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (isUnauthorized(err)) return navigate("/admin/login", { replace: true });
|
||||
setError(err.message || "Couldn't load the panel.");
|
||||
} finally {
|
||||
|
|
@ -121,7 +121,7 @@ export default function AdminPanel() {
|
|||
setRowError(null);
|
||||
try {
|
||||
mergeUser(await work());
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (isUnauthorized(err)) return navigate("/admin/login", { replace: true });
|
||||
setRowError({ id, message: err.message || "That didn't work." });
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { get, post, patch, del, ApiError } from "../../lib/api.js";
|
||||
import { get, post, patch, del, ApiError } from "../../lib/api.ts";
|
||||
import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
|
||||
import { useAdminDetail } from "../../lib/adminTitle.tsx";
|
||||
import { ADMIN_ENTITIES, slugify } from "../../lib/adminSchema.js";
|
||||
import { ADMIN_ENTITIES, slugify } from "../../lib/adminSchema.ts";
|
||||
import { atLeast } from "../../lib/roles.ts";
|
||||
import { Field, FieldGrid, Repeater, getPath, setPath } from "../../components/admin/fields.tsx";
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ function friendly(message, singular) {
|
|||
|
||||
export default function EntityEdit() {
|
||||
const { entity: entityKey, id } = useParams();
|
||||
const manifest = ADMIN_ENTITIES[entityKey];
|
||||
const manifest = ADMIN_ENTITIES[entityKey ?? ""];
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
||||
|
|
@ -120,17 +120,17 @@ export default function EntityEdit() {
|
|||
// names the field to read instead.
|
||||
const headingPath = slugPaths[slugPaths.length - 1] ?? manifest?.titleFrom;
|
||||
|
||||
const [form, setForm] = useState(null);
|
||||
const [options, setOptions] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [message, setMessage] = useState(null);
|
||||
const [form, setForm] = useState<any>(null);
|
||||
const [options, setOptions] = useState<any>({});
|
||||
const [errors, setErrors] = useState<any>({});
|
||||
const [message, setMessage] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [slugTouched, setSlugTouched] = useState(false);
|
||||
|
||||
// The last state the server confirmed. Everything else compares
|
||||
// against this to decide whether there's anything to lose.
|
||||
const baseline = useRef(null);
|
||||
const baseline = useRef<any>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!manifest) return;
|
||||
|
|
|
|||
|
|
@ -15,22 +15,22 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Link, Navigate, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { get, ApiError } from "../../lib/api.js";
|
||||
import { get, ApiError } from "../../lib/api.ts";
|
||||
import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
|
||||
import { ADMIN_ENTITIES } from "../../lib/adminSchema.js";
|
||||
import { ADMIN_ENTITIES } from "../../lib/adminSchema.ts";
|
||||
import { atLeast } from "../../lib/roles.ts";
|
||||
|
||||
export default function EntityList() {
|
||||
const { entity: entityKey } = useParams();
|
||||
const manifest = ADMIN_ENTITIES[entityKey];
|
||||
const manifest = ADMIN_ENTITIES[entityKey ?? ""];
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [params, setParams] = useSearchParams();
|
||||
const [rows, setRows] = useState([]);
|
||||
const [options, setOptions] = useState({});
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [options, setOptions] = useState<any>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [error, setError] = useState<any>(null);
|
||||
const [query, setQuery] = useState(params.get("q") ?? "");
|
||||
|
||||
// Minimum rank, never equality. POST /api/admin/:entity is gated
|
||||
|
|
@ -79,7 +79,7 @@ export default function EntityList() {
|
|||
setParams(next, { replace: true });
|
||||
}
|
||||
|
||||
function labelFor(filter, value) {
|
||||
function labelFor(filter) {
|
||||
if (filter.optionsFrom) {
|
||||
return (options[filter.optionsFrom] ?? []).map((o) => [o.id, o.label]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@
|
|||
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useAuth } from "../../lib/auth.tsx";
|
||||
import { atLeast } from "../../lib/roles.ts";
|
||||
import { ADMIN_HOME } from "./adminNav.js";
|
||||
import { atLeast, type Role } from "../../lib/roles.ts";
|
||||
import { ADMIN_HOME } from "./adminNav.ts";
|
||||
|
||||
export default function RequireRole({ role = "superadmin" }) {
|
||||
export default function RequireRole({ role = "superadmin" }: { role?: Role }) {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
// RequireAuth is already showing its own placeholder above this.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* ═══════════════════════════════════════════════════════════════
|
||||
ADMIN NAVIGATION — src/pages/admin/adminNav.js
|
||||
ADMIN NAVIGATION — src/pages/admin/adminNav.ts
|
||||
|
||||
Lifted out of AdminLayout because two things read it now: the
|
||||
header tabs and the card grid on the home page. Adding an entity
|
||||
|
|
@ -99,7 +99,7 @@ export const PANEL_NAV = {
|
|||
superOnly: true,
|
||||
};
|
||||
|
||||
export const NAV = [...CMS_NAV, FORMS_NAV, PANEL_NAV];
|
||||
export const NAV: any[] = [...CMS_NAV, FORMS_NAV, PANEL_NAV];
|
||||
|
||||
/* What this user may see. Call it with the user from useAuth. */
|
||||
export function navFor(user) {
|
||||
|
|
@ -42,9 +42,8 @@
|
|||
import { useMemo, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { typesPresent, useEvents } from '../../data/eventData.js'
|
||||
import type { EventFilter } from '../../data/eventData.js'
|
||||
import { EVENT_TYPES, eventTypeLabel } from '../../lib/eventTypes.ts'
|
||||
import { typesPresent, useEvents } from '../../data/eventData.ts'
|
||||
import { EVENT_TYPES, eventTypeLabel, type EventType } from '../../lib/eventTypes.ts'
|
||||
import { firstOccurrenceFrom, occurrencesBetween, seriesTimes } from '../../lib/eventSeries.ts'
|
||||
import { eventHref } from '../../lib/hrefs.ts'
|
||||
import type { EventListItem } from '../../lib/useContent.ts'
|
||||
|
|
@ -63,7 +62,11 @@ const BODY = '#4a6b72'
|
|||
|
||||
const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
|
||||
type EventCalendarProps = EventFilter & {
|
||||
type EventCalendarProps = {
|
||||
section?: string
|
||||
host?: string
|
||||
status?: EventListItem['status']
|
||||
type?: EventType | EventType[]
|
||||
accent?: string
|
||||
/** Which visitor controls render. A pinned prop hides its own. */
|
||||
controls?: CalendarControl[]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { splitByStatus, typesPresent, useEvents } from "../../data/eventData.js";
|
||||
import { splitByStatus, typesPresent, useEvents } from "../../data/eventData.ts";
|
||||
import { eventHref } from "../../lib/hrefs.ts";
|
||||
import { EVENT_TYPES, eventTypeLabel } from "../../lib/eventTypes.ts";
|
||||
import { seriesLabel } from "../../lib/eventSeries.ts";
|
||||
|
|
@ -454,7 +454,7 @@ export default function EventListCards({
|
|||
accent = TEAL,
|
||||
defaultColor,
|
||||
empty = "· Events coming soon, stay connected for announcements ·",
|
||||
}) {
|
||||
}: any) {
|
||||
const { events: fetched, loading, error } = useEvents({
|
||||
section,
|
||||
host,
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@
|
|||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
import { useState } from "react";
|
||||
import { post, ApiError } from "../../lib/api.js";
|
||||
import { PAGE_LINKS, PAGE_SECTIONS } from "../../navConfig.js";
|
||||
import { FEEDBACK_TYPES } from "../../data/feedbackTypes.js";
|
||||
import { post, ApiError } from "../../lib/api.ts";
|
||||
import { PAGE_LINKS, PAGE_SECTIONS } from "../../navConfig.ts";
|
||||
import { FEEDBACK_TYPES } from "../../data/feedbackTypes.ts";
|
||||
|
||||
const ACCENT = "#138ba0";
|
||||
const MUTED = "#4a6b72";
|
||||
|
|
@ -217,7 +217,7 @@ function describeLocation(page, section) {
|
|||
/* ── The form ────────────────────────────────────────────────── */
|
||||
|
||||
export default function FeedbackForm() {
|
||||
const [type, setType] = useState(null);
|
||||
const [type, setType] = useState<any>(null);
|
||||
const [page, setPage] = useState(SITE_WIDE);
|
||||
const [section, setSection] = useState(WHOLE_PAGE);
|
||||
const [message, setMessage] = useState("");
|
||||
|
|
@ -229,8 +229,8 @@ export default function FeedbackForm() {
|
|||
|
||||
// idle → sending → sent, or back to idle with an error to show.
|
||||
const [status, setStatus] = useState("idle");
|
||||
const [formError, setFormError] = useState(null);
|
||||
const [fieldErrors, setFieldErrors] = useState({});
|
||||
const [formError, setFormError] = useState<any>(null);
|
||||
const [fieldErrors, setFieldErrors] = useState<any>({});
|
||||
|
||||
const sending = status === "sending";
|
||||
const ready = Boolean(type) && message.trim().length >= MIN_MESSAGE;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
initialsFor,
|
||||
orgPath,
|
||||
useOrganizations,
|
||||
} from "../../data/organizations.js";
|
||||
} from "../../data/organizations.ts";
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
ORGANIZATION LIST — CARDS
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCommunity } from "../../data/chapters.js";
|
||||
import { useCommunity } from "../../data/chapters.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
import ArrowLink from "../../components/ArrowLink.tsx";
|
||||
import { initialsFor, orgPath } from "../../data/organizations.js";
|
||||
import { AREAS, AREA_NAMES } from "../../data/mapGrid.js";
|
||||
import { initialsFor, orgPath } from "../../data/organizations.ts";
|
||||
import { AREAS, AREA_NAMES } from "../../data/mapGrid.ts";
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
ORGANIZATION LIST — MAP
|
||||
|
|
@ -25,7 +25,7 @@ import { AREAS, AREA_NAMES } from "../../data/mapGrid.js";
|
|||
two colors. If you later want true geography, the swap point is
|
||||
<RegionMap> — everything else works off the data.
|
||||
|
||||
The tile layout comes from mapGrid.js; who paints what comes
|
||||
The tile layout comes from mapGrid.ts; who paints what comes
|
||||
from the API. A state and the Canada band are the same shape
|
||||
now, a tile with a span, so the map is one loop.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
|
@ -59,7 +59,7 @@ const INTL_TITLE = "International Unity Regions";
|
|||
vanishing. When organization and person pages arrive this should
|
||||
move to a shared component; small enough to live here until then.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
function Blocks({ blocks = [], color }) {
|
||||
function Blocks({ blocks = [] as any[], color }) {
|
||||
if (blocks.length === 0) return null;
|
||||
|
||||
return (
|
||||
|
|
@ -145,7 +145,7 @@ function Tile({
|
|||
size,
|
||||
width = size,
|
||||
label,
|
||||
slices = [],
|
||||
slices = [] as any[],
|
||||
count = 0,
|
||||
selected,
|
||||
hovered,
|
||||
|
|
@ -248,7 +248,7 @@ function Tile({
|
|||
);
|
||||
}
|
||||
|
||||
function RegionMap({ slices, chapterCounts, ...props }) {
|
||||
function RegionMap({ slices, chapterCounts, ...props }: any) {
|
||||
const size = TILE - PAD * 2;
|
||||
|
||||
return (
|
||||
|
|
@ -306,7 +306,7 @@ function LegendButton({ region, selected, setSelected, hovered, setHovered }) {
|
|||
);
|
||||
}
|
||||
|
||||
function Legend({ domestic, international, onMapIds, ...props }) {
|
||||
function Legend({ domestic, international, onMapIds, ...props }: any) {
|
||||
const { selected, setSelected } = props;
|
||||
|
||||
// A region is on the map if it paints a tile. West Central used
|
||||
|
|
@ -615,7 +615,7 @@ function ChapterDetail({ chapter, region, onClose }) {
|
|||
<div className="mt-5 flex flex-wrap gap-3">
|
||||
{orgPath(chapter) && (
|
||||
<Link
|
||||
to={orgPath(chapter)}
|
||||
to={orgPath(chapter)!}
|
||||
className="py-2 px-5 rounded-xl font-700 text-sm transition-transform duration-200 hover:scale-105"
|
||||
style={{ background: colorOf(region), color: "#ffffff" }}
|
||||
>
|
||||
|
|
@ -769,14 +769,14 @@ export default function OrgListMap({ view = "map", accent = FALLBACK_COLOR }) {
|
|||
subtextFor,
|
||||
} = useCommunity();
|
||||
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [hovered, setHovered] = useState(null);
|
||||
const [openId, setOpenId] = useState(null); // no card open on arrival
|
||||
const [selected, setSelected] = useState<any>(null);
|
||||
const [hovered, setHovered] = useState<any>(null);
|
||||
const [openId, setOpenId] = useState<any>(null); // no card open on arrival
|
||||
|
||||
// The list scrolls itself to whatever the map or legend points at.
|
||||
const listRef = useRef(null);
|
||||
const regionRefs = useRef({});
|
||||
const chapterRefs = useRef({});
|
||||
const listRef = useRef<any>(null);
|
||||
const regionRefs = useRef<any>({});
|
||||
const chapterRefs = useRef<any>({});
|
||||
|
||||
const scrollListTo = el => {
|
||||
const box = listRef.current;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
initialsFor,
|
||||
orgPath,
|
||||
useOrganizations,
|
||||
} from "../../data/organizations.js";
|
||||
} from "../../data/organizations.ts";
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
ORGANIZATION LIST — VERTICAL
|
||||
|
|
@ -47,7 +47,7 @@ const FALLBACK_COLOR = "#4a6b72";
|
|||
it — a link nested in a button is invalid, and a screen reader
|
||||
announces the whole row as one confused control.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
function RowButton({ as: As = "button", color, children, ...rest }) {
|
||||
function RowButton({ as: As = "button", color, children, ...rest }: any) {
|
||||
return (
|
||||
<As
|
||||
className="shrink-0 whitespace-nowrap py-2 px-4 rounded-xl font-700 text-sm transition-transform duration-200 hover:scale-105"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue