ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

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

fix: popup build modes

mock vs dev vs production now possible

byarielm.fyi ade26c9b 7f2dcdab

verified
+172 -118
+72 -10
packages/extension/README.md
··· 4 4 5 5 ## Development 6 6 7 - **Prerequisites:** 8 - - ATlast dev server must be running at `http://127.0.0.1:8888` 9 - - You must be logged in to ATlast before using the extension 7 + ### Build Modes 8 + 9 + The extension supports three build modes: 10 + 11 + 1. **Mock Mode** (`pnpm run build:mock`) 12 + - For UI/UX testing only 13 + - Shows dev toolbar with state toggle buttons 14 + - No backend or browser API checks 15 + - Uses fake data for all operations 16 + - Perfect for rapid UI development 17 + 18 + 2. **Dev Mode** (`pnpm run build:dev` or `pnpm run build`) 19 + - Full extension functionality with local backend 20 + - Connects to `http://127.0.0.1:8888` 21 + - Requires ATlast dev server running 22 + - Checks server health on startup 23 + - Must be logged in to ATlast 24 + 25 + 3. **Production Mode** (`pnpm run build:prod`) 26 + - Full extension functionality with production backend 27 + - Connects to `https://atlast.byarielm.fyi` 28 + - No server health checks (assumes always online) 29 + - Must be logged in to production ATlast 10 30 11 31 ### Build Extension 12 32 ··· 14 34 # From project root: 15 35 cd packages/extension 16 36 pnpm install 17 - pnpm run build # Dev build (uses http://127.0.0.1:8888) 18 - pnpm run build:prod # Production build (uses https://atlast.byarielm.fyi) 37 + 38 + # Choose your build mode: 39 + pnpm run build:mock # Mock mode - UI testing only 40 + pnpm run build:dev # Dev mode - local backend (default) 41 + pnpm run build:prod # Production mode - production backend 19 42 ``` 20 43 21 - The built extension will be in `dist/chrome/`. 44 + The built extension will be in: 45 + - Chrome: `dist/chrome/` 46 + - Firefox: `dist/firefox/` 22 47 23 - ### Load in Chrome for Testing 48 + ### Load in Browser for Testing 24 49 50 + **Chrome/Edge:** 25 51 1. Open Chrome and navigate to `chrome://extensions` 26 52 2. Enable **Developer mode** (toggle in top right) 27 53 3. Click **Load unpacked** 28 54 4. Select the `packages/extension/dist/chrome/` directory 29 55 5. The extension should now appear in your extensions list 30 56 57 + **Firefox:** 58 + 1. Open Firefox and navigate to `about:debugging#/runtime/this-firefox` 59 + 2. Click **Load Temporary Add-on** 60 + 3. Navigate to `packages/extension/dist/firefox/` and select `manifest.json` 61 + 4. The extension will appear in your extensions list 62 + 63 + **Important:** After rebuilding, click the reload button next to the extension in the browser's extension management page. 64 + 31 65 ### Testing the Extension 32 66 33 - #### Step 0: Start ATlast Dev Server 67 + #### Testing Mock Mode 68 + 69 + Mock mode is perfect for UI testing without backend dependencies: 70 + 71 + 1. Build in mock mode: `pnpm run build:mock` 72 + 2. Load extension in browser 73 + 3. Open popup - you'll see a dev toolbar at the bottom 74 + 4. Click buttons to toggle between states (Idle, Ready, Scraping, Complete, Error, etc.) 75 + 5. Test UI layouts, colors, and interactions 76 + 77 + #### Testing Dev/Prod Mode 78 + 79 + For full functionality testing: 80 + 81 + **Step 0: Start ATlast Dev Server** (Dev mode only) 34 82 35 83 ```bash 36 84 # From project root: ··· 103 151 [Popup] Ready 104 152 ``` 105 153 154 + #### Error Handling 155 + 156 + The extension now shows proper error states in the UI instead of browser alerts: 157 + 158 + - **Upload Errors**: Shows specific error message with troubleshooting tips based on build mode 159 + - **Scraping Errors**: Guides user to correct page and provides context-specific help 160 + - **Server Offline** (Dev mode only): Shows instructions to start local server 161 + - **Not Logged In**: Provides button to open ATlast login page 162 + 163 + All errors include: 164 + - User-friendly message explaining what went wrong 165 + - Specific troubleshooting steps to resolve the issue 166 + - Technical details in collapsible section for debugging 167 + 106 168 #### Common Issues 107 169 108 170 **Issue: Extension shows "Not logged in to ATlast"** 109 171 110 172 Solution: 111 - 1. Open `http://127.0.0.1:8888` in a new tab 173 + 1. Click "Open ATlast" button in the error state 112 174 2. Log in with your Bluesky handle 113 175 3. Return to extension and click "Check Again" 114 176 115 - **Issue: Extension shows "ATlast server not running"** 177 + **Issue: Extension shows "Server not available"** (Dev mode only) 116 178 117 179 Solution: 118 180 1. Start dev server: `npx netlify-cli dev --filter @atlast/web`
+14 -6
packages/extension/build.js
··· 9 9 const __dirname = path.dirname(fileURLToPath(import.meta.url)); 10 10 11 11 const watch = process.argv.includes('--watch'); 12 - const isProd = process.argv.includes('--prod') || process.env.NODE_ENV === 'production'; 13 - const mode = isProd ? 'production' : 'development'; 12 + 13 + // Determine build mode: mock, dev, or prod 14 + let mode = 'dev'; // default 15 + if (process.argv.includes('--mock')) { 16 + mode = 'mock'; 17 + } else if (process.argv.includes('--prod') || process.env.NODE_ENV === 'production') { 18 + mode = 'prod'; 19 + } else if (process.argv.includes('--dev')) { 20 + mode = 'dev'; 21 + } 14 22 15 23 // Environment-specific configuration 16 - const ATLAST_API_URL = mode === 'production' 24 + const ATLAST_API_URL = mode === 'prod' 17 25 ? 'https://atlast.byarielm.fyi' 18 26 : 'http://127.0.0.1:8888'; 19 27 ··· 30 38 // Build configuration base 31 39 const buildConfigBase = { 32 40 bundle: true, 33 - minify: !watch, 34 - sourcemap: watch ? 'inline' : false, 41 + minify: mode === 'prod' && !watch, 42 + sourcemap: mode !== 'prod' || watch ? 'inline' : false, 35 43 target: 'es2020', 36 44 format: 'esm', 37 45 define: { ··· 113 121 114 122 // Import cssnano dynamically for production minification 115 123 const plugins = [tailwindcss, autoprefixer]; 116 - if (isProd) { 124 + if (mode === 'prod') { 117 125 const cssnano = (await import('cssnano')).default; 118 126 plugins.push(cssnano); 119 127 }
-44
packages/extension/manifest.json
··· 1 - { 2 - "manifest_version": 3, 3 - "name": "ATlast Importer", 4 - "version": "1.0.0", 5 - "description": "Import your Twitter/X follows to find them on Bluesky", 6 - "permissions": [ 7 - "activeTab", 8 - "storage" 9 - ], 10 - "host_permissions": [ 11 - "https://twitter.com/*", 12 - "https://x.com/*", 13 - "http://127.0.0.1:8888/*", 14 - "http://localhost:8888/*", 15 - "https://atlast.byarielm.fyi/*" 16 - ], 17 - "background": { 18 - "service_worker": "background/service-worker.js", 19 - "type": "module" 20 - }, 21 - "content_scripts": [ 22 - { 23 - "matches": [ 24 - "https://twitter.com/*", 25 - "https://x.com/*" 26 - ], 27 - "js": ["content/index.js"], 28 - "run_at": "document_idle" 29 - } 30 - ], 31 - "action": { 32 - "default_popup": "popup/popup.html", 33 - "default_icon": { 34 - "16": "assets/icon-16.png", 35 - "48": "assets/icon-48.png", 36 - "128": "assets/icon-128.png" 37 - } 38 - }, 39 - "icons": { 40 - "16": "assets/icon-16.png", 41 - "48": "assets/icon-48.png", 42 - "128": "assets/icon-128.png" 43 - } 44 - }
+4 -2
packages/extension/package.json
··· 5 5 "private": true, 6 6 "type": "module", 7 7 "scripts": { 8 - "build": "node build.js", 8 + "build": "node build.js --dev", 9 + "build:mock": "node build.js --mock", 10 + "build:dev": "node build.js --dev", 9 11 "build:prod": "node build.js --prod", 10 - "dev": "node build.js --watch", 12 + "dev": "node build.js --dev --watch", 11 13 "dev:popup": "vite", 12 14 "package:chrome": "cd dist/chrome && zip -r ../chrome.zip .", 13 15 "package:firefox": "cd dist/firefox && zip -r ../firefox.zip .",
+17 -20
packages/extension/src/popup/popup.html
··· 30 30 <!-- Idle state --> 31 31 <div 32 32 id="state-idle" 33 - class="w-full text-center flex flex-col justify-center" 33 + class="hidden w-full text-center flex flex-col justify-center" 34 34 > 35 35 <div class="text-5xl mb-4">🔍</div> 36 36 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 48 48 <!-- Ready state --> 49 49 <div 50 50 id="state-ready" 51 - class="w-full text-center flex flex-col justify-center" 51 + class="hidden w-full text-center flex flex-col justify-center" 52 52 > 53 53 <div class="text-5xl mb-4">✅</div> 54 54 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 65 65 <!-- Scraping state --> 66 66 <div 67 67 id="state-scraping" 68 - class="w-full text-center flex flex-col justify-center" 68 + class="hidden w-full text-center flex flex-col justify-center" 69 69 > 70 70 <div class="text-5xl mb-4 spinner">⏳</div> 71 71 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 91 91 <!-- Complete state --> 92 92 <div 93 93 id="state-complete" 94 - class="w-full text-center flex flex-col justify-center" 94 + class="hidden w-full text-center flex flex-col justify-center" 95 95 > 96 96 <div class="text-5xl mb-4">🎉</div> 97 97 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 117 117 <!-- Uploading state --> 118 118 <div 119 119 id="state-uploading" 120 - class="w-full text-center flex flex-col justify-center" 120 + class="hidden w-full text-center flex flex-col justify-center" 121 121 > 122 122 <div class="text-5xl mb-4 spinner">📤</div> 123 123 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 128 128 <!-- Error state --> 129 129 <div 130 130 id="state-error" 131 - class="w-full text-center flex flex-col justify-center" 131 + class="hidden w-full text-center flex flex-col justify-center" 132 132 > 133 133 <div class="text-5xl mb-4">⚠️</div> 134 134 <p ··· 140 140 class="text-left text-[13px] text-purple-900 dark:text-cyan-100 mt-2 p-3 bg-white/50 dark:bg-slate-900/50 rounded border-l-[3px] border-orange-600" 141 141 > 142 142 <p class="font-semibold mb-2">Try these steps:</p> 143 - <ul id="error-tips-list" class="list-disc pl-4 space-y-1"> 144 - </ul> 143 + <ul 144 + id="error-tips-list" 145 + class="list-disc pl-4 space-y-1" 146 + ></ul> 145 147 </div> 146 148 <details class="mt-3 text-left"> 147 149 <summary ··· 165 167 <!-- Server offline state --> 166 168 <div 167 169 id="state-offline" 168 - class="w-full flex flex-col justify-center text-center" 170 + class="hidden w-full flex flex-col justify-center text-center" 169 171 > 170 172 <div class="text-5xl mb-4">🔌</div> 171 173 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 183 185 </p> 184 186 <button 185 187 id="btn-check-server" 186 - class="w-full px-6 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-lg font-medium shadow-md hover:shadow-lg transition-all mt-4" 188 + class="w-full px-6 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-lg font-bold shadow-md hover:shadow-lg transition-all mt-4" 187 189 > 188 190 Check Again 189 191 </button> ··· 192 194 <!-- Not logged in state --> 193 195 <div 194 196 id="state-not-logged-in" 195 - class="w-full text-center flex flex-col justify-center" 197 + class="hidden w-full text-center flex flex-col justify-center" 196 198 > 197 199 <div class="text-5xl mb-4">🔐</div> 198 200 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> 199 - Not logged in to ATlast 200 - </p> 201 - <p 202 - class="text-[13px] text-red-600 dark:text-red-400 mt-2 p-3 bg-white-50 dark:bg-slate-950/50 rounded border-l-2 border-red-600" 203 - > 204 - Please log in to ATlast first, then return here to scan. 201 + Log in to ATlast to begin 205 202 </p> 206 203 <button 207 204 id="btn-open-atlast" ··· 211 208 </button> 212 209 <button 213 210 id="btn-retry-login" 214 - class="w-full px-4 py-2 text-purple-750 dark:text-cyan-250 hover:text-purple-950 dark:hover:text-cyan-50 transition-colors mt-2" 211 + class="w-full px-4 pt-2 text-purple-750 dark:text-cyan-250 hover:text-purple-950 dark:hover:text-cyan-50 transition-colors mt-2" 215 212 > 216 213 Check Again 217 214 </button> ··· 220 217 221 218 <footer class="text-left"> 222 219 <div 223 - class="mt-4 px-10 py-2 -indent-6 border-orange-650/50 dark:border-amber-400/50 bg-white/50 dark:bg-slate-900/50" 220 + class="mt-2 px-10 py-2 -indent-6 border-orange-650/50 dark:border-amber-400/50 bg-white/50 dark:bg-slate-900/50" 224 221 > 225 222 <p class="text-sm text-purple-900 dark:text-cyan-100"> 226 223 💡 <strong>Need help?</strong> Contact @byarielm.fyi on ··· 242 239 </footer> 243 240 </div> 244 241 245 - <script type="module" src="popup.ts"></script> 242 + <script type="module" src="popup.js"></script> 246 243 </body> 247 244 </html>
+65 -36
packages/extension/src/popup/popup.ts
··· 7 7 } from "../lib/messaging.js"; 8 8 9 9 // Build mode injected at build time 10 - declare const __BUILD_MODE__: string; 11 - const IS_DEV_MODE = __BUILD_MODE__ === "development"; 10 + declare const __BUILD_MODE__: "mock" | "dev" | "prod"; 11 + const IS_MOCK_MODE = __BUILD_MODE__ === "mock"; 12 + const IS_DEV_MODE = __BUILD_MODE__ === "dev"; 13 + const IS_PROD_MODE = __BUILD_MODE__ === "prod"; 12 14 13 15 /** 14 16 * DOM elements ··· 331 333 } 332 334 333 335 /** 334 - * Initialize dev mode 336 + * Initialize mock mode (UI testing with dev toolbar, no backend) 335 337 */ 336 - function initDevMode(): void { 337 - console.log("[Popup Dev] Initializing development mode..."); 338 + function initMockMode(): void { 339 + console.log("[Popup Mock] Initializing mock mode..."); 338 340 339 341 // Inject dev UI 340 342 injectDevBanner(); ··· 350 352 351 353 // Set up event listeners for dev mode 352 354 elements.btnStart.addEventListener("click", () => { 353 - console.log("[Popup Dev] Start scan clicked"); 355 + console.log("[Popup Mock] Start scan clicked"); 354 356 elements.btnStart.disabled = true; 355 357 devSimulateScraping(); 356 358 }); 357 359 358 360 elements.btnUpload.addEventListener("click", () => { 359 - console.log("[Popup Dev] Upload clicked"); 360 - alert("In dev mode - would open ATlast with results!"); 361 + console.log("[Popup Mock] Upload clicked"); 362 + alert("In mock mode - would open ATlast with results!"); 361 363 }); 362 364 363 365 elements.btnRetry.addEventListener("click", () => { 364 - console.log("[Popup Dev] Retry clicked"); 366 + console.log("[Popup Mock] Retry clicked"); 365 367 devState = { 366 368 status: "ready", 367 369 platform: "twitter", ··· 372 374 }); 373 375 374 376 elements.btnCheckServer.addEventListener("click", () => { 375 - console.log("[Popup Dev] Check server clicked"); 377 + console.log("[Popup Mock] Check server clicked"); 376 378 devState = { 377 379 status: "ready", 378 380 platform: "twitter", ··· 382 384 }); 383 385 384 386 elements.btnOpenAtlast.addEventListener("click", () => { 385 - console.log("[Popup Dev] Open ATlast clicked"); 387 + console.log("[Popup Mock] Open ATlast clicked"); 386 388 window.open("http://127.0.0.1:8888", "_blank"); 387 389 }); 388 390 389 391 elements.btnRetryLogin.addEventListener("click", () => { 390 - console.log("[Popup Dev] Retry login clicked"); 392 + console.log("[Popup Mock] Retry login clicked"); 391 393 devState = { 392 394 status: "ready", 393 395 platform: "twitter", ··· 396 398 updateUI(devState); 397 399 }); 398 400 399 - console.log("[Popup Dev] Ready"); 401 + console.log("[Popup Mock] Ready"); 400 402 } 401 403 402 404 // ============================================================================ ··· 418 420 pollForUpdates(); 419 421 } catch (error) { 420 422 console.error("[Popup] Error starting scrape:", error); 421 - alert("Error: Make sure you are on a Twitter/X Following page"); 423 + 424 + // Show error state instead of alert 425 + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; 426 + const errorState: ExtensionState = { 427 + status: "error", 428 + error: errorMessage, 429 + errorCategory: "SCRAPE_START_ERROR", 430 + errorUserMessage: "Failed to start scan", 431 + errorTroubleshootingTips: [ 432 + "Make sure you're on a Twitter/X Following page", 433 + "Navigate to x.com/following or twitter.com/following", 434 + "Refresh the page and try again", 435 + ], 436 + }; 437 + 438 + updateUI(errorState); 422 439 elements.btnStart.disabled = false; 423 440 } 424 441 } ··· 470 487 browser.tabs.create({ url: resultsUrl }); 471 488 } catch (error) { 472 489 console.error("[Popup] Error uploading:", error); 473 - alert("Error uploading to ATlast. Please try again."); 490 + 491 + // Show error state instead of alert 492 + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; 493 + const errorState: ExtensionState = { 494 + status: "error", 495 + error: errorMessage, 496 + errorCategory: "UPLOAD_ERROR", 497 + errorUserMessage: "Failed to upload results to ATlast", 498 + errorTroubleshootingTips: [ 499 + IS_PROD_MODE 500 + ? "Make sure you're logged in at atlast.byarielm.fyi" 501 + : "Make sure the dev server is running at http://127.0.0.1:8888", 502 + "Check your internet connection", 503 + "Try uploading again", 504 + ], 505 + }; 506 + 507 + updateUI(errorState); 474 508 elements.btnUpload.disabled = false; 475 - showState("complete"); 476 509 } 477 510 } 478 511 ··· 518 551 console.log("[Popup] Server is offline"); 519 552 showState("offline"); 520 553 521 - // Show appropriate message based on build mode 554 + // Show appropriate message - dev instructions only in dev mode 522 555 const apiUrl = getApiUrl(); 523 - const isDev = __BUILD_MODE__ === "development"; 524 556 525 - // Hide dev instructions in production 526 - if (!isDev) { 557 + // Hide dev instructions in non-dev modes 558 + if (!IS_DEV_MODE) { 527 559 elements.devInstructions.classList.add("hidden"); 528 560 } 529 561 530 - elements.serverUrl.textContent = isDev 562 + elements.serverUrl.textContent = IS_DEV_MODE 531 563 ? `Development server at ${apiUrl}` 532 564 : `Cannot reach ${apiUrl}`; 533 565 ··· 539 571 } 540 572 541 573 /** 542 - * Initialize production mode 574 + * Initialize real mode (dev or prod with actual backend) 543 575 */ 544 - async function initProdMode(): Promise<void> { 545 - console.log("[Popup] Initializing popup..."); 546 - 547 - // Check server health first (only in dev mode) 548 - const { getApiUrl } = await import("../lib/api-client.js"); 549 - const isDev = 550 - getApiUrl().includes("127.0.0.1") || getApiUrl().includes("localhost"); 576 + async function initRealMode(): Promise<void> { 577 + console.log(`[Popup] Initializing in ${__BUILD_MODE__} mode...`); 551 578 552 - if (isDev) { 579 + // Check server health only in dev mode 580 + if (IS_DEV_MODE) { 553 581 const serverOnline = await checkServer(); 554 582 if (!serverOnline) { 555 583 // Set up retry button ··· 560 588 const online = await checkServer(); 561 589 if (online) { 562 590 // Server is back online, re-initialize 563 - initProdMode(); 591 + initRealMode(); 564 592 } else { 565 593 elements.btnCheckServer.disabled = false; 566 594 elements.btnCheckServer.textContent = "Check Again"; ··· 572 600 573 601 // Check if user is logged in to ATlast 574 602 console.log("[Popup] Checking login status..."); 575 - const { checkSession } = await import("../lib/api-client.js"); 603 + const { checkSession, getApiUrl } = await import("../lib/api-client.js"); 576 604 const session = await checkSession(); 577 605 578 606 if (!session) { ··· 591 619 const newSession = await checkSession(); 592 620 if (newSession) { 593 621 // User is now logged in, re-initialize 594 - initProdMode(); 622 + initRealMode(); 595 623 } else { 596 624 elements.btnRetryLogin.disabled = false; 597 625 elements.btnRetryLogin.textContent = "Check Again"; ··· 643 671 // ============================================================================ 644 672 645 673 function init(): void { 646 - if (IS_DEV_MODE) { 647 - initDevMode(); 674 + if (IS_MOCK_MODE) { 675 + initMockMode(); 648 676 } else { 649 - initProdMode(); 677 + // Both dev and prod modes use real backend 678 + initRealMode(); 650 679 } 651 680 } 652 681