From 72d3ab353bddc360bf96b322226cde0deef0e973 Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Thu, 19 Mar 2026 08:39:44 +0000 Subject: [PATCH 1/2] add linkedin handles --- .../action-buttons/CreateProfileDialog.tsx | 18 ++++++++++++++ .../action-buttons/EditProfileDialog.tsx | 24 ++++++++++++++++++- .../components/profiles/list/ProfileCard.tsx | 3 +++ .../components/profiles/list/ProfilesList.tsx | 2 ++ .../profiles/profile-page/ProfileActions.tsx | 3 +++ .../profiles/profile-page/ProfileHeader.tsx | 14 +++++++++++ .../profiles/profile-page/ProfileMain.tsx | 2 ++ .../src/lib/constants/profileConstants.ts | 9 ++++--- frontend/src/lib/types/api.d.ts | 2 ++ frontend/src/lib/types/profiles.d.ts | 2 ++ 10 files changed, 75 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx b/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx index 56db5fa..ffc7aa8 100644 --- a/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx +++ b/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx @@ -31,6 +31,7 @@ const formSchema = z.object({ description: z.string().optional(), githubLogin: z.string().optional(), twitterHandle: z.string().optional(), + linkedinAccount: z.string().optional(), }); type FormValues = z.infer; @@ -52,6 +53,7 @@ export function CreateProfileButton() { description: values.description || "", github_login: values.githubLogin || "", twitter_handle: values.twitterHandle || "", + linkedin_account: values.linkedinAccount || "", }, }); await queryClient.invalidateQueries({ queryKey: ["profiles"] }); @@ -138,6 +140,22 @@ export function CreateProfileButton() { )} /> + ( + + LinkedIn Handle + +
+ @ + +
+
+ +
+ )} + />
)} + {linkedinAccount && ( +
+ + @{linkedinAccount} + +
+ )} diff --git a/frontend/src/components/profiles/profile-page/ProfileMain.tsx b/frontend/src/components/profiles/profile-page/ProfileMain.tsx index e926d68..c9edcba 100644 --- a/frontend/src/components/profiles/profile-page/ProfileMain.tsx +++ b/frontend/src/components/profiles/profile-page/ProfileMain.tsx @@ -25,6 +25,7 @@ export function ProfileMain({ address }: { address: string }) { description={profile?.description} githubLogin={profile?.github_login} twitterHandle={profile?.twitter_handle} + linkedinAccount={profile?.linkedin_account} />
diff --git a/frontend/src/lib/constants/profileConstants.ts b/frontend/src/lib/constants/profileConstants.ts index 5c3b73b..e53cfb1 100644 --- a/frontend/src/lib/constants/profileConstants.ts +++ b/frontend/src/lib/constants/profileConstants.ts @@ -7,6 +7,7 @@ export const PROFILES: Profile[] = [ description: "Full-stack developer passionate about Web3 and Rust", githubLogin: "alice-dev", twitterHandle: "alice_dev", + linkedinAccount: "alice-developer", attestationCount: 5, attestations: [ { @@ -35,6 +36,7 @@ export const PROFILES: Profile[] = [ description: "Smart contract developer and DeFi enthusiast", githubLogin: "bob-builder", twitterHandle: "bob_builder", + linkedinAccount: "bob-builder", attestationCount: 3, attestations: [ { @@ -53,10 +55,11 @@ export const PROFILES: Profile[] = [ }, { address: "0x5555...7777", - name: "", - description: "", + name: "", + description: "", githubLogin: undefined, - twitterHandle: undefined, + twitterHandle: undefined, + linkedinAccount: undefined, attestationCount: 2, attestations: [ { diff --git a/frontend/src/lib/types/api.d.ts b/frontend/src/lib/types/api.d.ts index 9a211fa..1eda18c 100644 --- a/frontend/src/lib/types/api.d.ts +++ b/frontend/src/lib/types/api.d.ts @@ -4,6 +4,7 @@ export type CreateProfileInput = { avatar_url?: string; github_login?: string; twitter_handle?: string; + linkedin_account?: string; }; export type UpdateProfileInput = { @@ -12,6 +13,7 @@ export type UpdateProfileInput = { avatar_url?: string; github_login?: string; twitter_handle?: string; + linkedin_account?: string; }; export type UpdateProfileResponse = unknown; diff --git a/frontend/src/lib/types/profiles.d.ts b/frontend/src/lib/types/profiles.d.ts index e6e1439..56926cc 100644 --- a/frontend/src/lib/types/profiles.d.ts +++ b/frontend/src/lib/types/profiles.d.ts @@ -11,6 +11,7 @@ export type Profile = { description: string; githubLogin?: string; twitterHandle?: string; + linkedinAccount?: string; attestationCount: number; attestations: ProfileAttestation[]; }; @@ -22,6 +23,7 @@ export type ProfileFromAPI = { avatar_url?: string; github_login?: string; twitter_handle?: string; + linkedin_account?: string; created_at?: string; updated_at?: string; }; \ No newline at end of file From 1a9408e05455423301777aca6eaaf1ae81be90c0 Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Tue, 24 Mar 2026 14:59:47 +0000 Subject: [PATCH 2/2] support full linkedin urls in profiles --- .../action-buttons/CreateProfileDialog.tsx | 23 +++++++++--- .../action-buttons/EditProfileDialog.tsx | 23 +++++++++--- .../profiles/profile-page/ProfileHeader.tsx | 37 +++++++++++++++++-- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx b/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx index ffc7aa8..173531d 100644 --- a/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx +++ b/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx @@ -26,12 +26,20 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; +const linkedinUrlRegex = /^(?:https?:\/\/(?:www\.)?linkedin\.com\/in\/)?[a-zA-Z0-9-]{3,100}\/?$/; + const formSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), description: z.string().optional(), githubLogin: z.string().optional(), twitterHandle: z.string().optional(), - linkedinAccount: z.string().optional(), + linkedinAccount: z + .string() + .optional() + .refine( + (value) => !value || linkedinUrlRegex.test(value), + "LinkedIn URL must be in format: https://www.linkedin.com/in/your-handle or just the handle (3-100 characters)" + ), }); type FormValues = z.infer; @@ -145,13 +153,16 @@ export function CreateProfileButton() { name="linkedinAccount" render={({ field }) => ( - LinkedIn Handle + LinkedIn Profile URL -
- @ - -
+
+

+ Paste your full LinkedIn URL or just the handle +

)} diff --git a/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx b/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx index 2a0dd36..9a37ffc 100644 --- a/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx +++ b/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx @@ -35,12 +35,20 @@ interface EditProfileDialogProps { children: React.ReactNode; } +const linkedinUrlRegex = /^(?:https?:\/\/(?:www\.)?linkedin\.com\/in\/)?[a-zA-Z0-9-]{3,100}\/?$/; + const formSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), description: z.string().optional(), githubLogin: z.string().optional(), twitterHandle: z.string().optional(), - linkedinAccount: z.string().optional(), + linkedinAccount: z + .string() + .optional() + .refine( + (value) => !value || linkedinUrlRegex.test(value), + "LinkedIn URL must be in format: https://www.linkedin.com/in/your-handle or just the handle (3-100 characters)" + ), }); type FormValues = z.infer; @@ -178,13 +186,16 @@ export function EditProfileDialog({ name="linkedinAccount" render={({ field }) => ( - LinkedIn Handle + LinkedIn Profile URL -
- @ - -
+
+

+ Paste your full LinkedIn URL or just the handle +

)} diff --git a/frontend/src/components/profiles/profile-page/ProfileHeader.tsx b/frontend/src/components/profiles/profile-page/ProfileHeader.tsx index 1eee7fd..2aae8e7 100644 --- a/frontend/src/components/profiles/profile-page/ProfileHeader.tsx +++ b/frontend/src/components/profiles/profile-page/ProfileHeader.tsx @@ -5,6 +5,34 @@ import CopyAddressToClipboard from "@/components/CopyAddressToClipboard"; import { GithubIcon } from "@/components/ui/GithubIcon"; import { XIcon } from "@/components/ui/XIcon"; +// Helper function to extract handle or full URL from LinkedIn input +const parseLinkedinAccount = ( + account: string +): { displayHandle: string; profileUrl: string } => { + if (!account) return { displayHandle: "", profileUrl: "" }; + + // If it's already a full URL, extract the handle and use it as-is + if (account.startsWith("http")) { + // Already a full URL, just ensure it's properly formatted + const url = + account.endsWith("/") || account.endsWith("recruit") + ? account + : account + "/"; + const handle = + account.match(/\/in\/([^/?]+)/)?.[1] || account.split("/").pop() || ""; + return { + displayHandle: handle || "LinkedIn", + profileUrl: url, + }; + } + + // It's just a handle, construct the full URL + return { + displayHandle: account, + profileUrl: `https://www.linkedin.com/in/${account}/`, + }; +}; + interface ProfileHeaderProps { address: string; name?: string; @@ -30,6 +58,9 @@ export function ProfileHeader({ !!address && connectedAddress.toLowerCase() === address.toLowerCase(); + const linkedinData = + linkedinAccount && parseLinkedinAccount(linkedinAccount); + return (
@@ -78,15 +109,15 @@ export function ProfileHeader({
)} - {linkedinAccount && ( + {linkedinData && ( )}