this repo has no description
3
fork

Configure Feed

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

Add gpx blob and rushed leaflet

+92 -45
+19 -8
frontend/components/App.tsx
··· 4 4 import { RunForm } from "./RunForm.tsx"; 5 5 import { SignIn } from "./SignIn.tsx"; 6 6 import { AtUri } from "https://esm.sh/@atproto/api@0.15.5" 7 + import { Run } from "../../shared/run_lexicon.ts"; 7 8 8 9 type RunsResponse = { 9 10 records: { 10 11 uri: string; 11 - value: { 12 - date_iso: string; 13 - created_at: string; 14 - distance_meters: number; 15 - duration_seconds: number; 16 - note: string; 17 - } 12 + value: Run; 18 13 }[] 19 14 } 20 15 ··· 41 36 onClick={fetchRuns}> 42 37 Get my runs 43 38 </button>} 44 - {runsResponse?.records.map(({ uri, value: { note, date_iso, distance_meters, duration_seconds } }) => ( 39 + <div id="map"></div> 40 + {runsResponse?.records.map(({ uri, value: { note, date_iso, distance_meters, duration_seconds, gpx } }) => ( 45 41 <div> 46 42 <p>distance (kilometers): {distance_meters / 1000}</p> 47 43 <p>duration (minutes): {duration_seconds / 60}</p> 48 44 <p>date: {date_iso}</p> 49 45 <p>note: {note}</p> 46 + {gpx && <p><button type="button" onClick={() => { 47 + const map = window.L.map('map'); 48 + window.L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 49 + attribution: 'Map data &copy; <a href="http://www.osm.org">OpenStreetMap</a>' 50 + }).addTo(map); 51 + 52 + const options = { 53 + async: true, 54 + polyline_options: { color: 'red' }, 55 + }; 56 + 57 + new window.L.GPX(`${session.server.issuer}/xrpc/com.atproto.sync.getBlob?did=${session.did}&cid=${gpx.ref}`, options).on('loaded', (e) => { 58 + map.fitBounds(e.target.getBounds()); 59 + }).addTo(map); 60 + }}>Load gpx</button></p>} 50 61 <button type="button" onClick={() => deleteRun(new AtUri(uri).rkey)}> 51 62 delete 52 63 </button>
+37 -20
frontend/components/RunForm.tsx
··· 1 1 /** @jsxImportSource https://esm.sh/react@19.1.0 */ 2 2 import { useActionState, useContext } from "https://esm.sh/react@19.1.0"; 3 - import { runSchemaMap } from "../../shared/run_lexicon.ts"; 3 + import { Run, runSchemaMap } from "../../shared/run_lexicon.ts"; 4 4 import { ATProtoContext } from "../context.ts"; 5 + import { type BlobRef } from "https://esm.sh/@atproto/api@0.15.5"; 5 6 6 7 export function RunForm() { 7 8 const { session, agent } = useContext(ATProtoContext); 8 9 const [error, submitRun, isPending] = useActionState( 9 10 async (_: unknown, formData: FormData) => { 10 - const values = { 11 + const gpxFile = formData.get("gpx") as File; 12 + let gpxBlobRef: { blob: BlobRef } | undefined; 13 + if (gpxFile) { 14 + const uploadBlobResponse = await agent.com.atproto.repo.uploadBlob(new Blob([gpxFile]), { 15 + encoding: "application/gpx+xml", 16 + }); 17 + console.log("Uploaded GPX file:", gpxFile); 18 + console.log("Upload blob response:", uploadBlobResponse); 19 + gpxBlobRef = uploadBlobResponse.data; 20 + } else { 21 + console.log("No GPX file provided"); 22 + } 23 + 24 + const values: Run = { 11 25 distance_meters: Number(formData.get("distance_kilometers")) * 1000, 12 - date_iso: formData.get("date_iso"), 13 - note: formData.get("note"), 26 + date_iso: formData.get("date_iso").toString(), 27 + note: formData.get("note").toString(), 14 28 duration_seconds: Number(formData.get("duration_minutes")) * 60, 15 29 created_at: new Date().toISOString(), 30 + gpx: gpxFile ? gpxBlobRef.blob : undefined, 16 31 } 17 32 18 - const parsed = runSchemaMap.defs.main.record.safeParse(values) 33 + // const parsed = runSchemaMap.defs.main.record.safeParse(values) 19 34 20 - if (parsed.success) { 21 - console.log({ data: parsed.data }) 22 - await agent.com.atproto.repo.createRecord({ 23 - repo: session.did, 24 - collection: "me.wilb.test.run", 25 - record: { 26 - $type: "me.wilb.test.run", 27 - ...values, 28 - } 29 - }) 30 - } else { 31 - console.log({ error: parsed.error }) 32 - return parsed.error 33 - } 35 + // if (parsed.success) { 36 + // console.log({ data: parsed.data }) 37 + const createRecordResponse = await agent.com.atproto.repo.createRecord({ 38 + repo: session.did, 39 + collection: "me.wilb.test.run", 40 + record: { 41 + $type: "me.wilb.test.run", 42 + ...values, 43 + } 44 + }) 45 + console.log("Created record response:", createRecordResponse); 46 + // } else { 47 + // console.log({ error: parsed.error }) 48 + // return parsed.error 49 + // } 34 50 }, null 35 51 ) 36 52 return ( ··· 40 56 <input required name="date_iso" type="date" placeholder="date" /> 41 57 <input required name="duration_minutes" type="number" placeholder="duration (in minutes)" /> 42 58 <input name="note" type="text" placeholder="note" /> 59 + <span>GPX file: <input name="gpx" type="file" /></span> 43 60 <input type="submit" value="Submit run" /> 44 61 </form> 45 62 {isPending && <p>submitting...</p>} 46 - {error && <pre>{error}</pre>} 63 + {error && <pre>{error as any}</pre>} 47 64 </> 48 65 ) 49 66 }
+5 -2
frontend/index.html
··· 6 6 <title>ProtoRuns</title> 7 7 <link rel="stylesheet" href="/public/style.css"> 8 8 <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🏃</text></svg>"> 9 - </head> 9 + <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> 10 + </head> 10 11 <body> 12 + <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> 13 + <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-gpx/2.1.2/gpx.min.js" defer></script> 11 14 <main> 12 - <div id="root"></div> 15 + <div id="root"></div> 13 16 </main> 14 17 <script src="https://esm.town/v/std/catch"></script> 15 18 <script src="/frontend/index.tsx" type="module"></script>
+3 -1
public/style.css
··· 10 10 flex-direction: column; 11 11 gap: 2rem; 12 12 margin-bottom: 2rem; 13 - } 13 + } 14 + 15 + #map { height: 180px; }
+28 -14
shared/run_lexicon.ts
··· 1 1 import { lexiconToZod } from "https://esm.sh/lexicon-to-zod@1.0.2"; 2 + import { type BlobRef } from "https://esm.sh/@atproto/api@0.15.5"; 2 3 3 4 const lexicon = { 4 - "lexicon": 1, 5 - "id": "me.wilb.test.run", 6 - "defs": { 7 - "main": { 8 - "type": "record", 9 - "record": { 10 - "type": "object", 11 - "required": ["date", "distance"], 12 - "properties": { 13 - "distance_meters": { "type": "integer" }, 14 - "date_iso": { "type": "string", "format": "datetime" }, 15 - "duration_seconds": { "type": "number" }, 16 - "note": { "type": "string" }, 17 - "createdAt": { "type": "string", "format": "datetime" }, 5 + lexicon: 1, 6 + id: "me.wilb.test.run", 7 + defs: { 8 + main: { 9 + type: "record", 10 + record: { 11 + type: "object", 12 + required: ["date_iso", "distance_meters"], 13 + properties: { 14 + distance_meters: { type: "integer" }, 15 + date_iso: { type: "string", format: "datetime" }, 16 + duration_seconds: { type: "number" }, 17 + note: { type: "string" }, 18 + createdAt: { type: "string", format: "datetime" }, 19 + gpx: { 20 + type: "blob", 21 + description: "GPX file of your run", 22 + }, 18 23 }, 19 24 }, 20 25 }, 21 26 }, 27 + }; 28 + 29 + export type Run = { 30 + date_iso: string; 31 + created_at?: string; 32 + distance_meters: number; 33 + duration_seconds?: number; 34 + note?: string; 35 + gpx?: BlobRef; 22 36 }; 23 37 24 38 export const runSchemaMap = lexiconToZod(lexicon);