app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit bbd361944443f3f51eeba4b7ad41705f3f300828
parent 5e2557629ba6666a5fdd22bc3bdde4f541e71ad1
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Sun,  1 Sep 2024 10:13:50 +0000

Add `models/trade-product`, edit app root page, edit models routes, edit nostr routes, add css

Diffstat:
Msrc/app.css | 35++++++++++++++++++++++++++++++++++-
Msrc/app.html | 2++
Msrc/lib/client.ts | 2+-
Dsrc/lib/utils.ts | 16----------------
Asrc/lib/utils/index.ts | 16++++++++++++++++
Asrc/lib/utils/models.ts | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/routes/(app)/+page.svelte | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/routes/(app)/models/location-gcs/+page.svelte | 85+++++++++++--------------------------------------------------------------------
Asrc/routes/(app)/models/trade-product/+page.svelte | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/routes/(app)/models/trade-product/add/+page.svelte | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/routes/(app)/nostr/keys/+page.svelte | 16++++++++--------
Msrc/routes/(app)/nostr/notes/post/+page.svelte | 4++--
Msrc/routes/(app)/nostr/profile/+page.svelte | 12++++++------
Msrc/routes/(app)/settings/+page.svelte | 52++++++++++++++++++++++++++--------------------------
Msrc/routes/(conf)/+layout.ts | 8++++----
Msrc/routes/(conf)/conf/nostr/+page.svelte | 10+++++-----
Msrc/routes/+layout.svelte | 20++++++++++----------
Astatic/assets/keyva.min.js | 1+
Mtailwind.config.ts | 8++++++++
19 files changed, 671 insertions(+), 173 deletions(-)

