Type src/ against API and database shapes, fixing implicit anys
Adds .d.ts declarations beside the untyped JS modules imported from TS (api.js, navConfig.js, adminSchema.js, adminNav.js, src/data/*), with shapes taken from the server routes and migrations. get/post/ patch/del now return unknown unless the caller names the response. Fixes found along the way: - website/email/instagram are bare strings from splitLinks, not Link objects; OrganizationDetail's website and email pills rendered with no href or label and now link correctly. - The chapter map falls back to FALLBACK_COLOR for a region with no colour instead of painting its tiles black. Adds defineSection() so each page manifest entry's props are checked against its own Component. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5286cae9ca
commit
2428f4412a
39 changed files with 1318 additions and 343 deletions
|
|
@ -1,8 +1,15 @@
|
|||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { splitByStatus, typesPresent, useEvents } from "../../data/eventData.js";
|
||||
import {
|
||||
splitByStatus,
|
||||
typesPresent,
|
||||
useEvents,
|
||||
type EventFilter,
|
||||
} from "../../data/eventData.js";
|
||||
import { eventHref } from "../../lib/hrefs.ts";
|
||||
import { EVENT_TYPES, eventTypeLabel } from "../../lib/eventTypes.ts";
|
||||
import { EVENT_TYPES, eventTypeLabel, type EventType } from "../../lib/eventTypes.ts";
|
||||
import type { EventListItem } from "../../lib/useContent.ts";
|
||||
import type { SectionToggleProps } from "../../lib/sections.tsx";
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
EVENT LIST — CARDS
|
||||
|
|
@ -24,7 +31,7 @@ import { EVENT_TYPES, eventTypeLabel } from "../../lib/eventTypes.ts";
|
|||
nothing but retreats shows no control at all.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
const LOGO_FILES = import.meta.glob("../../assets/event-logos/*.svg", {
|
||||
const LOGO_FILES = import.meta.glob<string>("../../assets/event-logos/*.svg", {
|
||||
eager: true,
|
||||
import: "default",
|
||||
});
|
||||
|
|
@ -33,7 +40,7 @@ const LOGOS = Object.fromEntries(
|
|||
Object.entries(LOGO_FILES).map(([path, src]) => [path.split("/").pop(), src])
|
||||
);
|
||||
|
||||
const logoSrc = file => (file && LOGOS[file]) || null;
|
||||
const logoSrc = (file: string | null | undefined) => (file && LOGOS[file]) || null;
|
||||
|
||||
/* Last resort only. The API already falls back to the host
|
||||
organization's logo when an event doesn't name its own, so this
|
||||
|
|
@ -41,7 +48,15 @@ const logoSrc = file => (file && LOGOS[file]) || null;
|
|||
const DEFAULT_ORG_LOGO = null;
|
||||
|
||||
/* An <img> that removes itself if the file 404s. */
|
||||
function Logo({ file, alt = "", className }) {
|
||||
function Logo({
|
||||
file,
|
||||
alt = "",
|
||||
className,
|
||||
}: {
|
||||
file: string | null | undefined;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
const src = logoSrc(file);
|
||||
if (!src || failed) return null;
|
||||
|
|
@ -122,7 +137,15 @@ const InstagramIcon = ({ id = "ig-gradient" }) => (
|
|||
carousel has the same constraint — it needs the card's click to
|
||||
mean "bring this one to the front" on anything that isn't the
|
||||
active slide. */
|
||||
function TitleLink({ ev, linked, children }) {
|
||||
function TitleLink({
|
||||
ev,
|
||||
linked,
|
||||
children,
|
||||
}: {
|
||||
ev: Pick<EventListItem, "id">;
|
||||
linked: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
if (!linked) return <>{children}</>;
|
||||
return (
|
||||
<Link to={eventHref(ev.id)} className="hover:underline underline-offset-4">
|
||||
|
|
@ -149,6 +172,16 @@ function TitleLink({ ev, linked, children }) {
|
|||
`linked` is the one thing a caller turns off: a card on the
|
||||
event's own page shouldn't link to the page it's already on.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
type CardProps = {
|
||||
ev: EventListItem;
|
||||
defaultColor?: string;
|
||||
accent?: string;
|
||||
compact?: boolean;
|
||||
interactive?: boolean;
|
||||
linked?: boolean;
|
||||
showType?: boolean;
|
||||
};
|
||||
|
||||
export function Card({
|
||||
ev,
|
||||
defaultColor = TEAL,
|
||||
|
|
@ -157,7 +190,7 @@ export function Card({
|
|||
interactive = true,
|
||||
linked = true,
|
||||
showType = false,
|
||||
}) {
|
||||
}: CardProps) {
|
||||
const past = ev.status === "past";
|
||||
const color = ev.color || defaultColor;
|
||||
const orgLogo = ev.org_logo || DEFAULT_ORG_LOGO;
|
||||
|
|
@ -353,14 +386,23 @@ export function Card({
|
|||
them visible is one tap, and the row reads as what the section
|
||||
contains rather than as a form control.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
export function TypeFilter({ types, active, setActive, accent }) {
|
||||
const chip = on => ({
|
||||
type TypeFilterValue = EventType | "all";
|
||||
|
||||
type TypeFilterProps = {
|
||||
types: typeof EVENT_TYPES;
|
||||
active: TypeFilterValue;
|
||||
setActive: (id: TypeFilterValue) => void;
|
||||
accent: string;
|
||||
};
|
||||
|
||||
export function TypeFilter({ types, active, setActive, accent }: TypeFilterProps) {
|
||||
const chip = (on: boolean) => ({
|
||||
border: `1px solid ${accent}`,
|
||||
background: on ? accent : "transparent",
|
||||
color: on ? "#ffffff" : accent,
|
||||
});
|
||||
|
||||
const button = (id, label) => (
|
||||
const button = (id: TypeFilterValue, label: string) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => setActive(id)}
|
||||
|
|
@ -388,8 +430,12 @@ export function TypeFilter({ types, active, setActive, accent }) {
|
|||
/* ═══════════════════════════════════════════════════════════════
|
||||
TOGGLE — the control for the section heading's action bar
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
export function EventCardsToggle({ view, setView, accent }) {
|
||||
const btn = active => ({
|
||||
export function EventCardsToggle({
|
||||
view,
|
||||
setView,
|
||||
accent,
|
||||
}: Pick<SectionToggleProps, "view" | "setView" | "accent">) {
|
||||
const btn = (active: boolean) => ({
|
||||
background: active ? accent : "transparent",
|
||||
color: active ? "#ffffff" : accent,
|
||||
});
|
||||
|
|
@ -441,6 +487,14 @@ export function EventCardsToggle({ view, setView, accent }) {
|
|||
differently to someone waiting, so they're distinguished rather
|
||||
than all falling through to "coming soon".
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
type EventListCardsProps = EventFilter & {
|
||||
/** "carousel" or "grid". */
|
||||
view?: string;
|
||||
accent?: string;
|
||||
defaultColor?: string;
|
||||
empty?: string;
|
||||
};
|
||||
|
||||
export default function EventListCards({
|
||||
section,
|
||||
host,
|
||||
|
|
@ -450,7 +504,7 @@ export default function EventListCards({
|
|||
accent = TEAL,
|
||||
defaultColor,
|
||||
empty = "· Events coming soon, stay connected for announcements ·",
|
||||
}) {
|
||||
}: EventListCardsProps) {
|
||||
const { events: fetched, loading, error } = useEvents({
|
||||
section,
|
||||
host,
|
||||
|
|
@ -461,7 +515,7 @@ export default function EventListCards({
|
|||
|
||||
const [index, setIndex] = useState(0);
|
||||
const [showPast, setShowPast] = useState(false);
|
||||
const [activeType, setActiveType] = useState("all");
|
||||
const [activeType, setActiveType] = useState<TypeFilterValue>("all");
|
||||
|
||||
/* What this band holds, which is what the chips offer — not the
|
||||
full list of declared types, three quarters of which would be
|
||||
|
|
@ -515,7 +569,7 @@ export default function EventListCards({
|
|||
const next = () => setIndex(i => Math.min(events.length - 1, i + 1));
|
||||
|
||||
// Arrows and dots follow the section accent, not the active card.
|
||||
const arrowStyle = enabled => ({
|
||||
const arrowStyle = (enabled: boolean) => ({
|
||||
border: `1px solid ${accent}`,
|
||||
background: "rgba(255,255,255,0.85)",
|
||||
color: enabled ? accent : "#b8c6c9",
|
||||
|
|
@ -523,7 +577,7 @@ export default function EventListCards({
|
|||
opacity: enabled ? 1 : 0.4,
|
||||
});
|
||||
|
||||
const notice = text => (
|
||||
const notice = (text: string) => (
|
||||
<p className="max-w-6xl mx-auto px-6 font-600" style={{ color: accent }}>
|
||||
{text}
|
||||
</p>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue