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 ? (
-

- ) : (
-
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) => (
-
- ))}
-
- ) : (
-
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 ? (
+

+ ) : (
+
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) => (
+
+ ))}
+
+ ) : (
+
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 || [],
+});