commit 354cd0ee1dbd9acfba9abb87fec3f477195a5e7c
parent 3e926a7d20d53d188a922473ca4fd1f33af57319
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:
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();