ATProto Browser

ATProto Browser

Experimental browser for the Atmosphere

Record data

{
  "uri": "at://did:plc:krxbvxvis5skq7jj6eot23ul/sh.tangled.repo.pull/3ln5hd72r2422",
  "cid": "bafyreic5dk4qfx5khpssvbbpzmh6ekrtjholkrpuscpl7ou5x6b35nxqoe",
  "value": {
    "$type": "sh.tangled.repo.pull",
    "patch": "diff --git a/bun.lock b/bun.lock\nindex fc7d5fc50a301cbf281b582e5c831ec486a5b637..a8af3bbbe5c4bf89552d68aa1f386d5c845ca82c 100644\n--- a/bun.lock\n+++ b/bun.lock\n@@ -5,10 +5,14 @@     \"\": {\n       \"name\": \"takes\",\n       \"dependencies\": {\n         \"@sentry/bun\": \"^9.10.1\",\n+        \"@types/react\": \"^19.1.2\",\n+        \"@types/react-dom\": \"^19.1.2\",\n         \"bottleneck\": \"^2.19.5\",\n         \"colors\": \"^1.4.0\",\n         \"drizzle-kit\": \"^0.30.6\",\n         \"drizzle-orm\": \"^0.41.0\",\n+        \"react\": \"^19.1.0\",\n+        \"react-dom\": \"^19.1.0\",\n         \"slack-edge\": \"^1.3.7\",\n         \"yaml\": \"^2.7.1\",\n       },\n@@ -189,6 +193,10 @@     \"@types/pg\": [\"@types/pg@8.6.1\", \"\", { \"dependencies\": { \"@types/node\": \"*\", \"pg-protocol\": \"*\", \"pg-types\": \"^2.2.0\" } }, \"sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==\"],\n \n     \"@types/pg-pool\": [\"@types/pg-pool@2.0.6\", \"\", { \"dependencies\": { \"@types/pg\": \"*\" } }, \"sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==\"],\n \n+    \"@types/react\": [\"@types/react@19.1.2\", \"\", { \"dependencies\": { \"csstype\": \"^3.0.2\" } }, \"sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==\"],\n+\n+    \"@types/react-dom\": [\"@types/react-dom@19.1.2\", \"\", { \"peerDependencies\": { \"@types/react\": \"^19.0.0\" } }, \"sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==\"],\n+\n     \"@types/shimmer\": [\"@types/shimmer@1.2.0\", \"\", {}, \"sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==\"],\n \n     \"@types/tedious\": [\"@types/tedious@4.0.14\", \"\", { \"dependencies\": { \"@types/node\": \"*\" } }, \"sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==\"],\n@@ -208,6 +216,8 @@\n     \"cjs-module-lexer\": [\"cjs-module-lexer@1.4.3\", \"\", {}, \"sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==\"],\n \n     \"colors\": [\"colors@1.4.0\", \"\", {}, \"sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==\"],\n+\n+    \"csstype\": [\"csstype@3.1.3\", \"\", {}, \"sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==\"],\n \n     \"data-uri-to-buffer\": [\"data-uri-to-buffer@4.0.1\", \"\", {}, \"sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==\"],\n \n@@ -275,11 +285,17 @@     \"postgres-interval\": [\"postgres-interval@1.2.0\", \"\", { \"dependencies\": { \"xtend\": \"^4.0.0\" } }, \"sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==\"],\n \n     \"promise-limit\": [\"promise-limit@2.7.0\", \"\", {}, \"sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==\"],\n \n+    \"react\": [\"react@19.1.0\", \"\", {}, \"sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==\"],\n+\n+    \"react-dom\": [\"react-dom@19.1.0\", \"\", { \"dependencies\": { \"scheduler\": \"^0.26.0\" }, \"peerDependencies\": { \"react\": \"^19.1.0\" } }, \"sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==\"],\n+\n     \"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==\"],\n \n     \"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==\"],\n \n     \"resolve-pkg-maps\": [\"resolve-pkg-maps@1.0.0\", \"\", {}, \"sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==\"],\n+\n+    \"scheduler\": [\"scheduler@0.26.0\", \"\", {}, \"sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==\"],\n \n     \"semver\": [\"semver@7.7.1\", \"\", { \"bin\": { \"semver\": \"bin/semver.js\" } }, \"sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==\"],\n \ndiff --git a/package.json b/package.json\nindex 9f08315c26f064ac6a33e1032a58281bff5b5bbb..c8f7b0755a06dd4b413f7ba2638d5fca59fe79f8 100644\n--- a/package.json\n+++ b/package.json\n@@ -21,10 +21,14 @@ \t\t\"typescript\": \"^5\"\n \t},\n \t\"dependencies\": {\n \t\t\"@sentry/bun\": \"^9.10.1\",\n+\t\t\"@types/react\": \"^19.1.2\",\n+\t\t\"@types/react-dom\": \"^19.1.2\",\n \t\t\"bottleneck\": \"^2.19.5\",\n \t\t\"colors\": \"^1.4.0\",\n \t\t\"drizzle-kit\": \"^0.30.6\",\n \t\t\"drizzle-orm\": \"^0.41.0\",\n+\t\t\"react\": \"^19.1.0\",\n+\t\t\"react-dom\": \"^19.1.0\",\n \t\t\"slack-edge\": \"^1.3.7\",\n \t\t\"yaml\": \"^2.7.1\"\n \t}\ndiff --git a/public/index.html b/public/index.html\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..91109ddc4b8af4d3d8abaeb6adec923958777609\n--- /dev/null\n+++ b/public/index.html\n@@ -0,0 +1,10 @@\n+<!doctype html>\n+<html>\n+\t<head>\n+\t\t<title>Smokie's Home</title>\n+\t</head>\n+\t<body>\n+\t\t<div id=\"root\"></div>\n+\t\t<script type=\"module\" src=\"../src/features/frontend/index.tsx\"></script>\n+\t</body>\n+</html>\ndiff --git a/src/features/api/routes/recentTakes.ts b/src/features/api/routes/recentTakes.ts\nindex c2ac0fe7871306ca5b628f28dd1415e79996b82d..193f59adac31233e82ccf46f92067cddccd7f1d8 100644\n--- a/src/features/api/routes/recentTakes.ts\n+++ b/src/features/api/routes/recentTakes.ts\n@@ -1,4 +1,4 @@\n-import { eq, desc, and } from \"drizzle-orm\";\n+import { eq, desc, and, or } from \"drizzle-orm\";\n import { db } from \"../../../libs/db\";\n import { takes as takesTable } from \"../../../libs/schema\";\n import { handleApiError } from \"../../../libs/apiError\";\n@@ -8,7 +8,12 @@ \ttry {\n \t\tconst recentTakes = await db\n \t\t\t.select()\n \t\t\t.from(takesTable)\n-\t\t\t.where(eq(takesTable.status, \"approved\"))\n+\t\t\t.where(\n+\t\t\t\tor(\n+\t\t\t\t\teq(takesTable.status, \"approved\"),\n+\t\t\t\t\teq(takesTable.status, \"uploaded\"),\n+\t\t\t\t),\n+\t\t\t)\n \t\t\t.orderBy(desc(takesTable.completedAt))\n \t\t\t.limit(40);\n \ndiff --git a/src/features/frontend/app.tsx b/src/features/frontend/app.tsx\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..ff2fcbc020a7218b6f27df7aceda10ec40057aaf\n--- /dev/null\n+++ b/src/features/frontend/app.tsx\n@@ -0,0 +1,117 @@\n+import { useEffect, useState } from \"react\";\n+import { prettyPrintTime } from \"../../libs/time\";\n+import { fetchUserData } from \"../../libs/cachet\";\n+\n+export function App() {\n+\tconst [takes, setTakes] = useState<\n+\t\t{\n+\t\t\tid: string;\n+\t\t\tuserId: string;\n+\t\t\tdescription: string;\n+\t\t\tcompletedAt: Date;\n+\t\t\tstatus: string;\n+\t\t\tmp4Url: string;\n+\t\t\telapsedTime: number;\n+\t\t}[]\n+\t>([]);\n+\n+\tconst [userData, setUserData] = useState<{\n+\t\t[key: string]: { displayName: string; imageUrl: string };\n+\t}>({});\n+\tuseEffect(() => {\n+\t\tasync function loadUserData() {\n+\t\t\tconst userIds = takes.map((take) => take.userId);\n+\t\t\tconst uniqueIds = [...new Set(userIds)];\n+\t\t\ttry {\n+\t\t\t\tfor (const id of uniqueIds) {\n+\t\t\t\t\tconst data = await fetchUserData(id);\n+\t\t\t\t\tsetUserData((prevData) => ({\n+\t\t\t\t\t\t...prevData,\n+\t\t\t\t\t\t[id]: {\n+\t\t\t\t\t\t\tdisplayName: data.displayName,\n+\t\t\t\t\t\t\timageUrl: data.image,\n+\t\t\t\t\t\t},\n+\t\t\t\t\t}));\n+\t\t\t\t}\n+\t\t\t} catch (error) {\n+\t\t\t\tconsole.error(\"Error fetching user data:\", error);\n+\t\t\t}\n+\t\t}\n+\t\tloadUserData();\n+\t}, [takes]);\n+\n+\tuseEffect(() => {\n+\t\tasync function getTakes() {\n+\t\t\tconst res = await fetch(\"/api/recentTakes\");\n+\t\t\tconst data = await res.json();\n+\t\t\tsetTakes(data.takes);\n+\t\t}\n+\t\tgetTakes();\n+\t}, []);\n+\n+\treturn (\n+\t\t<div className=\"container\">\n+\t\t\t<h1 className=\"title\">Recent Takes</h1>\n+\t\t\t<div className=\"takes-grid\">\n+\t\t\t\t{takes.map((take) => (\n+\t\t\t\t\t<div key={take.id} className=\"take-card\">\n+\t\t\t\t\t\t<div className=\"take-header\">\n+\t\t\t\t\t\t\t<h2 className=\"take-title\">{take.description}</h2>\n+\t\t\t\t\t\t\t<div className=\"user-pill\">\n+\t\t\t\t\t\t\t\t<div className=\"user-info\">\n+\t\t\t\t\t\t\t\t\t<img\n+\t\t\t\t\t\t\t\t\t\tsrc={userData[take.userId]?.imageUrl}\n+\t\t\t\t\t\t\t\t\t\talt=\"Profile\"\n+\t\t\t\t\t\t\t\t\t\tclassName=\"profile-image\"\n+\t\t\t\t\t\t\t\t\t/>\n+\t\t\t\t\t\t\t\t\t<span className=\"user-name\">\n+\t\t\t\t\t\t\t\t\t\t{userData[take.userId]?.displayName ??\n+\t\t\t\t\t\t\t\t\t\t\ttake.userId}\n+\t\t\t\t\t\t\t\t\t</span>\n+\t\t\t\t\t\t\t\t</div>\n+\t\t\t\t\t\t\t\t<span\n+\t\t\t\t\t\t\t\t\tclassName={`status-badge status-${take.status}`}\n+\t\t\t\t\t\t\t\t>\n+\t\t\t\t\t\t\t\t\t{take.status}\n+\t\t\t\t\t\t\t\t</span>\n+\t\t\t\t\t\t\t</div>\n+\t\t\t\t\t\t</div>\n+\n+\t\t\t\t\t\t<div className=\"take-meta\">\n+\t\t\t\t\t\t\t<div className=\"meta-item\">\n+\t\t\t\t\t\t\t\t<span className=\"meta-label\">Completed:</span>\n+\t\t\t\t\t\t\t\t<span className=\"meta-value\">\n+\t\t\t\t\t\t\t\t\t{new Date(\n+\t\t\t\t\t\t\t\t\t\ttake.completedAt,\n+\t\t\t\t\t\t\t\t\t).toLocaleString()}\n+\t\t\t\t\t\t\t\t</span>\n+\t\t\t\t\t\t\t</div>\n+\t\t\t\t\t\t\t<div className=\"meta-item\">\n+\t\t\t\t\t\t\t\t<span className=\"meta-label\">Duration:</span>\n+\t\t\t\t\t\t\t\t<span className=\"meta-value\">\n+\t\t\t\t\t\t\t\t\t{prettyPrintTime(take.elapsedTime)}\n+\t\t\t\t\t\t\t\t</span>\n+\t\t\t\t\t\t\t</div>\n+\t\t\t\t\t\t</div>\n+\n+\t\t\t\t\t\t{take.mp4Url && (\n+\t\t\t\t\t\t\t<div className=\"video-container\">\n+\t\t\t\t\t\t\t\t<video controls className=\"take-video\">\n+\t\t\t\t\t\t\t\t\t<source\n+\t\t\t\t\t\t\t\t\t\tsrc={take.mp4Url}\n+\t\t\t\t\t\t\t\t\t\ttype=\"video/mp4\"\n+\t\t\t\t\t\t\t\t\t/>\n+\t\t\t\t\t\t\t\t\t<track\n+\t\t\t\t\t\t\t\t\t\tkind=\"captions\"\n+\t\t\t\t\t\t\t\t\t\tsrc=\"\"\n+\t\t\t\t\t\t\t\t\t\tlabel=\"Captions\"\n+\t\t\t\t\t\t\t\t\t/>\n+\t\t\t\t\t\t\t\t</video>\n+\t\t\t\t\t\t\t</div>\n+\t\t\t\t\t\t)}\n+\t\t\t\t\t</div>\n+\t\t\t\t))}\n+\t\t\t</div>\n+\t\t</div>\n+\t);\n+}\ndiff --git a/src/features/frontend/index.tsx b/src/features/frontend/index.tsx\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..8aafe719d90673cef53bb75ff90f85c9e6a21da8\n--- /dev/null\n+++ b/src/features/frontend/index.tsx\n@@ -0,0 +1,10 @@\n+import \"./styles.css\";\n+import { createRoot } from \"react-dom/client\";\n+import { App } from \"./app.tsx\";\n+\n+document.addEventListener(\"DOMContentLoaded\", () => {\n+\tconst element = document.getElementById(\"root\");\n+\tif (!element) throw new Error(\"Root element not found\");\n+\tconst root = createRoot(element);\n+\troot.render(<App />);\n+});\ndiff --git a/src/features/frontend/styles.css b/src/features/frontend/styles.css\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..51e226f88225e80dc94caf5ad315d3f08bb716ff\n--- /dev/null\n+++ b/src/features/frontend/styles.css\n@@ -0,0 +1,105 @@\n+body {\n+\tbackground-color: #f5f5f5;\n+\tfont-family:\n+\t\t-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial,\n+\t\tsans-serif;\n+\tline-height: 1.6;\n+\tcolor: #333;\n+\tmargin: 0;\n+\tpadding: 20px;\n+\tmin-height: 100vh;\n+}\n+\n+.container {\n+\tmax-width: 1200px;\n+\tmargin: 0 auto;\n+\tpadding: 2rem;\n+}\n+\n+.title {\n+\tfont-size: 2.5rem;\n+\tmargin-bottom: 2rem;\n+\ttext-align: center;\n+}\n+\n+.takes-grid {\n+\tdisplay: grid;\n+\tgrid-template-columns: repeat(auto-fill, minmax(300px, 1fr));\n+\tgap: 2rem;\n+}\n+\n+.take-card {\n+\tbackground: white;\n+\tborder-radius: 12px;\n+\tpadding: 1.5rem;\n+\tbox-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n+}\n+\n+.take-header {\n+\tdisplay: flex;\n+\tjustify-content: space-between;\n+\talign-items: center;\n+\tmargin-bottom: 1rem;\n+}\n+\n+.take-title {\n+\tfont-size: 1.25rem;\n+\tmargin: 0;\n+}\n+\n+.profile-image {\n+\twidth: 1.5rem;\n+\tborder-radius: 50%;\n+\tmargin-right: 0.5rem;\n+\tobject-fit: cover;\n+}\n+\n+.user-pill {\n+\tdisplay: flex;\n+\talign-items: center;\n+\tpadding: 0rem 0rem 0rem 0.3rem;\n+\tborder-radius: 999px;\n+\tbackground: #f8f9fa;\n+\tgap: 0.75rem;\n+}\n+\n+.user-info {\n+\tdisplay: flex;\n+\talign-items: center;\n+}\n+\n+.status-badge {\n+\tpadding: 0.25rem 0.75rem;\n+\tborder-radius: 999px;\n+}\n+\n+.status-approved {\n+\tbackground: #e6f4ea;\n+\tcolor: #1e7e34;\n+}\n+\n+.take-meta {\n+\tmargin-bottom: 1rem;\n+}\n+\n+.meta-item {\n+\tdisplay: flex;\n+\tmargin-bottom: 0.5rem;\n+}\n+\n+.meta-label {\n+\tfont-weight: 500;\n+\tmargin-right: 0.5rem;\n+\tmin-width: 80px;\n+}\n+\n+.video-container {\n+\tmargin-top: 1rem;\n+\tborder-radius: 8px;\n+\toverflow: hidden;\n+}\n+\n+.take-video {\n+\twidth: 100%;\n+\tdisplay: block;\n+}\ndiff --git a/src/index.ts b/src/index.ts\nindex dbd5d4e333361efe1b9e71131c02bf507a7ef92e..7cb8ca40f822523a75882b670d8af3d0cbd7c076 100644\n--- a/src/index.ts\n+++ b/src/index.ts\n@@ -1,6 +1,7 @@\n import { SlackApp } from \"slack-edge\";\n \n import { takes } from \"./features/index\";\n+import frontend from \"../public/index.html\";\n \n import { t } from \"./libs/template\";\n import { blog } from \"./libs/Logger\";\n@@ -56,9 +57,9 @@ takes();\n \n Bun.serve({\n \tport: process.env.PORT || 3000,\n-\tdevelopment: environment === \"development\",\n+\tdevelopment: environment === \"dev\",\n \troutes: {\n-\t\t\"/\": new Response(`Hello World from ${name}@${version}`),\n+\t\t\"/\": frontend,\n \t\t\"/health\": new Response(\"OK\"),\n \t},\n \tasync fetch(request: Request) {\ndiff --git a/src/libs/cachet.ts b/src/libs/cachet.ts\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..ca1f6a7e16b8feb559cb2fad7d23928fa228a18d\n--- /dev/null\n+++ b/src/libs/cachet.ts\n@@ -0,0 +1,12 @@\n+export async function fetchUserData(userId: string) {\n+\tconst res = await fetch(`https://cachet.dunkirk.sh/users/${userId}/`);\n+\tconst json = await res.json();\n+\n+\treturn {\n+\t\tid: json.id,\n+\t\texpiration: json.expiration,\n+\t\tuser: json.user,\n+\t\tdisplayName: json.displayName,\n+\t\timage: json.image,\n+\t};\n+}\ndiff --git a/tsconfig.json b/tsconfig.json\nindex ab0f0b0964d4e67df859488c9dfaf3fad62cbe8a..90556126cffe493b3fc3d541dce4dcc1a299d596 100644\n--- a/tsconfig.json\n+++ b/tsconfig.json\n@@ -1,28 +1,28 @@\n {\n-  \"compilerOptions\": {\n-    // Environment setup & latest features\n-    \"lib\": [\"esnext\"],\n-    \"target\": \"ESNext\",\n-    \"module\": \"ESNext\",\n-    \"moduleDetection\": \"force\",\n-    \"jsx\": \"react-jsx\",\n-    \"allowJs\": true,\n+\t\"compilerOptions\": {\n+\t\t// Environment setup & latest features\n+\t\t\"lib\": [\"esnext\", \"dom\"],\n+\t\t\"target\": \"ESNext\",\n+\t\t\"module\": \"ESNext\",\n+\t\t\"moduleDetection\": \"force\",\n+\t\t\"jsx\": \"react-jsx\",\n+\t\t\"allowJs\": true,\n \n-    // Bundler mode\n-    \"moduleResolution\": \"bundler\",\n-    \"allowImportingTsExtensions\": true,\n-    \"verbatimModuleSyntax\": true,\n-    \"noEmit\": true,\n+\t\t// Bundler mode\n+\t\t\"moduleResolution\": \"bundler\",\n+\t\t\"allowImportingTsExtensions\": true,\n+\t\t\"verbatimModuleSyntax\": true,\n+\t\t\"noEmit\": true,\n \n-    // Best practices\n-    \"strict\": true,\n-    \"skipLibCheck\": true,\n-    \"noFallthroughCasesInSwitch\": true,\n-    \"noUncheckedIndexedAccess\": true,\n+\t\t// Best practices\n+\t\t\"strict\": true,\n+\t\t\"skipLibCheck\": true,\n+\t\t\"noFallthroughCasesInSwitch\": true,\n+\t\t\"noUncheckedIndexedAccess\": true,\n \n-    // Some stricter flags (disabled by default)\n-    \"noUnusedLocals\": false,\n-    \"noUnusedParameters\": false,\n-    \"noPropertyAccessFromIndexSignature\": false\n-  }\n+\t\t// Some stricter flags (disabled by default)\n+\t\t\"noUnusedLocals\": false,\n+\t\t\"noUnusedParameters\": false,\n+\t\t\"noPropertyAccessFromIndexSignature\": false\n+\t}\n }\n",
    "title": "feat: add frontend and move to wakatime",
    "pullId": 1,
    "source": {
      "branch": "v2"
    },
    "targetRepo": "at://did:plc:krxbvxvis5skq7jj6eot23ul/sh.tangled.repo/3ln5h5btuww22",
    "targetBranch": "main"
  }
}