mirror of
https://github.com/placeholder-soft/web.git
synced 2026-01-12 22:45:00 +08:00
Feat: Edit and update your favorite "Pinned Casts" (#850)
* load frames from a cast and display * add more example * fetcn and display cast from neymar APi * tidy up * tidy up * advanced embed casts * design cleanup * image sanitazation * cleanup
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import '@rainbow-me/rainbowkit/styles.css';
|
||||
import '@coinbase/onchainkit/styles.css';
|
||||
|
||||
import {
|
||||
Provider as CookieManagerProvider,
|
||||
|
||||
@@ -22,6 +22,30 @@ export function middleware(req: NextRequest) {
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
// Open img and media csp on username profile to support frames
|
||||
if (url.pathname.startsWith('/name/')) {
|
||||
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
|
||||
|
||||
// Open image src
|
||||
const cspHeader = `
|
||||
img-src 'self' https: data:;
|
||||
media-src 'self' https: data: blob:;
|
||||
`;
|
||||
|
||||
const contentSecurityPolicyHeaderValue = cspHeader.replace(/\s{2,}/g, ' ').trim();
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
requestHeaders.set('x-nonce', nonce);
|
||||
requestHeaders.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);
|
||||
const response = NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
},
|
||||
});
|
||||
response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
if (url.pathname === '/guides/run-a-base-goerli-node') {
|
||||
url.host = 'docs.base.org';
|
||||
url.pathname = '/tutorials/run-a-base-node';
|
||||
|
||||
@@ -108,6 +108,7 @@ const contentSecurityPolicy = {
|
||||
'https://browser-intake-datadoghq.com', // datadog
|
||||
'https://*.datadoghq.com', //datadog
|
||||
'https://translate.googleapis.com', // Let user translate our website
|
||||
'https://sdk-api.neynar.com/', // Neymar API
|
||||
],
|
||||
'frame-ancestors': ["'self'", baseXYZDomains],
|
||||
'form-action': ["'self'", baseXYZDomains],
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"ethers": "5.7.2",
|
||||
"framer-motion": "^8.5.5",
|
||||
"hls.js": "^1.5.14",
|
||||
"is-ipfs": "^8.0.4",
|
||||
"jose": "^5.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import Fieldset from 'apps/web/src/components/Fieldset';
|
||||
import Hint, { HintVariants } from 'apps/web/src/components/Hint';
|
||||
import Input from 'apps/web/src/components/Input';
|
||||
import Label from 'apps/web/src/components/Label';
|
||||
|
||||
import {
|
||||
UsernameTextRecordKeys,
|
||||
textRecordsKeysForDisplay,
|
||||
textRecordsKeysPlaceholderForDisplay,
|
||||
} from 'apps/web/src/utils/usernames';
|
||||
import { ChangeEvent, ReactNode, useCallback, useEffect, useId, useState } from 'react';
|
||||
|
||||
type UsernameCastInputProps = {
|
||||
value?: string;
|
||||
onChange: (value: string, index: number) => void;
|
||||
index: number;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
const warpcastUrlRegex = /^https:\/\/warpcast\.com\/([a-zA-Z0-9._-]+)\/0x[0-9a-fA-F]+$/;
|
||||
|
||||
function UsernameCastInput({ value = '', onChange, index, disabled }: UsernameCastInputProps) {
|
||||
const onChangeCastUrl = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(event.target.value, index);
|
||||
},
|
||||
[index, onChange],
|
||||
);
|
||||
|
||||
const validCast = !value || value.match(warpcastUrlRegex);
|
||||
|
||||
return (
|
||||
<Fieldset>
|
||||
<Input
|
||||
key={`cast_frame_${value}`}
|
||||
placeholder={textRecordsKeysPlaceholderForDisplay[UsernameTextRecordKeys.Casts]}
|
||||
onChange={onChangeCastUrl}
|
||||
disabled={disabled}
|
||||
className="rounded-md border border-gray-40/20 p-2 text-black"
|
||||
value={value}
|
||||
type="url"
|
||||
/>
|
||||
{!validCast && <Hint variant={HintVariants.Error}>Must be a Warpcast URL</Hint>}
|
||||
</Fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export type UsernameCastsFieldProps = {
|
||||
labelChildren?: ReactNode;
|
||||
onChange: (key: UsernameTextRecordKeys, value: string) => void;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const MAX_CASTS = 4;
|
||||
|
||||
export default function UsernameCastsField({
|
||||
labelChildren = textRecordsKeysForDisplay[UsernameTextRecordKeys.Casts],
|
||||
onChange,
|
||||
value,
|
||||
disabled = false,
|
||||
}: UsernameCastsFieldProps) {
|
||||
const [casts, setCastsUrls] = useState<string[]>(value.split(',').filter((keyword) => !!keyword));
|
||||
|
||||
useEffect(() => {
|
||||
onChange(UsernameTextRecordKeys.Casts, casts.join(','));
|
||||
}, [casts, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setCastsUrls(value.split(',').filter((keyword) => !!keyword));
|
||||
}, [value]);
|
||||
|
||||
const UsernameCastsFieldId = useId();
|
||||
|
||||
const onChangeCast = useCallback(
|
||||
(inputValue: string, index: number) => {
|
||||
const newCasts = [...casts];
|
||||
newCasts[index] = inputValue;
|
||||
setCastsUrls(newCasts);
|
||||
},
|
||||
[casts],
|
||||
);
|
||||
|
||||
return (
|
||||
<Fieldset className="w-full">
|
||||
{labelChildren && <Label htmlFor={UsernameCastsFieldId}>{labelChildren}</Label>}
|
||||
{Array.from(Array(MAX_CASTS).keys()).map((index) => (
|
||||
<UsernameCastInput
|
||||
key={`cast_frame_${index}`}
|
||||
onChange={onChangeCast}
|
||||
disabled={disabled}
|
||||
index={index}
|
||||
value={casts[index]}
|
||||
/>
|
||||
))}
|
||||
</Fieldset>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useUsernameProfile } from 'apps/web/src/components/Basenames/UsernameProfileContext';
|
||||
import UsernameProfileSectionTitle from 'apps/web/src/components/Basenames/UsernameProfileSectionTitle';
|
||||
import NeymarCast from 'apps/web/src/components/NeymarCast';
|
||||
import useReadBaseEnsTextRecords from 'apps/web/src/hooks/useReadBaseEnsTextRecords';
|
||||
|
||||
export default function UsernameProfileCasts() {
|
||||
const { profileUsername, profileAddress } = useUsernameProfile();
|
||||
|
||||
const { existingTextRecords } = useReadBaseEnsTextRecords({
|
||||
address: profileAddress,
|
||||
username: profileUsername,
|
||||
});
|
||||
const casts = existingTextRecords.casts.split(',').filter((cast) => !!cast);
|
||||
|
||||
if (casts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<UsernameProfileSectionTitle title="Pinned casts" />
|
||||
<ul className="mt-6 grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||
{casts.map((cast) => (
|
||||
<li key={cast}>
|
||||
<NeymarCast identifier={cast} type="url" />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import UsernameProfileSectionBadges from 'apps/web/src/components/Basenames/UsernameProfileSectionBadges';
|
||||
import UsernameProfileSectionExplore from 'apps/web/src/components/Basenames/UsernameProfileSectionExplore';
|
||||
import BadgeContextProvider from 'apps/web/src/components/Basenames/UsernameProfileSectionBadges/BadgeContext';
|
||||
import UsernameProfileCasts from 'apps/web/src/components/Basenames/UsernameProfileCasts';
|
||||
|
||||
export default function UsernameProfileContent() {
|
||||
return (
|
||||
<div className="flex flex-col gap-8 rounded-2xl border border-[#EBEBEB] p-4 shadow-lg md:gap-12 md:p-12">
|
||||
<UsernameProfileCasts />
|
||||
<UsernameProfileSectionBadges />
|
||||
|
||||
<BadgeContextProvider>
|
||||
<UsernameProfileSectionBadges />
|
||||
</BadgeContextProvider>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
textRecordsSocialFieldsEnabled,
|
||||
UsernameTextRecordKeys,
|
||||
} from 'apps/web/src/utils/usernames';
|
||||
import UsernameCastsField from 'apps/web/src/components/Basenames/UsernameCastsField';
|
||||
|
||||
const settingTabClass = classNames(
|
||||
'flex flex-col justify-between gap-8 text-gray/60 md:items-center p-4 md:p-8',
|
||||
@@ -89,6 +90,14 @@ export default function UsernameProfileSettingsManageProfile() {
|
||||
disabled={writeTextRecordsIsPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 w-full">
|
||||
<UsernameCastsField
|
||||
onChange={onChangeTextRecord}
|
||||
value={updatedTextRecords[UsernameTextRecordKeys.Casts]}
|
||||
disabled={writeTextRecordsIsPending}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
{/* Settings UI: The save section */}
|
||||
<div className="md:p-center flex items-center justify-between gap-4 border-t border-[#EBEBEB] p-4 md:p-8">
|
||||
|
||||
216
apps/web/src/components/NeymarCast/index.tsx
Normal file
216
apps/web/src/components/NeymarCast/index.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import ImageWithLoading from 'apps/web/src/components/ImageWithLoading';
|
||||
import NeymarFrame from 'apps/web/src/components/NeymarFrame';
|
||||
import { fetchCast, NeymarCastData } from 'apps/web/src/utils/frames';
|
||||
import Link from 'next/link';
|
||||
import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Hls from 'hls.js';
|
||||
import { useErrors } from 'apps/web/contexts/Errors';
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Image embed
|
||||
const isImageUrl = (url: string): boolean => {
|
||||
try {
|
||||
// This will trigger an error if it's not a valid URL
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
// Ends with an image format
|
||||
return /\.(jpeg|jpg|gif|png|webp|bmp|svg)$/.test(parsedUrl.pathname);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Video embed
|
||||
const isVideoUrl = (url: string): boolean => url.endsWith('.m3u8') || url.endsWith('.mp4');
|
||||
|
||||
type NativeVideoPlayerProps = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
function NativeVideoPlayer({ url }: NativeVideoPlayerProps) {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
if (Hls.isSupported() && url.endsWith('.m3u8')) {
|
||||
const hls = new Hls();
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(videoRef.current);
|
||||
} else {
|
||||
videoRef.current.src = url;
|
||||
}
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
return <video ref={videoRef} controls muted className="overflow-hidden rounded-2xl" />;
|
||||
}
|
||||
|
||||
// Links in text
|
||||
const WARPCAST_DOMAIN = 'https://warpcast.com';
|
||||
|
||||
const channelRegex = /(^|\s)\/\w+/g;
|
||||
const mentionRegex = /@\w+(\.eth)?/g;
|
||||
const urlRegex = /((https?:\/\/)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})(\/[^\s]*)?)/g;
|
||||
const combinedRegex = new RegExp(
|
||||
`(${channelRegex.source})|(${mentionRegex.source})|(${urlRegex.source})`,
|
||||
'g',
|
||||
);
|
||||
|
||||
const generateUrl = (match: string): string => {
|
||||
if (channelRegex.test(match)) {
|
||||
return `${WARPCAST_DOMAIN}/~/channel${match.trim()}`;
|
||||
} else if (mentionRegex.test(match)) {
|
||||
return `${WARPCAST_DOMAIN}/${match.substring(1)}`;
|
||||
} else if (urlRegex.test(match)) {
|
||||
return match.startsWith('http') ? match : `http://${match}`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
function ParagraphWithLinks({ text }: { text: string }) {
|
||||
const textWithLinks = useMemo(() => {
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
const results: React.ReactNode[] = [];
|
||||
while ((match = combinedRegex.exec(text)) !== null) {
|
||||
const matchIndex = match.index;
|
||||
if (lastIndex < matchIndex) {
|
||||
const justText = text.slice(lastIndex, matchIndex);
|
||||
|
||||
results.push(justText);
|
||||
}
|
||||
|
||||
const matchedUrl = match[0].trim();
|
||||
const url = generateUrl(matchedUrl);
|
||||
results.push(
|
||||
<Link key={matchIndex} href={url} target="_blank" className="break-words text-blue-500">
|
||||
{matchedUrl}
|
||||
</Link>,
|
||||
);
|
||||
|
||||
lastIndex = combinedRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
const justText = text.slice(lastIndex);
|
||||
results.push(justText);
|
||||
}
|
||||
return results;
|
||||
}, [text]);
|
||||
|
||||
return textWithLinks;
|
||||
}
|
||||
|
||||
export default function NeymarCast({
|
||||
identifier,
|
||||
type,
|
||||
}: {
|
||||
identifier: string;
|
||||
type: 'url' | 'hash';
|
||||
}) {
|
||||
const [data, setData] = useState<NeymarCastData['cast']>();
|
||||
const { logError } = useErrors();
|
||||
useEffect(() => {
|
||||
fetchCast({ type, identifier })
|
||||
.then((result) => {
|
||||
if (result) setData(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
logError(error, 'Failed to load Cast');
|
||||
});
|
||||
}, [identifier, logError, type]);
|
||||
|
||||
const onClickCast = useCallback(
|
||||
(event: MouseEvent<HTMLButtonElement>) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
const isLink = target.tagName === 'a';
|
||||
const parentIsLink = target.closest('a');
|
||||
|
||||
if (isLink || parentIsLink) return;
|
||||
|
||||
window.open(identifier, '_blank');
|
||||
},
|
||||
[identifier],
|
||||
);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const frames = data.frames ?? [];
|
||||
const framesUrls = frames.map((frame) => frame.frames_url);
|
||||
const embeds = data.embeds ?? [];
|
||||
|
||||
// Frames are both in .frames and .embed
|
||||
const filteredEmbeds = embeds.filter((embed) => !framesUrls.includes(embed.url));
|
||||
const { hash, text, author, parent_url: parentUrl } = data;
|
||||
|
||||
const textParagraph = text.split('\n\n');
|
||||
|
||||
const castWrapperClasses = classNames(
|
||||
'flex cursor-pointer flex-col gap-4 text-left',
|
||||
'max-h-[30rem] overflow-hidden rounded-3xl border border-gray-40/20 p-8 relative',
|
||||
'hover:border-blue-500 transition-all',
|
||||
"after:content-[''] after:absolute after:w-full after:h-[2rem] after:bottom-0 after:left-0",
|
||||
'after:bg-gradient-to-b after:from-transparent after:to-white',
|
||||
);
|
||||
|
||||
return (
|
||||
<button className={castWrapperClasses} onClick={onClickCast} type="button">
|
||||
{author && (
|
||||
<Link href={parentUrl} target="_blank">
|
||||
<header className="flex items-center gap-4">
|
||||
{author.pfp_url && (
|
||||
<ImageWithLoading
|
||||
src={author.pfp_url}
|
||||
wrapperClassName="rounded-full h-[3rem] max-h-[3rem] min-h-[3rem] w-[3rem] min-w-[3rem] max-w-[3rem]"
|
||||
imageClassName="object-cover min-h-full min-w-full"
|
||||
alt={`${author.display_name} Profile picture`}
|
||||
width={48}
|
||||
height={48}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<strong className="block">{author.display_name}</strong>
|
||||
<span className="text-gray-40">@{author.username}</span>
|
||||
</div>
|
||||
</header>
|
||||
</Link>
|
||||
)}
|
||||
{textParagraph.length > 0 && (
|
||||
<ul className="flex flex-col gap-2">
|
||||
{textParagraph.map((paragraph, index) => (
|
||||
// It's fine to disable index warning here since the order will never change (static cast)
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<li key={`${paragraph}_${index}`}>
|
||||
<ParagraphWithLinks text={paragraph} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{filteredEmbeds.length > 0 && (
|
||||
<ul>
|
||||
{filteredEmbeds.map((embed, index) => (
|
||||
<li key={embed.url}>
|
||||
{embed.url && isImageUrl(embed.url) && (
|
||||
<ImageWithLoading
|
||||
src={`${embed.url}_${index}`}
|
||||
alt="image"
|
||||
wrapperClassName="rounded-3xl overflow-hidden"
|
||||
/>
|
||||
)}
|
||||
{embed.url && isVideoUrl(embed.url) && <NativeVideoPlayer url={embed.url} />}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{frames.length > 0 && (
|
||||
<ul className="flex flex-col gap-2">
|
||||
{frames.map((frame) => (
|
||||
<li key={frame.title}>
|
||||
<NeymarFrame frame={frame} hash={hash} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
16
apps/web/src/components/NeymarFrame/index.tsx
Normal file
16
apps/web/src/components/NeymarFrame/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import ImageWithLoading from 'apps/web/src/components/ImageWithLoading';
|
||||
import { NeynarFrame } from 'apps/web/src/utils/frames';
|
||||
|
||||
// Frame displayed from Neymar API data
|
||||
// No buttons or interactions for now, just a link to the frame source
|
||||
export default function NeymarFrame({ frame }: { hash: string; frame: NeynarFrame }) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-3xl border border-gray-40/20">
|
||||
{frame.frames_url && (
|
||||
<a href={frame.frames_url} target="_blank" rel="noopener noreferrer">
|
||||
<ImageWithLoading src={frame.image} alt={`Frame image for ${frame.frames_url}`} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export default function useReadBaseEnsTextRecords({
|
||||
[UsernameTextRecordKeys.Email]: '',
|
||||
[UsernameTextRecordKeys.Phone]: '',
|
||||
[UsernameTextRecordKeys.Avatar]: '',
|
||||
[UsernameTextRecordKeys.Casts]: '',
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -56,9 +56,9 @@ export default function useWriteBaseEnsTextRecords({
|
||||
const updateTextRecords = useCallback(
|
||||
(key: UsernameTextRecordKeys, value: string) => {
|
||||
setUpdatedTextRecords((previousTextRecords) => ({
|
||||
...previousTextRecords,
|
||||
[key]: value,
|
||||
}));
|
||||
...previousTextRecords,
|
||||
[key]: value,
|
||||
}));
|
||||
},
|
||||
[setUpdatedTextRecords],
|
||||
);
|
||||
|
||||
66
apps/web/src/utils/frames.ts
Normal file
66
apps/web/src/utils/frames.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { encodeUrlQueryParams } from 'apps/web/src/utils/urls';
|
||||
|
||||
// TODO: There's way more that neymar returns but we only need this for now
|
||||
export type NeymarButton = {
|
||||
action_type: string;
|
||||
index: number;
|
||||
post_url: string;
|
||||
title: string;
|
||||
target?: string;
|
||||
};
|
||||
|
||||
export type NeynarFrame = {
|
||||
buttons: NeymarButton[];
|
||||
frames_url: string;
|
||||
image: string;
|
||||
image_aspect_ratio: string;
|
||||
title: string;
|
||||
post_url?: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export type NeynarEmbed = {
|
||||
// metadata: {};
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type NeymarEmbedCast = {
|
||||
cast_id: { fid: number; hash: string };
|
||||
};
|
||||
|
||||
export type NeymarCastData = {
|
||||
cast: {
|
||||
frames: NeynarFrame[];
|
||||
embeds: NeynarEmbed[];
|
||||
hash: string;
|
||||
author: {
|
||||
display_name: string;
|
||||
pfp_url: string;
|
||||
username: string;
|
||||
};
|
||||
text: string;
|
||||
parent_url: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function fetchCast({
|
||||
identifier,
|
||||
type,
|
||||
}: {
|
||||
identifier: string;
|
||||
type: 'url' | 'hash';
|
||||
}) {
|
||||
const url = `https://api.neynar.com/v2/farcaster/cast?${encodeUrlQueryParams({
|
||||
identifier,
|
||||
type,
|
||||
})}`;
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: { accept: 'application/json', api_key: process.env.NEXT_PUBLIC_NEYNAR_API_KEY },
|
||||
};
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const data = (await response.json()) as NeymarCastData;
|
||||
return data.cast;
|
||||
} catch (error) {}
|
||||
}
|
||||
@@ -48,11 +48,13 @@ export const USERNAME_DESCRIPTION_MAX_LENGTH = 200;
|
||||
|
||||
// DANGER: Changing this post-mainnet launch means the stored data won't be accessible via the updated key
|
||||
export enum UsernameTextRecordKeys {
|
||||
// Defaults
|
||||
Description = 'description',
|
||||
Keywords = 'keywords',
|
||||
Url = 'url',
|
||||
Email = 'email',
|
||||
Phone = 'phone',
|
||||
Avatar = 'avatar',
|
||||
|
||||
// Socials
|
||||
Github = 'com.github',
|
||||
@@ -62,7 +64,8 @@ export enum UsernameTextRecordKeys {
|
||||
Telegram = 'org.telegram',
|
||||
Discord = 'com.discord',
|
||||
|
||||
Avatar = 'avatar',
|
||||
// Basename specifics
|
||||
Casts = 'casts',
|
||||
}
|
||||
|
||||
// The social enabled for the current registration / profile pages
|
||||
@@ -149,6 +152,7 @@ export const textRecordsKeysEnabled = [
|
||||
UsernameTextRecordKeys.Telegram,
|
||||
UsernameTextRecordKeys.Discord,
|
||||
UsernameTextRecordKeys.Avatar,
|
||||
UsernameTextRecordKeys.Casts,
|
||||
];
|
||||
|
||||
export const textRecordsKeysForDisplay = {
|
||||
@@ -164,6 +168,7 @@ export const textRecordsKeysForDisplay = {
|
||||
[UsernameTextRecordKeys.Telegram]: 'Telegram',
|
||||
[UsernameTextRecordKeys.Discord]: 'Discord',
|
||||
[UsernameTextRecordKeys.Avatar]: 'Avatar',
|
||||
[UsernameTextRecordKeys.Casts]: 'Pinned Casts',
|
||||
};
|
||||
|
||||
export const textRecordsKeysPlaceholderForDisplay = {
|
||||
@@ -179,6 +184,7 @@ export const textRecordsKeysPlaceholderForDisplay = {
|
||||
[UsernameTextRecordKeys.Telegram]: 'Username',
|
||||
[UsernameTextRecordKeys.Discord]: 'Username',
|
||||
[UsernameTextRecordKeys.Avatar]: 'Avatar',
|
||||
[UsernameTextRecordKeys.Casts]: 'https://warpcast.com/...',
|
||||
};
|
||||
|
||||
export const textRecordsEngineersKeywords = [
|
||||
|
||||
@@ -380,6 +380,7 @@ __metadata:
|
||||
eslint-config-next: ^13.1.6
|
||||
ethers: 5.7.2
|
||||
framer-motion: ^8.5.5
|
||||
hls.js: ^1.5.14
|
||||
is-ipfs: ^8.0.4
|
||||
jose: ^5.4.1
|
||||
jsonwebtoken: ^9.0.2
|
||||
@@ -15477,6 +15478,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hls.js@npm:^1.5.14":
|
||||
version: 1.5.14
|
||||
resolution: "hls.js@npm:1.5.14"
|
||||
checksum: 7e8385dd188cc2fbf089d3daed12a4b0905f1468be1b1598ddba07edc9d398f9639a054b8f2a73c2d09a177eecc6559c0030ee051611f109e23559004d953d3e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hmac-drbg@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "hmac-drbg@npm:1.0.1"
|
||||
|
||||
Reference in New Issue
Block a user