diff --git a/web/src/components/Header.astro b/web/src/components/Header.astro index f4c70fb..f186858 100644 --- a/web/src/components/Header.astro +++ b/web/src/components/Header.astro @@ -160,6 +160,7 @@ const splashText = showSplashText ? sample(splashTexts) : null = { label: string /** Notice that the first capture group is then used to format the value */ matcher: RegExp - formatter: (value: string) => string | null + formatter: (match: RegExpMatchArray) => string | null icon: string } @@ -24,82 +24,96 @@ export const { label: type ? transformCase(type, 'title') : String(type), icon: 'ri:shield-fill', matcher: /(.*)/, - formatter: (value) => value, + formatter: ([, value]) => value ?? String(value), }), [ { type: 'email', label: 'Email', matcher: /mailto:(.+)/, - formatter: (value) => value, + formatter: ([, value]) => value ?? 'Email', icon: 'ri:mail-line', }, { type: 'telephone', label: 'Telephone', matcher: /tel:(.+)/, - formatter: (value) => { - return parsePhoneNumberWithError(value).formatInternational() + formatter: ([, value]) => { + return value ? parsePhoneNumberWithError(value).formatInternational() : 'Telephone' }, icon: 'ri:phone-line', }, { type: 'whatsapp', label: 'WhatsApp', - matcher: /https?:\/\/(?:www\.)?wa\.me\/(.+)/, - formatter: (value) => { - return parsePhoneNumberWithError(value).formatInternational() + matcher: /^https?:\/\/(?:www\.)?wa\.me\/(.+)/, + formatter: ([, value]) => { + return value ? parsePhoneNumberWithError(value).formatInternational() : 'WhatsApp' }, icon: 'ri:whatsapp-line', }, { type: 'telegram', label: 'Telegram', - matcher: /https?:\/\/(?:www\.)?t\.me\/(.+)/, - formatter: (value) => `t.me/${value}`, + matcher: /^https?:\/\/(?:www\.)?t\.me\/(.+)/, + formatter: ([, value]) => (value ? `t.me/${value}` : 'Telegram'), icon: 'ri:telegram-line', }, { type: 'linkedin', label: 'LinkedIn', - matcher: /https?:\/\/(?:www\.)?linkedin\.com\/(?:in|company)\/(.+)/, - formatter: (value) => `in/${value}`, + matcher: /^https?:\/\/(?:www\.)?linkedin\.com\/(?:in|company)\/(.+)/, + formatter: ([, value]) => (value ? `in/${value}` : 'LinkedIn'), icon: 'ri:linkedin-box-line', }, { type: 'x', label: 'X', - matcher: /https?:\/\/(?:www\.)?x\.com\/(.+)/, - formatter: (value) => `@${value}`, + matcher: /^https?:\/\/(?:www\.)?x\.com\/(.+)/, + formatter: ([, value]) => (value ? `@${value}` : 'X'), icon: 'ri:twitter-x-line', }, { type: 'instagram', label: 'Instagram', - matcher: /https?:\/\/(?:www\.)?instagram\.com\/(.+)/, - formatter: (value) => `@${value}`, + matcher: /^https?:\/\/(?:www\.)?instagram\.com\/(.+)/, + formatter: ([, value]) => (value ? `@${value}` : 'Instagram'), icon: 'ri:instagram-line', }, { type: 'matrix', label: 'Matrix', - matcher: /https?:\/\/(?:www\.)?matrix\.to\/#\/(.+)/, - formatter: (value) => value, + matcher: /^https?:\/\/(?:www\.)?matrix\.to\/#\/(.+)/, + formatter: ([, value]) => (value ? `#${value}` : 'Matrix'), icon: 'ri:hashtag', }, { type: 'bitcointalk', label: 'BitcoinTalk', - matcher: /https?:\/\/(?:www\.)?bitcointalk\.org/, + matcher: /^https?:\/\/(?:www\.)?bitcointalk\.org/, formatter: () => 'BitcoinTalk', icon: 'ri:btc-line', }, - // Website must go last because it's a catch-all { + type: 'simplex', + label: 'SimpleX Chat', + matcher: /^https?:\/\/(?:www\.)?(simplex\.chat)\//, + formatter: () => 'SimpleX Chat', + icon: 'simplex', + }, + { + type: 'nostr', + label: 'Nostr', + matcher: /\b(npub1[a-zA-Z0-9]{58})\b/, + formatter: () => 'Nostr', + icon: 'nostr', + }, + { + // Website must go last because it's a catch-all type: 'website', label: 'Website', - matcher: /https?:\/\/(?:www\.)?((?:[a-zA-Z0-9-]+\.)+[a-zA-Z]+)/, - formatter: (value) => value, + matcher: /^https?:\/\/(?:www\.)?((?:[a-zA-Z0-9-]+\.)+[a-zA-Z]+)/, + formatter: ([, value]) => value ?? 'Website', icon: 'ri:global-line', }, ] as const satisfies ContactMethodInfo[] @@ -107,10 +121,10 @@ export const { export function formatContactMethod(url: string) { for (const contactMethod of contactMethods) { - const captureGroup = url.match(contactMethod.matcher)?.[1] - if (!captureGroup) continue + const match = url.match(contactMethod.matcher) + if (!match) continue - const formattedValue = contactMethod.formatter(captureGroup) + const formattedValue = contactMethod.formatter(match) if (!formattedValue) continue return { diff --git a/web/src/pages/about.mdx b/web/src/pages/about.mdx index 93e94e0..8277d54 100644 --- a/web/src/pages/about.mdx +++ b/web/src/pages/about.mdx @@ -67,6 +67,15 @@ Once submitted, you get a unique tracking page where you can monitor its status All new listings begin as **unlisted** — they're only accessible via direct link and won't appear in search results. After a brief admin review to confirm the request isn't spam or inappropriate, the listing will be marked as **Community Contributed**. +#### Requirements + +To list a new service, it must fulfill these requirements: + +- Publicly available website explaining what the service is about +- Terms of service or FAQ document + +For example, just a Telegram link is not a valid service. + ### Suggestion Review Process #### First Review diff --git a/web/src/pages/access-denied.astro b/web/src/pages/access-denied.astro index 7f9d7c9..8cf9738 100644 --- a/web/src/pages/access-denied.astro +++ b/web/src/pages/access-denied.astro @@ -50,6 +50,7 @@ if (reasonType === 'admin-required' && Astro.locals.user?.admin) {