diff --git a/src/app/profile/[wallet]/page.tsx b/src/app/profile/[wallet]/page.tsx new file mode 100644 index 0000000..980cd60 --- /dev/null +++ b/src/app/profile/[wallet]/page.tsx @@ -0,0 +1,15 @@ +import { PublicProfilePage } from "@/components/profile/public-profile-page"; + +type PublicProfileRouteProps = { + params: Promise<{ + wallet: string; + }>; +}; + +export default async function PublicProfileRoute({ + params, +}: PublicProfileRouteProps) { + const { wallet } = await params; + + return ; +} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 4331f7b..352cce5 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -1,9 +1,10 @@ "use client"; import React, { useEffect, useState } from "react"; -import { useWallet } from "@/hooks/use-wallet"; +import { ProfileContent } from "@/components/profile/profile-content"; import { WalletButton } from "@/components/wallet-button"; -import { Copy, CheckCheck, PenTool } from "lucide-react"; +import { useWallet } from "@/hooks/use-wallet"; +import { getProfileViewData } from "@/lib/profile"; import { RegistrationService } from "@/services/registrations/registrations-service"; const getErrorMessage = (error: unknown) => { @@ -89,17 +90,12 @@ export default function ProfilePage() { await RegistrationService.getRegistrationByWallet(address); if (registration) { - const displayNameFallback = registration.unique_name - ? registration.unique_name.trim() - : [registration.first_name, registration.last_name] - .filter(Boolean) - .join(" ") - .trim(); + const profile = getProfileViewData(registration); - setDisplayName(displayNameFallback || "Name"); - setProfileImage(registration.profile_picture_url || null); - setBadges(registration.badges || []); - setEventsAttended(registration.events_attended || []); + setDisplayName(profile.displayName); + setProfileImage(profile.profileImage); + setBadges(profile.badges); + setEventsAttended(profile.eventsAttended); } else { setDisplayName("Name"); setProfileImage(null); @@ -185,145 +181,22 @@ export default function ProfilePage() { } return ( -
- {/* Profile Card */} -
-
-
- {profileImage ? ( - Profile - ) : ( -
No Image
- )} - -
- -
- {isEditingName ? ( -
- setDisplayName(e.target.value)} - className="w-full md:w-auto text-xl font-bold border rounded px-3 py-2" - onKeyDown={(e) => { - if (e.key === "Enter") { - handleNameSave(); - } - }} - /> - -
- ) : ( -
-

{displayName}

- -
- )} -
- - {address} - -
- - -
-
- {statusMessage ? ( -
- {statusMessage} -
- ) : null} -
-
-
- -
- {/* Badges*/} -
-

Badges

- -
- {profileLoading ? ( -

Loading badges…

- ) : badges.length > 0 ? ( -
- {badges.map((badge, index) => ( -
- - {badge} - -
- ))} -
- ) : ( -

No badges yet.

- )} -
-
- - {/* Event History*/} -
-

Events Attended

- -
- {profileLoading ? ( -

Loading event history…

- ) : eventsAttended.length > 0 ? ( -
- {eventsAttended.map((eventName, index) => ( -
-

{eventName}

-
- ))} -
- ) : ( -

No events attended yet.

- )} -
-
-
-
+ disconnect()} + onImageUpload={handleImageUpload} + onNameChange={setDisplayName} + onNameEdit={() => setIsEditingName(true)} + onNameSave={handleNameSave} + /> ); } diff --git a/src/components/profile/profile-content.tsx b/src/components/profile/profile-content.tsx new file mode 100644 index 0000000..2f835c9 --- /dev/null +++ b/src/components/profile/profile-content.tsx @@ -0,0 +1,192 @@ +"use client"; + +import React from "react"; +import { CheckCheck, Copy, PenTool } from "lucide-react"; + +type ProfileContentProps = { + address: string; + badges: string[]; + copied: boolean; + displayName: string; + eventsAttended: string[]; + isEditingName?: boolean; + isReadOnly?: boolean; + profileImage: string | null; + profileLoading: boolean; + statusMessage?: string | null; + onCopyAddress: () => void; + onDisconnect?: () => void; + onImageUpload?: (event: React.ChangeEvent) => void; + onNameChange?: (displayName: string) => void; + onNameEdit?: () => void; + onNameSave?: () => void; +}; + +export function ProfileContent({ + address, + badges, + copied, + displayName, + eventsAttended, + isEditingName = false, + isReadOnly = false, + profileImage, + profileLoading, + statusMessage, + onCopyAddress, + onDisconnect, + onImageUpload, + onNameChange, + onNameEdit, + onNameSave, +}: ProfileContentProps) { + return ( +
+ {/* Profile Card */} +
+
+
+ {profileImage ? ( + Profile + ) : ( +
No Image
+ )} + {!isReadOnly && onImageUpload ? ( + + ) : null} +
+ +
+ {!isReadOnly && isEditingName ? ( +
+ onNameChange?.(event.target.value)} + className="w-full md:w-auto text-xl font-bold border rounded px-3 py-2" + onKeyDown={(event) => { + if (event.key === "Enter") { + onNameSave?.(); + } + }} + /> + +
+ ) : ( +
+

{displayName}

+ {!isReadOnly ? ( + + ) : null} +
+ )} +
+ + {address} + +
+ + {!isReadOnly && onDisconnect ? ( + + ) : null} +
+
+ {statusMessage ? ( +
+ {statusMessage} +
+ ) : null} +
+
+
+ +
+ {/* Badges*/} +
+

Badges

+ +
+ {profileLoading ? ( +

Loading badges...

+ ) : badges.length > 0 ? ( +
+ {badges.map((badge, index) => ( +
+ + {badge} + +
+ ))} +
+ ) : ( +

No badges yet.

+ )} +
+
+ + {/* Event History*/} +
+

Events Attended

+ +
+ {profileLoading ? ( +

Loading event history...

+ ) : eventsAttended.length > 0 ? ( +
+ {eventsAttended.map((eventName, index) => ( +
+

{eventName}

+
+ ))} +
+ ) : ( +

No events attended yet.

+ )} +
+
+
+
+ ); +} diff --git a/src/components/profile/public-profile-page.tsx b/src/components/profile/public-profile-page.tsx new file mode 100644 index 0000000..79b9ce4 --- /dev/null +++ b/src/components/profile/public-profile-page.tsx @@ -0,0 +1,97 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { ProfileContent } from "@/components/profile/profile-content"; +import { getProfileViewData, type ProfileViewData } from "@/lib/profile"; +import { RegistrationService } from "@/services/registrations/registrations-service"; + +type PublicProfilePageProps = { + walletAddress: string; +}; + +const emptyProfile: ProfileViewData = { + displayName: "Name", + profileImage: null, + badges: [], + eventsAttended: [], +}; + +export function PublicProfilePage({ walletAddress }: PublicProfilePageProps) { + const [copied, setCopied] = useState(false); + const [profile, setProfile] = useState(emptyProfile); + const [profileLoading, setProfileLoading] = useState(true); + const [statusMessage, setStatusMessage] = useState(null); + + const normalizedWallet = + RegistrationService.normalizeWalletAddress(walletAddress); + + useEffect(() => { + let isActive = true; + + const loadProfile = async () => { + setProfileLoading(true); + setStatusMessage(null); + + try { + const registration = + await RegistrationService.getRegistrationByWallet(normalizedWallet); + + if (!isActive) { + return; + } + + if (registration) { + setProfile(getProfileViewData(registration)); + } else { + setProfile(emptyProfile); + setStatusMessage("No profile data found for this wallet."); + } + } catch (error) { + console.error("Unable to load public profile data", error); + + if (isActive) { + setProfile(emptyProfile); + setStatusMessage("Unable to load profile data."); + } + } finally { + if (isActive) { + setProfileLoading(false); + } + } + }; + + if (normalizedWallet) { + loadProfile(); + } else { + setProfile(emptyProfile); + setProfileLoading(false); + setStatusMessage("No wallet address provided."); + } + + return () => { + isActive = false; + }; + }, [normalizedWallet]); + + const copyAddress = () => { + if (!normalizedWallet) return; + navigator.clipboard.writeText(normalizedWallet); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); +} diff --git a/src/lib/profile.ts b/src/lib/profile.ts new file mode 100644 index 0000000..0dafcea --- /dev/null +++ b/src/lib/profile.ts @@ -0,0 +1,41 @@ +import { RegistrationService } from "@/services/registrations/registrations-service"; + +export type ProfileRegistration = NonNullable< + Awaited> +>; + +export type ProfileViewData = { + displayName: string; + profileImage: string | null; + badges: string[]; + eventsAttended: string[]; +}; + +export const getProfileDisplayName = ( + registration: ProfileRegistration | null, +) => { + if (!registration) { + return "Name"; + } + + const uniqueName = registration.unique_name?.trim(); + if (uniqueName) { + return uniqueName; + } + + const fallbackName = [registration.first_name, registration.last_name] + .filter(Boolean) + .join(" ") + .trim(); + + return fallbackName || "Name"; +}; + +export const getProfileViewData = ( + registration: ProfileRegistration | null, +): ProfileViewData => ({ + displayName: getProfileDisplayName(registration), + profileImage: registration?.profile_picture_url || null, + badges: registration?.badges || [], + eventsAttended: registration?.events_attended || [], +});