Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 2 additions & 16 deletions src/app/profile/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PublicProfilesService,
type PublicProfile,
} from "@/services/public-profiles-service";
import { AttendedEvents } from "@/components/attended-events";

export default function PublicProfilePage() {
const { id } = useParams<{ id: string }>();
Expand Down Expand Up @@ -138,22 +139,7 @@ export default function PublicProfilePage() {
<p className="text-2xl font-bold">Events Attended</p>

<div className="mt-8">
{profile.events_attended.length > 0 ? (
<div className="space-y-4">
{profile.events_attended.map((eventName, index) => (
<div
key={`${eventName}-${index}`}
className="rounded-3xl bg-primary/10 px-4 py-4 sm:px-6 sm:py-5 dark:bg-white/10"
>
<p className="font-semibold text-lg">{eventName}</p>
</div>
))}
</div>
) : (
<p className="text-muted-foreground dark:text-white/70">
No events attended yet.
</p>
)}
<AttendedEvents attendanceReferences={profile.events_attended} />
</div>
</div>
</div>
Expand Down
117 changes: 5 additions & 112 deletions src/app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import React, { useEffect, useRef, useState } from "react";
import Image from "next/image";
import { useWallet } from "@/hooks/use-wallet";
import { WalletButton } from "@/components/wallet-button";
import { CheckCheck, Copy, PenTool } from "lucide-react";
Expand All @@ -10,7 +9,7 @@ import { useSignMessage } from "wagmi";
import { ProfileVisibilityToggle } from "@/components/profile-visibility-toggle";
import { IoQrCode } from "react-icons/io5";
import QrCode from "qrcode";
import { getSupabase } from "@/services/supabase";
import { AttendedEvents } from "@/components/attended-events";

const getErrorMessage = (error: unknown) => {
if (error instanceof Error) {
Expand All @@ -24,25 +23,6 @@ const getErrorMessage = (error: unknown) => {
return String(error);
};

const EVENT_THUMBNAIL_MAP: Record<string, string> = {
"launch night": "/images/Launchnight1.jpg",
"industry night": "/images/Industrynight1.jpg",
govdojo: "/images/GovDojo1.jpg",
};

const normalizeEventName = (eventName: string) =>
eventName.trim().toLowerCase();

const getEventImage = (eventName: string) => {
const normalized = normalizeEventName(eventName);
if (normalized.includes("launch")) return EVENT_THUMBNAIL_MAP["launch night"];
if (normalized.includes("industry"))
return EVENT_THUMBNAIL_MAP["industry night"];
if (normalized.includes("govdojo") || normalized.includes("gov"))
return EVENT_THUMBNAIL_MAP.govdojo;
return "/images/Launchnight1.jpg";
};

export default function ProfilePage() {
const { address, isConnected, disconnect, mounted } = useWallet();
const { signMessageAsync } = useSignMessage();
Expand All @@ -52,9 +32,6 @@ export default function ProfilePage() {
const [profileImage, setProfileImage] = useState<string | null>(null);
const [badges, setBadges] = useState<string[]>([]);
const [eventsAttended, setEventsAttended] = useState<string[]>([]);
const [eventDetails, setEventDetails] = useState<
Record<string, { title: string; eventUrl: string | null }>
>({});
const [profileLoading, setProfileLoading] = useState(true);
const [walletRegistered, setWalletRegistered] = useState(false);
const [walletChecking, setWalletChecking] = useState(true);
Expand Down Expand Up @@ -195,46 +172,6 @@ export default function ProfilePage() {
useEffect(() => {
let cancelled = false;

const fetchAttendedEventDetails = async (attended: string[]) => {
if (attended.length === 0) return;

try {
const supabase = getSupabase();
const { data, error } = await supabase
.from("events")
.select("id,title,event_url")
.in("id", attended);

if (error) {
console.error("Failed to load attended event details", error);
return;
}

if (!cancelled && data) {
const details = data.reduce(
(
acc: Record<string, { title: string; eventUrl: string | null }>,
event,
) => {
if (event.id) {
acc[event.id] = {
title: event.title || event.id,
eventUrl: event.event_url || null,
};
}
return acc;
},
{},
);
setEventDetails(details);
}
} catch (error) {
if (!cancelled) {
console.error("Failed to load attended event images", error);
}
}
};

const loadProfile = async () => {
if (!address) {
setWalletRegistered(false);
Expand Down Expand Up @@ -286,7 +223,6 @@ export default function ProfilePage() {
setBadges(registration.badges || []);
setEventsAttended(attended);
setProfileVisible(registration.profile_visible ?? true);
await fetchAttendedEventDetails(attended);
} else {
setDisplayName("Name");
setProfileImage(null);
Expand Down Expand Up @@ -615,53 +551,10 @@ export default function ProfilePage() {
<p className="text-2xl font-bold">Events Attended</p>

<div className="mt-8">
{profileLoading ? (
<p className="text-muted-foreground dark:text-white/70">
Loading event history…
</p>
) : eventsAttended.length > 0 ? (
<div className="space-y-4">
{eventsAttended.map((eventId, index) => {
const eventDetail = eventDetails[eventId];
const eventName = eventDetail?.title || eventId;
const eventUrl = eventDetail?.eventUrl;
const eventImage = eventUrl ?? getEventImage(eventName);
return (
<div
key={`${eventName}-${index}`}
className="overflow-hidden rounded-3xl border border-primary/20 bg-white/95 shadow-sm dark:border-white/10 dark:bg-slate-900"
>
<div className="relative aspect-[16/9] w-full">
{eventUrl ? (
<img
src={eventImage}
alt={eventName}
className="h-full w-full object-cover"
/>
) : (
<Image
src={eventImage}
alt={eventName}
fill
sizes="(max-width: 768px) 100vw, 33vw"
className="object-cover"
/>
)}
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent px-4 py-3">
<p className="text-sm font-semibold text-white">
{eventName}
</p>
</div>
</div>
</div>
);
})}
</div>
) : (
<p className="text-muted-foreground dark:text-white/70">
No events attended yet.
</p>
)}
<AttendedEvents
attendanceReferences={eventsAttended}
profileLoading={profileLoading}
/>
</div>
</div>
</div>
Expand Down
126 changes: 126 additions & 0 deletions src/components/attended-events.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"use client";

import { useEffect, useState } from "react";

import {
EventService,
type AttendedEventResolution,
} from "@/services/event-service";

type AttendedEventsProps = {
attendanceReferences: string[];
profileLoading?: boolean;
};

const EMPTY_RESOLUTION: AttendedEventResolution = {
events: [],
unavailableCount: 0,
};

export function AttendedEvents({
attendanceReferences,
profileLoading = false,
}: AttendedEventsProps) {
const [resolution, setResolution] =
useState<AttendedEventResolution>(EMPTY_RESOLUTION);
const [detailsLoading, setDetailsLoading] = useState(false);
const [loadFailed, setLoadFailed] = useState(false);

useEffect(() => {
let cancelled = false;

if (attendanceReferences.length === 0) {
setResolution(EMPTY_RESOLUTION);
setDetailsLoading(false);
setLoadFailed(false);
return;
}

setDetailsLoading(true);
setLoadFailed(false);

EventService.getAttendedEvents(attendanceReferences)
.then((result) => {
if (!cancelled) {
setResolution(result);
}
})
.catch((error) => {
console.error("Unable to load attended event details", error);

if (!cancelled) {
setResolution(EMPTY_RESOLUTION);
setLoadFailed(true);
}
})
.finally(() => {
if (!cancelled) {
setDetailsLoading(false);
}
});

return () => {
cancelled = true;
};
}, [attendanceReferences]);

if (profileLoading || detailsLoading) {
return (
<p className="text-muted-foreground dark:text-white/70">
Loading event history…
</p>
);
}

if (loadFailed) {
return (
<p className="text-muted-foreground dark:text-white/70">
Event history is unavailable right now.
</p>
);
}

if (attendanceReferences.length === 0) {
return (
<p className="text-muted-foreground dark:text-white/70">
No events attended yet.
</p>
);
}

return (
<div className="space-y-4">
{resolution.events.map((event) => (
<div
key={event.id}
className="overflow-hidden rounded-3xl border border-primary/20 bg-white/95 shadow-sm dark:border-white/10 dark:bg-slate-900"
>
<div className="relative aspect-[16/9] w-full bg-neutral-200 dark:bg-neutral-700">
{event.eventUrl ? (
<img
src={event.eventUrl}
alt={`${event.title} poster`}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full items-center justify-center px-4 text-center text-sm text-muted-foreground dark:text-white/65">
Image unavailable
</div>
)}
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent px-4 py-3">
<p className="text-sm font-semibold text-white">{event.title}</p>
</div>
</div>
</div>
))}

{resolution.unavailableCount > 0 && (
<p className="rounded-2xl bg-primary/10 px-4 py-3 text-sm text-muted-foreground dark:bg-white/10 dark:text-white/70">
{resolution.unavailableCount === 1
? "Details for one past event are no longer available."
: `Details for ${resolution.unavailableCount} past events are no longer available.`}
</p>
)}
</div>
);
}
65 changes: 65 additions & 0 deletions src/services/event-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";

import { resolveAttendedEvents } from "./event-service";

const EVENTS = [
{
id: "ed6707c8-232b-403e-ac17-6e64eef72790",
title: "Launch Night",
event_url: "https://example.com/launch.webp",
},
{
id: "deba40f2-35af-47a3-8827-448146588e09",
title: "Crochet Workshop",
event_url: "https://example.com/crochet.jpg",
},
];

describe("resolveAttendedEvents", () => {
it("resolves UUID attendance values to the event title and poster", () => {
const result = resolveAttendedEvents([EVENTS[0].id], EVENTS);

expect(result).toEqual({
events: [
{
id: EVENTS[0].id,
title: "Launch Night",
eventUrl: "https://example.com/launch.webp",
},
],
unavailableCount: 0,
});
});

it("supports legacy title values without case or whitespace sensitivity", () => {
const result = resolveAttendedEvents([" crochet workshop "], EVENTS);

expect(result.events[0]).toMatchObject({
id: EVENTS[1].id,
title: "Crochet Workshop",
eventUrl: "https://example.com/crochet.jpg",
});
expect(result.unavailableCount).toBe(0);
});

it("does not expose dangling event IDs or assign them an unrelated poster", () => {
const result = resolveAttendedEvents(
["62d08d91-6a21-4949-9041-e2bd2c15c86d"],
EVENTS,
);

expect(result).toEqual({ events: [], unavailableCount: 1 });
});

it("deduplicates repeated attendance values while preserving order", () => {
const result = resolveAttendedEvents(
[EVENTS[1].id, EVENTS[0].id, EVENTS[1].id],
EVENTS,
);

expect(result.events.map((event) => event.id)).toEqual([
EVENTS[1].id,
EVENTS[0].id,
]);
});
});
Loading
Loading