Sifa professional network frontend (Next.js, React, TailwindCSS)
sifa.id/
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})();