diff options
| author | Adam Soltys <[email protected]> | 2026-02-27 10:09:16 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-02-27 10:09:16 -0800 |
| commit | bf7d8d373c11cd7a6e1487175147c9a84e775a98 (patch) | |
| tree | 82fb58568de073ff03dfca3d55d8ce5adcf51608 /src | |
| parent | 2aa2ac29eb241fc8ded3671e728e578e72652056 (diff) | |
| download | coinos-ui-bf7d8d373c11cd7a6e1487175147c9a84e775a98.tar.gz coinos-ui-bf7d8d373c11cd7a6e1487175147c9a84e775a98.zip | |
map updates
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/About.svelte | 2 | ||||
| -rw-r--r-- | src/components/Footer.svelte | 25 | ||||
| -rw-r--r-- | src/components/LandingHero.svelte | 4 | ||||
| -rw-r--r-- | src/components/LocationsMap.svelte | 168 | ||||
| -rw-r--r-- | src/locales/en.json | 2 | ||||
| -rw-r--r-- | src/routes/(app)/locations/+server.ts | 16 | ||||
| -rw-r--r-- | src/routes/(app)/settings/security/+page.svelte | 35 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.server.ts | 18 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.svelte | 62 | ||||
| -rw-r--r-- | src/routes/+page.svelte | 2 | ||||
| -rw-r--r-- | src/routes/map/+page.server.ts | 10 | ||||
| -rw-r--r-- | src/routes/map/+page.svelte | 5 |
12 files changed, 238 insertions, 111 deletions
diff --git a/src/components/About.svelte b/src/components/About.svelte index 874ec8db..b2f376ed 100644 --- a/src/components/About.svelte +++ b/src/components/About.svelte @@ -60,7 +60,7 @@ </div> </div> -<div> +<div class="mb-20"> <h3 class="text-5xl font-medium mb-5 text-center">{$t("about.locations.header")}</h3> <p class="text-secondary text-xl w-full md:w-10/12 lg:w-2/3 2xl:w-[800px] mx-auto mb-10"> {@html $t("about.locations.description")} diff --git a/src/components/Footer.svelte b/src/components/Footer.svelte index 8e80151c..0bdee16e 100644 --- a/src/components/Footer.svelte +++ b/src/components/Footer.svelte @@ -9,7 +9,6 @@ titleID: "nostr", }, { url: "https://t.me/coinoswallet", titleID: "telegram" }, - { url: "https://linktr.ee/coinos.io", titleID: "linktree" }, ]; const companyLinks = [ @@ -20,21 +19,18 @@ <footer class="space-y-10 md:space-y-0 text-xl block md:flex justify-around pb-8 px-4"> <div class="space-y-5"> - <div class="text-center w-full"> + <div class="text-center w-full hidden md:block"> <a href="/" aria-label="Home"> <iconify-icon noobserver icon="coinos:logo" width="224"></iconify-icon> </a> </div> - <div class="flex gap-2 justify-center sm:hidden"> - <div> - <a href="/register"> - <button class="btn">{$t("nav.startInSeconds")}</button> - </a> - </div> + <div class="w-full sm:hidden"> + <a href="/register"> + <button class="btn btn-accent w-full text-2xl">{$t("nav.startInSeconds")}</button> + </a> </div> - <div class="w-full flex flex-wrap sm:flex-nowrap gap-2 justify-center items-center"> + <div class="w-full hidden sm:flex gap-2 justify-center items-center"> <DarkToggle /> - <LocaleSelector /> </div> </div> @@ -62,6 +58,11 @@ </ul> </div> - <iconify-icon noobserver icon="coinos:shield" class="flex justify-center" width="96" - ></iconify-icon> + <div class="space-y-5"> + <div class="flex justify-center"> + <LocaleSelector /> + </div> + <iconify-icon noobserver icon="coinos:shield" class="flex justify-center" width="96" + ></iconify-icon> + </div> </footer> diff --git a/src/components/LandingHero.svelte b/src/components/LandingHero.svelte index dae0bf2b..44def229 100644 --- a/src/components/LandingHero.svelte +++ b/src/components/LandingHero.svelte @@ -27,11 +27,11 @@ {$t("landing.subheader")} </h2> - <div class="flex justify-center"> + <div> <a href="/register"> <button use:focus - class="btn !rounded-full bg-primary text-primary-content hover:bg-primary/80" + class="btn !rounded-full bg-primary text-primary-content hover:bg-primary/80 w-full text-2xl" > {$t("nav.startInSeconds")} </button> diff --git a/src/components/LocationsMap.svelte b/src/components/LocationsMap.svelte index 185a6ae5..dbc56375 100644 --- a/src/components/LocationsMap.svelte +++ b/src/components/LocationsMap.svelte @@ -4,11 +4,12 @@ import Popup from "$comp/Popup.svelte"; import { back } from "$lib/utils"; - let { locations } = $props(); let map: any; + let maplibreRef: any; + let mapReady = $state(false); let mapContainer: any = $state(), mapWrapper: any = $state(); - let markers: any[] = []; + let markers: any[] = $state([]); let search: string = $state(""); let clearSearch = (e: any) => (search = ""); @@ -20,13 +21,10 @@ if (mapWrapper.requestFullscreen) { mapWrapper.requestFullscreen(); } else if (mapWrapper.mozRequestFullScreen) { - /* Firefox */ mapWrapper.mozRequestFullScreen(); } else if (mapWrapper.webkitRequestFullscreen) { - /* Chrome, Safari & Opera */ mapWrapper.webkitRequestFullscreen(); } else if (mapWrapper.msRequestFullscreen) { - /* IE/Edge */ mapWrapper.msRequestFullscreen(); } @@ -36,13 +34,10 @@ if (doc.exitFullscreen) { doc.exitFullscreen(); } else if (doc.mozCancelFullScreen) { - /* Firefox */ doc.mozCancelFullScreen(); } else if (doc.webkitExitFullscreen) { - /* Chrome, Safari and Opera */ doc.webkitExitFullscreen(); } else if (doc.msExitFullscreen) { - /* IE/Edge */ doc.msExitFullscreen(); } } @@ -84,16 +79,6 @@ await new Promise((r) => setTimeout(r, 2500)); if (!timeout) return; - // - // map.flyTo({ - // zoom: 10.5, - // center: marker.getLngLat(), - // essential: true, - // easing: function (t) { - // return 1 - Math.pow(1 - t, 3); - // }, - // duration: 1000, - // }); } let toggle = () => { @@ -159,8 +144,6 @@ for (let marker of markers) { let isInView = bounds.contains(marker.getLngLat()); - let { tags } = marker; - if (isInView) { inview.push(marker); } @@ -170,10 +153,98 @@ }, 200); let locationMarkers: any = {}; + let addedIds = new Set(); let popups: any = {}; let counter = 0; let maplibrePromise: any; + function addMarkers(locations: any[]) { + let maplibre = maplibreRef; + if (!map || !maplibre) return; + + locations.forEach((location) => { + if (location["deleted_at"]) return; + if (addedIds.has(location.id)) return; + addedIds.add(location.id); + + let { lat, lon, tags } = location["osm_json"]; + if (!(lat && lon)) return; + + let popupContainer = document.createElement("div"); + + mount(Popup, { + target: popupContainer, + props: { tags }, + }); + + let marker = new maplibre.Marker({ color: "#F7931A", scale: 0.65 }) + .setLngLat([lon, lat]) + .setPopup(new maplibre.Popup().setDOMContent(popupContainer)) + .addTo(map); + + marker.id = counter++; + + marker.getElement().addEventListener("click", () => { + let p = marker.getPopup(); + popups[marker.id] = p; + p.setDOMContent(popupContainer); + setTimeout(() => { + selected = marker; + setTimeout(scroll, 10); + }, 50); + p.on("close", () => { + selected = undefined; + delete popups[marker.id]; + }); + }); + + marker.tags = tags; + locationMarkers[location.id] = marker; + + markers.push(marker); + }); + + updateLabelVisibility(); + } + + let fetchController: AbortController | null = null; + + async function fetchNearby(lat: number, lon: number, radius: number) { + if (fetchController) fetchController.abort(); + fetchController = new AbortController(); + + try { + let params = new URLSearchParams({ + lat: String(lat), + lon: String(lon), + radius: String(Math.ceil(radius)), + count: "100", + }); + let res = await fetch(`/locations?${params}`, { signal: fetchController.signal }); + let data = await res.json(); + if (data.locations) addMarkers(data.locations); + } catch (e: any) { + if (e.name !== "AbortError") console.log("fetch nearby failed", e); + } + } + + function getViewportRadius() { + let bounds = map.getBounds(); + let center = map.getCenter(); + let ne = bounds.getNorthEast(); + + let dlat = ne.lat - center.lat; + let dlng = ne.lng - center.lng; + let km = Math.sqrt(dlat * dlat + dlng * dlng) * 111; + return Math.max(km, 10); + } + + let fetchOnMove = debounce(() => { + if (!map) return; + let center = map.getCenter(); + fetchNearby(center.lat, center.lng, getViewportRadius()); + }, 500); + const loadMaplibre = async () => { if ((window as any).maplibregl) return (window as any).maplibregl; if (maplibrePromise) return maplibrePromise; @@ -210,6 +281,8 @@ await tick(); if (!mapContainer) return; loadMaplibre().then((maplibre) => { + maplibreRef = maplibre; + map = new maplibre.Map({ container: mapContainer, style: "https://tiles.stadiamaps.com/styles/stamen_toner.json", @@ -217,51 +290,26 @@ zoom: 10.5, }); - map.on("load", () => { - locations.forEach((location) => { - if (location["deleted_at"]) return; - - let { lat, lon, tags } = location["osm_json"]; - if (!(lat && lon)) return; - - let element = document.createElement("div"); - element.className = "marker"; - - let popupContainer = document.createElement("div"); + map.scrollZoom.setWheelZoomRate(1 / 60); + map.scrollZoom.setZoomRate(1 / 30); - mount(Popup, { - target: popupContainer, - props: { tags }, - }); + map.on("load", () => { + mapReady = true; - let marker = new maplibre.Marker({ color: "#F7931A", scale: 0.65 }) - .setLngLat([lon, lat]) - .setPopup(new maplibre.Popup().setDOMContent(popupContainer)) - .addTo(map); + // Initial fetch for default view + fetchNearby(49.26, -123.05, 50); - marker.id = counter++; + // Fetch more on pan/zoom + map.on("moveend", fetchOnMove); - marker.getElement().addEventListener("click", () => { - let p = marker.getPopup(); - popups[marker.id] = p; - p.setDOMContent(popupContainer); - setTimeout(() => { - selected = marker; - setTimeout(scroll, 10); - }, 50); - p.on("close", () => { - selected = undefined; - delete popups[marker.id]; - }); + // Center on user if available + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition((position) => { + let { longitude: lng, latitude: lat } = position.coords; + map.flyTo({ center: [lng, lat], zoom: 11, duration: 1000 }); + fetchNearby(lat, lng, 50); }); - - marker.tags = tags; - locationMarkers[location.id] = marker; - - markers.push(marker); - }); - - updateLabelVisibility(); + } }); map.on("mousedown", stopFlying); diff --git a/src/locales/en.json b/src/locales/en.json index 196c55df..3d76e0ea 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -288,7 +288,7 @@ "profile": "Profile", "register": "Register", "account": "Account", - "startInSeconds": "Start in Seconds", + "startInSeconds": "Start Now", "signIn": "Sign In", "signOut": "Sign out", "dashboard": "Dashboard", diff --git a/src/routes/(app)/locations/+server.ts b/src/routes/(app)/locations/+server.ts index 9980c5fc..ecbb234b 100644 --- a/src/routes/(app)/locations/+server.ts +++ b/src/routes/(app)/locations/+server.ts @@ -1,11 +1,23 @@ import { get } from "$lib/utils"; import { json } from "@sveltejs/kit"; -export async function GET({ setHeaders }) { +export async function GET({ setHeaders, url }) { + const lat = url.searchParams.get("lat"); + const lon = url.searchParams.get("lon"); + const radius = url.searchParams.get("radius"); + const count = url.searchParams.get("count"); + let locations = []; try { - ({ locations } = await get("/locations")); + if (lat && lon) { + const params = new URLSearchParams({ lat, lon }); + if (radius) params.set("radius", radius); + if (count) params.set("count", count); + ({ locations } = await get(`/locations/nearby?${params}`)); + } else { + ({ locations } = await get("/locations")); + } setHeaders({ "cache-control": "public, max-age=600" }); } catch { console.log("failed to fetch locations"); diff --git a/src/routes/(app)/settings/security/+page.svelte b/src/routes/(app)/settings/security/+page.svelte index 33981645..0991c679 100644 --- a/src/routes/(app)/settings/security/+page.svelte +++ b/src/routes/(app)/settings/security/+page.svelte @@ -4,9 +4,12 @@ import { t } from "$lib/translations"; import Pin from "$comp/Pin.svelte"; import Qr from "$comp/Qr.svelte"; + import Spinner from "$comp/Spinner.svelte"; import { copy, post, success, fail } from "$lib/utils"; import { save, pin as current } from "$lib/store"; import { getNsec } from "$lib/nostr"; + import { registerPasskey } from "$lib/passkey"; + import { rememberPrfKey, defaultRememberForMs } from "$lib/passwordCache"; import { invalidate } from "$app/navigation"; import { page } from "$app/stores"; @@ -147,6 +150,22 @@ fail("Failed to disable 2FA, try again"); } }; + let passkeyLoading = $state(false); + + let addPasskey = async () => { + passkeyLoading = true; + try { + const prfKey = await registerPasskey(); + rememberPrfKey(prfKey, defaultRememberForMs); + success("Passkey added"); + } catch (e: any) { + if (e.name !== "NotAllowedError") { + fail(e.message || "Passkey registration failed"); + } + } + passkeyLoading = false; + }; + let verifying = $derived(pin?.length > 5); $effect(() => { @@ -220,10 +239,14 @@ </div> <div> - <a href={`/account/seed`} class="contents"> - <button class="btn btn-lg w-full rounded-2xl whitespace-nowrap"> - <iconify-icon noobserver icon="ph:plus-circle-bold" width="32"></iconify-icon> - {$t("accounts.addAccount")} - </button> - </a> + <span class="font-bold mb-1">Passkey</span> + <p class="text-secondary mb-1">Add a passkey for passwordless sign-in</p> + <button type="button" class="btn" onclick={addPasskey} disabled={passkeyLoading}> + {#if passkeyLoading} + <Spinner /> + {:else} + <iconify-icon noobserver icon="ph:fingerprint-bold" width="32"></iconify-icon> + {/if} + Add Passkey + </button> </div> diff --git a/src/routes/(static)/register/+page.server.ts b/src/routes/(static)/register/+page.server.ts index 5df89541..55331c5a 100644 --- a/src/routes/(static)/register/+page.server.ts +++ b/src/routes/(static)/register/+page.server.ts @@ -57,6 +57,24 @@ export const actions = { redirect(303, loginRedirect || `/${username}`); }, + passkeyCreate: async ({ request }) => { + const ip = request.headers.get("cf-connecting-ip") ?? ""; + const form = await fd(request); + const { username, password, challenge, recaptcha, picture } = form; + + const user = { picture, username, password, challenge, recaptcha }; + const headers: Record<string, string> = {}; + if (ip) headers["cf-connecting-ip"] = ip; + + try { + const { sk, token } = await post("/register", { user }, headers); + return { token, username, sk }; + } catch (e) { + const { message } = e as Error; + return fail(400, { error: message }); + } + }, + activate: async ({ cookies, request }) => { const form = await fd(request); const { token, username, sk, pubkey } = form; diff --git a/src/routes/(static)/register/+page.svelte b/src/routes/(static)/register/+page.svelte index 689863e5..e2d53fd0 100644 --- a/src/routes/(static)/register/+page.svelte +++ b/src/routes/(static)/register/+page.svelte @@ -15,7 +15,7 @@ import Spinner from "$comp/Spinner.svelte"; import { focus, fail, versions, post } from "$lib/utils"; - import { registerPasskey, loginWithPasskey } from "$lib/passkey"; + import { registerPasskey } from "$lib/passkey"; import { rememberPrfKey, defaultRememberForMs } from "$lib/passwordCache"; import { avatar, signer, password, pin, loginRedirect } from "$lib/store"; import { t } from "$lib/translations"; @@ -186,28 +186,64 @@ let passkeyRegister = async () => { passkeyLoading = true; try { - const { credential, challengeId, prfKey } = await loginWithPasskey(); - const loginData = new FormData(); - loginData.append("credential", JSON.stringify(credential)); - loginData.append("challengeId", challengeId); - loginData.append("loginRedirect", (redirect ?? "") as string); + let name = + username || + uniqueNamesGenerator({ + dictionaries: [animals, NumberDictionary.generate({ min: 10, max: 99 })], + length: 2, + separator: "", + }); + + let pw = Array.from(crypto.getRandomValues(new Uint8Array(32)), (b) => + b.toString(16).padStart(2, "0"), + ).join(""); + + const recaptcha = await getRecaptchaToken(); + + const formData = new FormData(); + formData.append("username", name); + formData.append("password", pw); + formData.append("challenge", challenge); + formData.append("recaptcha", recaptcha as string); + formData.append("picture", `${$page.url.origin}/api/public/${punks[index]}.webp`); + + const response = await fetch("/register?/passkeyCreate", { + method: "POST", + body: formData, + }); + + const result: any = deserialize(await response.text()); + if (result.type !== "success") { + fail(result.data?.error || "Account creation failed"); + passkeyLoading = false; + return; + } + + const { token, sk } = result.data; + const prfKey = await registerPasskey(token); + + const activateData = new FormData(); + activateData.append("token", token); + activateData.append("username", name); + if (sk) activateData.append("sk", sk); + activateData.append("loginRedirect", (redirect ?? "") as string); - const loginResponse = await fetch("/login?/passkey", { + const activateResponse = await fetch("/register?/activate", { method: "POST", - body: loginData, + body: activateData, }); - const loginResult = deserialize(await loginResponse.text()); + const activateResult = deserialize(await activateResponse.text()); - if (loginResult.type === "success" || loginResult.type === "redirect") { + if (activateResult.type === "success" || activateResult.type === "redirect") { rememberPrfKey(prfKey, defaultRememberForMs); await invalidateAll(); } - applyAction(loginResult); + applyAction(activateResult); } catch (e: any) { if (e.name !== "NotAllowedError") { - fail(e.message || "Passkey login failed"); + fail(e.message || "Passkey registration failed"); } } passkeyLoading = false; @@ -394,7 +430,7 @@ {:else} <iconify-icon noobserver icon="ph:fingerprint-bold" width="24"></iconify-icon> {/if} - <div class="my-auto">Sign in with Passkey</div> + <div class="my-auto">Register with Passkey</div> </button> <button type="button" class="btn" onclick={nostrLogin}> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 85cbe75b..2f4c7dda 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -26,7 +26,7 @@ <LandingHeader {howItWorks} {faq} {about} {user} /> -<main class="space-y-40 py-20 md:py-32 lg:py-36 xl:py-40 px-5 md:px-0 pt-20"> +<main class="space-y-20 md:space-y-40 py-20 md:py-32 lg:py-36 xl:py-40 px-5 md:px-0 pt-20"> <LandingHero /> <LandingInfoCard image="lightning-qr" diff --git a/src/routes/map/+page.server.ts b/src/routes/map/+page.server.ts index c3b14d0f..7b039895 100644 --- a/src/routes/map/+page.server.ts +++ b/src/routes/map/+page.server.ts @@ -1,11 +1,3 @@ -import { get } from "$lib/utils"; - export const load = async () => { - let data = { locations: [] }; - - try { - data = await get("/locations"); - } catch {} - - return data; + return {}; }; diff --git a/src/routes/map/+page.svelte b/src/routes/map/+page.svelte index 49953479..83a8f98a 100644 --- a/src/routes/map/+page.svelte +++ b/src/routes/map/+page.svelte @@ -1,10 +1,7 @@ <script> import LocationsMap from "$comp/LocationsMap.svelte"; - let { data } = $props(); - - let locations = $derived(data.locations); </script> <div class="h-screen w-screen"> - <LocationsMap {locations} /> + <LocationsMap /> </div> |
