diff --git a/src/assets/social/DynamicSocialIcon.tsx b/src/assets/social/DynamicSocialIcon.tsx index c662b8b32..ebe9606e3 100644 --- a/src/assets/social/DynamicSocialIcon.tsx +++ b/src/assets/social/DynamicSocialIcon.tsx @@ -13,9 +13,14 @@ export const socialIconTypes = { 'com.youtube': dynamic(() => import('./SocialYoutube.svg')), 'org.telegram': dynamic(() => import('./SocialTelegram.svg')), 'xyz.mirror': dynamic(() => import('./SocialMirrorColour.svg')), + 'xyz.poap': dynamic(() => import('./SocialPoap.svg')), + 'xyz.farcaster': dynamic(() => import('./SocialFarcaster.svg')), + 'co.zora': dynamic(() => import('./SocialZora.svg')), email: dynamic(() => import('@ensdomains/thorin').then((m) => m.EnvelopeSVG)), } +export type SocialIconType = keyof typeof socialIconTypes + export const socialIconColors = { 'com.discord': '#5A57DD', 'com.discourse': undefined, @@ -25,6 +30,9 @@ export const socialIconColors = { 'com.youtube': '#FF0000', 'org.telegram': '#2BABEE', 'xyz.mirror': undefined, + 'xyz.poap': '#6534FF', + 'xyz.farcaster': '##6A3CFF', + 'co.zora': undefined, email: '#000000', } diff --git a/src/assets/social/SocialFarcaster.svg b/src/assets/social/SocialFarcaster.svg new file mode 100644 index 000000000..74dacc32d --- /dev/null +++ b/src/assets/social/SocialFarcaster.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/social/SocialPoap.svg b/src/assets/social/SocialPoap.svg new file mode 100644 index 000000000..abffb92f8 --- /dev/null +++ b/src/assets/social/SocialPoap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/social/SocialZora.svg b/src/assets/social/SocialZora.svg new file mode 100644 index 000000000..2bd12fda3 --- /dev/null +++ b/src/assets/social/SocialZora.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/constants/customAccountRecordKeys.ts b/src/constants/customAccountRecordKeys.ts new file mode 100644 index 000000000..2b4817adc --- /dev/null +++ b/src/constants/customAccountRecordKeys.ts @@ -0,0 +1,111 @@ +/** + * Custom Account Records Configuration + * + * This file contains all configuration for custom account record types (like POAP). + * It serves as a model for how social accounts could be refactored in the future, + * with all data consolidated in one location. + */ + +import { type SocialIconType } from '@app/assets/social/DynamicSocialIcon' +import { normaliseTwitterRecordValue } from '@app/utils/records/normaliseTwitterRecordValue' + +/* eslint-disable @typescript-eslint/naming-convention */ + +/** + * Configuration object for a custom account record type + */ +export type CustomAccountConfig = { + key: K + icon: SocialIconType + color?: string + label: string + type: 'link' | 'copy' + getUrl?: (value: string) => string + getValue?: (value: string) => string +} + +/** + * Registry of all custom account record configurations. + * This is the single source of truth - ensures every key has exactly one config. + * + * To add a new custom account: + * 1. Add a new property to this registry object + * 2. Register the icon in src/assets/social/DynamicSocialIcon.tsx + * 3. Create the corresponding SVG file + */ +const customAccountRegistry = { + 'xyz.poap': { + key: 'xyz.poap', + icon: 'xyz.poap', + color: '#6534FF', + label: 'POAP', + type: 'link', + getUrl: (value) => `https://collectors.poap.xyz/scan/${value}`, + }, + 'xyz.farcaster': { + key: 'xyz.farcaster', + icon: 'xyz.farcaster', + color: '#6A3CFF', + label: 'Farcaster', + type: 'link', + getUrl: (value) => `https://farcaster.xyz/${value}`, + }, + 'co.zora': { + key: 'co.zora', + icon: 'co.zora', + label: 'Zora', + type: 'link', + getValue: normaliseTwitterRecordValue, + getUrl: (value) => `https://zora.co/@${value}`, + }, +} as const satisfies Record + +/** + * TypeScript type for custom account record keys. + * Derived directly from the registry to ensure consistency. + */ +export type CustomAccountRecordKey = keyof typeof customAccountRegistry + +/** + * Array of custom account record keys. + * Derived from the registry - do not modify directly. + */ +export const customAccountRecordKeys = Object.keys( + customAccountRegistry, +) as CustomAccountRecordKey[] + +/** + * Array of custom account configurations. + * Derived from the registry - do not modify directly. + */ +export const customAccountRecords: ReadonlyArray> = + Object.values(customAccountRegistry) + +/** + * Map for O(1) config lookups + */ +const customAccountConfigMap = new Map(customAccountRecords.map((config) => [config.key, config])) + +/** + * Check if a key is a custom account record key + */ +export function isCustomAccountKey(key: string): key is CustomAccountRecordKey { + return customAccountConfigMap.has(key as CustomAccountRecordKey) +} + +/** + * Get display data for a custom account + */ +export function getCustomAccountData(key: string, value: string) { + const config = customAccountConfigMap.get(key as CustomAccountRecordKey) + if (!config) return null + + return { + icon: config.icon, + color: config.color, + label: config.label, + value: config.getValue ? config.getValue(value) : value, + type: config.type, + urlFormatter: config.getUrl?.(value), + } +} diff --git a/src/utils/getSocialData.ts b/src/utils/getSocialData.ts index 2f643c4bd..72019a3ed 100644 --- a/src/utils/getSocialData.ts +++ b/src/utils/getSocialData.ts @@ -1,57 +1,94 @@ +import { getCustomAccountData } from '@app/constants/customAccountRecordKeys' + import { normaliseTwitterRecordValue } from './records/normaliseTwitterRecordValue' -export const getSocialData = (iconKey: string, value: string) => { - switch (iconKey) { - case 'twitter': - case 'com.twitter': - case 'x': - case 'com.x': - return { - icon: 'com.twitter', - color: '#000000', - label: 'X', - value: normaliseTwitterRecordValue(value), - type: 'link', - urlFormatter: `https://x.com/${value.replace(/^@/, '')}`, - } - case 'github': - case 'com.github': - return { - icon: 'com.github', - color: '#000000', - label: 'GitHub', - value, - type: 'link', - urlFormatter: `https://github.com/${value}`, - } - case 'discord': - case 'com.discord': - return { - icon: 'com.discord', - color: '#5A57DD', - label: 'Discord', - value, - type: 'copy', - } - case 'telegram': - case 'org.telegram': - return { - icon: 'org.telegram', - color: '#2BABEE', - label: 'Telegram', - value, - type: 'link', - urlFormatter: `https://t.me/${value}`, - } - case 'email': - return { - icon: 'email', - color: '#000000', - label: 'Email', - value, - type: 'copy', - } - default: - return null +type SocialDataConfig = { + icon: string + color: string + label: string + type: 'link' | 'copy' + getValue?: (value: string) => string + getUrl?: (value: string) => string +} + +// Define base configurations +const twitterConfig: SocialDataConfig = { + icon: 'com.twitter', + color: '#000000', + label: 'X', + type: 'link', + getValue: normaliseTwitterRecordValue, + getUrl: (value) => `https://x.com/${value.replace(/^@/, '')}`, +} + +const githubConfig: SocialDataConfig = { + icon: 'com.github', + color: '#000000', + label: 'GitHub', + type: 'link', + getUrl: (value) => `https://github.com/${value}`, +} + +const discordConfig: SocialDataConfig = { + icon: 'com.discord', + color: '#5A57DD', + label: 'Discord', + type: 'copy', +} + +const telegramConfig: SocialDataConfig = { + icon: 'org.telegram', + color: '#2BABEE', + label: 'Telegram', + type: 'link', + getUrl: (value) => `https://t.me/${value}`, +} + +/* eslint-disable @typescript-eslint/naming-convention */ +const socialDataMap: Record = { + // Twitter/X aliases + twitter: twitterConfig, + 'com.twitter': twitterConfig, + x: twitterConfig, + 'com.x': twitterConfig, + + // GitHub aliases + github: githubConfig, + 'com.github': githubConfig, + + // Discord aliases + discord: discordConfig, + 'com.discord': discordConfig, + + // Telegram aliases + telegram: telegramConfig, + 'org.telegram': telegramConfig, + + // Email + email: { + icon: 'email', + color: '#000000', + label: 'Email', + type: 'copy', + }, +} +/* eslint-enable @typescript-eslint/naming-convention */ + +export function getSocialData(iconKey: string, value: string) { + // Check custom account records first + const customData = getCustomAccountData(iconKey, value) + if (customData) return customData + + // Then check standard social records + const config = socialDataMap[iconKey] + if (!config) return null + + return { + icon: config.icon, + color: config.color, + label: config.label, + value: config.getValue?.(value) ?? value, + type: config.type, + urlFormatter: config.getUrl?.(value), } } diff --git a/src/utils/records/categoriseProfileTextRecords.ts b/src/utils/records/categoriseProfileTextRecords.ts index 0b2f45b96..4e08c2ba7 100644 --- a/src/utils/records/categoriseProfileTextRecords.ts +++ b/src/utils/records/categoriseProfileTextRecords.ts @@ -1,5 +1,6 @@ import type { DecodedText } from '@ensdomains/ensjs/dist/types' +import { isCustomAccountKey } from '@app/constants/customAccountRecordKeys' import { supportedGeneralRecordKeys, SupportedGeneralRecordsKey, @@ -44,10 +45,12 @@ export const categoriseAndTransformTextRecords = ({ }>( (acc, record) => { const normalisedRecord = normaliseProfileAccountsRecord(record) + // Check if it's a supported social record OR a custom account key if ( supportedSocialRecordKeys.includes( normalisedRecord.normalisedKey as unknown as SupportedSocialRecordKey, - ) + ) || + isCustomAccountKey(record.key) ) { const normalisedRecordWithVerifications = appendVerificationProps?.(normalisedRecord) || normalisedRecord