[READ-ONLY] Mirror of https://github.com/mrgnw/morganwill.com.
morganwill.com
7.4 kB
276 lines
1/**
2 * @typedef {Object} Link
3 * @property {string} title
4 * @property {string} alias
5 * @property {string} url
6 * @property {string} blurb
7 * @property {string[]} colors
8 * @property {string} [qr]
9 * @property {boolean} [strokeIcon]
10 */
11
12/**
13 * @typedef {Object} LinkTemplate
14 * @property {string} title - Unique identifier for the link type
15 * @property {string} alias - Short alias for URL params
16 * @property {string} [urlTemplate] - URL template with {value} placeholder (omit for hardcoded)
17 * @property {string} [url] - Hardcoded URL (for non-overridable links like blog, email)
18 * @property {string} blurb - Display description
19 * @property {string[]} colors - Brand colors
20 * @property {boolean} [strokeIcon] - Icon uses stroke instead of fill
21 * @property {string} [envVar] - Environment variable name for the value
22 */
23
24/** @type {LinkTemplate[]} */
25export const linkTemplates = [
26 {
27 title: "instagram",
28 alias: "ig",
29 url: "https://instagram.com/zenfo.co",
30 blurb: "Instagram photo portfolio",
31 colors: ["#833ab4", "#fd1d1d", "#fcb045"],
32 },
33 {
34 title: "linkedin",
35 alias: "li",
36 url: "https://linkedin.com/in/mrgnw",
37 blurb: "LinkedIn profile",
38 colors: ["#0A66C2", "#004182"],
39 },
40 {
41 title: "github",
42 alias: "gh",
43 url: "https://github.com/mrgnw",
44 blurb: "GitHub profile",
45 colors: ["#6e5494", "#24292e"],
46 strokeIcon: true,
47 },
48 {
49 title: "blog",
50 alias: "blog",
51 url: "https://blog.morganwill.com",
52 blurb: "Blog",
53 colors: ["#ff6b6b", "#ee5a24"],
54 },
55 {
56 title: "bluesky",
57 alias: "bsky",
58 url: "https://bsky.app/profile/xcc.es",
59 blurb: "Bluesky profile",
60 colors: ["#0085ff", "#00c2ff"],
61 },
62 {
63 title: "discord",
64 alias: "dsc",
65 url: "http://discordapp.com/users/mrgnw#5838",
66 blurb: "Message on Discord",
67 colors: ["#5865F2", "#404EED"],
68 },
69 {
70 title: "telegram",
71 alias: "tg",
72 url: "https://t.me/mrgnw",
73 blurb: "Message on Telegram",
74 colors: ["#26A5E4", "#0088cc"],
75 strokeIcon: true,
76 },
77 {
78 title: "phone",
79 alias: "phone",
80 urlTemplate: "sms:{value}",
81 blurb: "Text me",
82 colors: ["#34c759", "#28a745"],
83 envVar: "PHONE_NUMBER",
84 },
85 {
86 title: "whatsapp",
87 alias: "wa",
88 urlTemplate: "https://wa.me/{value}",
89 blurb: "Message on WhatsApp",
90 colors: ["#25D366", "#128C7E"],
91 envVar: "WHATSAPP_NUMBER",
92 },
93 {
94 title: "line",
95 alias: "ln",
96 urlTemplate: "https://line.me/ti/p/~{value}",
97 blurb: "Message on LINE",
98 colors: ["#00B900", "#009C00"],
99 strokeIcon: true,
100 envVar: "LINE_ID",
101 },
102 {
103 title: "signal",
104 alias: "sig",
105 urlTemplate: "https://signal.me/#p/{value}",
106 blurb: "Message on Signal",
107 colors: ["#3A76F0", "#2D5FBE"],
108 strokeIcon: true,
109 envVar: "SIGNAL_ID",
110 },
111 {
112 title: "email",
113 alias: "email",
114 urlTemplate: "mailto:{value}",
115 blurb: "Send an email",
116 colors: ["#EA4335", "#FBBC05"],
117 envVar: "CONTACT_EMAIL",
118 },
119 {
120 title: "duolingo",
121 alias: "duo",
122 url: "https://invite.duolingo.com/profile-share/mrgnw",
123 blurb: "Add me on Duolingo",
124 colors: ["#58CC02", "#1CB0F6"],
125 },
126 {
127 title: "cv",
128 alias: "cv",
129 url: "https://cv.morganwill.com",
130 blurb: "View CV / Resume",
131 colors: ["#4A90D9", "#2C5282"],
132 strokeIcon: true,
133 },
134];
135
136/**
137 * @typedef {Object} Link
138 * @property {string} title
139 * @property {string} alias
140 * @property {string} url
141 * @property {string} blurb
142 * @property {string[]} colors
143 * @property {string} [qr]
144 * @property {boolean} [strokeIcon]
145 */
146
147/**
148 * Build a link from a template, with value from env var or param
149 * @param {LinkTemplate} template
150 * @param {string | null} paramValue - Value from URL params
151 * @param {string | null} envValue - Value from environment variable
152 * @returns {Link | null} - Returns null if no value is available
153 */
154export function buildLink(template, paramValue, envValue) {
155 // Priority: params > env > hardcoded url
156 let value = paramValue ?? envValue;
157
158 // If we have a hardcoded URL, use it (no template needed)
159 if (template.url) {
160 return {
161 title: template.title,
162 alias: template.alias,
163 url: template.url,
164 blurb: template.blurb,
165 colors: template.colors,
166 strokeIcon: template.strokeIcon,
167 };
168 }
169
170 // If we have a template, we need a value
171 if (template.urlTemplate) {
172 // For WhatsApp, strip non-numeric characters from the value
173 if (template.title === "whatsapp" && value) {
174 value = value.replace(/[^0-9]/g, "");
175 }
176
177 // Only return link if we have a value
178 if (value) {
179 return {
180 title: template.title,
181 alias: template.alias,
182 url: template.urlTemplate.replace("{value}", value),
183 blurb: template.blurb,
184 colors: template.colors,
185 strokeIcon: template.strokeIcon,
186 };
187 }
188 }
189
190 return null;
191}
192
193/**
194 * @typedef {Object} CustomLinkDefinition
195 * @property {string} type - The link type (matches template title)
196 * @property {string} value - The value to use (e.g., phone number, username)
197 * @property {string} [name] - Optional custom title/name
198 * @property {string} [alias] - Optional custom alias
199 */
200
201/**
202 * Parse CUSTOM_LINKS JSON from environment and generate link templates
203 * @param {string | null | undefined} customLinksJson - JSON string from env var
204 * @param {Map<string, number>} existingCounts - Map of type to count of existing links
205 * @returns {LinkTemplate[]} - Array of custom link templates
206 */
207export function parseCustomLinks(customLinksJson, existingCounts) {
208 if (!customLinksJson) return [];
209
210 try {
211 /** @type {CustomLinkDefinition[]} */
212 const customLinks = JSON.parse(customLinksJson);
213
214 if (!Array.isArray(customLinks)) {
215 console.warn("CUSTOM_LINKS must be an array");
216 return [];
217 }
218
219 // Track how many custom links we've added per type
220 /** @type {Map<string, number>} */
221 const customCounts = new Map();
222
223 return customLinks
224 .map((custom) => {
225 // Find the base template for this type
226 const baseTemplate = linkTemplates.find((t) => t.title === custom.type);
227
228 if (!baseTemplate) {
229 console.warn(`Unknown link type: ${custom.type}`);
230 return null;
231 }
232
233 // Determine name and alias
234 let name = custom.name;
235 let alias = custom.alias;
236
237 if (!name || !alias) {
238 // Calculate the suffix number
239 const existingCount = existingCounts.get(custom.type) || 0;
240 const customCount = customCounts.get(custom.type) || 0;
241 const totalCount = existingCount + customCount;
242
243 if (totalCount === 0) {
244 // First link of this type - use default name and alias
245 name = name || baseTemplate.title;
246 alias = alias || baseTemplate.alias;
247 } else {
248 // Additional link - add number suffix
249 const suffix = totalCount + 1;
250 name = name || `${baseTemplate.title}${suffix}`;
251 alias = alias || `${baseTemplate.alias}${suffix}`;
252 }
253 }
254
255 // Increment custom count for this type
256 customCounts.set(custom.type, (customCounts.get(custom.type) || 0) + 1);
257
258 // Create a new template with the custom value hardcoded as env var
259 return {
260 title: name,
261 alias: alias,
262 urlTemplate: baseTemplate.urlTemplate,
263 url: baseTemplate.url,
264 blurb: baseTemplate.blurb,
265 colors: baseTemplate.colors,
266 strokeIcon: baseTemplate.strokeIcon,
267 envVar: `__CUSTOM_${name.toUpperCase()}__`, // Special env var marker
268 __customValue: custom.value, // Store the value directly
269 };
270 })
271 .filter(Boolean);
272 } catch (error) {
273 console.warn("Failed to parse CUSTOM_LINKS:", error);
274 return [];
275 }
276}