From ba08fc087bf261b031b1b6633087018c501e9d2f Mon Sep 17 00:00:00 2001 From: Julio Ruiz Date: Fri, 14 Aug 2026 01:17:06 -0500 Subject: [PATCH] Add church-registered flag + fix country search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pastors: churchRegistered boolean; the registration code field now shows only when the church is registered (admin condition), and is cleared when the flag is turned off - Frontend modal: "Iglesia registrada" toggle reveals a conditional "Código de registro" input; two new toggleable table columns - Search: country is stored as an ISO code, so a country typed by name (e.g. "venezuela", accent-insensitive) now resolves to its code(s) and matches Co-Authored-By: Claude Opus 4.8 --- src/app/(frontend)/PastorsTable.tsx | 50 +++++++++++++++++++++++++++++ src/app/(frontend)/actions.ts | 8 +++++ src/app/(frontend)/page.tsx | 32 ++++++++++++++++++ src/collections/Pastors.ts | 13 +++++++- src/payload-types.ts | 5 +++ 5 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/app/(frontend)/PastorsTable.tsx b/src/app/(frontend)/PastorsTable.tsx index f8653ce..d9228b6 100644 --- a/src/app/(frontend)/PastorsTable.tsx +++ b/src/app/(frontend)/PastorsTable.tsx @@ -18,6 +18,8 @@ export type PastorRow = { countryName: string | null countryFlag: string impactedPeople: number | null + churchRegistered: boolean + registrationNumber: string | null inTelegram: boolean notes: string | null letterUrl: string | null @@ -36,6 +38,8 @@ type ColumnKey = | 'state' | 'country' | 'impactedPeople' + | 'churchRegistered' + | 'registrationNumber' | 'inTelegram' | 'letter' @@ -122,6 +126,22 @@ export function PastorsTable({ rows }: { rows: PastorRow[] }) { cell: (p) => p.impactedPeople === null ? dash : p.impactedPeople.toLocaleString('es'), }, + { + key: 'churchRegistered', + label: 'Iglesia registrada', + align: 'center', + cell: (p) => + p.churchRegistered ? ( + + ) : ( + No + ), + }, + { + key: 'registrationNumber', + label: 'Código de registro', + cell: (p) => {p.registrationNumber || dash}, + }, { key: 'inTelegram', label: 'Telegram', @@ -343,6 +363,8 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: ( const [impactedPeople, setImpactedPeople] = useState( pastor?.impactedPeople != null ? String(pastor.impactedPeople) : '', ) + const [churchRegistered, setChurchRegistered] = useState(pastor?.churchRegistered ?? false) + const [registrationNumber, setRegistrationNumber] = useState(pastor?.registrationNumber ?? '') const [inTelegram, setInTelegram] = useState(pastor?.inTelegram ?? false) const [notes, setNotes] = useState(pastor?.notes ?? '') @@ -365,6 +387,8 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: ( state, country, impactedPeople: impactedPeople.trim() === '' ? null : Number(impactedPeople), + churchRegistered, + registrationNumber: churchRegistered ? registrationNumber : null, inTelegram, notes, } @@ -510,6 +534,32 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: ( onChange={(e) => setImpactedPeople(e.target.value)} /> + Iglesia registrada + setChurchRegistered(e.target.checked)} + /> + + {churchRegistered && ( + <> + + setRegistrationNumber(e.target.value)} + /> + + )} + En Telegram { return String.fromCodePoint(base + (cc.charCodeAt(0) - 65), base + (cc.charCodeAt(1) - 65)) } +// Accent/case-insensitive normalizer for search matching ("méxico" ~ "mexico"). +const normalize = (s: string): string => + s + .normalize('NFD') + .replace(/\p{Diacritic}/gu, '') + .toLowerCase() + .trim() + +// All ISO 3166-1 alpha-2 codes, so a country typed by name resolves to its code +// (country is stored as the ISO code, e.g. "MX" for México). +// prettier-ignore +const ALPHA2 = ['AD','AE','AF','AG','AI','AL','AM','AO','AQ','AR','AS','AT','AU','AW','AX','AZ','BA','BB','BD','BE','BF','BG','BH','BI','BJ','BL','BM','BN','BO','BQ','BR','BS','BT','BV','BW','BY','BZ','CA','CC','CD','CF','CG','CH','CI','CK','CL','CM','CN','CO','CR','CU','CV','CW','CX','CY','CZ','DE','DJ','DK','DM','DO','DZ','EC','EE','EG','EH','ER','ES','ET','FI','FJ','FK','FM','FO','FR','GA','GB','GD','GE','GF','GG','GH','GI','GL','GM','GN','GP','GQ','GR','GS','GT','GU','GW','GY','HK','HM','HN','HR','HT','HU','ID','IE','IL','IM','IN','IO','IQ','IR','IS','IT','JE','JM','JO','JP','KE','KG','KH','KI','KM','KN','KP','KR','KW','KY','KZ','LA','LB','LC','LI','LK','LR','LS','LT','LU','LV','LY','MA','MC','MD','ME','MF','MG','MH','MK','ML','MM','MN','MO','MP','MQ','MR','MS','MT','MU','MV','MW','MX','MY','MZ','NA','NC','NE','NF','NG','NI','NL','NO','NP','NR','NU','NZ','OM','PA','PE','PF','PG','PH','PK','PL','PM','PN','PR','PS','PT','PW','PY','QA','RE','RO','RS','RU','RW','SA','SB','SC','SD','SE','SG','SH','SI','SJ','SK','SL','SM','SN','SO','SR','SS','ST','SV','SX','SY','SZ','TC','TD','TF','TG','TH','TJ','TK','TL','TM','TN','TO','TR','TT','TV','TW','TZ','UA','UG','UM','US','UY','UZ','VA','VC','VE','VG','VI','VN','VU','WF','WS','YE','YT','ZA','ZM','ZW'] + +// Precomputed [normalizedName, code] pairs in the display locale. +const COUNTRY_NAME_INDEX: [string, string][] = ALPHA2.map((code) => [ + normalize(localizedCountry(code) ?? code), + code, +]) + +// Country codes whose localized name contains the search term. +const countryCodesMatching = (term: string): string[] => { + const t = normalize(term) + if (!t) return [] + return COUNTRY_NAME_INDEX.filter(([name]) => name.includes(t)).map(([, code]) => code) +} + export default async function HomePage({ searchParams, }: { @@ -41,6 +67,9 @@ export default async function HomePage({ const payload = await getPayload({ config: await config }) + // Country is stored as an ISO code, so resolve any country *name* in the + // search term to its code(s) and match those too. + const countryCodes = countryCodesMatching(search) const where: Where = search ? { or: [ @@ -48,6 +77,7 @@ export default async function HomePage({ { email: { like: search } }, { city: { like: search } }, { country: { like: search } }, + ...(countryCodes.length ? [{ country: { in: countryCodes } }] : []), ], } : {} @@ -87,6 +117,8 @@ export default async function HomePage({ countryName: localizedCountry(p.country), countryFlag: flag(p.country), impactedPeople: p.impactedPeople ?? null, + churchRegistered: Boolean(p.churchRegistered), + registrationNumber: p.registrationNumber ?? null, inTelegram: Boolean(p.inTelegram), notes: p.notes ?? null, letterUrl: letter?.url ?? null, diff --git a/src/collections/Pastors.ts b/src/collections/Pastors.ts index 712aab6..661ed45 100644 --- a/src/collections/Pastors.ts +++ b/src/collections/Pastors.ts @@ -45,10 +45,21 @@ export const Pastors: CollectionConfig = { type: 'number', min: 0, }, + { + name: 'churchRegistered', + label: 'Church Registered', + type: 'checkbox', + defaultValue: false, + }, { name: 'registrationNumber', - label: 'Registration Number', + label: 'Registration Code', type: 'text', + admin: { + // Only relevant when the church is registered. + condition: (data) => Boolean(data?.churchRegistered), + description: 'Official registration code for the church.', + }, }, { name: 'inTelegram', diff --git a/src/payload-types.ts b/src/payload-types.ts index 35dca7d..25a8e2e 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -232,6 +232,10 @@ export interface Pastor { */ country?: string | null; impactedPeople?: number | null; + churchRegistered?: boolean | null; + /** + * Official registration code for the church. + */ registrationNumber?: string | null; inTelegram?: boolean | null; /** @@ -434,6 +438,7 @@ export interface PastorsSelect { state?: T; country?: T; impactedPeople?: T; + churchRegistered?: T; registrationNumber?: T; inTelegram?: T; telephone?: T;