An HTML-only Bluesky frontend

Implemented profile class Got requests to /profile/[HANDLE] to show the cooresponding profile

+62 -110
+8 -3
main.ts
··· 1 - Deno.serve((req) => { 1 + import Profile from "./profile.ts"; 2 + 3 + Deno.serve(async (req) => { 2 4 const url = new URL(req.url); 3 5 const path = url.pathname; 4 6 ··· 12 14 /* Profile */ 13 15 const profilePattern = new URLPattern({ pathname: "/profile/:actor/" }); 14 16 if (profilePattern.test(url)) { 15 - const actorName = profilePattern.exec(url)?.pathname.groups.actor; 16 - return new Response(actorName, headers); 17 + const actorName = profilePattern.exec(url)?.pathname.groups.actor!; 18 + 19 + const profile = new Profile(actorName); 20 + 21 + return new Response(await profile.generateProfile(), headers); 17 22 } 18 23 19 24 /* Redirect to homepage */
+1 -4
page.ts
··· 1 - export default class Page { 2 - 3 - } 4 - 1 + export default class Page {}
+53 -16
profile.ts
··· 1 + import { AtpAgent } from "npm:@atproto/api"; 1 2 import Page from "./page.ts"; 2 3 3 - class Profile extends Page { 4 - title: string; 4 + const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 5 + 6 + export default class Profile extends Page { 7 + handle: string; 5 8 6 - constructor(title: string) { 9 + constructor(handle: string) { 7 10 super(); 8 - this.title = title; 11 + this.handle = handle; 9 12 } 10 13 11 - generateHTML() { 14 + async generateProfile() { 15 + const { data } = await agent.api.app.bsky.actor.getProfile({ 16 + actor: this.handle, 17 + }); 18 + const { 19 + avatar, 20 + displayName, 21 + handle, 22 + followersCount, 23 + followsCount, 24 + postsCount, 25 + } = data; 26 + 12 27 return ` 13 - <!DOCTYPE html> 14 - <html> 15 - <head> 16 - <meta charset="utf-8"> 17 - <title>${this.title}</title> 18 - </head> 19 - <body> 20 - <h1>Hello, World!</h1> 21 - <p>This is a simple HTML page generated by Node.js server-side.</p> 22 - </body> 23 - </html> 28 + <div class="profile"> 29 + <table width="95%" cellspacing="0" cellpadding="0"> 30 + <tr> 31 + <td align="center" valign="middle"> 32 + <table width="40%" border="0" cellspacing="0" cellpadding="0"> 33 + <tr> 34 + <td align="right"> 35 + <nobr> 36 + <h1>${displayName}<br> 37 + <sup><sup><small><small>@${handle}</small></small></sup></sup> 38 + </h1> 39 + </nobr> 40 + <small> 41 + <span> 42 + <nobr> 43 + Followers <strong>${followersCount}</strong>&nbsp; 44 + Following <strong>${followsCount}</strong>&nbsp; 45 + Posts <strong>${postsCount}</strong> 46 + </nobr> 47 + </span> 48 + <small> 49 + </td> 50 + <td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td> 51 + <td valign="bottom" align="left"> 52 + <img alt="" draggable="false" src="${avatar}" width="115"> 53 + </td> 54 + 55 + </tr> 56 + </table> 57 + </td> 58 + </tr> 59 + </table> 60 + </div> 24 61 `; 25 62 } 26 63 }
-87
server.ts
··· 1 - import { serve } from "https://deno.land/std@0.105.0/http/server.ts"; 2 - 3 - const server = serve({ port: 8000 }); 4 - 5 - console.log("HTTP webserver running. Access it at: http://localhost:8000/"); 6 - 7 - for await (const request of server) { 8 - const url = request.url; 9 - let profileContent = ""; 10 - 11 - const match = url.match(/^\/profile\/(.+)$/); 12 - if (match) { 13 - const handle = match[1]; 14 - try { 15 - const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${handle}`); 16 - if (response.ok) { 17 - const data = await response.json(); 18 - const { avatar, displayName, handle, description, followersCount, followsCount, postsCount } = data; 19 - 20 - console.log(data); 21 - 22 - if ("" == displayName) 23 - displayName == "No Display Name"; 24 - 25 - profileContent = ` 26 - <div class="profile"> 27 - <table width="95%" cellspacing="0" cellpadding="0"> 28 - <tr> 29 - <td align="center" valign="middle"> 30 - <table width="40%" border="0" cellspacing="0" cellpadding="0"> 31 - <tr> 32 - <td align="right"> 33 - <nobr> 34 - <h1>${displayName}<br> 35 - <sup><sup><small><small>@${handle}</small></small></sup></sup> 36 - </h1> 37 - </nobr> 38 - <small> 39 - <span> 40 - <nobr> 41 - Followers <strong>${followersCount}</strong>&nbsp; 42 - Following <strong>${followsCount}</strong>&nbsp; 43 - Posts <strong>${postsCount}</strong> 44 - </nobr> 45 - </span> 46 - <small> 47 - </td> 48 - <td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td> 49 - <td valign="bottom" align="left"> 50 - <img alt="" draggable="false" src="${avatar}" width="115"> 51 - </td> 52 - 53 - </tr> 54 - </table> 55 - </td> 56 - </tr> 57 - </table> 58 - </div> 59 - `; 60 - } else { 61 - profileContent = "<p>Failed to fetch profile data.</p>"; 62 - } 63 - } catch (error) { 64 - profileContent = `<p>Error: ${error.message}</p>`; 65 - } 66 - } else { 67 - profileContent = "<p>No profile handle provided in the URL.</p>"; 68 - } 69 - 70 - const html = ` 71 - <!DOCTYPE html> 72 - <html lang="en"> 73 - <head> 74 - <meta charset="UTF-8"> 75 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 76 - <meta name="color-scheme" content="light dark"> 77 - <title>HTML Sky</title> 78 - </head> 79 - <body> 80 - ${profileContent} 81 - </body> 82 - </html> 83 - `; 84 - 85 - request.respond({ status: 200, body: html }); 86 - } 87 -