Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/
at main 299 lines 13 kB view raw
1(function () { 2 'use strict'; 3 4 function escapeHtml(str) { 5 var div = document.createElement('div'); 6 div.appendChild(document.createTextNode(str || '')); 7 return div.innerHTML; 8 } 9 10 function getBaseUrl(scriptEl) { 11 var src = scriptEl.getAttribute('src') || ''; 12 try { 13 var url = new URL(src); 14 return url.origin; 15 } catch { 16 return 'https://sifa.id'; 17 } 18 } 19 20 function formatCompact(n) { 21 if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'; 22 if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K'; 23 return String(n); 24 } 25 26 var APP_COLORS = { 27 bluesky: { bg: '#e0f2fe', text: '#075985' }, 28 whitewind: { bg: '#f1f5f9', text: '#334155' }, 29 smokesignal: { bg: '#ffedd5', text: '#9a3412' }, 30 frontpage: { bg: '#ede9fe', text: '#5b21b6' }, 31 picosky: { bg: '#fce7f3', text: '#9d174d' }, 32 linkat: { bg: '#d1fae5', text: '#065f46' }, 33 pastesphere: { bg: '#fef3c7', text: '#92400e' }, 34 }; 35 var FALLBACK_COLOR = { bg: '#f3f4f6', text: '#374151' }; 36 37 var sifaIconSvg = 38 '<svg viewBox="0 0 256 256" class="sifa-icon" role="img" aria-label="Sifa">' + 39 '<g transform="matrix(0.333333,0,0,0.333333,37.583333,37.083333)">' + 40 '<path d="M128,71.5C159.183,71.5 184.5,96.817 184.5,128C184.5,159.183 159.183,184.5 128,184.5C96.817,184.5 71.5,159.183 71.5,128C71.5,96.817 96.817,71.5 128,71.5ZM128,104.5C115.03,104.5 104.5,115.03 104.5,128C104.5,140.97 115.03,151.5 128,151.5C140.97,151.5 151.5,140.97 151.5,128C151.5,115.03 140.97,104.5 128,104.5Z" fill="currentColor"/>' + 41 '</g>' + 42 '<g transform="matrix(0.333333,0,0,0.333333,37.583333,37.083333)">' + 43 '<path d="M174.866,194.259C182.45,189.218 192.7,191.282 197.741,198.866C202.782,206.45 200.718,216.7 193.134,221.741C175.432,233.507 150.846,240.5 128,240.5C66.284,240.5 15.5,189.716 15.5,128C15.5,66.284 66.284,15.5 128,15.5C189.716,15.5 240.5,66.284 240.5,128C240.5,160.538 225.46,184.5 196,184.5C166.54,184.5 151.5,160.538 151.5,128L151.5,88C151.5,78.893 158.893,71.5 168,71.5C177.107,71.5 184.5,78.893 184.5,88L184.5,128C184.5,134.408 185.237,140.363 187.279,145.164C188.851,148.858 191.536,151.5 196,151.5C200.464,151.5 203.149,148.858 204.721,145.164C206.763,140.363 207.5,134.408 207.5,128C207.5,84.388 171.612,48.5 128,48.5C84.388,48.5 48.5,84.388 48.5,128C48.5,171.612 84.388,207.5 128,207.5C144.415,207.5 162.148,202.713 174.866,194.259Z" fill="currentColor"/>' + 44 '</g>' + 45 '<path d="M176,47.75 L208,79.75 L176,111.75 L144,79.75 Z" fill="none" stroke="currentColor" stroke-width="12"/>' + 46 '<path d="M80,144 L112,176 L80,208 L48,176 Z" fill="none" stroke="currentColor" stroke-width="12"/>' + 47 '<path d="M152,192 L176,160 L200,192" fill="none" stroke="currentColor" stroke-width="11"/>' + 48 '</svg>'; 49 50 function buildStyles(theme) { 51 var lightVars = 52 '--sifa-bg:#fff;--sifa-card:#fff;--sifa-text:#111;--sifa-muted:#666;--sifa-border:#e5e5e5;--sifa-primary:#6366f1;'; 53 var darkVars = 54 '--sifa-bg:#1a1a2e;--sifa-card:#16213e;--sifa-text:#eee;--sifa-muted:#888;--sifa-border:#333;--sifa-primary:#6366f1;'; 55 56 var themeBlock = ''; 57 if (theme === 'dark') { 58 themeBlock = ':host{' + darkVars + '}'; 59 } else if (theme === 'light') { 60 themeBlock = ':host{' + lightVars + '}'; 61 } else { 62 themeBlock = 63 ':host{' + lightVars + '}@media(prefers-color-scheme:dark){:host{' + darkVars + '}}'; 64 } 65 66 return ( 67 themeBlock + 68 ":host{display:block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:var(--sifa-text);}" + 69 '.card{background:var(--sifa-card);border:1px solid var(--sifa-border);border-radius:12px;padding:20px;max-width:400px;}' + 70 '.top{display:flex;align-items:flex-start;gap:10px;}' + 71 '.avatar-link{flex-shrink:0;}' + 72 '.info{flex:1;min-width:0;}' + 73 '.name-row a{text-decoration:none;color:inherit;}' + 74 '.name{font-size:15px;font-weight:600;margin:0;}' + 75 '.handle{font-size:12px;color:var(--sifa-muted);margin:0;}' + 76 '.avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;}' + 77 '.avatar-placeholder{width:48px;height:48px;border-radius:50%;background:var(--sifa-primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:600;}' + 78 '.headline{font-size:13px;color:var(--sifa-muted);margin:6px 0 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}' + 79 '.location{font-size:12px;color:var(--sifa-muted);margin:4px 0 0;}' + 80 '.open-to{display:flex;flex-wrap:wrap;gap:4px;margin:8px 0 0;}' + 81 '.pill{font-size:11px;padding:2px 8px;border-radius:10px;background:var(--sifa-primary);color:#fff;}' + 82 '.activity-row{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;font-size:12px;color:var(--sifa-muted);}' + 83 '.app-badges{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;}' + 84 '.app-badge{font-size:10px;font-weight:500;padding:2px 8px;border-radius:10px;}' + 85 '.app-badges-link{text-decoration:none;color:inherit;display:block;}' + 86 '.app-badges-link:hover .app-badge{opacity:0.8;}' + 87 '.app-badge-more{background:var(--sifa-border);color:var(--sifa-muted);font-style:normal;}' + 88 '.footer{margin-top:12px;padding-top:10px;border-top:1px solid var(--sifa-border);display:flex;align-items:center;justify-content:space-between;}' + 89 '.cta{display:inline-block;font-size:13px;color:var(--sifa-primary);text-decoration:none;font-weight:500;}' + 90 '.cta:hover{text-decoration:underline;}' + 91 '.sifa-icon{width:16px;height:16px;color:var(--sifa-muted);opacity:0.5;}' + 92 '.error{font-size:14px;color:var(--sifa-muted);padding:16px;text-align:center;}' 93 ); 94 } 95 96 function renderCard(data) { 97 var avatarHtml; 98 if (data.avatar) { 99 avatarHtml = 100 '<img class="avatar" src="' + 101 escapeHtml(data.avatar) + 102 '" alt="' + 103 escapeHtml(data.displayName) + 104 '">'; 105 } else { 106 var letter = (data.displayName || data.handle || '?').charAt(0).toUpperCase(); 107 avatarHtml = '<div class="avatar-placeholder">' + escapeHtml(letter) + '</div>'; 108 } 109 110 var headlineHtml = data.headline 111 ? '<div class="headline">' + escapeHtml(data.headline) + '</div>' 112 : ''; 113 114 var locationHtml = data.location 115 ? '<div class="location">' + escapeHtml(data.location) + '</div>' 116 : ''; 117 118 var openToHtml = ''; 119 if (data.openTo && data.openTo.length > 0) { 120 var pills = ''; 121 for (var i = 0; i < data.openTo.length; i++) { 122 pills += '<span class="pill">' + escapeHtml(data.openTo[i]) + '</span>'; 123 } 124 openToHtml = '<div class="open-to">' + pills + '</div>'; 125 } 126 127 // Activity row: follower count + PDS provider 128 // Prefer AT Protocol follower count over Sifa-internal count 129 // (mirrors src/lib/follower-utils.ts resolveDisplayFollowers) 130 var isAtproto = data.atprotoFollowersCount != null && data.atprotoFollowersCount > 0; 131 var displayFollowers = isAtproto ? data.atprotoFollowersCount : data.followersCount; 132 var activityHtml = ''; 133 var activityItems = ''; 134 if (displayFollowers && displayFollowers > 0) { 135 var followerSuffix = isAtproto ? ' followers on Bluesky' : ' followers'; 136 activityItems += 137 '<span>' + escapeHtml(formatCompact(displayFollowers)) + followerSuffix + '</span>'; 138 } 139 if (data.pdsProvider) { 140 activityItems += '<span>on ' + escapeHtml(data.pdsProvider.name) + '</span>'; 141 } 142 if (activityItems) { 143 activityHtml = '<div class="activity-row">' + activityItems + '</div>'; 144 } 145 146 // Active apps indicators (max 2, linked to sifa.id) 147 var appsHtml = ''; 148 if (data.activeApps && data.activeApps.length > 0) { 149 var maxShow = 2; 150 var badges = ''; 151 var shown = Math.min(data.activeApps.length, maxShow); 152 for (var k = 0; k < shown; k++) { 153 var app = data.activeApps[k]; 154 var colors = APP_COLORS[app.id] || FALLBACK_COLOR; 155 badges += 156 '<span class="app-badge" style="background:' + 157 colors.bg + 158 ';color:' + 159 colors.text + 160 '">' + 161 escapeHtml(app.name) + 162 '</span>'; 163 } 164 var overflow = data.activeApps.length - shown; 165 if (overflow > 0) { 166 badges += 167 '<span class="app-badge app-badge-more">and ' + overflow + ' more on sifa.id</span>'; 168 } 169 appsHtml = 170 '<a class="app-badges-link" href="' + 171 escapeHtml(data.profileUrl) + 172 '" target="_blank" rel="noopener">' + 173 '<div class="app-badges">' + 174 badges + 175 '</div></a>'; 176 } 177 178 var footerHtml = 179 '<div class="footer">' + 180 '<a class="cta" href="' + 181 escapeHtml(data.profileUrl) + 182 '" target="_blank" rel="noopener">View on Sifa</a>' + 183 sifaIconSvg + 184 '</div>'; 185 186 return ( 187 '<div class="card">' + 188 '<div class="top">' + 189 '<a class="avatar-link" href="' + 190 escapeHtml(data.profileUrl) + 191 '" target="_blank" rel="noopener">' + 192 avatarHtml + 193 '</a>' + 194 '<div class="info">' + 195 '<div class="name-row"><a href="' + 196 escapeHtml(data.profileUrl) + 197 '" target="_blank" rel="noopener">' + 198 '<p class="name">' + 199 escapeHtml(data.displayName || data.handle) + 200 '</p></a></div>' + 201 '<p class="handle">@' + 202 escapeHtml(data.handle) + 203 '</p>' + 204 '</div>' + 205 '</div>' + 206 headlineHtml + 207 locationHtml + 208 openToHtml + 209 activityHtml + 210 appsHtml + 211 footerHtml + 212 '</div>' 213 ); 214 } 215 216 function initSifaEmbeds() { 217 var scripts = document.querySelectorAll("script[src*='embed.js']"); 218 var promises = []; 219 220 for (var i = 0; i < scripts.length; i++) { 221 (function (scriptEl) { 222 var did = scriptEl.getAttribute('data-did'); 223 var handle = scriptEl.getAttribute('data-handle'); 224 var identifier = did || handle; 225 if (!identifier) return; 226 227 var theme = scriptEl.getAttribute('data-theme') || 'auto'; 228 var baseUrl = getBaseUrl(scriptEl); 229 var apiUrl = baseUrl + '/api/embed/' + encodeURIComponent(identifier) + '/data'; 230 231 var container = document.createElement('div'); 232 container.className = 'sifa-embed'; 233 var shadow = container.attachShadow({ mode: 'open' }); 234 235 scriptEl.parentNode.insertBefore(container, scriptEl.nextSibling); 236 237 var promise = fetch(apiUrl) 238 .then(function (res) { 239 if (!res.ok) throw new Error('Not found'); 240 return res.json(); 241 }) 242 .then(function (data) { 243 var styleEl = document.createElement('style'); 244 styleEl.textContent = buildStyles(theme); 245 shadow.appendChild(styleEl); 246 247 var wrapper = document.createElement('div'); 248 wrapper.innerHTML = renderCard(data); 249 shadow.appendChild(wrapper); 250 251 // Track embed load (excluding sifa.id itself) 252 try { 253 if (window.location.hostname !== 'sifa.id') { 254 fetch(baseUrl + '/u/api/send', { 255 method: 'POST', 256 headers: { 'Content-Type': 'application/json' }, 257 body: JSON.stringify({ 258 type: 'event', 259 payload: { 260 website: '7f659ec9-5d5f-4ee4-96e0-10d8bcefd69d', 261 url: window.location.pathname, 262 name: 'embed-load', 263 hostname: window.location.hostname, 264 data: { handle: identifier, host: window.location.hostname }, 265 }, 266 }), 267 }).catch(function () {}); 268 } 269 } catch (_e) {} 270 }) 271 .catch(function () { 272 var styleEl = document.createElement('style'); 273 styleEl.textContent = buildStyles(theme); 274 shadow.appendChild(styleEl); 275 276 var errDiv = document.createElement('div'); 277 errDiv.innerHTML = '<div class="error">Profile not found</div>'; 278 shadow.appendChild(errDiv); 279 }); 280 281 promises.push(promise); 282 })(scripts[i]); 283 } 284 285 return Promise.all(promises); 286 } 287 288 window.initSifaEmbeds = initSifaEmbeds; 289 290 if (typeof module !== 'undefined' && module.exports) { 291 module.exports = { initSifaEmbeds: initSifaEmbeds }; 292 } 293 294 if (document.readyState === 'loading') { 295 document.addEventListener('DOMContentLoaded', initSifaEmbeds); 296 } else { 297 initSifaEmbeds(); 298 } 299})();