diff --git a/src/app.css b/src/app.css @@ -7,6 +7,28 @@ @tailwind utilities; @layer components { + .form-select-e { + @apply select select-sm border-0 focus:border-0 outline-0 focus:outline-0; + } + + .form-line-e { + @apply pl-4 justify-center items-center; + } + + .form-select-e, + .form-line-e, + .form-line { + @apply flex flex-row h-form_line w-full; + } + + .form-input { + @apply input flex flex-row w-full p-0 border-0 focus:border-0 outline-0 focus:outline-0 placeholder:font-[300] bg-transparent; + } + + .form-input-invalid { + @apply outline outline-[2px] outline-red-300/70; + } + .taps { @apply active:scale-[94%] active:opacity-80 transition-all; } @@ -23,9 +45,16 @@ @apply h-line w-line; } + .button-base + .button-simple, + .button-submit { + @apply flex flex-row justify-center items-center font-mono text-sm lowercase transition-all select-none cursor-none; + } + + .surface-1, .button-simple, .button-submit { - @apply flex flex-row justify-center items-center bg-layer-1-surface font-mono text-sm lowercase text-layer-2-glyph transition-all select-none cursor-none; + @apply bg-layer-1-surface text-layer-2-glyph; } .flex-fill { @@ -68,6 +97,10 @@ @apply select-none cursor-none; } + pre { + @apply select-none cursor-none; + } + @font-face { font-family: 'SFProDisplay'; src: url('/webfonts/sf-pro-display/SFProDisplay-BlackItalic.woff2') format('woff2'), diff --git a/src/app.html b/src/app.html @@ -9,6 +9,8 @@ <link rel="stylesheet" type="text/css" href="/phosphor-icons/light.css" /> <link rel="stylesheet" type="text/css" href="/phosphor-icons/regular.css" /> + <script src="/assets/keyva.min.js"></script> + <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no viewport-fit=cover" /> %sveltekit.head% diff --git a/src/lib/client.ts b/src/lib/client.ts @@ -1,3 +1,3 @@ import { ClientCapacitor } from "@radroots/client"; -export const cl = new ClientCapacitor(); +export const lc = new ClientCapacitor(); diff --git a/src/lib/utils.ts b/src/lib/utils.ts @@ -1,15 +0,0 @@ -import { goto } from "$app/navigation"; -import { cl } from "./client"; - -export const restart = async (route_to: true | string): Promise<void> => { - try { - await cl.window.splash_show(); - if (route_to) { - if (route_to === true) await goto(`/`); - else await goto(route_to) - } - location.reload(); - } catch (e) { - console.log(`(error) restart `, e); - } -}; -\ No newline at end of file diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts @@ -0,0 +1,15 @@ +import { goto } from "$app/navigation"; +import { lc } from "../client"; + +export const restart = async (route_to?: true | string): Promise<void> => { + try { + await lc.window.splash_show(); + if (route_to) { + if (route_to === true) await goto(`/`); + else await goto(route_to) + } + location.reload(); + } catch (e) { + console.log(`(error) restart `, e); + } +}; +\ No newline at end of file diff --git a/src/lib/utils/models.ts b/src/lib/utils/models.ts @@ -0,0 +1,76 @@ +import { lc } from "$lib/client"; +import { location_geohash } from "@radroots/utils"; + +export const location_gcs_add = async (): Promise<boolean> => { + try { + const loc_gcs = await lc.geo.current(); + if ( + loc_gcs && + typeof loc_gcs !== `string` + ) { + const loc_gcs_label = + await lc.dialog.prompt({ + title: `Geolocation Label`, + message: `What is the name of the location.`, + input_placeholder: `Enter location name`, + }); + if (loc_gcs_label === false) return false; + + const { lat, lng } = loc_gcs; + const geohash = location_geohash( + lat, + lng, + ); + const fields = { + lat: lat.toString(), + lng: lng.toString(), + geohash, + label: loc_gcs_label, + }; + const exe_res = + await lc.db.location_gcs_add( + fields, + ); + if ( + typeof exe_res !== `string` && + `id` in exe_res + ) { + return true; + } else if ( + typeof exe_res === `string` && + exe_res === + `*-location-gcs-geohash-unique` + ) { + await lc.dialog.alert( + `This location has already been added.`, + ); + } + } else if ( + loc_gcs && + typeof loc_gcs === `string` + ) { + const dcf_res = await lc.dialog.confirm( + `Location permissions are required to read geolocation.`, + ); + if (dcf_res) { + await lc.settings.open( + lc.platform === `ios` + ? { + ios: { + setting: `LocationServices`, + }, + } + : { + android: { + setting: `Location`, + }, + }, + ); + } + } + return false; + } catch (e) { + console.log(`(error) location_gcs_add `, e); + return false; + } +}; +\ No newline at end of file diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte @@ -1,9 +1,17 @@ <script lang="ts"> import { goto } from "$app/navigation"; - import { cl } from "$lib/client"; + import { lc } from "$lib/client"; + import LayoutView from "$lib/components/layout-view.svelte"; + import { _cf } from "$lib/conf"; import { app_tab_active, app_tabs_visible } from "$lib/stores"; import { NDKKind } from "@nostr-dev-kit/ndk"; - import { ndk, ndk_event, ndk_user, t } from "@radroots/svelte-lib"; + import { + glyph as Glyph, + ndk, + ndk_event, + ndk_user, + type GlyphKey, + } from "@radroots/svelte-lib"; import { onMount } from "svelte"; onMount(async () => { @@ -28,28 +36,115 @@ }); console.log(JSON.stringify(ev, null, 4), `ev`); if (ev) await ev.publish(); - cl.dialog.alert(`Published content ${JSON.stringify(content)}`); + lc.dialog.alert(`Published content ${JSON.stringify(content)}`); } catch (e) { console.log(`(error) nostr_note_pub `, e); } }; + + /* +<div class={`grid grid-cols-12 w-full gap-8 pt-6 px-6`}> + <button + class={`button-base surface-1 col-span-6 h-24 rounded-2xl font-[500] text-lg font-mono`} + on:click={async () => { + await goto(`/models/location-gcs`); + }} + > + {`Post `} + </button> + <button + class={`button-base surface-1 col-span-6 h-32 rounded-2xl font-[500] text-lg font-mono`} + on:click={async () => { + await goto(`/models/location-gcs`); + }} + > + {`Post `} + </button> + <button + class={`button-base surface-1 col-span-6 h-32 rounded-2xl font-[500] text-lg font-mono`} + on:click={async () => { + await goto(`/models/location-gcs`); + }} + > + {`Post `} + </button> + + </div> + */ + + let buttons: { route: string; label: string; key: GlyphKey }[] = [ + { + route: `/models/trade-product/add`, + label: `Post Goods`, + key: `handbag-simple`, + }, + ]; </script> -<div class={`flex flex-col w-full pt-16 gap-8 justify-center items-center`}> - <button - class={`button-simple`} - on:click={async () => { - await cl.dialog.alert(`Hi! You're platform is ${cl.platform}`); - }} - > - {$t(`app.name`)} - </button> - <button - class={`button-simple`} - on:click={async () => { - await goto(`/models/location-gcs`); - }} +<LayoutView> + <div + class={`flex flex-col w-full justify-start items-start pt-6 gap-6 px-6`} > - {"models geolocation"} - </button> -</div> + <div class={`flex flex-row w-full px-1 justify-start items-center`}> + <p class={`font-mono font-[500] text-layer-2-glyph text-lg`}> + {`radroots ${_cf.root_symbol}`} + </p> + </div> + <button + class={`flex flex-row w-full h-8 px-4 gap-2 justify-start items-center surface-1 rounded-xl active:bg-layer-1-surface_a transition-all`} + > + <Glyph + basis={{ + key: `magnifying-glass`, + dim: `sm-`, + weight: `bold`, + }} + /> + <p class={`font-sans font-[500] text-layer-2-glyph/70`}> + {`Search`} + </p> + </button> + <div + class={`grid grid-cols-12 flex flex-row w-full gap-4 justify-start items-center`} + > + {#each buttons as btn} + <button + class={`surface-1 col-span-6 flex flex-col h-20 py-2 px-3 justify-between rounded-2xl font-[500] text-lg font-mono active:bg-layer-1-surface_a transition-all`} + on:click={async () => { + await goto(btn.route); + }} + > + <div + class={`flex flex-row w-full justify-between items-center`} + > + <div class={`flex flex-row justify-start items-center`}> + <Glyph + basis={{ + key: btn.key, + dim: `md`, + weight: `bold`, + }} + /> + </div> + <div class={`flex flex-row justify-start items-center`}> + <Glyph + basis={{ + key: `caret-right`, + dim: `sm`, + weight: `bold`, + }} + /> + </div> + </div> + <div + class={`flex flex-row w-full justify-start items-center`} + > + <div class={`flex flex-row justify-start items-center`}> + {btn.label} + </div> + </div> + </button> + {/each} + </div> + </div> +</LayoutView> diff --git a/src/routes/(app)/models/location-gcs/+page.svelte b/src/routes/(app)/models/location-gcs/+page.svelte @@ -1,15 +1,15 @@ <script lang="ts"> - import { cl } from "$lib/client"; + import { lc } from "$lib/client"; import LayoutTrellis from "$lib/components/layout-trellis.svelte"; import LayoutView from "$lib/components/layout-view.svelte"; import Nav from "$lib/components/nav.svelte"; import { app_tabs_visible } from "$lib/stores"; + import { location_gcs_add } from "$lib/utils/models"; import { type LocationGcs } from "@radroots/client"; import { trellis as Trellis } from "@radroots/svelte-lib"; - import { location_geohash } from "@radroots/utils"; import { onMount } from "svelte"; - let locations_all: LocationGcs[] = []; + let models_list: LocationGcs[] = []; onMount(async () => { try { @@ -22,10 +22,10 @@ const fetch_models = async (): Promise<void> => { try { - const res = await cl.db.location_gcs_get({ + const res = await lc.db.location_gcs_get({ list: [`all`], }); - if (typeof res !== `string`) locations_all = res; + if (typeof res !== `string`) models_list = res; } catch (e) { console.log(`(error) fetch_models `, e); } @@ -53,75 +53,12 @@ ], }, callback: async () => { - const loc_gcs = await cl.geo.current(); - if ( - loc_gcs && - typeof loc_gcs !== `string` - ) { - const loc_gcs_label = - await cl.dialog.prompt({ - title: `Geolocation Label`, - message: `What is the name of the location.`, - input_placeholder: `Enter location name`, - }); - if (loc_gcs_label === false) return; - - const { lat, lng } = loc_gcs; - const geohash = location_geohash( - lat, - lng, - ); - const fields = { - lat: lat.toString(), - lng: lng.toString(), - geohash, - label: loc_gcs_label, - }; - const exe_res = - await cl.db.location_gcs_add( - fields, - ); - if ( - typeof exe_res !== `string` && - `id` in exe_res - ) { - await fetch_models(); - } else if ( - typeof exe_res === `string` && - exe_res === - `*-location-gcs-geohash-unique` - ) { - await cl.dialog.alert( - `This location has already been added.`, - ); - } - } else if ( - loc_gcs && - typeof loc_gcs === `string` - ) { - const dcf_res = await cl.dialog.confirm( - `Enable location permission is required.`, - ); - if (dcf_res) { - await cl.settings.open( - cl.platform === `ios` - ? { - ios: { - setting: `LocationServices`, - }, - } - : { - android: { - setting: `Location`, - }, - }, - ); - } - } + const res = await location_gcs_add(); + if (res === true) await fetch_models(); }, }, }, - locations_all.length + models_list.length ? { touch: { label: { @@ -143,15 +80,15 @@ }} /> <div class={`flex flex-col justify-center items-center pt-4 px-4`}> - {#if locations_all.length > 0} + {#if models_list.length > 0} <p class={`font-sans font-[400] text-layer-0-glyph text-xs`}> {"Your locations:"} </p> - {#each locations_all as location} + {#each models_list as li} <div class={`flex flex-col justify-center items-center`}> <pre class={`font-sans font-[400] text-layer-0-glyph text-xs`}>{JSON.stringify( - location, + li, null, 4, )} diff --git a/src/routes/(app)/models/trade-product/+page.svelte b/src/routes/(app)/models/trade-product/+page.svelte @@ -0,0 +1,113 @@ +<script lang="ts"> + import { goto } from "$app/navigation"; + import { lc } from "$lib/client"; + import LayoutTrellis from "$lib/components/layout-trellis.svelte"; + import LayoutView from "$lib/components/layout-view.svelte"; + import Nav from "$lib/components/nav.svelte"; + import { app_tabs_visible } from "$lib/stores"; + import { type TradeProduct } from "@radroots/client"; + import { trellis as Trellis } from "@radroots/svelte-lib"; + import { onMount } from "svelte"; + + let models_list: TradeProduct[] = []; + + onMount(async () => { + try { + app_tabs_visible.set(false); + await fetch_models(); + } catch (e) { + } finally { + } + }); + + const fetch_models = async (): Promise<void> => { + try { + const res = await lc.db.trade_product_get({ + list: [`all`], + }); + if (typeof res !== `string`) models_list = res; + } catch (e) { + console.log(`(error) fetch_models `, e); + } + }; +</script> + +<LayoutView> + <LayoutTrellis> + <Trellis + basis={{ + args: { + layer: 1, + title: { + value: `Trade Products`, + }, + list: [ + { + touch: { + label: { + left: [ + { + value: `Add new product`, + classes: `capitalize`, + }, + ], + }, + callback: async () => { + await goto(`/models/trade-product/add`); + }, + end: { + icon: { + key: `caret-right`, + }, + }, + }, + }, + ], + }, + }} + /> + <div class={`flex flex-col justify-center items-center pt-4 px-4`}> + {#if models_list.length > 0} + <p class={`font-sans font-[400] text-layer-0-glyph text-xs`}> + {"Your products:"} + </p> + {#each models_list as li} + <div class={`flex flex-col justify-center items-center`}> + <pre + class={`font-sans font-[400] text-layer-0-glyph text-xs`}>{JSON.stringify( + li, + null, + 4, + )} + </pre> + </div> + {/each} + {:else} + <p class={`font-sans font-[400] text-layer-0-glyph text-xs`}> + {"No products saved"} + </p> + {/if} + </div> + </LayoutTrellis> +</LayoutView> +<Nav + basis={{ + prev: { + label: `Home`, + route: `/`, + }, + title: { + label: `Products`, + }, + option: { + glyph: { + key: `arrow-counter-clockwise`, + dim: `md`, + classes: `text-layer-1-glyph-hl taps`, + }, + callback: async () => { + await fetch_models(); + }, + }, + }} +/> diff --git a/src/routes/(app)/models/trade-product/add/+page.svelte b/src/routes/(app)/models/trade-product/add/+page.svelte @@ -0,0 +1,232 @@ +<script lang="ts"> + import { goto } from "$app/navigation"; + import { lc } from "$lib/client"; + import ButtonSubmit from "$lib/components/button-submit.svelte"; + import LayoutTrellis from "$lib/components/layout-trellis.svelte"; + import LayoutView from "$lib/components/layout-view.svelte"; + import Nav from "$lib/components/nav.svelte"; + import { location_gcs_add } from "$lib/utils/models"; + import { + parse_trade_product_form_keys, + trade_product_form_fields, + trade_product_form_vals, + type LocationGcs, + } from "@radroots/client"; + import { + fmt_id, + input_form as InputForm, + kv, + loading as Loading, + } from "@radroots/svelte-lib"; + import { onMount } from "svelte"; + + const texts_key = { + opt_lcs_add: `location_gcs-add-current`, + }; + + const texts = { + 1: `No locations saved`, + 2: `Add new location`, + }; + + const texts_kv: Record<string, string> = { + key: `Name`, + }; + + let loading = false; + let loading_location = false; + + let ls_model_location_gcs: LocationGcs[] = []; + let val_model_location_gcs_selected = ``; + + $: { + if (ls_model_location_gcs.length && !val_model_location_gcs_selected) + val_model_location_gcs_selected = ls_model_location_gcs[0].id; + } + + onMount(async () => { + try { + await fetch_models_location_gcs(); + } catch (e) { + } finally { + } + }); + + const fetch_models_location_gcs = async (): Promise<void> => { + try { + const res = await lc.db.location_gcs_get({ + list: [`all`], + }); + if (typeof res !== `string`) ls_model_location_gcs = res; + } catch (e) { + console.log(`(error) fetch_models_location_gcs `, e); + } + }; + + const add_model_location_gcs = async (): Promise<void> => { + try { + loading_location = true; + await location_gcs_add(); + await fetch_models_location_gcs(); + } catch (e) { + console.log(`(error) add_model_location_gcs `, e); + } finally { + loading_location = false; + } + }; + + const submit = async (): Promise<void> => { + try { + loading = true; + const vals = trade_product_form_vals; + for (const [k, field] of Object.entries( + trade_product_form_fields, + )) { + const field_k = parse_trade_product_form_keys(k); + if (!field_k) continue; + const field_id = fmt_id(field_k); + const field_val = await $kv.get(field_id); + + if ( + (!field.optional && !field.validation.test(field_val)) || + (field.optional && + field_val && + !field.validation.test(field_val)) + ) { + loading = false; + await lc.dialog.alert( + `Invalid product ${texts_kv[field_k]?.toLowerCase() || field_k} value.`, + ); + return; + } + vals[field_k] = field_val; + } + const res = await lc.db.trade_product_add(vals); + if (typeof res !== `string` && !Array.isArray(res)) { + await goto(`/models/trade-product`); + } + } catch (e) { + console.log(`(error) submit `, e); + } finally { + loading = false; + } + }; +</script> + +<LayoutView> + <LayoutTrellis> + <div + class={`flex flex-col w-full px-4 gap-3 justify-center items-center`} + > + {#each Object.entries(trade_product_form_fields) as [field_k, field], field_i} + {#if field_i === 1} + <div + class={`flex flex-col w-full gap-1 justify-start items-start`} + > + <div + class={`flex flex-row w-full px-2 justify-start items-center`} + > + <p + class={`font-sans font-[400] uppercase text-layer-2-glyph text-sm`} + > + {`Product - Location`} + </p> + </div> + {#if loading_location} + <div + class={`form-line surface-1 flex flex-row justify-center items-center rounded-xl`} + > + <Loading basis={{ dim: `xs-` }} /> + </div> + {:else} + <select + class={`form-select-e w-full bg-layer-1-surface rounded-xl text-layer-2-glyph`} + bind:value={val_model_location_gcs_selected} + on:change={async ({ currentTarget: el }) => { + const val = el.value; + if (val === texts_key.opt_lcs_add) { + val_model_location_gcs_selected = ``; + await add_model_location_gcs(); + } + }} + > + {#if ls_model_location_gcs.length} + {#each ls_model_location_gcs as li} + <option value={li.id}> + {li.label} + </option> + {/each} + {:else} + <option disabled selected={true}> + {texts[1]} + </option> + {/if} + <option value={texts_key.opt_lcs_add}> + {texts[2]} + </option> + </select> + {/if} + </div> + {/if} + <div + class={`flex flex-col w-full gap-1 justify-start items-start`} + > + <div + class={`flex flex-row w-full px-2 justify-start items-center`} + > + <p + class={`font-sans font-[400] uppercase text-layer-2-glyph text-sm`} + > + {`Product - ${texts_kv[field_k] || field_k}`} + </p> + </div> + <div class={`form-line-e bg-layer-1-surface rounded-xl`}> + <InputForm + basis={{ + id: fmt_id(field_k), + layer: 1, + sync: true, + field: { + charset: field.charset, + validate: field.validation, + }, + }} + /> + </div> + </div> + {/each} + <div class={`flex flex-row w-full pt-4 justify-end items-center`}> + <ButtonSubmit + basis={{ + callback: async () => { + await submit(); + }, + loading, + }} + /> + </div> + </div> + </LayoutTrellis> +</LayoutView> +<Nav + basis={{ + prev: { + label: `Products`, + route: `/models/trade-product`, + }, + title: { + label: `Trade Product`, + }, + option: { + glyph: { + key: `info`, + dim: `md`, + classes: `text-layer-1-glyph-hl taps`, + }, + callback: async () => { + //await fetch_models(); + alert(`Todo!`); + }, + }, + }} +/> diff --git a/src/routes/(app)/nostr/keys/+page.svelte b/src/routes/(app)/nostr/keys/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { cl } from "$lib/client"; + import { lc } from "$lib/client"; import LayoutTrellis from "$lib/components/layout-trellis.svelte"; import LayoutView from "$lib/components/layout-view.svelte"; import Nav from "$lib/components/nav.svelte"; @@ -12,9 +12,9 @@ onMount(async () => { try { - const public_key = await cl.preferences.get(_cf.pref_key_active); + const public_key = await lc.preferences.get(_cf.pref_key_active); if (public_key) nostr_public_key = public_key; - const secret_key = await cl.keystore.get(`nostr:key:${public_key}`); + const secret_key = await lc.keystore.get(`nostr:key:${public_key}`); if (secret_key) nostr_secret_key = secret_key; } catch (e) { } finally { @@ -23,7 +23,7 @@ async function copyToClipboard(text: string) { navigator.clipboard.writeText(text).then(async () => { - await cl.dialog.alert( + await lc.dialog.alert( `Copied nostr key "${text.slice(0, 12)}..." to clipboard.`, ); }); @@ -60,7 +60,7 @@ label: { left: [ { - value: cl.nostr.lib.npub( + value: lc.nostr.lib.npub( nostr_public_key, ), }, @@ -69,7 +69,7 @@ callback: async () => { await copyToClipboard( - cl.nostr.lib.npub(nostr_public_key), + lc.nostr.lib.npub(nostr_public_key), ); }, }, @@ -106,7 +106,7 @@ label: { left: [ { - value: cl.nostr.lib.nsec( + value: lc.nostr.lib.nsec( nostr_secret_key, ), }, @@ -115,7 +115,7 @@ callback: async () => { await copyToClipboard( - cl.nostr.lib.nsec(nostr_secret_key), + lc.nostr.lib.nsec(nostr_secret_key), ); }, }, diff --git a/src/routes/(app)/nostr/notes/post/+page.svelte b/src/routes/(app)/nostr/notes/post/+page.svelte @@ -1,6 +1,6 @@ <script lang="ts"> import { goto } from "$app/navigation"; - import { cl } from "$lib/client"; + import { lc } from "$lib/client"; import ButtonSubmit from "$lib/components/button-submit.svelte"; import LayoutTrellis from "$lib/components/layout-trellis.svelte"; import LayoutView from "$lib/components/layout-view.svelte"; @@ -14,7 +14,7 @@ const nostr_note_publish = async (): Promise<void> => { try { if (!value_note_content) { - await cl.dialog.alert(`You must write something to post.`); + await lc.dialog.alert(`You must write something to post.`); return; } diff --git a/src/routes/(app)/nostr/profile/+page.svelte b/src/routes/(app)/nostr/profile/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { cl } from "$lib/client"; + import { lc } from "$lib/client"; import LayoutTrellis from "$lib/components/layout-trellis.svelte"; import LayoutView from "$lib/components/layout-view.svelte"; import Nav from "$lib/components/nav.svelte"; @@ -53,28 +53,28 @@ const nostr_metadata_publish = async (): Promise<void> => { try { - const kind0_name = await cl.dialog.prompt({ + const kind0_name = await lc.dialog.prompt({ title: `Name`, message: `What is your personal name.`, input_placeholder: `Enter your name`, }); if (kind0_name === false) return; - const kind0_display_name = await cl.dialog.prompt({ + const kind0_display_name = await lc.dialog.prompt({ title: `Profile Name`, message: `What is your profile name.`, input_placeholder: `Enter profile name`, }); if (kind0_display_name === false) return; - const kind0_about = await cl.dialog.prompt({ + const kind0_about = await lc.dialog.prompt({ title: `About`, message: `What is your about me blurb.`, input_placeholder: `Enter about me`, }); if (kind0_about === false) return; - const kind0_website = await cl.dialog.prompt({ + const kind0_website = await lc.dialog.prompt({ title: `About`, message: `What is your website.`, input_placeholder: `Enter website`, @@ -89,7 +89,7 @@ if (kind0_website) content.website = kind0_website; if (Object.keys(content).length === 0) { - await cl.dialog.alert( + await lc.dialog.alert( `You must specify at least one profile field.`, ); return; diff --git a/src/routes/(app)/settings/+page.svelte b/src/routes/(app)/settings/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { cl } from "$lib/client"; + import { lc } from "$lib/client"; import LayoutTrellis from "$lib/components/layout-trellis.svelte"; import LayoutView from "$lib/components/layout-view.svelte"; import Nav from "$lib/components/nav.svelte"; @@ -43,7 +43,7 @@ ], }, callback: async () => { - await cl.haptics.impact(); + await lc.haptics.impact(); app_thc.set(toggle_color_mode($app_thc)); }, }, @@ -76,10 +76,10 @@ }, }, callback: async () => { - const public_key = await cl.preferences.get( + const public_key = await lc.preferences.get( _cf.pref_key_active, ); - await cl.dialog.alert( + await lc.dialog.alert( `Hi! This is your nostr public key ${public_key}`, ); }, @@ -101,14 +101,14 @@ }, }, callback: async () => { - const public_key = await cl.preferences.get( + const public_key = await lc.preferences.get( _cf.pref_key_active, ); console.log(`public_key `, public_key); - const secret_key = await cl.keystore.get( + const secret_key = await lc.keystore.get( `nostr:key:${public_key}`, ); - await cl.dialog.alert( + await lc.dialog.alert( `Hi! This is your nostr secret key ${secret_key}`, ); }, @@ -142,24 +142,24 @@ }, }, callback: async () => { - const confirm = await cl.dialog.confirm( + const confirm = await lc.dialog.confirm( `Hi! This will delete your saved keys.`, ); if (confirm) { const nostr_public_key = - await cl.preferences.get( + await lc.preferences.get( _cf.pref_key_active, ); if (nostr_public_key) { - await cl.keystore.remove( + await lc.keystore.remove( `nostr:key:${nostr_public_key}`, ); - await cl.preferences.remove( + await lc.preferences.remove( _cf.pref_key_active, ); await restart(); } else { - await cl.dialog.alert( + await lc.dialog.alert( `There is no public key preference saved.`, ); } @@ -190,8 +190,8 @@ ], }, callback: async () => { - const pos = await cl.geo.current(); - await cl.dialog.alert(JSON.stringify(pos)); + const pos = await lc.geo.current(); + await lc.dialog.alert(JSON.stringify(pos)); }, }, }, @@ -218,7 +218,7 @@ }, callback: async () => { const url = `https://radroots.org`; - await cl.browser.open(url); + await lc.browser.open(url); }, }, }, @@ -232,7 +232,7 @@ ], }, callback: async () => { - await cl.share.open({ + await lc.share.open({ title: `Radroots Home Page`, text: `Find farmers around the world.`, url: `https://radroots.org`, @@ -251,12 +251,12 @@ ], }, callback: async () => { - const public_key = await cl.preferences.get( + const public_key = await lc.preferences.get( _cf.pref_key_active, ); - const npub = cl.nostr.lib.npub(public_key); + const npub = lc.nostr.lib.npub(public_key); const url = `https://primal.net/p/${npub}`; - await cl.browser.open(url); + await lc.browser.open(url); }, }, }, @@ -282,8 +282,8 @@ ], }, callback: async () => { - const data = await cl.device.info(); - await cl.dialog.alert(JSON.stringify(data)); + const data = await lc.device.info(); + await lc.dialog.alert(JSON.stringify(data)); }, }, }, @@ -297,8 +297,8 @@ ], }, callback: async () => { - const data = await cl.device.battery(); - await cl.dialog.alert(JSON.stringify(data)); + const data = await lc.device.battery(); + await lc.dialog.alert(JSON.stringify(data)); }, }, }, @@ -330,7 +330,7 @@ }, }, callback: async () => { - await cl.haptics.impact("less"); + await lc.haptics.impact("less"); }, }, }, @@ -350,7 +350,7 @@ }, }, callback: async () => { - await cl.haptics.impact("more"); + await lc.haptics.impact("more"); }, }, }, @@ -370,7 +370,7 @@ }, }, callback: async () => { - await cl.haptics.vibrate(500); + await lc.haptics.vibrate(500); }, }, }, diff --git a/src/routes/(conf)/+layout.ts b/src/routes/(conf)/+layout.ts @@ -1,13 +1,13 @@ import { goto } from '$app/navigation'; -import { cl } from '$lib/client'; +import { lc } from '$lib/client'; import { _cf } from '$lib/conf'; import type { LayoutLoad, LayoutLoadEvent } from '../$types'; export const load: LayoutLoad = async ({ url }: LayoutLoadEvent) => { try { - const key_active = await cl.preferences.get(_cf.pref_key_active); + const key_active = await lc.preferences.get(_cf.pref_key_active); if (key_active) { - const ks_keys = await cl.keystore.keys(); + const ks_keys = await lc.keystore.keys(); const active_nostr_key = ks_keys?.find( (i) => i === `nostr:key:${key_active}`, ); @@ -17,7 +17,7 @@ export const load: LayoutLoad = async ({ url }: LayoutLoadEvent) => { } } } catch (e) { } finally { - await cl.window.splash_hide(); + await lc.window.splash_hide(); return {}; }; }; diff --git a/src/routes/(conf)/conf/nostr/+page.svelte b/src/routes/(conf)/conf/nostr/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { cl } from "$lib/client"; + import { lc } from "$lib/client"; import { _cf } from "$lib/conf"; import { restart } from "$lib/utils"; import { sleep } from "@radroots/svelte-lib"; @@ -9,14 +9,14 @@ <button class={`flex flex-row justify-center items-center text-white`} on:click={async () => { - const sk_hex = cl.nostr.lib.generate_key(); - const pk_hex = cl.nostr.lib.public_key(sk_hex); - const new_key_added = await cl.keystore.set( + const sk_hex = lc.nostr.lib.generate_key(); + const pk_hex = lc.nostr.lib.public_key(sk_hex); + const new_key_added = await lc.keystore.set( `nostr:key:${pk_hex}`, sk_hex, ); if (new_key_added) { - await cl.preferences.set(_cf.pref_key_active, pk_hex); + await lc.preferences.set(_cf.pref_key_active, pk_hex); await sleep(500); await restart(true); } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte @@ -5,7 +5,7 @@ PUBLIC_DATABASE_NAME, PUBLIC_NOSTR_RELAY_DEFAULTS, } from "$env/static/public"; - import { cl } from "$lib/client"; + import { lc } from "$lib/client"; import LayoutWindow from "$lib/components/layout-window.svelte"; import { _cf } from "$lib/conf"; import { @@ -35,7 +35,7 @@ import { onMount } from "svelte"; import "../app.css"; - let render_pwa = browser && cl.platform === `web`; + let render_pwa = browser && lc.platform === `web`; if (render_pwa) { const el = document.createElement(`jeep-sqlite`); document.body.appendChild(el); @@ -73,13 +73,13 @@ app_thc.subscribe((app_thc) => { const color_mode = parse_color_mode(app_thc); theme_set(parse_theme_key($app_thm), color_mode); - cl.window.status_style(color_mode); + lc.window.status_style(color_mode); }); app_thm.subscribe((app_thm) => { const color_mode = parse_color_mode($app_thc); theme_set(parse_theme_key(app_thm), color_mode); - cl.window.status_style(color_mode); + lc.window.status_style(color_mode); }); app_sqlite.subscribe((app_sqlite) => { @@ -90,7 +90,7 @@ app_nostr_key.subscribe(async (app_nostr_key) => { try { if (!app_nostr_key) return; - const private_key = await cl.keystore.get( + const private_key = await lc.keystore.get( `nostr:key:${app_nostr_key}`, ); if (private_key) { @@ -117,12 +117,12 @@ app_config.subscribe(async (app_config) => { try { if (!app_config) return; - app_sqlite.set(!!(await cl.db.connect(PUBLIC_DATABASE_NAME))); - const active_nostr_pk = await cl.preferences.get( + app_sqlite.set(!!(await lc.db.connect(PUBLIC_DATABASE_NAME))); + const active_nostr_pk = await lc.preferences.get( _cf.pref_key_active, ); console.log(`active_nostr_pk `, active_nostr_pk); - const active_nostr_sk = await cl.keystore.get( + const active_nostr_sk = await lc.keystore.get( `nostr:key:${active_nostr_pk}`, ); console.log(`active_nostr_sk `, active_nostr_sk); @@ -133,7 +133,7 @@ ) app_nostr_key.set(active_nostr_pk); else { - await cl.preferences.remove(_cf.pref_key_active); + await lc.preferences.remove(_cf.pref_key_active); await goto(`/conf/nostr`); } } catch (e) { @@ -158,7 +158,7 @@ } catch (e) { console.log(`(app_render) error `, e); } finally { - await cl.window.splash_hide(); + await lc.window.splash_hide(); } }); </script> diff --git a/static/assets/keyva.min.js b/static/assets/keyva.min.js @@ -0,0 +1 @@ +"use strict"; class Keyva { static unbound = IDBKeyRange.lowerBound(Number.MIN_SAFE_INTEGER); static prefix(prefix) { return IDBKeyRange.bound(prefix, prefix + "￿") } static async each() { const databases = await indexedDB.databases(); return databases.map((db => db.name)).filter((s => !!s && s.startsWith(this.kvPrefix))) } static async delete(...names) { names = names.length ? names.map((n => n.startsWith(this.kvPrefix) ? n : this.kvPrefix + n)) : await this.each(); Promise.all(names.map((n => this.asPromise(indexedDB.deleteDatabase(n))))) } static kvPrefix = "-radroots-"; constructor(options = {}) { const idx = options.indexes || []; this.indexes = (Array.isArray(idx) ? idx : [idx]).sort(); this.name = Keyva.kvPrefix + (options.name || "") } indexes; name; async get(k) { const store = await this.getStore("readonly"); return Array.isArray(k) ? Promise.all(k.map((key => Keyva.asPromise(store.get(key))))) : Keyva.asPromise(store.get(k)) } async each(options = {}, only) { const store = await this.getStore("readonly"); const target = options.index ? store.index(options.index) : store; const limit = options.limit; const range = options.range; if (only === "keys") return Keyva.asPromise(target.getAllKeys(range, limit)); if (only === "values") return Keyva.asPromise(target.getAll(range, limit)); let keys = []; let values = []; await Promise.allSettled([new Promise((async r => { const results = await Keyva.asPromise(target.getAllKeys(range, limit)); keys.push(...results); r() })), new Promise((async r => { const results = await Keyva.asPromise(target.getAll(range, limit)); values.push(...results); r() }))]); const tuples = []; for (let i = -1; ++i < keys.length;)tuples.push([keys[i], values[i]]); return tuples } async set(a, b) { const store = await this.getStore("readwrite"); if (Array.isArray(a)) { for (const entry of a) store.put(entry[1], entry[0]); return Keyva.asPromise(store.transaction) } store.put(b, a); return Keyva.asPromise(store.transaction) } async delete(arg) { const store = await this.getStore("readwrite"); arg ??= Keyva.unbound; if (Array.isArray(arg)) { for (const key of arg) store.delete(key) } else store.delete(arg); return Keyva.asPromise(store.transaction) } async getStore(mode) { const db = await this.getDatabase(); return db.transaction(this.name, mode).objectStore(this.name) } async getDatabase() { if (!this.database) { await this.maybeFixSafari(); let quit = false; let version; let indexNamesAdded = []; let indexNamesRemoved = []; for (; ;) { const request = indexedDB.open(this.name, version); request.onupgradeneeded = () => { const db = request.result; const tx = request.transaction; const store = tx.objectStoreNames.contains(this.name) ? tx.objectStore(this.name) : db.createObjectStore(this.name); for (const index of indexNamesAdded) store.createIndex(index, index); for (const index of indexNamesRemoved) store.deleteIndex(index) }; this.database = await Keyva.asPromise(request); if (quit) break; const tx = this.database.transaction(this.name, "readonly"); const store = tx.objectStore(this.name); const indexNames = Array.from(store.indexNames).sort(); tx.abort(); indexNamesAdded = this.indexes.filter((n => !indexNames.includes(n))); indexNamesRemoved = indexNames.filter((n => !this.indexes.includes(n))); if (indexNamesAdded.length + indexNamesRemoved.length === 0) break; quit = true; this.database.close(); version = this.database.version + 1 } } return this.database } database = null; async maybeFixSafari() { if (!/Version\/14\.\d*\s*Safari\//.test(navigator.userAgent)) return; let id = 0; return new Promise((resolve => { const hit = () => indexedDB.databases().finally(resolve); id = setInterval(hit, 50); hit() })).finally((() => clearInterval(id))) } static asPromise(request) { return new Promise(((resolve, reject) => { request.oncomplete = request.onsuccess = () => resolve(request.result); request.onabort = request.onerror = () => reject(request.error) })) } } if (typeof module === "object") Object.assign(module.exports, { Keyva: Keyva }); diff --git a/tailwind.config.ts b/tailwind.config.ts @@ -4,6 +4,10 @@ import type { Config } from "tailwindcss"; import tailwind_default from "tailwindcss/defaultTheme"; const { fontFamily: tw_font } = tailwind_default; +const heights_form = { + line: "42px", +}; + const heights = { line: `46px`, tabs_base: `64px`, @@ -41,6 +45,7 @@ const config: Config = { }, height: { ...heights, + ...Object.fromEntries(Object.entries(heights_form).map(([k, v]) => [`form_${k}`, v])), }, width: { ...widths, @@ -68,6 +73,9 @@ const config: Config = { borderWidth: { "line": "1px" }, + borderRadius: { + input_form: "8px", + }, } }, plugins: [