Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +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()
.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<typeof formSchema>;
Expand All @@ -52,6 +61,7 @@ export function CreateProfileButton() {
description: values.description || "",
github_login: values.githubLogin || "",
twitter_handle: values.twitterHandle || "",
linkedin_account: values.linkedinAccount || "",
},
});
await queryClient.invalidateQueries({ queryKey: ["profiles"] });
Expand Down Expand Up @@ -138,6 +148,25 @@ export function CreateProfileButton() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="linkedinAccount"
render={({ field }) => (
<FormItem>
<FormLabel>LinkedIn Profile URL</FormLabel>
<FormControl>
<Input
placeholder="https://www.linkedin.com/in/your-handle/"
{...field}
/>
</FormControl>
<p className="text-xs text-gray-500 mt-1">
Paste your full LinkedIn URL or just the handle
</p>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2 pt-2">
<DialogClose asChild>
<Button type="button" variant="secondary">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,24 @@ interface EditProfileDialogProps {
description?: string;
githubLogin?: string;
twitterHandle?: string;
linkedinAccount?: string;
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()
.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<typeof formSchema>;
Expand All @@ -49,6 +59,7 @@ export function EditProfileDialog({
description,
githubLogin,
twitterHandle,
linkedinAccount,
children,
}: EditProfileDialogProps) {
const [open, setOpen] = useState(false);
Expand All @@ -62,6 +73,7 @@ export function EditProfileDialog({
description: description || "",
githubLogin: githubLogin || "",
twitterHandle: twitterHandle || "",
linkedinAccount: linkedinAccount || "",
},
});

Expand All @@ -72,9 +84,10 @@ export function EditProfileDialog({
description: description || "",
githubLogin: githubLogin || "",
twitterHandle: twitterHandle || "",
linkedinAccount: linkedinAccount || "",
});
}
}, [open, name, description, githubLogin, twitterHandle, form]);
}, [open, name, description, githubLogin, twitterHandle, linkedinAccount, form]);

const onSubmit = async (values: FormValues) => {
try {
Expand All @@ -84,6 +97,7 @@ export function EditProfileDialog({
description: values.description || "",
github_login: values.githubLogin || "",
twitter_handle: values.twitterHandle || "",
linkedin_account: values.linkedinAccount || "",
},
});
await queryClient.invalidateQueries({ queryKey: ["profiles"] });
Expand Down Expand Up @@ -167,6 +181,25 @@ export function EditProfileDialog({
</FormItem>
)}
/>
<FormField
control={form.control}
name="linkedinAccount"
render={({ field }) => (
<FormItem>
<FormLabel>LinkedIn Profile URL</FormLabel>
<FormControl>
<Input
placeholder="https://www.linkedin.com/in/your-handle/"
{...field}
/>
</FormControl>
<p className="text-xs text-gray-500 mt-1">
Paste your full LinkedIn URL or just the handle
</p>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2 pt-2">
<DialogClose asChild>
<Button type="button" variant="secondary">
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/profiles/list/ProfileCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface ProfileCardProps {
avatar?: string;
githubLogin?: string;
twitterHandle?: string;
linkedinAccount?: string;
attestationCount: number;
attestations: Array<{
id: string;
Expand All @@ -37,6 +38,7 @@ export function ProfileCard({
avatar,
githubLogin,
twitterHandle,
linkedinAccount,
attestationCount,
attestations,
}: ProfileCardProps) {
Expand Down Expand Up @@ -99,6 +101,7 @@ export function ProfileCard({
description={description}
githubLogin={githubLogin}
twitterHandle={twitterHandle}
linkedinAccount={linkedinAccount}
>
<Button
variant="ghost"
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/profiles/list/ProfilesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function ProfilesList() {
description: p.description || "",
githubLogin: p.github_login,
twitterHandle: p.twitter_handle,
linkedinAccount: p.linkedin_account,
attestationCount: 0,
attestations: [],
}))
Expand Down Expand Up @@ -118,6 +119,7 @@ export function ProfilesList() {
description={profile.description}
githubLogin={profile.githubLogin}
twitterHandle={profile.twitterHandle}
linkedinAccount={profile.linkedinAccount}
attestationCount={profile.attestationCount}
attestations={profile.attestations}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ export function ProfileActions({
description,
githubLogin,
twitterHandle,
linkedinAccount,
}: {
address?: string;
name?: string;
description?: string;
githubLogin?: string;
twitterHandle?: string;
linkedinAccount?: string;
}) {
const { address: connectedAddress } = useAccount();
const isOwner =
Expand All @@ -33,6 +35,7 @@ export function ProfileActions({
description={description}
githubLogin={githubLogin}
twitterHandle={twitterHandle}
linkedinAccount={linkedinAccount}
>
<Button variant="outline" className="flex items-center gap-2">
<Edit className="h-4 w-4" /> Edit Profile
Expand Down
45 changes: 45 additions & 0 deletions frontend/src/components/profiles/profile-page/ProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,49 @@ 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;
description?: string;
githubLogin?: string;
twitterHandle?: string;
linkedinAccount?: string;
}

export function ProfileHeader({
address,
name,
githubLogin,
twitterHandle,
linkedinAccount,
}: ProfileHeaderProps) {
const displayName = name || (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : "Profile");
const displayAddress = address ? `${address.slice(0, 6)}...${address.slice(-4)}` : "";
Expand All @@ -28,6 +58,9 @@ export function ProfileHeader({
!!address &&
connectedAddress.toLowerCase() === address.toLowerCase();

const linkedinData =
linkedinAccount && parseLinkedinAccount(linkedinAccount);

return (
<header className="flex items-start gap-4">
<div className="h-16 w-16 rounded-full bg-gray-200 flex items-center justify-center">
Expand Down Expand Up @@ -76,6 +109,18 @@ export function ProfileHeader({
</a>
</div>
)}
{linkedinData && (
<div className="flex items-center gap-1.5 mt-1">
<a
href={linkedinData.profileUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-700 hover:text-indigo-600 hover:underline"
>
🔗 {linkedinData.displayHandle}
</a>
</div>
)}
<AddressTokenBalance address={address as `0x${string}`} />
</div>
</header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
<div className="mt-6">
<ProfileActions
Expand All @@ -33,6 +34,7 @@ export function ProfileMain({ address }: { address: string }) {
description={profile?.description}
githubLogin={profile?.github_login}
twitterHandle={profile?.twitter_handle}
linkedinAccount={profile?.linkedin_account}
/>
</div>
<ProfileDescription description={profile?.description} />
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/lib/constants/profileConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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: [
{
Expand All @@ -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: [
{
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/types/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type CreateProfileInput = {
avatar_url?: string;
github_login?: string;
twitter_handle?: string;
linkedin_account?: string;
};

export type UpdateProfileInput = {
Expand All @@ -12,6 +13,7 @@ export type UpdateProfileInput = {
avatar_url?: string;
github_login?: string;
twitter_handle?: string;
linkedin_account?: string;
};

export type UpdateProfileResponse = unknown;
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/types/profiles.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type Profile = {
description: string;
githubLogin?: string;
twitterHandle?: string;
linkedinAccount?: string;
attestationCount: number;
attestations: ProfileAttestation[];
};
Expand All @@ -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;
};
Loading