web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

commit 4e039f1e965e70be70173488e74ba4c19b85734a
parent 4894384e2bb575746dec6ef1d3fd6ef08ee0ff45
Author: triesap <triesap@radroots.dev>
Date:   Mon,  3 Nov 2025 21:16:31 +0000

apps-lib-market: add comment, follower, and following data to profile load types; synchronize theme mode with system setting; refactor profile view to rebuild from indexed metadata, rendering listings with formatted quantities, prices, and location, and introducing per-listing comment display with improved ui structure and typography

Diffstat:
Mapps-lib-market/src/lib/types/profile/load.ts | 5++++-
Mapps-lib-market/src/lib/utils/app/theme.ts | 6++++--
Mapps-lib-market/src/lib/views/profile/profile-indexed.svelte | 360+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
3 files changed, 241 insertions(+), 130 deletions(-)

diff --git a/apps-lib-market/src/lib/types/profile/load.ts b/apps-lib-market/src/lib/types/profile/load.ts @@ -1,4 +1,4 @@ -import type { RadrootsListingEventMetadata, RadrootsProfileEventMetadata } from "@radroots/events-bindings"; +import type { RadrootsCommentEventMetadata, RadrootsListingEventMetadata, RadrootsProfileEventMetadata } from "@radroots/events-bindings"; export type PageLoadProfileData = { public_key: string; @@ -13,5 +13,8 @@ export type PageLoadProfileDataEvents = } | { profile: RadrootsProfileEventMetadata; listings: RadrootsListingEventMetadata[]; + listing_comments: Record<string, RadrootsCommentEventMetadata[]>; + followers: number; + following: number; } ); diff --git a/apps-lib-market/src/lib/utils/app/theme.ts b/apps-lib-market/src/lib/utils/app/theme.ts @@ -12,8 +12,10 @@ export const init_theme = async (): Promise<void> => { let mode = await idb.read_global("theme_mode"); let key = await idb.read_global("theme_key"); - if (!mode) { - mode = get_system_theme(); + const mode_sys = get_system_theme(); + + if (!mode || mode !== mode_sys) { + mode = mode_sys; await idb.save_global("theme_mode", mode); } diff --git a/apps-lib-market/src/lib/views/profile/profile-indexed.svelte b/apps-lib-market/src/lib/views/profile/profile-indexed.svelte @@ -19,9 +19,13 @@ type NDKUserProfile, } from "@nostr-dev-kit/ndk"; import { Glyph, ndk } from "@radroots/apps-lib"; + import type { RadrootsCoreQuantityPrice } from "@radroots/core-bindings"; import type { RadrootsListing, + RadrootsListingEventMetadata, + RadrootsListingQuantity, RadrootsProfile, + RadrootsProfileEventMetadata, } from "@radroots/events-bindings"; import { on_ndk_event, @@ -34,38 +38,57 @@ let trade: TradeFlowService | null = $state(null); - const listings_mgr = create_radroots_listing_manager( - ("listings" in basis.indexed.events - ? basis.indexed.events.listings - : []) as RadrootsListingNostrEvent[], - ); + const listings_mgr = create_radroots_listing_manager(); + const profiles_mgr = create_radroots_profile_manager(); - const profiles_mgr = create_radroots_profile_manager( - ("profile" in basis.indexed.events - ? basis.indexed.events.profile - : undefined) as RadrootsProfileNostrEvent | undefined, - ); + function to_indexed_listing_payload_from_metadata( + m: RadrootsListingEventMetadata, + ): IndexedEventsStorePayload<RadrootsListing> { + return { + id: m.id, + kind: 30402, + author: m.author, + published_at: m.published_at ?? 0, + data: m.listing, + source: "indexed", + }; + } - const initial_indexed_listings: RadrootsListingNostrEvent[] = ( - "listings" in basis.indexed.events ? basis.indexed.events.listings : [] - ) as RadrootsListingNostrEvent[]; + function to_indexed_profile_payload_from_metadata( + m: RadrootsProfileEventMetadata, + ): IndexedEventsStorePayload<RadrootsProfile> { + return { + id: m.id, + kind: 0, + author: m.author, + published_at: m.published_at ?? 0, + data: m.profile, + source: "indexed", + }; + } - const initial_indexed_profile_row: RadrootsProfileNostrEvent | undefined = ( - "profile" in basis.indexed.events - ? basis.indexed.events.profile - : undefined - ) as RadrootsProfileNostrEvent | undefined; + function current_listings_meta(): RadrootsListingEventMetadata[] { + return "listings" in basis.indexed.events + ? basis.indexed.events.listings + : []; + } + + function current_profile_meta(): RadrootsProfileEventMetadata { + return basis.indexed.events.profile; + } let listings_buffer = $state<IndexedEventsStorePayload<RadrootsListing>[]>( - initial_indexed_listings - .filter((r) => r?.listing?.d_tag) - .map(listings_mgr.to_indexed_payload), + current_listings_meta() + .filter((r) => r.listing && r.listing.d_tag) + .map(to_indexed_listing_payload_from_metadata), ); let profile_buffer = $state<IndexedEventsStorePayload<RadrootsProfile> | null>( - initial_indexed_profile_row - ? profiles_mgr.to_indexed_payload(initial_indexed_profile_row) + current_profile_meta() + ? to_indexed_profile_payload_from_metadata( + current_profile_meta(), + ) : null, ); @@ -82,42 +105,26 @@ let last_pk = $state(basis.indexed.public_key); $effect(() => { if (basis.indexed.public_key !== last_pk) { - const new_indexed_listings: RadrootsListingNostrEvent[] = ( - "listings" in basis.indexed.events - ? basis.indexed.events.listings - : [] - ) as RadrootsListingNostrEvent[]; - - const new_indexed_profile_row: - | RadrootsProfileNostrEvent - | undefined = ( - "profile" in basis.indexed.events - ? basis.indexed.events.profile - : undefined - ) as RadrootsProfileNostrEvent | undefined; + const new_listings_meta = current_listings_meta(); + const new_profile_meta = current_profile_meta(); - listings_buffer = new_indexed_listings - .filter((r) => r?.listing?.d_tag) - .map(listings_mgr.to_indexed_payload); + listings_buffer = new_listings_meta + .filter((r) => r.listing && r.listing.d_tag) + .map(to_indexed_listing_payload_from_metadata); - profile_buffer = new_indexed_profile_row - ? profiles_mgr.to_indexed_payload(new_indexed_profile_row) + profile_buffer = new_profile_meta + ? to_indexed_profile_payload_from_metadata(new_profile_meta) : null; have_live_listings = false; have_live_profiles = false; - listings_mgr.init_from_indexed(new_indexed_listings); - profiles_mgr.init_from_indexed(new_indexed_profile_row); - - // drive the trade service to only follow the new author trade?.set_filter_authors([basis.indexed.public_key]); last_pk = basis.indexed.public_key; } }); - // Profile + Listings (non-trade) live updates const sub = $ndk.subscribe( { kinds: [NDKKind.Metadata, NDKKind.Classified], @@ -128,10 +135,14 @@ onEvent: (event: NDKEvent) => { const parsed = on_ndk_event(event); if (parsed && "listing" in parsed) { - listings_mgr.on_parsed_event(parsed); + listings_mgr.on_parsed_event( + parsed as RadrootsListingNostrEvent, + ); if (!have_live_listings) have_live_listings = true; } else if (parsed && "profile" in parsed) { - profiles_mgr.on_parsed_event(parsed); + profiles_mgr.on_parsed_event( + parsed as RadrootsProfileNostrEvent, + ); if (!have_live_profiles) have_live_profiles = true; } }, @@ -139,21 +150,14 @@ ); onMount(async () => { - // Instantiate the TradeFlowService (it manages its own subscription internally) trade = create_trade_flow_service({ ndk: $ndk, ndk_user_store: () => { - // Use the active signer’s user. Fail fast if missing. const u = $ndk.activeUser; if (!u) throw new Error("No active NDK user/signer found."); return u; }, - // Optional: you can also pass authors/kinds here up front - // authors: [basis.indexed.public_key], - // kinds: [...defaults...], }); - - // Narrow to this profile’s pubkey (seller) — you can change this at runtime trade.set_filter_authors([basis.indexed.public_key]); }); @@ -178,6 +182,49 @@ basis.indexed.events.profile.profile.name } (@${basis.indexed.events.profile.profile.name}) ${head_title_suffix}`, ); + + function fmtQty(q: RadrootsListingQuantity): string { + const v = q && q.value ? q.value : undefined; + const amt = v && v.amount ? v.amount : ""; + const unit = v && v.unit ? v.unit : ""; + const lab = v && v.label ? v.label : q && q.label ? q.label : ""; + const pieces = [amt, unit, lab].filter((s) => s && `${s}`.length > 0); + return pieces.join(" "); + } + + function fmtPrice(p: RadrootsCoreQuantityPrice): string { + const a = p && p.amount ? p.amount : undefined; + const q = p && p.quantity ? p.quantity : undefined; + const price = a && a.amount ? a.amount : ""; + const cur = a && a.currency ? a.currency : ""; + const qamt = q && q.amount ? q.amount : ""; + const qun = q && q.unit ? q.unit : ""; + const left = [price, cur] + .filter((s) => s && `${s}`.length > 0) + .join(" "); + const right = [qamt, qun] + .filter((s) => s && `${s}`.length > 0) + .join(" "); + return right ? `${left} per ${right}` : left; + } + + function commentsFor(listingId: string) { + if (!("listings" in basis.indexed.events)) return []; + if (!("listing_comments" in basis.indexed.events)) return []; + const key = listingId.toLowerCase(); + const m = basis.indexed.events.listing_comments; + return m && m[key] ? m[key] : []; + } + + function toDate(ts?: number): string { + if (!ts) return ""; + try { + const d = new Date(ts * 1000); + return d.toLocaleDateString(); + } catch { + return ""; + } + } </script> <svelte:head> @@ -230,95 +277,154 @@ </LayoutColumnHeadingDisplaySimple> {/snippet} {#snippet subheading()} - <p class={`font-rsfd font-[600] text-sm text-black_panther`}> + <p class={`font-sans font-[400] text-sm text-black_panther`}> {basis.indexed.events.profile.profile.about} </p> {/snippet} </LayoutColumnHeading> <LayoutColumnHeadingViewButtons /> </LayoutColumnEntry> + {#if "listings" in basis.indexed.events} <LayoutColumnEntry basis={{ classes: `gap-4` }}> {#each basis.indexed.events.listings as ev (ev.id)} - <div - class={`relative flex flex-col w-full justify-center items-center`} - > - <div - class={`flex flex-col h-[10rem] w-full justify-center items-center bg-white`} - > - <div - class={`flex flex-row w-full justify-center items-center`} - > + <div class={`relative flex w-full flex-col`}> + <div class={`flex w-full flex-col gap-2 p-4`}> + <div class={`flex w-full flex-row justify-between`}> <p - class={`font-sans font-[400] text-sm text-black_panther`} + class={`font-sans text-base font-[500] text-black_panther`} > {ev.listing.product.title} </p> + <p + class={`font-sans text-xs font-[500] text-cloak_grey`} + > + {toDate(ev.published_at)} + </p> + </div> + <p + class={`font-sans text-sm font-[400] text-black_panther/80`} + > + {ev.listing.product.summary} + </p> + <div class={`flex w-full flex-wrap gap-2 pt-1`}> + <span + class={`rounded-sm bg-ly1 px-2 py-0.5 text-xs font-[600] text-black_panther/90`} + > + {ev.listing.product.category} + </span> + {#if ev.listing.product.process} + <span + class={`rounded-sm bg-ly1 px-2 py-0.5 text-xs font-[600] text-black_panther/90`} + > + {ev.listing.product.process} + </span> + {/if} + {#if ev.listing.product.year} + <span + class={`rounded-sm bg-ly1 px-2 py-0.5 text-xs font-[600] text-black_panther/90`} + > + {ev.listing.product.year} + </span> + {/if} + {#if ev.listing.product.lot} + <span + class={`rounded-sm bg-ly1 px-2 py-0.5 text-xs font-[600] text-black_panther/90`} + > + {ev.listing.product.lot} + </span> + {/if} + </div> + <div class={`flex w-full flex-row gap-4 pt-2`}> + <div class={`flex flex-col`}> + <p + class={`font-sans text-xs font-[700] text-black_panther/70`} + > + Quantities + </p> + <p + class={`font-sans text-sm text-black_panther/90`} + > + {ev.listing.quantities + .map((q) => fmtQty(q)) + .filter((s) => s.length > 0) + .join(", ")} + </p> + </div> + <div class={`flex flex-col`}> + <p + class={`font-sans text-xs font-[700] text-black_panther/70`} + > + Prices + </p> + <p + class={`font-sans text-sm text-black_panther/90`} + > + {ev.listing.prices + .map((p) => fmtPrice(p)) + .filter((s) => s.length > 0) + .join(" · ")} + </p> + </div> + </div> + <div class={`flex w-full flex-row gap-2 pt-2`}> + <p + class={`font-sans text-xs font-[700] text-black_panther/70`} + > + Location + </p> + <p + class={`font-sans text-sm text-black_panther/90`} + > + {#if ev.listing.location} + {ev.listing.location.primary} + {ev.listing.location.city + ? `, ${ev.listing.location.city}` + : ""} + {ev.listing.location.region + ? `, ${ev.listing.location.region}` + : ""} + {ev.listing.location.country + ? `, ${ev.listing.location.country.toUpperCase()}` + : ""} + {:else} + {"Unlisted"} + {/if} + </p> </div> </div> + + <div class={`flex w-full flex-col gap-2 p-4`}> + <p + class={`font-sans text-xs font-[700] uppercase tracking-wide text-black_panther/70`} + > + Comments + </p> + {#each commentsFor(ev.id) as c (c.id)} + <div + class={`flex w-full flex-col gap-1 rounded-sm bg-ly1/40 p-3`} + > + <p + class={`font-sans text-xs font-[600] text-black_panther/70`} + > + {toDate(c.published_at)} + </p> + <p + class={`font-sans text-sm font-[400] text-black_panther`} + > + {c.comment && c.comment.content + ? c.comment.content + : ""} + </p> + </div> + {:else} + <p class={`font-sans text-sm text-cloak_grey`}> + No comments yet + </p> + {/each} + </div> </div> {/each} </LayoutColumnEntry> {/if} </LayoutColumn> - -<!-- - -<div class="flex flex-col w-full gap-4 p-4 justify-start items-start"> - <div class="flex flex-row pl-2 justify-start items-center"> - <a href={`/`}> - <p class="font-sans font-[400] text-base text-ly0-gl">go back</p> - </a> - </div> - - <button - class="flex flex-col w-full p-4 justify-start items-start bg-ly1" - onclick={() => console.log(`profile_view?.id`, profile_view?.id)} - > - {#if profile_view} - <p class="font-sans font-[400] text-base text-ly0-gl">profile:</p> - <p class="font-sans font-[400] text-base text-ly0-gl break-all"> - {profile_view.data.nip05} - </p> - {:else} - <p class="font-sans font-[400] text-base text-ly0-gl">no profile</p> - {/if} - </button> - - <div class="flex flex-col w-full gap-4 justify-start items-start"> - {#if listings_view.length} - {#each listings_view as listing} - <button - class="flex flex-col w-full p-4 justify-start items-start bg-ly1" - onclick={() => console.log(`listing.id`, listing.id)} - > - <p class="font-sans font-[400] text-base text-ly0-gl"> - listing: - </p> - <p - class="font-sans font-[400] text-base text-ly0-gl break-all" - > - {listing.kind} - </p> - <p - class="font-sans font-[400] text-base text-ly0-gl break-all" - > - {listing.data.d_tag} - </p> - </button> - - <ProfileListing - basis={{ - trade, - listing_event: listing, - }} - /> - {/each} - {:else} - <p class="font-sans font-[400] text-base text-ly0-gl"> - no listings - </p> - {/if} - </div> -</div> --->