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
5 changes: 5 additions & 0 deletions .changeset/quick-doodles-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensadmin": minor
Comment thread
notrab marked this conversation as resolved.
---

Add support for telegram, linkedin and reddit profiles on name detail page
53 changes: 0 additions & 53 deletions apps/ensadmin/src/app/name/[name]/SocialLinks.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,37 @@ interface AdditionalRecordsProps {
texts: Record<string, unknown> | null | undefined;
}

const EXCLUDED_RECORDS = [
const RECORDS_ALREADY_DISPLAYED_ELSEWHERE = [
"description",
"url",
"email",
"com.twitter",
"com.github",
"com.farcaster",
"org.telegram",
Comment thread
shrugs marked this conversation as resolved.
"com.linkedin",
"com.reddit",
"avatar",
"header",
"name",
];

export function AdditionalRecords({ texts }: AdditionalRecordsProps) {
if (!texts) {
return null;
}
if (!texts) return null;

const additionalRecords = Object.entries(texts).filter(
([key]) => !EXCLUDED_RECORDS.includes(key),
const records = Object.entries(texts).filter(
([key]) => !RECORDS_ALREADY_DISPLAYED_ELSEWHERE.includes(key),
);

if (additionalRecords.length === 0) {
return null;
}
if (records.length === 0) return null;

return (
<Card>
<CardHeader>
<CardTitle>Additional Records</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{additionalRecords.map(([key, value]) => (
{records.map(([key, value]) => (
<div key={key} className="flex items-start justify-between">
<span className="text-sm font-medium text-gray-500 min-w-0 flex-1">{key}</span>
<span className="text-sm text-gray-900 ml-4 break-all">{String(value)}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,23 @@ import { Skeleton } from "@/components/ui/skeleton";

export function NameDetailPageSkeleton() {
return (
<div className="container mx-auto p-6 max-w-4xl">
<div className="mb-8">
<div className="flex items-center gap-4 mb-4">
<Skeleton className="h-20 w-20 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<div className="flex items-center gap-2">
<Skeleton className="h-6 w-20" />
<Skeleton className="h-8 w-32" />
<div className="container mx-auto p-6 max-w-4xl flex flex-col gap-8">
<Card>
<div className="h-48"></div>
<CardContent className="pt-6">
<div className="flex items-center gap-4 mb-4 -mt-16">
<Skeleton className="h-20 w-20 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<div className="flex items-center gap-2">
<Skeleton className="h-6 w-20" />
<Skeleton className="h-8 w-32" />
</div>
</div>
</div>
</div>
</div>

<ProfileSkeleton />
</div>
);
}
</CardContent>
</Card>

export function ProfileSkeleton() {
return (
<div className="space-y-6">
<Card>
<CardHeader>
<Skeleton className="h-6 w-32" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ExternalLinkWithIcon } from "@/components/external-link-with-icon";
import { NameDisplay } from "@/components/identity/utils";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
import { beautifyUrl } from "@/lib/beautify-url";

interface ProfileHeaderProps {
name: string;
Expand All @@ -19,19 +20,35 @@ export function ProfileHeader({ name, avatarUrl, headerImage, websiteUrl }: Prof
const getValidHeaderImageUrl = (headerImage: string | null | undefined): string | null => {
if (!headerImage) return null;

let url: URL;
try {
const url = new URL(headerImage);
if (url.protocol === "http:" || url.protocol === "https:") {
return headerImage;
}
// For any other URI types (ipfs, data, NFT URIs, etc.), fallback to default
url = new URL(headerImage);
} catch {
return null;
}

if (url.protocol === "http:" || url.protocol === "https:") return headerImage;

// For any other URI types (ipfs, data, NFT URIs, etc.), fallback to default
return null;
};

const normalizeWebsiteUrl = (url: string | null | undefined): URL | null => {
if (!url) return null;

try {
try {
return new URL(url);
} catch {
return new URL(`https://${url}`);
}
} catch {
return null;
}
};

const validHeaderImageUrl = getValidHeaderImageUrl(headerImage);
const normalizedWebsiteUrl = normalizeWebsiteUrl(websiteUrl);

return (
<Card className="overflow-hidden mb-8">
Expand All @@ -57,12 +74,9 @@ export function ProfileHeader({ name, avatarUrl, headerImage, websiteUrl }: Prof
<NameDisplay className="text-3xl font-bold" name={name} />
</h1>
<div className="flex items-center gap-3 mt-1">
{websiteUrl && (
<ExternalLinkWithIcon
href={websiteUrl.startsWith("http") ? websiteUrl : `https://${websiteUrl}`}
className="text-sm"
>
{websiteUrl.replace(/^https?:\/\//, "")}
{normalizedWebsiteUrl && (
<ExternalLinkWithIcon href={normalizedWebsiteUrl.toString()} className="text-sm">
{beautifyUrl(normalizedWebsiteUrl)}
</ExternalLinkWithIcon>
)}
</div>
Expand Down
125 changes: 125 additions & 0 deletions apps/ensadmin/src/app/name/[name]/_components/SocialLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"use client";

import { useMemo } from "react";

import { ExternalLinkWithIcon } from "@/components/external-link-with-icon";
import { LinkedInIcon } from "@/components/icons/LinkedInIcon";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { SiFarcaster, SiGithub, SiReddit, SiTelegram, SiX } from "@icons-pack/react-simple-icons";

const SOCIAL_LINK_KEYS = [
"com.twitter",
"com.farcaster",
"com.github",
"org.telegram",
"com.linkedin",
"com.reddit",
] as const;

type SocialLinkKey = (typeof SOCIAL_LINK_KEYS)[number];
type SocialLinkValue = string;

export function SocialLinks({
links,
}: { links: { key: SocialLinkKey; value: SocialLinkValue }[] }) {
if (links.length === 0) return null;

return (
<Card>
<CardHeader>
<CardTitle>Social Links</CardTitle>
</CardHeader>
<CardContent className="gap-3 flex flex-col md:flex-row flex-wrap">
{links.map(({ key, value }) => {
switch (key) {
case "com.twitter": {
return (
<div key={key} className="inline-flex items-center gap-2">
<SiX size={16} className="text-gray-500" />
<ExternalLinkWithIcon href={`https://twitter.com/${value}`} className="text-sm">
@{value}
</ExternalLinkWithIcon>
</div>
);
}
case "com.github": {
return (
<div key={key} className="inline-flex items-center gap-2">
<SiGithub size={16} className="text-gray-500" />
<ExternalLinkWithIcon href={`https://github.com/${value}`} className="text-sm">
{value}
</ExternalLinkWithIcon>
</div>
);
}
case "com.farcaster": {
return (
<div key={key} className="inline-flex items-center gap-2">
<SiFarcaster size={16} className="text-gray-500" />
<ExternalLinkWithIcon href={`https://warpcast.com/${value}`} className="text-sm">
@{value}
</ExternalLinkWithIcon>
</div>
);
}
case "org.telegram": {
return (
<div key={key} className="inline-flex items-center gap-2">
<SiTelegram size={16} className="text-gray-500" />
<ExternalLinkWithIcon href={`https://t.me/${value}`} className="text-sm">
@{value}
</ExternalLinkWithIcon>
</div>
);
}
case "com.linkedin": {
return (
<div key={key} className="inline-flex items-center gap-2">
<LinkedInIcon className="text-gray-500 size-4 fill-current" />
<ExternalLinkWithIcon
href={`https://linkedin.com/in/${value}`}
className="text-sm"
>
{value}
</ExternalLinkWithIcon>
</div>
);
}
case "com.reddit": {
return (
<div key={key} className="inline-flex items-center gap-2">
<SiReddit size={16} className="text-gray-500" />
<ExternalLinkWithIcon href={`https://reddit.com/u/${value}`} className="text-sm">
u/{value}
</ExternalLinkWithIcon>
</div>
);
}
default:
console.warn(`Unsupported Social provided: '${key}' with value '${value}'.`);
return null;
}
})}
</CardContent>
</Card>
);
}

SocialLinks.Texts = function SocialLinksTexts({
texts,
}: { texts: Record<string, string | null | undefined> }) {
const links = useMemo(
() =>
SOCIAL_LINK_KEYS
// map social keys to a set of links
.map((key) => ({ key, value: texts[key] }))
// filter those links by those that exist
.filter(
(link): link is { key: SocialLinkKey; value: SocialLinkValue } =>
typeof link.value === "string",
),
[texts],
);

return <SocialLinks links={links} />;
};
Loading