Skip to content

Commit e639b07

Browse files
authored
feat: Distributor badges link to relevant source (#210)
1 parent 3ee14d9 commit e639b07

File tree

2 files changed

+293
-4
lines changed

2 files changed

+293
-4
lines changed

apps/app/src/components/DistributorBadges.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,48 @@
11
import { Badge } from "./ui/badge";
2+
import {
3+
getDistributorUrl,
4+
getDistributorDisplayName,
5+
type DistributorConfig,
6+
} from "../lib/distributor-urls";
27

38
export function DistributorBadges({
49
distribute,
510
}: {
6-
distribute: { plugin: string }[];
11+
distribute: DistributorConfig[];
712
}) {
813
return (
914
<div className="flex flex-wrap gap-1 max-w-full overflow-hidden">
1015
{distribute.map((distributor, index) => {
11-
const pluginName = distributor.plugin.replace("@curatedotfun/", "");
16+
const displayName = getDistributorDisplayName(distributor);
17+
const url = getDistributorUrl(distributor);
18+
19+
const badgeContent = (
20+
<Badge className="text-black border border-stone-500 rounded-md bg-stone-50 shadow-none px-2 py-0.5 text-xs whitespace-nowrap flex-shrink-0 cursor-pointer hover:bg-stone-100 transition-colors">
21+
{displayName}
22+
</Badge>
23+
);
24+
25+
// If we have a URL, wrap in a link, otherwise just render the badge
26+
if (url) {
27+
return (
28+
<a
29+
key={`${distributor.plugin}-${index}`}
30+
href={url}
31+
target="_blank"
32+
rel="noopener noreferrer"
33+
className="inline-block"
34+
>
35+
{badgeContent}
36+
</a>
37+
);
38+
}
39+
1240
return (
1341
<Badge
1442
key={`${distributor.plugin}-${index}`}
15-
className="text-black border border-stone-500 rounded-md bg-stone-50 shadow-none capitalize px-2 py-0.5 text-xs whitespace-nowrap flex-shrink-0"
43+
className="text-black border border-stone-500 rounded-md bg-stone-50 shadow-none px-2 py-0.5 text-xs whitespace-nowrap flex-shrink-0"
1644
>
17-
{pluginName}
45+
{displayName}
1846
</Badge>
1947
);
2048
})}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/**
2+
* Distributor URL utility functions
3+
*
4+
* This module provides functions to generate appropriate URLs for different
5+
* distributor types based on their configuration.
6+
*/
7+
8+
export interface DistributorConfig {
9+
plugin: string;
10+
config?: Record<string, unknown>;
11+
}
12+
13+
/**
14+
* Checks if a value is a template variable (contains {{ }})
15+
*/
16+
function isTemplateVariable(value: unknown): boolean {
17+
return typeof value === "string" && value.includes("{{");
18+
}
19+
20+
/**
21+
* Safely gets a string value from config, returning null if it's a template
22+
*/
23+
function getConfigString(
24+
config: Record<string, unknown> | undefined,
25+
key: string,
26+
): string | null {
27+
const value = config?.[key];
28+
if (typeof value === "string" && !isTemplateVariable(value)) {
29+
return value;
30+
}
31+
return null;
32+
}
33+
34+
/**
35+
* Generate URLs for RSS distributor
36+
*/
37+
function getRssUrl(config: Record<string, unknown> | undefined): string | null {
38+
// RSS distributors have a serviceUrl in their config that points to the RSS feed
39+
const serviceUrl = getConfigString(config, "serviceUrl");
40+
return serviceUrl;
41+
}
42+
43+
/**
44+
* Generate URLs for Twitter distributor
45+
*/
46+
function getTwitterUrl(config: Record<string, unknown> | undefined): string {
47+
// Twitter links to the specific profile if available
48+
const username =
49+
getConfigString(config, "username") || getConfigString(config, "handle");
50+
if (username) {
51+
return `https://twitter.com/${username.replace("@", "")}`;
52+
}
53+
return "https://twitter.com";
54+
}
55+
56+
/**
57+
* Generate URLs for Telegram distributor
58+
*/
59+
function getTelegramUrl(config: Record<string, unknown> | undefined): string {
60+
// Telegram links to the channel if chatId is available
61+
const chatId =
62+
getConfigString(config, "chatId") || getConfigString(config, "channelId");
63+
if (chatId) {
64+
const cleanChatId = chatId.replace("@", "");
65+
return `https://t.me/${cleanChatId}`;
66+
}
67+
return "https://telegram.org";
68+
}
69+
70+
/**
71+
* Generate URLs for Notion distributor
72+
*/
73+
function getNotionUrl(config: Record<string, unknown> | undefined): string {
74+
// Notion links to the table/database if available
75+
const databaseId = getConfigString(config, "databaseId");
76+
if (databaseId) {
77+
return `https://notion.so/${databaseId}`;
78+
}
79+
return "https://notion.so";
80+
}
81+
82+
/**
83+
* Generate URLs for Crosspost distributor
84+
*/
85+
function getCrosspostUrl(config: Record<string, unknown> | undefined): string {
86+
// Crosspost links to the Open Crosspost platform
87+
// Could potentially link to specific account if signerId is available
88+
const signerId = getConfigString(config, "signerId");
89+
if (signerId) {
90+
return `https://near.social/mob.near/widget/MainPage.N.Profile.Page?accountId=${signerId}`;
91+
}
92+
return "https://opencrosspost.com";
93+
}
94+
95+
/**
96+
* Generate URLs for Discord distributor
97+
*/
98+
function getDiscordUrl(config: Record<string, unknown> | undefined): string {
99+
// Discord links to the specific channel if available
100+
const channelId = getConfigString(config, "channelId");
101+
if (channelId) {
102+
// Discord channel URLs format: https://discord.com/channels/serverId/channelId
103+
// Since we don't have serverId, we use @me which works for direct channel links
104+
return `https://discord.com/channels/@me/${channelId}`;
105+
}
106+
return "https://discord.com";
107+
}
108+
109+
/**
110+
* Generate URLs for NEAR Social distributor
111+
*/
112+
function getNearSocialUrl(config: Record<string, unknown> | undefined): string {
113+
// NEAR Social links to the specific account if available
114+
const accountId = getConfigString(config, "accountId");
115+
if (accountId) {
116+
return `https://near.social/mob.near/widget/MainPage.N.Profile.Page?accountId=${accountId}`;
117+
}
118+
return "https://near.social";
119+
}
120+
121+
/**
122+
* Generate URLs for Supabase distributor
123+
*/
124+
function getSupabaseUrl(config: Record<string, unknown> | undefined): string {
125+
// Supabase links to the project dashboard if URL is available
126+
const url = getConfigString(config, "url");
127+
if (url) {
128+
// Extract project reference from Supabase URL
129+
const match = url.match(/https:\/\/([^.]+)\.supabase\.co/);
130+
if (match) {
131+
const projectRef = match[1];
132+
return `https://supabase.com/dashboard/project/${projectRef}`;
133+
}
134+
}
135+
return "https://supabase.com";
136+
}
137+
138+
/**
139+
* Get the clean plugin name without the @curatedotfun/ prefix
140+
*/
141+
function getPluginName(plugin: string): string {
142+
return plugin.replace("@curatedotfun/", "");
143+
}
144+
145+
/**
146+
* Main function to get distributor URL based on plugin type and configuration
147+
*/
148+
export function getDistributorUrl(
149+
distributor: DistributorConfig,
150+
): string | null {
151+
const pluginName = getPluginName(distributor.plugin);
152+
153+
switch (pluginName) {
154+
case "rss":
155+
return getRssUrl(distributor.config);
156+
157+
case "twitter":
158+
case "twitter-distributor":
159+
return getTwitterUrl(distributor.config);
160+
161+
case "telegram":
162+
case "telegram-distributor":
163+
return getTelegramUrl(distributor.config);
164+
165+
case "notion":
166+
case "notion-distributor":
167+
return getNotionUrl(distributor.config);
168+
169+
case "crosspost":
170+
case "crosspost-distributor":
171+
return getCrosspostUrl(distributor.config);
172+
173+
case "discord":
174+
case "discord-distributor":
175+
return getDiscordUrl(distributor.config);
176+
177+
case "near-social":
178+
case "near-social-distributor":
179+
case "nearsocial":
180+
return getNearSocialUrl(distributor.config);
181+
182+
case "supabase":
183+
case "supabase-distributor":
184+
return getSupabaseUrl(distributor.config);
185+
186+
default:
187+
// For unknown distributors, return null to show badge without link
188+
return null;
189+
}
190+
}
191+
192+
/**
193+
* Get a display-friendly name for the distributor
194+
*/
195+
export function getDistributorDisplayName(
196+
distributor: DistributorConfig,
197+
): string {
198+
const pluginName = getPluginName(distributor.plugin);
199+
200+
// Convert plugin names to display names
201+
switch (pluginName) {
202+
case "near-social":
203+
case "nearsocial":
204+
return "NEAR Social";
205+
case "rss":
206+
return "RSS";
207+
default:
208+
// Capitalize first letter for other distributors
209+
return pluginName.charAt(0).toUpperCase() + pluginName.slice(1);
210+
}
211+
}
212+
213+
/**
214+
* Check if a distributor has a functional URL (not just a fallback)
215+
*/
216+
export function hasSpecificUrl(distributor: DistributorConfig): boolean {
217+
const pluginName = getPluginName(distributor.plugin);
218+
const config = distributor.config;
219+
220+
switch (pluginName) {
221+
case "rss":
222+
return !!getConfigString(config, "serviceUrl");
223+
224+
case "twitter":
225+
case "twitter-distributor":
226+
return !!(
227+
getConfigString(config, "username") || getConfigString(config, "handle")
228+
);
229+
230+
case "telegram":
231+
case "telegram-distributor":
232+
return !!(
233+
getConfigString(config, "chatId") ||
234+
getConfigString(config, "channelId")
235+
);
236+
237+
case "notion":
238+
case "notion-distributor":
239+
return !!getConfigString(config, "databaseId");
240+
241+
case "crosspost":
242+
case "crosspost-distributor":
243+
return !!getConfigString(config, "signerId");
244+
245+
case "discord":
246+
case "discord-distributor":
247+
return !!getConfigString(config, "channelId");
248+
249+
case "near-social":
250+
case "near-social-distributor":
251+
case "nearsocial":
252+
return !!getConfigString(config, "accountId");
253+
254+
case "supabase":
255+
case "supabase-distributor":
256+
return !!getConfigString(config, "url");
257+
258+
default:
259+
return false;
260+
}
261+
}

0 commit comments

Comments
 (0)