app

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

commit 223ae4455e99b97f4ced89d9dedfacbd0108ddf9
parent a9519ea3714bdf870d9aa9c3ed32d944e261064f
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Sat,  7 Dec 2024 18:02:11 +0000

Edit `/cfg/init` add logo circle, edit features, styles. Edit `/settings/profile` add profile detail layout, nostr sync metadata lifecycle. Add `/settings/profile/edit` with handlers to update nostr profile based on url param rkey, nostr sync metadata lifecycle. Edit app home adding logo circle. Edit components,  conf, routes, layouts.

Diffstat:
M.gitignore | 5+++--
Msrc/lib/components/image_upload_add_photo.svelte | 2+-
Msrc/lib/conf.ts | 7+++++++
Asrc/lib/util/nostr-sync.ts | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/lib/util/nostr.ts | 116-------------------------------------------------------------------------------
Msrc/routes/(app)/+layout.svelte | 6+++---
Msrc/routes/(app)/+page.svelte | 25++++++++++---------------
Msrc/routes/(app)/settings/profile/+page.svelte | 201++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Asrc/routes/(app)/settings/profile/edit/+page.svelte | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/routes/(cfg)/cfg/+layout.ts | 7++++---
Msrc/routes/(cfg)/cfg/error/+page.svelte | 4++--
Msrc/routes/(cfg)/cfg/init/+page.svelte | 64+++++++++++++++++++++++++++-------------------------------------
Msrc/routes/+layout.ts | 1+
13 files changed, 586 insertions(+), 217 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -50,4 +50,5 @@ dist git-diff.txt target tauri.conf.build* -/crates/tauri/gen -\ No newline at end of file +/crates/tauri/gen +.dev +\ No newline at end of file diff --git a/src/lib/components/image_upload_add_photo.svelte b/src/lib/components/image_upload_add_photo.svelte @@ -17,7 +17,7 @@ <div class={`relative flex flex-row w-full justify-center items-center`}> <button - class={`flex flex-row h-[5rem] w-[5rem] justify-center items-center bg-layer-0-surface/80 rounded-full`} + class={`flex flex-row h-[5rem] w-[5rem] justify-center items-center bg-layer-1-surface/60 rounded-full`} on:click={async () => { await handle_photo_add(); }} diff --git a/src/lib/conf.ts b/src/lib/conf.ts @@ -23,6 +23,12 @@ export const ascii = { dash: `—` } +export const err = { + nostr: { + no_relays: `error.nostr.no_relays_connected` + } +} + export const cfg = { app: { title: `Radroots`, @@ -40,6 +46,7 @@ export const cfg = { mount_el: 500, nostr_relay_poll_document: 3000, entry_focus: 2000, + load_notify: 3000, }, cmd: { layout_route: `*-route` diff --git a/src/lib/util/nostr-sync.ts b/src/lib/util/nostr-sync.ts @@ -0,0 +1,144 @@ +import { db, device, dialog } from "$lib/client"; +import { err, nostr_client, root_symbol } from "$lib/conf"; +import { NDKKind } from "@nostr-dev-kit/ndk"; +import type { NostrRelay } from "@radroots/models"; +import { app_nostr_key, ndk, ndk_user, nostr_sync_prevent, t } from "@radroots/svelte-lib"; +import { fmt_tags_basis_nip99, ndk_event, ndk_event_metadata, nevent_encode, num_str } from "@radroots/utils"; +import { get as get_store } from "svelte/store"; +import { throw_err } from "./error"; + +export const nostr_sync_metadata = async (): Promise<void> => { + try { + const $ndk = get_store(ndk); + const $ndk_user = get_store(ndk_user); + const $app_nostr_key = get_store(app_nostr_key); + const nostr_profile = await db.nostr_profile_get_one({ + public_key: $app_nostr_key + }); + if (`err` in nostr_profile) return throw_err(nostr_profile); + const ev_metadata = await ndk_event_metadata({ + $ndk, + $ndk_user, + metadata: nostr_profile.result + }); + if (ev_metadata) await ev_metadata.publish(); + } catch (e) { + console.log(`(error) nostr_sync_metadata `, e); + } +}; + +export const nostr_sync_classified = async (nostr_relays: NostrRelay[]): Promise<void> => { + const $ndk = get_store(ndk); + const $ndk_user = get_store(ndk_user); + try { + const trade_products_all = await db.trade_product_get({ + list: [`all`], + }); + if (`err` in trade_products_all) return throw_err(trade_products_all); + for (const trade_product of trade_products_all.results) { + console.log(`sync trade_product.id `, trade_product.id) + const trade_product_location_res = await db.location_gcs_get({ + list: [`on_trade_product`, { id: trade_product.id }], + }); + if (`err` in trade_product_location_res) return throw_err(trade_product_location_res); + const trade_product_location = trade_product_location_res.results[0]; + + const media_upload_res = await db.media_upload_get({ + list: [`on_trade_product`, { id: trade_product.id }], + }); + if (`err` in media_upload_res) return throw_err(media_upload_res); + const ev = await ndk_event({ + $ndk, + $ndk_user, + basis: { + kind: NDKKind.Classified, + content: ``, + tags: fmt_tags_basis_nip99({ + d_tag: trade_product.id, + client: nostr_client, + listing: { + key: trade_product.key, + category: trade_product.category, + title: trade_product.title, + summary: trade_product.summary, + process: trade_product.process, + lot: trade_product.lot, + profile: trade_product.profile, + year: num_str(trade_product.year), + }, + quantity: { + amt: num_str(trade_product.qty_amt), + unit: trade_product.qty_unit, + label: trade_product.qty_label + }, + price: { + amt: num_str(trade_product.price_amt), + currency: trade_product.price_currency, + qty_amt: num_str(trade_product.price_qty_amt), + qty_unit: trade_product.price_qty_unit, + }, + location: { + city: trade_product_location.gc_name, + region: trade_product_location.gc_admin1_name, + region_code: trade_product_location.gc_admin1_id, + country: trade_product_location.gc_country_name, + country_code: trade_product_location.gc_country_id, + lat: trade_product_location.lat, + lng: trade_product_location.lng, + geohash: trade_product_location.geohash, + }, + images: media_upload_res.results.length ? media_upload_res.results.map(i => ({ url: `${i.res_base}/${i.res_path}.${i.mime_type}` })) : undefined + }), + }, + }); + if (ev) { + ev.content = `radroots:[nostr:${nevent_encode({ + id: ev.id, + author: ev.pubkey, + relays: nostr_relays.map(i => i.url), + kind: NDKKind.Classified, + })}]` + await ev.publish(); + } + } + } catch (e) { + console.log(`(error) nostr_sync_classified `, e); + } +}; + +export const nostr_sync = async (): Promise<void> => { + const $nostr_sync_prevent = get_store(nostr_sync_prevent); + const $t = get_store(t); + const $app_nostr_key = get_store(app_nostr_key); + try { + if ($nostr_sync_prevent) { + const confirm = await dialog.confirm({ + message: `${$t(`error.client.nostr_sync_disabled`)}`, + }); + if (confirm) { + nostr_sync_prevent.set(false); + await nostr_sync(); + } + return; + } + console.log(`nostr_sync start`) + const nostr_relays = await db.nostr_relay_get({ + list: [`on_profile`, { public_key: $app_nostr_key }], + }); + if (`err` in nostr_relays) return throw_err(nostr_relays); + if (!nostr_relays.results.length) return throw_err(err.nostr.no_relays); + // + // sync + await nostr_sync_metadata(); + await nostr_sync_classified(nostr_relays.results); + console.log(`nostr_sync done`) + } catch (e) { + console.log(`(error) nostr_sync `, e); + } +}; + +export const nostr_tags_basis = (): string[][] => { + const tags: string[][] = []; + for (const tag of [`app${device.metadata?.version ? `/${device.metadata.version}` : ``}`]) tags.push([root_symbol, tag]) + return tags; +}; diff --git a/src/lib/util/nostr.ts b/src/lib/util/nostr.ts @@ -1,116 +0,0 @@ -import { db, dialog } from "$lib/client"; -import { nostr_client, root_symbol } from "$lib/conf"; -import { NDKKind } from "@nostr-dev-kit/ndk"; -import { app_nostr_key, ndk, ndk_user, nostr_sync_prevent, t } from "@radroots/svelte-lib"; -import { fmt_tags_basis_nip99, ndk_event, nevent_encode, num_str } from "@radroots/utils"; -import { get as get_store } from "svelte/store"; - -export const nostr_sync = async (): Promise<void> => { - try { - console.log(`run nostr sync!`) - const $t = get_store(t); - const $nostr_sync_prevent = get_store(nostr_sync_prevent); - const $app_nostr_key = get_store(app_nostr_key); - - if ($nostr_sync_prevent) { - const confirm = await dialog.confirm({ - message: `${$t(`error.client.nostr_sync_disabled`)}`, - cancel_label: `${$t(`common.cancel`)}`, - ok_label: `${$t(`common.ok`)}` - }); - if (confirm) { - nostr_sync_prevent.set(false); - await nostr_sync(); - } - return; - } - - const $ndk = get_store(ndk); - const $ndk_user = get_store(ndk_user); - - const nostr_relays_active = await db.nostr_relay_get({ - list: [`on_profile`, { public_key: $app_nostr_key }], - }); - if (`err` in nostr_relays_active) return; //@todo - if (!nostr_relays_active.results.length) return; //@todo - const trade_products_all = await db.trade_product_get({ - list: [`all`], - }); - if (`err` in trade_products_all) return; //@todo - for (const trade_product of trade_products_all.results) { - console.log(`SYNC trade_product.id `, trade_product.id) - const trade_product_location_res = await db.location_gcs_get({ - list: [`on_trade_product`, { id: trade_product.id }], - }); - if (`err` in trade_product_location_res) continue; //@todo - const trade_product_location = trade_product_location_res.results[0]; - - const media_upload_res = await db.media_upload_get({ - list: [`on_trade_product`, { id: trade_product.id }], - }); - if (`err` in media_upload_res) continue; //@todo - - const ev = await ndk_event({ - $ndk, - $ndk_user, - basis: { - kind: NDKKind.Classified, - content: ``, - tags: fmt_tags_basis_nip99({ - d_tag: trade_product.id, - client: nostr_client, - listing: { - key: trade_product.key, - category: trade_product.category, - title: trade_product.title, - summary: trade_product.summary, - process: trade_product.process, - lot: trade_product.lot, - profile: trade_product.profile, - year: num_str(trade_product.year), - }, - quantity: { - amt: num_str(trade_product.qty_amt), - unit: trade_product.qty_unit, - label: trade_product.qty_label - }, - price: { - amt: num_str(trade_product.price_amt), - currency: trade_product.price_currency, - qty_amt: num_str(trade_product.price_qty_amt), - qty_unit: trade_product.price_qty_unit, - }, - location: { - city: trade_product_location.gc_name, - region: trade_product_location.gc_admin1_name, - region_code: trade_product_location.gc_admin1_id, - country: trade_product_location.gc_country_name, - country_code: trade_product_location.gc_country_id, - lat: trade_product_location.lat, - lng: trade_product_location.lng, - geohash: trade_product_location.geohash, - }, - images: media_upload_res.results.length ? media_upload_res.results.map(i => ({ url: `${i.res_base}/${i.res_path}.${i.mime_type}` })) : undefined - }), - }, - }); - if (ev) { - ev.content = `radroots:[nostr:${nevent_encode({ - id: ev.id, - author: ev.pubkey, - relays: nostr_relays_active.results.map(i => i.url), - kind: NDKKind.Classified, - })}]` - await ev.publish(); - } - } - } catch (e) { - console.log(`(error) nostr_sync `, e); - } -}; - -export const nostr_tags_basis = (): string[][] => { - const tags: string[][] = []; - for (const tag of [`app/0.0.0`]) tags.push([root_symbol, tag]) - return tags; -}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte @@ -1,8 +1,8 @@ <script lang="ts"> import { db, geoc, keystore, notification } from "$lib/client"; - import { ks } from "$lib/conf"; + import { cfg, ks } from "$lib/conf"; import { fetch_relay_documents } from "$lib/util/fetch"; - import { nostr_sync } from "$lib/util/nostr"; + import { nostr_sync } from "$lib/util/nostr-sync"; import { app_cfg_type, app_geoc, @@ -41,7 +41,7 @@ app_init.subscribe(async (_app_init) => { try { if (!app_init) return; - await sleep(4000); + await sleep(cfg.delay.load_notify); await notification.init(); } catch (e) { console.log(`(app_init) error `, e); diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte @@ -1,19 +1,18 @@ <script lang="ts"> - import { db, device, dialog } from "$lib/client"; + import { db, dialog } from "$lib/client"; import { app_nostr_key, envelope_visible, EnvelopeLower, Glyph, LayoutView, + LogoCircleSm, nav_prev, route, t, } from "@radroots/svelte-lib"; import { onMount } from "svelte"; - $: device_metadata = device.metadata ? device.metadata.version : ``; - onMount(async () => { try { nav_prev.set([]); @@ -34,16 +33,12 @@ <LayoutView> <div class={`flex flex-row h-12 w-full px-6 justify-between items-center`}> <div class={`flex flex-row gap-2 justify-start items-center`}> - <p class={`font-mono font-[600] text-[1.3rem] text-layer-0-glyph`}> - {`radRoots`} + <LogoCircleSm /> + <p + class={`font-sansd italic font-[700] text-[1.7rem] text-layer-0-glyph`} + > + {`radroots`} </p> - {#if device_metadata} - <p - class={`font-mono font-[400] text-[1.3rem] text-layer-0-glyph`} - > - {`/${device_metadata}`} - </p> - {/if} </div> <button class={`flex flex-row justify-center items-center`} @@ -62,10 +57,10 @@ </button> </div> <div - class={`flex flex-col w-full pt-2 px-6 gap-2 justify-center items-center`} + class={`flex flex-col w-full pt-4 px-6 gap-4 justify-center items-center`} > <div class={`flex flex-row w-full justify-start items-center`}> - <p class={`font-sans font-[600] text-2xl text-layer-0-glyph`}> + <p class={`font-sansd font-[600] text-2xl text-layer-0-glyph`}> {`${$t(`common.general`)}`} </p> </div> @@ -77,7 +72,7 @@ }} > <p - class={`font-sans font-[600] text-xl text-layer-0-glyph capitalize tracking-wider opacity-active`} + class={`font-sans font-[700] text-xl text-layer-0-glyph capitalize tracking-wider opacity-active`} > {`${$t(`common.profile`)}`} </p> diff --git a/src/routes/(app)/settings/profile/+page.svelte b/src/routes/(app)/settings/profile/+page.svelte @@ -1,26 +1,36 @@ <script lang="ts"> - import { db, fs } from "$lib/client"; + import { db, dialog, fs } from "$lib/client"; import ImageUploadAddPhoto from "$lib/components/image_upload_add_photo.svelte"; import { ascii } from "$lib/conf"; import { kv_init_page } from "$lib/util/kv"; - import type { NostrProfile } from "@radroots/models"; + import { model_media_upload_add_list } from "$lib/util/models-media-upload"; + import { nostr_sync_metadata } from "$lib/util/nostr-sync"; + import { fmt_media_upload_url, type NostrProfile } from "@radroots/models"; import { app_nostr_key, Glyph, ImageBlob, + ImagePath, LayoutView, Nav, + route, t, } from "@radroots/svelte-lib"; - import { onMount } from "svelte"; + import { onDestroy, onMount } from "svelte"; type LoadData = { nostr_profile: NostrProfile; }; let ld: LoadData | undefined = undefined; + let loading_photo_upload = false; let opt_photo_path = ``; - let opt_display: `photos` | `following` | `followers` = `photos`; + type ViewDisplay = `photos` | `following` | `followers`; + let view_display: ViewDisplay = `photos`; + + $: { + console.log(JSON.stringify(ld, null, 4), `ld`); + } onMount(async () => { try { @@ -30,17 +40,25 @@ } }); + onDestroy(async () => { + try { + await nostr_sync_metadata(); + } catch (e) { + } finally { + } + }); + $: photo_overlay_visible = ld?.nostr_profile?.picture || opt_photo_path; - $: classes_photo_overlay_wrap = photo_overlay_visible - ? `bg-white/30 backdrop-blur-sm` - : ``; $: classes_photo_overlay_glyph = photo_overlay_visible ? `text-white` : `text-layer-0-glyph`; - - $: classes_photo_overlay_glyph_d = photo_overlay_visible - ? `text-white/40` + $: classes_photo_overlay_glyph_opt = photo_overlay_visible + ? `text-gray-300` : `text-layer-0-glyph`; + $: classes_photo_overlay_glyph_opt_selected = photo_overlay_visible + ? `text-white` + : `text-layer-1-glyph`; + const init_page = async (): Promise<void> => { try { await kv_init_page(); @@ -56,22 +74,102 @@ public_key: $app_nostr_key, }); if (`err` in nostr_profile_get_one) return; - - const load: LoadData = { + return { nostr_profile: nostr_profile_get_one.result, - }; - return load; + } satisfies LoadData; } catch (e) { console.log(`(error) load_data `, e); } }; + + const handle_profile_photo_add = async ( + file_path: string, + ): Promise<void> => { + try { + const confirm = await dialog.confirm({ + message: `The photo will be used for your profile. Do you want to continue?`, + }); + if (!confirm) return; + loading_photo_upload = true; + let photo_url = ``; + const media_upload_existing = await db.media_upload_get_one({ + file_path, + }); + if (`result` in media_upload_existing) + photo_url = fmt_media_upload_url(media_upload_existing.result); + else { + const media_uploads = await model_media_upload_add_list({ + photo_paths: [file_path], + }); + if (`alert` in media_uploads) { + await dialog.alert(media_uploads.alert); + return; + } else if (`confirm` in media_uploads) { + await dialog.confirm(media_uploads.confirm); + return; + } + if ( + `results` in media_uploads && + media_uploads.results.length + ) { + const media_upload = await db.media_upload_get_one({ + id: media_uploads.results[0], + }); + if (`result` in media_upload) + photo_url = fmt_media_upload_url(media_upload.result); + } + } + if (photo_url) { + const nostr_profile_update = await db.nostr_profile_update({ + on: { + public_key: $app_nostr_key, + }, + fields: { + picture: photo_url, + }, + }); + if (`err` in nostr_profile_update) { + await dialog.alert(`${$t(`error.client.unhandled`)}`); + return; + } + } + location.reload(); + } catch (e) { + console.log(`(error) handle_profile_photo_add `, e); + } finally { + loading_photo_upload = false; + } + }; </script> <LayoutView> <div - class={`relative flex flex-col min-h-[525px] h-[525px] w-full justify-center items-center bg-layer-2-surface`} + class={`relative flex flex-col min-h-[440px] h-[440px] w-full justify-center items-center bg-layer-2-surface fade-in`} > - {#if opt_photo_path} + {#if ld?.nostr_profile?.picture} + <ImagePath + basis={{ + path: ld.nostr_profile.picture, + }} + /> + <div class={`absolute top-4 right-4 flex flex-row`}> + <button + class={`flex flex-row h-12 w-12 justify-center items-center bg-layer-0-surface rounded-full layer-1-active-surface el-re`} + on:click={async () => { + alert(`@todo!`); + }} + > + <Glyph + basis={{ + classes: ``, + dim: `sm`, + weight: `bold`, + key: `images-square`, + }} + /> + </button> + </div> + {:else if opt_photo_path} {#await fs.read_bin(opt_photo_path) then file_data} <ImageBlob basis={{ @@ -87,7 +185,7 @@ </div> {/if} <div - class={`absolute bottom-0 left-0 flex flex-col h-[calc(100%-100%/1.618)] w-full px-6 gap-2 justify-end items-center ${classes_photo_overlay_wrap}`} + class={`absolute bottom-0 left-0 flex flex-col h-[calc(100%-100%/1.618)] w-full px-6 gap-2 justify-end items-center`} > <div class={`flex flex-col w-full gap-[2px] justify-center items-center`} @@ -97,10 +195,15 @@ > <button class={`group flex flex-row justify-center items-center`} - on:click={async () => {}} + on:click={async () => { + await route(`/settings/profile/edit`, [ + [`nostr_pk`, $app_nostr_key], + [`rkey`, `display_name`], + ]); + }} > <p - class={`font-sansd font-[600] text-[1.7rem] ${classes_photo_overlay_glyph} ${ld?.nostr_profile.display_name ? `` : `capitalize opacity-active`} el-re`} + class={`font-sansd font-[600] text-[2rem] ${classes_photo_overlay_glyph} ${ld?.nostr_profile.display_name ? `` : `capitalize opacity-active`} el-re`} > {ld?.nostr_profile.display_name ? ld.nostr_profile.display_name @@ -113,10 +216,19 @@ > <button class={`group flex flex-row justify-center items-center`} - on:click={async () => {}} + on:click={async () => { + const confirm = await dialog.confirm({ + message: `Updating your username will result in public links on your profile being updated. Do you want to continue?`, + }); + if (confirm) + await route(`/settings/profile/edit`, [ + [`nostr_pk`, $app_nostr_key], + [`rkey`, `name`], + ]); + }} > <p - class={`font-sans font-[600] text-[1.1rem] ${classes_photo_overlay_glyph} ${ld?.nostr_profile.name ? `` : `capitalize opacity-active`} el-re`} + class={`font-sansd font-[600] text-[1.1rem] ${classes_photo_overlay_glyph} ${ld?.nostr_profile.name ? `` : `capitalize opacity-active`} el-re`} > {ld?.nostr_profile.name ? `@${ld.nostr_profile.name}` @@ -147,10 +259,15 @@ <div class={`flex flex-row w-full justify-start items-center`}> <button class={`group flex flex-row justify-center items-center`} - on:click={async () => {}} + on:click={async () => { + await route(`/settings/profile/edit`, [ + [`nostr_pk`, $app_nostr_key], + [`rkey`, `about`], + ]); + }} > <p - class={`font-sans font-[400] text-[1.1rem] ${classes_photo_overlay_glyph} ${ld?.nostr_profile.about ? `` : `capitalize opacity-active`}`} + class={`font-sansd font-[400] text-[1.1rem] ${classes_photo_overlay_glyph} ${ld?.nostr_profile.about ? `` : `capitalize opacity-active`}`} > {ld?.nostr_profile.about ? `@${ld.nostr_profile.about}` @@ -165,54 +282,54 @@ <button class={`flex flex-row justify-center items-center`} on:click={async () => { - opt_display = `photos`; + view_display = `photos`; }} > <p - class={`font-sans text-[1.1rem] font-[600] ${opt_display === `photos` ? `text-layer-1-glyph_d` : `text-layer-0-glyph`} el-re`} + class={`font-sans text-[1.1rem] font-[600] capitalize ${view_display === `photos` ? classes_photo_overlay_glyph_opt_selected : classes_photo_overlay_glyph_opt} el-re`} > - {`Photos`} + {`photos`} </p> </button> <button class={`flex flex-row justify-center items-center`} on:click={async () => { - opt_display = `following`; + view_display = `following`; }} > <p - class={`font-sans text-[1.1rem] font-[600] ${opt_display === `following` ? `text-layer-1-glyph_d` : `text-layer-0-glyph`} el-re`} + class={`font-sans text-[1.1rem] font-[600] capitalize ${view_display === `following` ? classes_photo_overlay_glyph_opt_selected : classes_photo_overlay_glyph_opt} el-re`} > - {`Following`} + {`following`} </p> </button> <button class={`flex flex-row justify-center items-center`} on:click={async () => { - opt_display = `followers`; + view_display = `followers`; }} > <p - class={`font-sans text-[1.1rem] font-[600] ${opt_display === `followers` ? `text-layer-1-glyph_d` : `text-layer-0-glyph`} el-re`} + class={`font-sans text-[1.1rem] font-[600] capitalize ${view_display === `followers` ? classes_photo_overlay_glyph_opt_selected : classes_photo_overlay_glyph_opt} el-re`} > - {`Followers`} + {`followers`} </p> </button> </div> </div> </div> <div class={`flex flex-col w-full justify-start items-center`}> - {#if opt_display === `photos`} + {#if view_display === `photos`} <p class={`font-sans font-[400] text-layer-0-glyph`}> - {`photos `.repeat(500)} + {view_display} </p> - {:else if opt_display === `following`} + {:else if view_display === `following`} <p class={`font-sans font-[400] text-layer-0-glyph`}> - {`following `.repeat(500)} + {view_display} </p> - {:else if opt_display === `followers`} + {:else if view_display === `followers`} <p class={`font-sans font-[400] text-layer-0-glyph`}> - {`followers `.repeat(500)} + {view_display} </p> {/if} </div> @@ -220,8 +337,16 @@ <Nav basis={{ prev: { + loading: loading_photo_upload, label: `${$t(`common.home`)}`, route: `/`, + prevent_route: opt_photo_path + ? { + callback: async () => { + await handle_profile_photo_add(opt_photo_path); + }, + } + : undefined, }, title: { label: { diff --git a/src/routes/(app)/settings/profile/edit/+page.svelte b/src/routes/(app)/settings/profile/edit/+page.svelte @@ -0,0 +1,221 @@ +<script lang="ts"> + import { db, dialog } from "$lib/client"; + import { nostr_sync_metadata } from "$lib/util/nostr-sync"; + import { + nostr_profile_form_fields, + type NostrProfile, + type NostrProfileFields, + type NostrProfileFormFields, + parse_nostr_profile_form_keys, + } from "@radroots/models"; + import { + app_notify, + fmt_id, + InputElement, + kv, + LayoutView, + Nav, + qp_nostr_pk, + qp_rkey, + route, + t, + TextareaElement, + } from "@radroots/svelte-lib"; + import { onDestroy, onMount } from "svelte"; + + type LoadData = { + nostr_profile: NostrProfile; + field_key: keyof NostrProfileFields; + }; + let ld: LoadData | undefined = undefined; + + let page_initial_value = ``; + let page_input_value = ``; + + onMount(async () => { + try { + if (!$qp_rkey || !$qp_nostr_pk) { + app_notify.set( + `${$t(`icu.error_loading_*`, { value: `${$t(`common.page`)}` })}`, + ); + return; + } + ld = await load_page(); + } catch (e) { + } finally { + } + }); + + onDestroy(async () => { + try { + await nostr_sync_metadata(); + } catch (e) { + } finally { + } + }); + + $: translated_field_key = ld?.field_key + ? `${$t(`models.nostr_profile.fields.${ld.field_key}.label`)}` + : ``; + $: input_value_del = page_initial_value !== page_input_value; + const load_page = async (): Promise<LoadData | undefined> => { + try { + const nostr_profile = await db.nostr_profile_get_one({ + public_key: $qp_nostr_pk, + }); + if (`err` in nostr_profile) { + app_notify.set( + `${$t(`icu.error_loading_*`, { value: `${$t(`common.profile`)}` })}`, + ); + return; + } + + const field_key = parse_nostr_profile_form_keys($qp_rkey); + if (!field_key) { + app_notify.set(`${$t(`error.client.page.load`)}`); + return; + } + + const field_val = nostr_profile.result[field_key]; + if (field_val) { + page_input_value = field_val; + page_initial_value = field_val; + } + + const data: LoadData = { + nostr_profile: nostr_profile.result, + field_key, + }; + return data; + } catch (e) { + console.log(`(error) load_page `, e); + } + }; + + const submit = async (): Promise<void> => { + try { + if (!ld?.field_key || !ld?.nostr_profile) return; + const val = await kv.get(fmt_id($qp_rkey)); + if (!val) { + await route(`/settings/profile`); + return; + } + const validated = + nostr_profile_form_fields[ld.field_key].validation.test(val); + if (!validated) { + dialog.alert( + `${$t(`icu.invalid_*_entry`, { value: translated_field_key })}`, + ); + return; + } + const fields: Partial<NostrProfileFormFields> = {}; + fields[ld.field_key] = val; + const update_res = await db.nostr_profile_update({ + on: { + public_key: $qp_nostr_pk, + }, + fields, + }); + if (`err` in update_res) { + await dialog.alert(`${$t(`error.client.unhandled`)}`); + return; + } + await route(`/settings/profile`); + } catch (e) { + console.log(`(error) submit `, e); + } + }; +</script> + +<LayoutView> + {#if ld} + <div + class={`flex flex-col w-full pt-4 px-4 gap-1 justify-start items-center fade-in`} + > + <div class={`flex flex-row w-full justify-start items-center`}> + <p + class={`font-sans font-[400] text-[0.8rem] text-layer-0-glyph-label uppercase`} + > + {translated_field_key.replace(`Profile `, ``)} + </p> + </div> + {#if ld.field_key === `about`} + <TextareaElement + bind:value={page_input_value} + basis={{ + id: fmt_id(ld.field_key), + classes: `min-h-[8rem] pl-4`, + sync: true, + layer: 1, + placeholder: `Enter ${translated_field_key.toLowerCase()}`, + field: { + charset: + nostr_profile_form_fields[ld.field_key].charset, + validate: + nostr_profile_form_fields[ld.field_key] + .validation, + validate_keypress: true, + }, + callback_focus: async () => { + console.log(`hi`); + }, + }} + /> + {:else} + <InputElement + bind:value={page_input_value} + basis={{ + id: fmt_id(ld.field_key), + classes: `rounded-touch pl-4`, + sync: true, + layer: 1, + placeholder: `Enter ${translated_field_key.toLowerCase()}`, + field: { + charset: + nostr_profile_form_fields[ld.field_key].charset, + validate: + nostr_profile_form_fields[ld.field_key] + .validation, + validate_keypress: true, + }, + callback_focus: async () => { + console.log(`hi`); + }, + }} + /> + {/if} + </div> + {/if} +</LayoutView> +<Nav + basis={{ + prev: { + label: `${$t(`common.back`)}`, + route: `/settings/profile`, + prevent_route: input_value_del + ? { + callback: async () => { + if (input_value_del) await submit(); + }, + } + : undefined, + }, + title: { + label: { + classes: `capitalize`, + value: `${$t(`icu.edit_*`, { value: `${$t(`common.profile`)}` })}`, + }, + }, + /*option: { + label: { + classes: input_value_del ? `` : `opacity-60`, + value: ld?.nostr_profile[ld?.field_key] + ? `${$t(`common.update`)}` + : `${$t(`common.add`)}`, + }, + callback: async () => { + if (input_value_del) await submit(); + }, + },*/ + }} +/> diff --git a/src/routes/(cfg)/cfg/+layout.ts b/src/routes/(cfg)/cfg/+layout.ts @@ -1,13 +1,13 @@ import { keystore } from '$lib/client'; import { ks } from '$lib/conf'; -import { route } from '@radroots/svelte-lib'; +import { app_splash, route } from '@radroots/svelte-lib'; import type { LayoutLoad, LayoutLoadEvent } from './$types'; export const load: LayoutLoad = async ({ url }: LayoutLoadEvent) => { try { await keystore.init(); - console.log(`(load) (cfg) device `, url.pathname) + console.log(`(cfg) `, url.pathname) const ks_nostr_publickey = await keystore.get( ks.keys.nostr_publickey, ); @@ -20,8 +20,9 @@ export const load: LayoutLoad = async ({ url }: LayoutLoadEvent) => { return; } } + app_splash.set(false); } catch (e) { - console.log(`(load) (cfg) device ERROR`, e) + console.log(`(cfg) ERROR`, e) } finally { //await win.splash_hide(); return {}; diff --git a/src/routes/(cfg)/cfg/error/+page.svelte b/src/routes/(cfg)/cfg/error/+page.svelte @@ -4,7 +4,7 @@ </script> <LayoutView> - <div class={`flex flex-col gap-2 justify-start items-center`}> + <div class={`flex flex-col w-full gap-2 justify-start items-center`}> <p class={`font-sans font-[400] text-layer-0-glyph`}> The device is improperly configured. </p> @@ -13,7 +13,7 @@ on:click={async () => { await restart({ route: `/`, - notify_message: `Device restarted`, + notify_message: `The device was restarted`, }); }} > diff --git a/src/routes/(cfg)/cfg/init/+page.svelte b/src/routes/(cfg)/cfg/init/+page.svelte @@ -29,7 +29,7 @@ kv, LayoutView, Loading, - route, + LogoCircle, sleep, t, view_effect, @@ -541,51 +541,41 @@ > <div data-carousel-item={`cfg_init`} - class={`carousel-item flex flex-col w-full max-mobile_y:pt-28 pt-32 pb-4 justify-start items-center`} + class={`carousel-item flex flex-col w-full max-mobile_y:pt-28 pt-36 pb-4 justify-start items-center`} > - <div class={`flex flex-col gap-8 justify-start items-center`}> + <div class={`flex flex-col gap-1 justify-start items-center`}> <div - class={`flex flex-col gap-1 justify-start items-center`} + class={`flex flex-row w-full justify-center items-center`} > - <button - class={`flex flex-row justify-center items-center`} - on:click={async () => { - await route(`/`); - }} + <LogoCircle /> + </div> + <div + class={`flex flex-col w-full gap-2 justify-start items-center`} + > + <div + class={`flex flex-row w-full justify-start items-center`} > <p - class={`font-mono font-[700] text-layer-0-glyph text-4xl`} + class={`font-sans font-[400] text-sm text-layer-0-glyph-label uppercase`} > - {`${`${$t(`app.name`)}`}`} + {`${$t(`common.setup`)}`} </p> - </button> - <button - class={`flex flex-row justify-center items-center`} - on:click={async () => { - //@todo dev - }} + </div> + <div + class={`grid grid-cols-12 flex flex-col gap-4 w-full justify-start items-center`} > - <p - class={`font-mono font-[700] text-layer-0-glyph text-4xl`} - > - {`${`${$t(`common.setup`)}`}`} - </p> - </button> - </div> - <div - class={`grid grid-cols-12 flex flex-col gap-4 w-full justify-start items-center`} - > - {#each [`Configure your device`, `Choose a profile name`, `Terms of Use agreement`] as li, li_i} - <div - class={`col-span-12 flex flex-row justify-start items-center`} - > - <p - class={`font-mono font-[400] text-layer-0-glyph text-xl`} + {#each [`${$t(`common.configure_your_device`)}`, `${$t(`common.choose_a_profile_name`)}`, `${$t(`common.terms_of_use_agreement`)}`] as li, li_i} + <div + class={`col-span-12 flex flex-row justify-start items-center`} > - {`${li_i + 1}. ${li}`} - </p> - </div> - {/each} + <p + class={`font-mono font-[400] text-[1.1rem] text-layer-0-glyph`} + > + {`${li_i + 1}. ${li}`} + </p> + </div> + {/each} + </div> </div> </div> </div> diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts @@ -7,6 +7,7 @@ export const trailingSlash = 'always'; export const load: LayoutLoad = async ({ url }: LayoutLoadEvent) => { try { + console.log(`(root) `, url.pathname) const { language: nav_locale } = navigator; let locale = default_locale.toString(); const locales_avail = locales.get();