This repository has no description
0

Configure Feed

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

feat: add frontend

+307 -27
+16
bun.lock
··· 5 5 "name": "takes", 6 6 "dependencies": { 7 7 "@sentry/bun": "^9.10.1", 8 + "@types/react": "^19.1.2", 9 + "@types/react-dom": "^19.1.2", 8 10 "bottleneck": "^2.19.5", 9 11 "colors": "^1.4.0", 10 12 "drizzle-kit": "^0.30.6", 11 13 "drizzle-orm": "^0.41.0", 14 + "react": "^19.1.0", 15 + "react-dom": "^19.1.0", 12 16 "slack-edge": "^1.3.7", 13 17 "yaml": "^2.7.1", 14 18 }, ··· 189 193 190 194 "@types/pg-pool": ["@types/pg-pool@2.0.6", "", { "dependencies": { "@types/pg": "*" } }, "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ=="], 191 195 196 + "@types/react": ["@types/react@19.1.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw=="], 197 + 198 + "@types/react-dom": ["@types/react-dom@19.1.2", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw=="], 199 + 192 200 "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], 193 201 194 202 "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], ··· 208 216 "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], 209 217 210 218 "colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="], 219 + 220 + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 211 221 212 222 "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], 213 223 ··· 275 285 276 286 "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], 277 287 288 + "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], 289 + 290 + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], 291 + 278 292 "require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="], 279 293 280 294 "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], 281 295 282 296 "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], 297 + 298 + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], 283 299 284 300 "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], 285 301
+4
package.json
··· 21 21 }, 22 22 "dependencies": { 23 23 "@sentry/bun": "^9.10.1", 24 + "@types/react": "^19.1.2", 25 + "@types/react-dom": "^19.1.2", 24 26 "bottleneck": "^2.19.5", 25 27 "colors": "^1.4.0", 26 28 "drizzle-kit": "^0.30.6", 27 29 "drizzle-orm": "^0.41.0", 30 + "react": "^19.1.0", 31 + "react-dom": "^19.1.0", 28 32 "slack-edge": "^1.3.7", 29 33 "yaml": "^2.7.1" 30 34 }
+10
public/index.html
··· 1 + <!doctype html> 2 + <html> 3 + <head> 4 + <title>Smokie's Home</title> 5 + </head> 6 + <body> 7 + <div id="root"></div> 8 + <script type="module" src="../src/features/frontend/index.tsx"></script> 9 + </body> 10 + </html>
+7 -2
src/features/api/routes/recentTakes.ts
··· 1 - import { eq, desc, and } from "drizzle-orm"; 1 + import { eq, desc, and, or } from "drizzle-orm"; 2 2 import { db } from "../../../libs/db"; 3 3 import { takes as takesTable } from "../../../libs/schema"; 4 4 import { handleApiError } from "../../../libs/apiError"; ··· 8 8 const recentTakes = await db 9 9 .select() 10 10 .from(takesTable) 11 - .where(eq(takesTable.status, "approved")) 11 + .where( 12 + or( 13 + eq(takesTable.status, "approved"), 14 + eq(takesTable.status, "uploaded"), 15 + ), 16 + ) 12 17 .orderBy(desc(takesTable.completedAt)) 13 18 .limit(40); 14 19
+117
src/features/frontend/app.tsx
··· 1 + import { useEffect, useState } from "react"; 2 + import { prettyPrintTime } from "../../libs/time"; 3 + import { fetchUserData } from "../../libs/cachet"; 4 + 5 + export function App() { 6 + const [takes, setTakes] = useState< 7 + { 8 + id: string; 9 + userId: string; 10 + description: string; 11 + completedAt: Date; 12 + status: string; 13 + mp4Url: string; 14 + elapsedTime: number; 15 + }[] 16 + >([]); 17 + 18 + const [userData, setUserData] = useState<{ 19 + [key: string]: { displayName: string; imageUrl: string }; 20 + }>({}); 21 + useEffect(() => { 22 + async function loadUserData() { 23 + const userIds = takes.map((take) => take.userId); 24 + const uniqueIds = [...new Set(userIds)]; 25 + try { 26 + for (const id of uniqueIds) { 27 + const data = await fetchUserData(id); 28 + setUserData((prevData) => ({ 29 + ...prevData, 30 + [id]: { 31 + displayName: data.displayName, 32 + imageUrl: data.image, 33 + }, 34 + })); 35 + } 36 + } catch (error) { 37 + console.error("Error fetching user data:", error); 38 + } 39 + } 40 + loadUserData(); 41 + }, [takes]); 42 + 43 + useEffect(() => { 44 + async function getTakes() { 45 + const res = await fetch("/api/recentTakes"); 46 + const data = await res.json(); 47 + setTakes(data.takes); 48 + } 49 + getTakes(); 50 + }, []); 51 + 52 + return ( 53 + <div className="container"> 54 + <h1 className="title">Recent Takes</h1> 55 + <div className="takes-grid"> 56 + {takes.map((take) => ( 57 + <div key={take.id} className="take-card"> 58 + <div className="take-header"> 59 + <h2 className="take-title">{take.description}</h2> 60 + <div className="user-pill"> 61 + <div className="user-info"> 62 + <img 63 + src={userData[take.userId]?.imageUrl} 64 + alt="Profile" 65 + className="profile-image" 66 + /> 67 + <span className="user-name"> 68 + {userData[take.userId]?.displayName ?? 69 + take.userId} 70 + </span> 71 + </div> 72 + <span 73 + className={`status-badge status-${take.status}`} 74 + > 75 + {take.status} 76 + </span> 77 + </div> 78 + </div> 79 + 80 + <div className="take-meta"> 81 + <div className="meta-item"> 82 + <span className="meta-label">Completed:</span> 83 + <span className="meta-value"> 84 + {new Date( 85 + take.completedAt, 86 + ).toLocaleString()} 87 + </span> 88 + </div> 89 + <div className="meta-item"> 90 + <span className="meta-label">Duration:</span> 91 + <span className="meta-value"> 92 + {prettyPrintTime(take.elapsedTime)} 93 + </span> 94 + </div> 95 + </div> 96 + 97 + {take.mp4Url && ( 98 + <div className="video-container"> 99 + <video controls className="take-video"> 100 + <source 101 + src={take.mp4Url} 102 + type="video/mp4" 103 + /> 104 + <track 105 + kind="captions" 106 + src="" 107 + label="Captions" 108 + /> 109 + </video> 110 + </div> 111 + )} 112 + </div> 113 + ))} 114 + </div> 115 + </div> 116 + ); 117 + }
+10
src/features/frontend/index.tsx
··· 1 + import "./styles.css"; 2 + import { createRoot } from "react-dom/client"; 3 + import { App } from "./app.tsx"; 4 + 5 + document.addEventListener("DOMContentLoaded", () => { 6 + const element = document.getElementById("root"); 7 + if (!element) throw new Error("Root element not found"); 8 + const root = createRoot(element); 9 + root.render(<App />); 10 + });
+105
src/features/frontend/styles.css
··· 1 + body { 2 + background-color: #f5f5f5; 3 + font-family: 4 + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, 5 + sans-serif; 6 + line-height: 1.6; 7 + color: #333; 8 + margin: 0; 9 + padding: 20px; 10 + min-height: 100vh; 11 + } 12 + 13 + .container { 14 + max-width: 1200px; 15 + margin: 0 auto; 16 + padding: 2rem; 17 + } 18 + 19 + .title { 20 + font-size: 2.5rem; 21 + margin-bottom: 2rem; 22 + text-align: center; 23 + } 24 + 25 + .takes-grid { 26 + display: grid; 27 + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 28 + gap: 2rem; 29 + } 30 + 31 + .take-card { 32 + background: white; 33 + border-radius: 12px; 34 + padding: 1.5rem; 35 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 36 + } 37 + 38 + .take-header { 39 + display: flex; 40 + justify-content: space-between; 41 + align-items: center; 42 + margin-bottom: 1rem; 43 + } 44 + 45 + .take-title { 46 + font-size: 1.25rem; 47 + margin: 0; 48 + } 49 + 50 + .profile-image { 51 + width: 1.5rem; 52 + border-radius: 50%; 53 + margin-right: 0.5rem; 54 + object-fit: cover; 55 + } 56 + 57 + .user-pill { 58 + display: flex; 59 + align-items: center; 60 + padding: 0rem 0rem 0rem 0.3rem; 61 + border-radius: 999px; 62 + background: #f8f9fa; 63 + gap: 0.75rem; 64 + } 65 + 66 + .user-info { 67 + display: flex; 68 + align-items: center; 69 + } 70 + 71 + .status-badge { 72 + padding: 0.25rem 0.75rem; 73 + border-radius: 999px; 74 + } 75 + 76 + .status-approved { 77 + background: #e6f4ea; 78 + color: #1e7e34; 79 + } 80 + 81 + .take-meta { 82 + margin-bottom: 1rem; 83 + } 84 + 85 + .meta-item { 86 + display: flex; 87 + margin-bottom: 0.5rem; 88 + } 89 + 90 + .meta-label { 91 + font-weight: 500; 92 + margin-right: 0.5rem; 93 + min-width: 80px; 94 + } 95 + 96 + .video-container { 97 + margin-top: 1rem; 98 + border-radius: 8px; 99 + overflow: hidden; 100 + } 101 + 102 + .take-video { 103 + width: 100%; 104 + display: block; 105 + }
+3 -2
src/index.ts
··· 1 1 import { SlackApp } from "slack-edge"; 2 2 3 3 import { takes } from "./features/index"; 4 + import frontend from "../public/index.html"; 4 5 5 6 import { t } from "./libs/template"; 6 7 import { blog } from "./libs/Logger"; ··· 56 57 57 58 Bun.serve({ 58 59 port: process.env.PORT || 3000, 59 - development: environment === "development", 60 + development: environment === "dev", 60 61 routes: { 61 - "/": new Response(`Hello World from ${name}@${version}`), 62 + "/": frontend, 62 63 "/health": new Response("OK"), 63 64 }, 64 65 async fetch(request: Request) {
+12
src/libs/cachet.ts
··· 1 + export async function fetchUserData(userId: string) { 2 + const res = await fetch(`https://cachet.dunkirk.sh/users/${userId}/`); 3 + const json = await res.json(); 4 + 5 + return { 6 + id: json.id, 7 + expiration: json.expiration, 8 + user: json.user, 9 + displayName: json.displayName, 10 + image: json.image, 11 + }; 12 + }
+23 -23
tsconfig.json
··· 1 1 { 2 - "compilerOptions": { 3 - // Environment setup & latest features 4 - "lib": ["esnext"], 5 - "target": "ESNext", 6 - "module": "ESNext", 7 - "moduleDetection": "force", 8 - "jsx": "react-jsx", 9 - "allowJs": true, 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": ["esnext", "dom"], 5 + "target": "ESNext", 6 + "module": "ESNext", 7 + "moduleDetection": "force", 8 + "jsx": "react-jsx", 9 + "allowJs": true, 10 10 11 - // Bundler mode 12 - "moduleResolution": "bundler", 13 - "allowImportingTsExtensions": true, 14 - "verbatimModuleSyntax": true, 15 - "noEmit": true, 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 16 17 - // Best practices 18 - "strict": true, 19 - "skipLibCheck": true, 20 - "noFallthroughCasesInSwitch": true, 21 - "noUncheckedIndexedAccess": true, 17 + // Best practices 18 + "strict": true, 19 + "skipLibCheck": true, 20 + "noFallthroughCasesInSwitch": true, 21 + "noUncheckedIndexedAccess": true, 22 22 23 - // Some stricter flags (disabled by default) 24 - "noUnusedLocals": false, 25 - "noUnusedParameters": false, 26 - "noPropertyAccessFromIndexSignature": false 27 - } 23 + // Some stricter flags (disabled by default) 24 + "noUnusedLocals": false, 25 + "noUnusedParameters": false, 26 + "noPropertyAccessFromIndexSignature": false 27 + } 28 28 }