commit a39e8157b0e3d9e5627d44f5e6671819ce955abf
parent a9cfe61e53baac595ce39942b79abb512f4b9044
Author: triesap <triesap@radroots.dev>
Date: Fri, 21 Nov 2025 02:32:23 +0000
apps-lib-pwa: migrate and refactor profile view from `@radroots/apps-lib`
Diffstat:
12 files changed, 449 insertions(+), 8 deletions(-)
diff --git a/apps-lib-pwa/src/lib/components/button/button-round-nav.svelte b/apps-lib-pwa/src/lib/components/button/button-round-nav.svelte
@@ -0,0 +1,26 @@
+<script lang="ts">
+ import { LoadSymbol } from "$lib";
+ import type { IButtonNavRound } from "$lib/types/components/lib";
+ import { Glyph } from "@radroots/apps-lib";
+
+ let { basis }: { basis: IButtonNavRound } = $props();
+</script>
+
+<button
+ class={`flex flex-row h-12 w-12 justify-center items-center bg-ly1 rounded-full el-re`}
+ disabled={!!basis.disabled}
+ onclick={basis.callback}
+>
+ {#if basis.loading}
+ <LoadSymbol />
+ {:else}
+ <Glyph
+ basis={{
+ classes: `text-ly0-gl`,
+ dim: `sm+`,
+
+ key: basis.glyph,
+ }}
+ />
+ {/if}
+</button>
diff --git a/apps-lib-pwa/src/lib/components/farm/farms-preview-card.svelte b/apps-lib-pwa/src/lib/components/farm/farms-preview-card.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
import MapMarkerArea from "$lib/components/map/map-marker-area.svelte";
import Map from "$lib/components/map/map.svelte";
- import type { FarmExtended } from "$lib/types/views/farm";
+ import type { FarmExtended } from "$lib/types/views/farms";
import { get_context } from "@radroots/apps-lib";
import {
fmt_geolocation_address,
diff --git a/apps-lib-pwa/src/lib/components/lib/float-page.svelte b/apps-lib-pwa/src/lib/components/lib/float-page.svelte
@@ -0,0 +1,12 @@
+<script lang="ts">
+ import type { IFloatPage } from "$lib/types/components/lib";
+ import type { Snippet } from "svelte";
+
+ let { basis, children }: { basis: IFloatPage; children: Snippet } =
+ $props();
+</script>
+
+<div class={`absolute top-10 ${basis.posx}-6 flex flex-row`}>
+ {@render children()}
+</div>
+<div class="hidden left-6 right-6"></div>
diff --git a/apps-lib-pwa/src/lib/components/media/image-upload-photo-add.svelte b/apps-lib-pwa/src/lib/components/media/image-upload-photo-add.svelte
@@ -0,0 +1,46 @@
+<script lang="ts">
+ import { get_context, Glyph } from "@radroots/apps-lib";
+ import LoadSymbol from "../lib/load-symbol.svelte";
+
+ const { ls, lc_photos_add } = get_context(`lib`);
+
+ let {
+ basis,
+ photo_path = $bindable(``),
+ }: {
+ basis: {
+ loading?: boolean;
+ };
+ photo_path: string;
+ } = $props();
+</script>
+
+<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-ly1/60 rounded-full`}
+ onclick={async () => {
+ const photo_paths_add = await lc_photos_add();
+ if (photo_paths_add) photo_path = photo_paths_add[0];
+ }}
+ >
+ {#if basis.loading}
+ <LoadSymbol basis={{ dim: `md` }} />
+ {:else}
+ <Glyph
+ basis={{
+ classes: `text-[40px] text-ly2-gl`,
+ dim: `sm`,
+ key: `camera`,
+ }}
+ />
+ {/if}
+
+ <div
+ class={`absolute -bottom-[1.8rem] flex flex-row justify-start items-center`}
+ >
+ <p class={`font-arch font-[600] text-sm text-ly0-gl capitalize`}>
+ {`${$ls(`icu.add_*`, { value: `${$ls(`common.photo`)}` })}`}
+ </p>
+ </div>
+ </button>
+</div>
diff --git a/apps-lib-pwa/src/lib/index.ts b/apps-lib-pwa/src/lib/index.ts
@@ -3,6 +3,7 @@ export { default as ButtonLabelDashed } from "./components/button/button-label-d
export { default as ButtonLayoutBottom } from "./components/button/button-layout-bottom.svelte";
export { default as ButtonLayoutPair } from "./components/button/button-layout-pair.svelte";
export { default as ButtonLayout } from "./components/button/button-layout.svelte";
+export { default as ButtonRoundNav } from "./components/button/button-round-nav.svelte";
export { default as ButtonSimple } from "./components/button/button-simple.svelte";
export { default as FarmsAddDetail } from "./components/farm/farms-add-detail.svelte";
export { default as FarmsAddMap } from "./components/farm/farms-add-map.svelte";
@@ -18,6 +19,7 @@ export { default as LayoutWindow } from "./components/layout/layout-window.svelt
export { default as CarouselContainer } from "./components/lib/carousel-container.svelte";
export { default as CarouselItem } from "./components/lib/carousel-item.svelte";
export { default as Css } from "./components/lib/css.svelte";
+export { default as FloatPage } from "./components/lib/float-page.svelte";
export { default as InputPwa } from "./components/lib/input-pwa.svelte";
export { default as InputValue } from "./components/lib/input-value.svelte";
export { default as LoadCircle } from "./components/lib/load-circle.svelte";
@@ -31,9 +33,11 @@ export { default as WrapBorder } from "./components/lib/wrap-border.svelte";
export { default as MapMarkerAreaDisplay } from "./components/map/map-marker-area-display.svelte";
export { default as MapMarkerArea } from "./components/map/map-marker-area.svelte";
export { default as Map } from "./components/map/map.svelte";
+export { default as ImageUploadPhotoAdd } from "./components/media/image-upload-photo-add.svelte";
export { default as NavigationTabs } from "./components/navigation/navigation-tabs.svelte";
export { default as PageHeader } from "./components/navigation/page-header.svelte";
export { default as PageToolbar } from "./components/navigation/page-toolbar.svelte";
export { default as FarmsAdd } from "./views/farms/farms-add.svelte";
export { default as Farms } from "./views/farms/farms.svelte";
+export { default as Profile } from "./views/profile/profile.svelte";
export { default as Home } from "./views/root/home.svelte";
diff --git a/apps-lib-pwa/src/lib/types/components/lib.ts b/apps-lib-pwa/src/lib/types/components/lib.ts
@@ -26,10 +26,6 @@ export type IGlyphCircle = {
glyph: IGlyph
};
-export type IFloatPage = {
- posx: Omit<GeometryScreenPositionHorizontal, "center">;
-};
-
export type IButtonNavRound = ICb & IDisabledOpt & ILoadingOpt & IGlyphKey;
export type CarouselMouseEvent = MouseEvent & {
@@ -54,4 +50,8 @@ export type ICarouselItem<T extends string> = IClOpt & {
export type ILoadCircle = IClOpt & {
dim?: LoadingDimension;
+};
+
+export type IFloatPage = {
+ posx: Omit<GeometryScreenPositionHorizontal, "center">;
};
\ No newline at end of file
diff --git a/apps-lib-pwa/src/lib/types/views/farm.ts b/apps-lib-pwa/src/lib/types/views/farms.ts
diff --git a/apps-lib-pwa/src/lib/types/views/profile.ts b/apps-lib-pwa/src/lib/types/views/profile.ts
@@ -0,0 +1,12 @@
+import type { NostrProfile } from "@radroots/tangle-schema-bindings";
+
+export type IViewProfileData = {
+ profile: NostrProfile;
+};
+
+export type ViewProfileEditFieldKey = `name` | `display_name` | `about`;
+
+export type IViewProfileEditData = {
+ public_key: string;
+ field: ViewProfileEditFieldKey;
+};
diff --git a/apps-lib-pwa/src/lib/utils/farm/schema.ts b/apps-lib-pwa/src/lib/utils/farm/schema.ts
@@ -1,5 +1,5 @@
import { dev } from "$app/environment";
-import type { IViewFarmsAddSubmission, IViewFarmsProductsAddSubmitPayload } from "$lib/types/views/farm";
+import type { IViewFarmsAddSubmission, IViewFarmsProductsAddSubmitPayload } from "$lib/types/views/farms";
import { form_fields, schema_geocode_result, schema_geolocation_point, util_rxp, zf_numf_pos, zf_numi_pos, zf_price } from "@radroots/utils";
import { z } from "zod";
diff --git a/apps-lib-pwa/src/lib/views/farms/farms-add.svelte b/apps-lib-pwa/src/lib/views/farms/farms-add.svelte
@@ -8,7 +8,7 @@
import CarouselItem from "$lib/components/lib/carousel-item.svelte";
import PageToolbar from "$lib/components/navigation/page-toolbar.svelte";
import { app_platform } from "$lib/stores/app";
- import type { IViewFarmsAddSubmission } from "$lib/types/views/farm";
+ import type { IViewFarmsAddSubmission } from "$lib/types/views/farms";
import { schema_view_farms_add_submission } from "$lib/utils/farm/schema";
import { focus_map_marker } from "$lib/utils/map";
import {
diff --git a/apps-lib-pwa/src/lib/views/farms/farms.svelte b/apps-lib-pwa/src/lib/views/farms/farms.svelte
@@ -6,7 +6,7 @@
import LayoutView from "$lib/components/layout/layout-view.svelte";
import PageToolbar from "$lib/components/navigation/page-toolbar.svelte";
import type { IViewBasis } from "$lib/types/views";
- import type { IViewFarmsData } from "$lib/types/views/farm";
+ import type { IViewFarmsData } from "$lib/types/views/farms";
import {
Fade,
get_context,
diff --git a/apps-lib-pwa/src/lib/views/profile/profile.svelte b/apps-lib-pwa/src/lib/views/profile/profile.svelte
@@ -0,0 +1,341 @@
+<script lang="ts">
+ import { NavigationTabs, SelectMenu } from "$lib";
+ import ButtonRoundNav from "$lib/components/button/button-round-nav.svelte";
+ import FloatPage from "$lib/components/lib/float-page.svelte";
+ import ImageUploadPhotoAdd from "$lib/components/media/image-upload-photo-add.svelte";
+ import type { IViewBasis } from "$lib/types/views";
+ import type {
+ IViewProfileData,
+ ViewProfileEditFieldKey,
+ } from "$lib/types/views/profile";
+ import {
+ get_context,
+ Glyph,
+ idb_kv_init_page,
+ ImagePath,
+ symbols,
+ type IViewOnDestroy,
+ } from "@radroots/apps-lib";
+ import {
+ handle_err,
+ type CallbackPromise,
+ type CallbackPromiseGeneric,
+ } from "@radroots/utils";
+ import { onDestroy, onMount } from "svelte";
+
+ const { ls } = get_context(`lib`);
+
+ let {
+ basis,
+ photo_path = $bindable(``),
+ }: {
+ basis: IViewBasis<{
+ data?: IViewProfileData;
+ loading_photo_upload: boolean;
+ loading_photo_upload_open: boolean;
+ on_handle_back: CallbackPromiseGeneric<{
+ is_photo_existing: boolean;
+ }>;
+ on_handle_photo_options: CallbackPromise;
+ on_handle_edit_profile_field: CallbackPromiseGeneric<{
+ field: ViewProfileEditFieldKey;
+ }>;
+ }> &
+ IViewOnDestroy<{ public_key: string }>;
+ photo_path: string;
+ } = $props();
+
+ type ViewDisplay = `photos` | `following` | `followers`;
+ let view_display: ViewDisplay = $state(`photos`);
+
+ let val_sel_options_button = $state(``);
+
+ onMount(async () => {
+ try {
+ if (!basis.kv_init_prevent) await idb_kv_init_page();
+ } catch (e) {
+ handle_err(e, `on_mount`);
+ }
+ });
+
+ onDestroy(async () => {
+ try {
+ if (basis.data?.profile.public_key)
+ await basis.on_destroy({
+ public_key: basis.data.profile.public_key,
+ });
+ } catch (e) {
+ handle_err(e, `on_destroy`);
+ }
+ });
+
+ const photo_overlay_visible = $derived(
+ !!(basis.data?.profile.picture || photo_path),
+ );
+
+ const classes_photo_overlay_glyph = $derived(
+ photo_overlay_visible ? `text-white` : `text-ly0-gl`,
+ );
+
+ const classes_photo_overlay_glyph_opt = $derived(
+ photo_overlay_visible ? `text-gray-300` : `text-ly0-gl`,
+ );
+
+ const classes_photo_overlay_glyph_opt_selected = $derived(
+ photo_overlay_visible ? `text-white` : `text-ly1-gl_d`,
+ );
+</script>
+
+{#if basis.data}
+ <div
+ class={`relative flex flex-col min-h-[525px] h-[525px] w-full justify-center items-center bg-ly2 fade-in`}
+ >
+ <FloatPage
+ basis={{
+ posx: `left`,
+ }}
+ >
+ <ButtonRoundNav
+ basis={{
+ glyph: `arrow-left`,
+ loading: basis.loading_photo_upload,
+ callback: async () => {
+ await basis.on_handle_back({
+ is_photo_existing: photo_overlay_visible,
+ });
+ },
+ }}
+ />
+ </FloatPage>
+ <FloatPage
+ basis={{
+ posx: `right`,
+ }}
+ >
+ <SelectMenu
+ bind:value={val_sel_options_button}
+ basis={{
+ layer: 0,
+ options: [
+ {
+ entries: [
+ {
+ value: `*add-new`,
+ label: `Add new photo`,
+ },
+ ],
+ },
+ ],
+ }}
+ >
+ <ButtonRoundNav
+ basis={{
+ glyph: `images-square`,
+ callback: basis.on_handle_photo_options,
+ }}
+ />
+ </SelectMenu>
+ </FloatPage>
+ {#if basis.data.profile.picture || photo_path}
+ {@const img_path = photo_path || basis.data.profile.picture || ``}
+ <ImagePath basis={{ path: img_path }} />
+ {:else}
+ <div
+ class={`flex flex-row justify-start items-center -translate-y-8`}
+ >
+ <ImageUploadPhotoAdd
+ bind:photo_path
+ basis={{
+ loading: basis.loading_photo_upload_open,
+ }}
+ />
+ </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`}
+ >
+ <div
+ class={`flex flex-col w-full gap-[2px] justify-center items-center`}
+ >
+ <div
+ class={`flex flex-row h-10 w-full justify-start items-center`}
+ >
+ <button
+ class={`group flex flex-row justify-center items-center`}
+ onclick={async () => {
+ await basis.on_handle_edit_profile_field({
+ field: `display_name`,
+ });
+ }}
+ >
+ <p
+ class={`font-sansd font-[600] text-[2rem] ${classes_photo_overlay_glyph} ${
+ basis.data?.profile.name
+ ? ``
+ : `capitalize opacity-active`
+ } el-re`}
+ >
+ {#if basis.data?.profile.display_name}
+ {`${basis.data?.profile.display_name}`}
+ {:else if basis.data?.profile.name}
+ {`${
+ basis.data?.profile.display_name ||
+ basis.data?.profile.name ||
+ ``
+ }`}
+ {:else}
+ {`+ ${`${$ls(`icu.add_*`, {
+ value: `${$ls(`common.profile_name`)}`,
+ })}`}`}
+ {/if}
+ </p>
+ </button>
+ </div>
+ <div
+ class={`flex flex-row w-full gap-[6px] justify-start items-center`}
+ >
+ <button
+ class={`group flex flex-row justify-center items-center`}
+ onclick={async () => {
+ await basis.on_handle_edit_profile_field({
+ field: `name`,
+ });
+ }}
+ >
+ <p
+ class={`font-sansd font-[600] text-[1.1rem] ${classes_photo_overlay_glyph} ${
+ basis.data?.profile.name
+ ? ``
+ : `capitalize opacity-active`
+ } el-re`}
+ >
+ {#if basis.data?.profile.name}
+ {`@${basis.data.profile.name}`}
+ {:else}
+ {`+ ${`${$ls(`icu.add_*`, {
+ value: `${$ls(`common.username`)}`,
+ })}`}`}
+ {/if}
+ </p>
+ </button>
+ <p
+ class={`font-sans font-[400] ${classes_photo_overlay_glyph}`}
+ >
+ {symbols.bullet}
+ </p>
+ <button
+ class={`flex flex-row justify-center items-center`}
+ onclick={async () => {
+ alert(`@todo!`);
+ }}
+ >
+ <Glyph
+ basis={{
+ classes: `${classes_photo_overlay_glyph}`,
+ dim: `xs`,
+
+ key: `link-simple`,
+ }}
+ />
+ </button>
+ </div>
+ <div class={`flex flex-row w-full justify-start items-center`}>
+ <button
+ class={`group flex flex-row justify-center items-center`}
+ onclick={async () => {
+ await basis.on_handle_edit_profile_field({
+ field: `about`,
+ });
+ }}
+ >
+ <p
+ class={`font-sansd font-[400] text-[1.1rem] ${classes_photo_overlay_glyph} ${
+ basis.data?.profile.about
+ ? ``
+ : `capitalize opacity-active`
+ }`}
+ >
+ {#if basis.data?.profile.about}
+ {`${basis.data.profile.about}`}
+ {:else}
+ {`+ ${`${$ls(`icu.add_*`, {
+ value: `${$ls(`common.bio`)}`,
+ })}`}`}
+ {/if}
+ </p>
+ </button>
+ </div>
+ </div>
+ <div
+ class={`flex flex-row w-full pt-2 pb-6 gap-2 justify-start items-center`}
+ >
+ <button
+ class={`flex flex-row justify-center items-center`}
+ onclick={async () => {
+ view_display = `photos`;
+ }}
+ >
+ <p
+ 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`}
+ >
+ {`${$ls(`common.photos`)}`}
+ </p>
+ </button>
+ <button
+ class={`flex flex-row justify-center items-center`}
+ onclick={async () => {
+ view_display = `following`;
+ }}
+ >
+ <p
+ 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`}
+ >
+ {`${$ls(`common.following`)}`}
+ </p>
+ </button>
+ <button
+ class={`flex flex-row justify-center items-center`}
+ onclick={async () => {
+ view_display = `followers`;
+ }}
+ >
+ <p
+ 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`}
+ >
+ {`${$ls(`common.followers`)}`}
+ </p>
+ </button>
+ </div>
+ </div>
+ </div>
+ <div
+ class={`flex flex-col w-full min-h-[500px] justify-start items-center`}
+ >
+ {#if view_display === `photos`}
+ <p class={`font-sans font-[400] text-ly0-gl`}>
+ {view_display}
+ </p>
+ {:else if view_display === `following`}
+ <p class={`font-sans font-[400] text-ly0-gl`}>
+ {view_display}
+ </p>
+ {:else if view_display === `followers`}
+ <p class={`font-sans font-[400] text-ly0-gl`}>
+ {view_display}
+ </p>
+ {/if}
+ </div>
+ <NavigationTabs />
+{/if}