Skip to content
Draft
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
32 changes: 6 additions & 26 deletions apps/ensadmin/src/app/name/_components/AdditionalRecords.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,25 @@
"use client";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { ENSAdminProfile } from "@/hooks/use-ensadmin-profile";

interface AdditionalRecordsProps {
texts: Record<string, unknown> | null | undefined;
profile: ENSAdminProfile;
}

const RECORDS_ALREADY_DISPLAYED_ELSEWHERE = [
"description",
"url",
"email",
"com.twitter",
"com.github",
"com.farcaster",
"org.telegram",
"com.linkedin",
"com.reddit",
"avatar",
"header",
"name",
];

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

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

if (records.length === 0) return null;
export function AdditionalRecords({ profile }: AdditionalRecordsProps) {
if (profile.additionalTextRecords.length === 0) return null;

return (
<Card>
<CardHeader>
<CardTitle>Additional Records</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{records.map(([key, value]) => (
{profile.additionalTextRecords.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>
<span className="text-sm text-gray-900 ml-4 break-all">{value}</span>
</div>
))}
</CardContent>
Expand Down
13 changes: 6 additions & 7 deletions apps/ensadmin/src/app/name/_components/Addresses.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"use client";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { ENSAdminProfile } from "@/hooks/use-ensadmin-profile";

interface AddressesProps {
addresses: Record<string, unknown> | null | undefined;
profile: ENSAdminProfile;
}

export function Addresses({ addresses }: AddressesProps) {
if (!addresses || Object.keys(addresses).length === 0) {
export function Addresses({ profile }: AddressesProps) {
if (profile.addresses.length === 0) {
return null;
}

Expand All @@ -17,12 +18,10 @@ export function Addresses({ addresses }: AddressesProps) {
<CardTitle>Addresses</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{Object.entries(addresses).map(([coinType, address]) => (
{profile.addresses.map(({ coinType, address }) => (
<div key={coinType} className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-500">Coin Type {coinType}</span>
<code className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
{String(address)}
</code>
<code className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">{address}</code>
</div>
))}
</CardContent>
Expand Down
78 changes: 11 additions & 67 deletions apps/ensadmin/src/app/name/_components/NameDetailPageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"use client";

import { ASSUME_IMMUTABLE_QUERY, useRecords } from "@ensnode/ensnode-react";
import { type Name, type ResolverRecordsSelection } from "@ensnode/ensnode-sdk";
import { type Name } from "@ensnode/ensnode-sdk";

import { Card, CardContent } from "@/components/ui/card";
import { useActiveNamespace } from "@/hooks/active/use-active-namespace";
import { getCommonCoinTypes } from "@/lib/default-records-selection";
import { useENSAdminProfile } from "@/hooks/use-ensadmin-profile";

import { AdditionalRecords } from "./AdditionalRecords";
import { Addresses } from "./Addresses";
Expand All @@ -14,58 +12,12 @@ import { ProfileHeader } from "./ProfileHeader";
import { ProfileInformation } from "./ProfileInformation";
import { SocialLinks } from "./SocialLinks";

const HeaderPanelTextRecords = ["url", "avatar", "header"];
const ProfilePanelTextRecords = ["description", "email"];
const SocialLinksTextRecords = [
"com.twitter",
"com.github",
"com.farcaster",
"org.telegram",
"com.linkedin",
"com.reddit",
];
// TODO: Instead of explicitly listing AdditionalTextRecords, we should update
// `useRecords` so that we can ask it to return not only all the records we
// explicitly requested, but also any other records that were found onchain,
// no matter what their text record keys are. Below are two examples of
// additional text records set for lightwalker.eth on mainnet as an example.
// see: https://github.com/namehash/ensnode/issues/1083
const AdditionalTextRecords = ["status", "eth.ens.delegate"];
const AllRequestedTextRecords = [
...HeaderPanelTextRecords,
...ProfilePanelTextRecords,
...SocialLinksTextRecords,
...AdditionalTextRecords,
];

interface NameDetailPageContentProps {
name: Name;
}

export function NameDetailPageContent({ name }: NameDetailPageContentProps) {
const namespace = useActiveNamespace();

const selection = {
addresses: getCommonCoinTypes(namespace),
texts: AllRequestedTextRecords,
} as const satisfies ResolverRecordsSelection;

// TODO: Each app (including ENSAdmin) should define their own "wrapper" data model around
// their `useRecords` queries that is specific to their use case. For example, ENSAdmin should
// define a nicely designed data model such as `ENSProfile` (based on the subjective definition
// of what an ENS profile is within the context of ENSAdmin). Then, a hook such as `useENSProfile`
// should be defined that internally calls `useRecords` and then performs the data transformations
// that might be required to return the nice, clean, and specialized `ENSProfile` data model.
// The code in `ProfileHeader`, `ProfileInformation`, `SocialLinks`, `Addresses`, and `AdditionalRecords`
// should then be updated so that it takes as input only the nice and clean `ENSProfile` data model.
// These UI components should not need to consider the nuances or complexities of the raw `useRecords`
// data model. All those nuances and complexities should be mananaged in a single place (ex: `useENSProfile`).
// see: https://github.com/namehash/ensnode/issues/1082
const { data, status } = useRecords({
name,
selection,
query: ASSUME_IMMUTABLE_QUERY,
});
const { data: profile, status } = useENSAdminProfile({ name });

if (status === "pending") return <NameDetailPageSkeleton />;

Expand All @@ -78,25 +30,17 @@ export function NameDetailPageContent({ name }: NameDetailPageContentProps) {
</Card>
);

// TODO: Design and Implement Profile not found page
if (!profile) return null;

return (
<div className="container mx-auto p-6 max-w-4xl">
<ProfileHeader
name={name}
namespaceId={namespace}
headerImage={data?.records?.texts?.header}
websiteUrl={data?.records?.texts?.url}
/>
<ProfileHeader profile={profile} />
<div className="grid gap-6">
<ProfileInformation
description={data.records.texts.description}
email={data.records.texts.email}
/>

<SocialLinks.Texts texts={data.records.texts} />

<Addresses addresses={data.records.addresses} />

<AdditionalRecords texts={data.records.texts} />
<ProfileInformation profile={profile} />
<SocialLinks profile={profile} />
<Addresses profile={profile} />
<AdditionalRecords profile={profile} />
</div>
</div>
);
Expand Down
22 changes: 9 additions & 13 deletions apps/ensadmin/src/app/name/_components/ProfileHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
"use client";

import type { ENSNamespaceId, Name } from "@ensnode/ensnode-sdk";

import { EnsAvatar } from "@/components/ens-avatar";
import { NameDisplay } from "@/components/identity/utils";
import { ExternalLinkWithIcon } from "@/components/link";
import { Card, CardContent } from "@/components/ui/card";
import type { ENSAdminProfile } from "@/hooks/use-ensadmin-profile";
import { beautifyUrl } from "@/lib/beautify-url";

interface ProfileHeaderProps {
name: Name;
namespaceId: ENSNamespaceId;
headerImage?: string | null;
websiteUrl?: string | null;
profile: ENSAdminProfile;
}

export function ProfileHeader({ name, namespaceId, headerImage, websiteUrl }: ProfileHeaderProps) {
export function ProfileHeader({ profile }: ProfileHeaderProps) {
// Parse header image URI and only use it if it's HTTP/HTTPS
// TODO: Add support for more URI types as defined in ENSIP-12
// See: https://docs.ens.domains/ensip/12#uri-types
const getValidHeaderImageUrl = (headerImage: string | null | undefined): string | null => {
const getValidHeaderImageUrl = (headerImage: string | null): string | null => {
if (!headerImage) return null;

let url: URL;
Expand All @@ -35,7 +31,7 @@ export function ProfileHeader({ name, namespaceId, headerImage, websiteUrl }: Pr
return null;
};

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

try {
Expand All @@ -49,8 +45,8 @@ export function ProfileHeader({ name, namespaceId, headerImage, websiteUrl }: Pr
}
};

const validHeaderImageUrl = getValidHeaderImageUrl(headerImage);
const normalizedWebsiteUrl = normalizeWebsiteUrl(websiteUrl);
const validHeaderImageUrl = getValidHeaderImageUrl(profile.header.headerImageUrl);
const normalizedWebsiteUrl = normalizeWebsiteUrl(profile.header.websiteUrl);

return (
<Card className="overflow-hidden mb-8">
Expand All @@ -67,10 +63,10 @@ export function ProfileHeader({ name, namespaceId, headerImage, websiteUrl }: Pr
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<EnsAvatar className="-mt-16 h-20 w-20 ring-4 ring-white" name={name} />
<EnsAvatar className="-mt-16 h-20 w-20 ring-4 ring-white" name={profile.name} />
<div className="flex-1">
<h1>
<NameDisplay className="text-3xl font-bold" name={name} />
<NameDisplay className="text-3xl font-bold" name={profile.name} />
</h1>
<div className="flex items-center gap-3 mt-1">
{normalizedWebsiteUrl && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import { Mail } from "lucide-react";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { ENSAdminProfile } from "@/hooks/use-ensadmin-profile";

interface ProfileInformationProps {
description?: string | null;
email?: string | null;
profile: ENSAdminProfile;
}

export function ProfileInformation({ description, email }: ProfileInformationProps) {
export function ProfileInformation({ profile }: ProfileInformationProps) {
const { description, email } = profile.information;

if (!description && !email) {
return null;
}
Expand Down
49 changes: 8 additions & 41 deletions apps/ensadmin/src/app/name/_components/SocialLinks.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,27 @@
"use client";

import { SiFarcaster, SiGithub, SiReddit, SiTelegram, SiX } from "@icons-pack/react-simple-icons";
import { useMemo } from "react";

import { LinkedInIcon } from "@/components/icons/LinkedInIcon";
import { ExternalLinkWithIcon } from "@/components/link";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { ENSAdminProfile, ENSAdminSocialLinkKey } from "@/hooks/use-ensadmin-profile";

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;
interface SocialLinksProps {
profile: ENSAdminProfile;
}

export function SocialLinks({
links,
}: {
links: { key: SocialLinkKey; value: SocialLinkValue }[];
}) {
if (links.length === 0) return null;
export function SocialLinks({ profile }: SocialLinksProps) {
if (profile.socialLinks.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) {
{profile.socialLinks.map(({ key, value }) => {
switch (key as ENSAdminSocialLinkKey) {
case "com.twitter": {
return (
<div key={key} className="inline-flex items-center gap-2">
Expand Down Expand Up @@ -106,24 +94,3 @@ export function SocialLinks({
</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