diff --git a/app/hackathons/[id]/[projectId]/NostrProjectPageClient.tsx b/app/hackathons/[id]/[projectId]/NostrProjectPageClient.tsx index f94ff74..9ad3486 100644 --- a/app/hackathons/[id]/[projectId]/NostrProjectPageClient.tsx +++ b/app/hackathons/[id]/[projectId]/NostrProjectPageClient.tsx @@ -19,6 +19,7 @@ import { CircleDashed, } from "lucide-react"; import { + fetchCommunityProjects, fetchCommunityProjectsSnapshot, getCachedCommunityProjects, refetchCommunityProjectById, @@ -43,6 +44,7 @@ import { dedupeSoldierProfileMembers, soldierProfileHref, } from "@/lib/soldierProfileLinks"; +import { projectMatchesIdentifier } from "@/lib/projectIdentity"; import { mergeDataRelays } from "@/lib/nostrRelayConfig"; import { Trophy, Lightbulb, AlertTriangle } from "lucide-react"; import NewProjectModal from "@/components/NewProjectModal"; @@ -339,9 +341,12 @@ export default function NostrProjectPage({ setSearchPhase("cache"); setSearchProgress(null); const cached = getCachedCommunityProjects(); - let latest = cached?.find( - (p) => p.id === projectId && p.hackathon === hackathonId, - ) ?? null; + let latest = + cached?.find( + (p) => + p.hackathon === hackathonId && + projectMatchesIdentifier(p, projectId), + ) ?? null; if (latest && !cancelled) { showProject(latest); } @@ -356,7 +361,9 @@ export default function NostrProjectPage({ }); const fromSnapshot = snapshot.projects.find( - (p) => p.id === projectId && p.hackathon === hackathonId, + (p) => + p.hackathon === hackathonId && + projectMatchesIdentifier(p, projectId), ) ?? null; if (fromSnapshot && !cancelled) { latest = fromSnapshot; @@ -368,7 +375,7 @@ export default function NostrProjectPage({ if (!cancelled) setSearchPhase("relays"); const fresh = await refetchCommunityProjectById( - projectId, + latest?.id ?? projectId, TOP10_RELAYS, 5000, latest?.author, @@ -385,7 +392,22 @@ export default function NostrProjectPage({ if (fresh && fresh.hackathon === hackathonId) { showProject(fresh); } else if (!latest) { - setProject(null); + const broad = await fetchCommunityProjects(TOP10_RELAYS, { + perRelayTimeoutMs: 5000, + signal: snapshotAbort.signal, + onProgress: (progress) => { + if (!cancelled) setSearchProgress(progress); + }, + }); + if (cancelled) return; + const aliased = + broad.find( + (p) => + p.hackathon === hackathonId && + projectMatchesIdentifier(p, projectId), + ) ?? null; + if (aliased) showProject(aliased); + else setProject(null); } } catch (e) { if (e instanceof DOMException && e.name === "AbortError") return; diff --git a/lib/nostrCache.ts b/lib/nostrCache.ts index e1316ef..7bd9b2a 100644 --- a/lib/nostrCache.ts +++ b/lib/nostrCache.ts @@ -11,6 +11,7 @@ import { cacheLife, cacheTag } from "next/cache"; import { DEFAULT_RELAYS } from "./nostrRelayConfig"; import type { ProjectStatus } from "./hackathons"; +import { projectMatchesIdentifier } from "./projectIdentity"; const PROJECT_KIND = 30078; const PROJECT_TAG = "lacrypta-dev-project"; @@ -259,7 +260,9 @@ export async function getNostrProject( ): Promise { const all = await getAllSubmissionsCached(); return ( - all.find((p) => p.hackathon === hackathonId && p.id === projectId) ?? null + all.find( + (p) => p.hackathon === hackathonId && projectMatchesIdentifier(p, projectId), + ) ?? null ); } diff --git a/lib/projectIdentity.ts b/lib/projectIdentity.ts new file mode 100644 index 0000000..281bd2f --- /dev/null +++ b/lib/projectIdentity.ts @@ -0,0 +1,26 @@ +type ProjectIdentity = { + eventId?: string; + id: string; + name?: string; +}; + +export function slugifyProjectName(name: string): string { + return name + .normalize("NFKD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +export function projectMatchesIdentifier( + project: ProjectIdentity, + identifier: string, +): boolean { + const key = identifier.trim(); + if (!key) return false; + if (project.id === key) return true; + if (project.eventId === key) return true; + return project.name ? slugifyProjectName(project.name) === key : false; +}