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) {