[READ-ONLY] Mirror of https://github.com/mrgnw/morganwill.com. morganwill.com
0

Configure Feed

Select the types of activity you want to include in your feed.

morganwill.com / src / lib / links.js
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}