web_lib

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

commit 36308e8d2d0866d2be9b8e94a3e80305d0b0aa5a
parent c2ce1c379d53ecab0e1053a233ea3e034eabd59b
Author: triesap <triesap@radroots.dev>
Date:   Thu, 20 Nov 2025 16:42:57 +0000

apps-lib-pwa: migrate and refactor farms add view from `@radroots/apps-lib`, add carousel components

Diffstat:
Aapps-lib-pwa/src/lib/components/button/button-layout-bottom.svelte | 22++++++++++++++++++++++
Aapps-lib-pwa/src/lib/components/farm/farms-add-detail.svelte | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps-lib-pwa/src/lib/components/farm/farms-add-map.svelte | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapps-lib-pwa/src/lib/components/farm/farms-display-li-el.svelte -> apps-lib-pwa/src/lib/components/farm/farms-preview-card.svelte | 0
Aapps-lib-pwa/src/lib/components/form/form-line-ledger-label-select-label.svelte | 36++++++++++++++++++++++++++++++++++++
Aapps-lib-pwa/src/lib/components/form/form-line-ledger-select.svelte | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps-lib-pwa/src/lib/components/form/form-line-ledger.svelte | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps-lib-pwa/src/lib/components/lib/carousel-container.svelte | 21+++++++++++++++++++++
Aapps-lib-pwa/src/lib/components/lib/carousel-item.svelte | 45+++++++++++++++++++++++++++++++++++++++++++++
Aapps-lib-pwa/src/lib/components/lib/input-pwa.svelte | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps-lib-pwa/src/lib/components/lib/select-pwa.svelte | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps-lib-pwa/src/lib/components/lib/wrap-border.svelte | 22++++++++++++++++++++++
Mapps-lib-pwa/src/lib/components/map/map.svelte | 2+-
Mapps-lib-pwa/src/lib/index.ts | 14+++++++++++++-
Mapps-lib-pwa/src/lib/types/components/lib.ts | 24++++++++++++++++++++++--
Mapps-lib-pwa/src/lib/types/views/farm.ts | 26+++++++++++++++++++++++++-
Aapps-lib-pwa/src/lib/utils/farm/schema.ts | 29+++++++++++++++++++++++++++++
Aapps-lib-pwa/src/lib/views/farms/farms-add.svelte | 244+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapps-lib-pwa/src/lib/views/farms/farms.svelte | 4++--
19 files changed, 1097 insertions(+), 7 deletions(-)

diff --git a/apps-lib-pwa/src/lib/components/button/button-layout-bottom.svelte b/apps-lib-pwa/src/lib/components/button/button-layout-bottom.svelte @@ -0,0 +1,22 @@ +<script lang="ts"> + import { app_lo } from "$lib/stores/app"; + import type { Snippet } from "svelte"; + + let { + basis, + children, + }: { + basis?: { + hidden: boolean; + }; + children: Snippet; + } = $props(); +</script> + +{#if !basis?.hidden} + <div + class={`z-10 absolute bottom-0 h-lo_bottom_button_${$app_lo} flex flex-col w-full px-4 gap-1 justify-start items-center`} + > + {@render children()} + </div> +{/if} diff --git a/apps-lib-pwa/src/lib/components/farm/farms-add-detail.svelte b/apps-lib-pwa/src/lib/components/farm/farms-add-detail.svelte @@ -0,0 +1,108 @@ +<script lang="ts"> + import FormLineLedger from "$lib/components/form/form-line-ledger.svelte"; + import { get_context } from "@radroots/apps-lib"; + import { area_units, form_fields } from "@radroots/utils"; + + const { ls } = get_context(`lib`); + + let { + val_farmname = $bindable(``), + val_farmaddress = $bindable(``), + val_farmarea = $bindable(``), + val_farmarea_unit = $bindable(``), + val_farmcontact = $bindable(``), + farm_geop_lat, + farm_geop_lng, + }: { + val_farmname: string; + val_farmaddress: string; + val_farmarea: string; + val_farmarea_unit: string; + val_farmcontact: string; + farm_geop_lat: string; + farm_geop_lng: string; + } = $props(); +</script> + +<div + class={`flex flex-col h-[100vh] w-full px-6 pt-2 gap-4 justify-start items-center`} +> + <FormLineLedger + bind:value={val_farmaddress} + basis={{ + id: `farm_location`, + label: `${$ls(`common.farm_location`)}`, + input: { + placeholder: `${$ls(`icu.enter_*`, { + value: `${$ls(`common.farm_location`)}`.toLowerCase(), + })}`, + }, + }} + /> + + <FormLineLedger + basis={{ + id: `farm_coordinates`, + label: `${$ls(`common.farm_coordinates`)}`, + display_value: + farm_geop_lat && farm_geop_lng + ? `${farm_geop_lat}, ${farm_geop_lng}` + : undefined, + input: + farm_geop_lat && farm_geop_lng + ? undefined + : { + placeholder: `${$ls(`icu.enter_*`, { + value: `${$ls( + `common.farm_coordinates`, + )}`.toLowerCase(), + })}`, + }, + }} + /> + <FormLineLedger + bind:value={val_farmname} + basis={{ + id: `farm_name`, + label: `${$ls(`common.farm_name`)}`, + input: { + placeholder: `${$ls(`icu.enter_*`, { + value: `${$ls(`common.farm_name`)}`.toLowerCase(), + })}`, + }, + }} + /> + <FormLineLedger + bind:value={val_farmarea} + bind:value_label_sel={val_farmarea_unit} + basis={{ + id: `farm_size`, + label: `${$ls(`common.farm_size`)}`, + label_select: { + label: `${$ls(`units.area.${val_farmarea_unit}_ab`)}`, + entries: area_units.map((i) => ({ + value: i, + label: `${$ls(`units.area.${i}`)}`, + })), + }, + input: { + placeholder: `${`${$ls(`icu.enter_*`, { + value: `${$ls(`common.farm_size`)}`.toLowerCase(), + })}`} ${`${$ls(`units.area.${val_farmarea_unit}_pl`)}`.toLowerCase()}`, + field: form_fields.farm_size, + }, + }} + /> + <FormLineLedger + bind:value={val_farmcontact} + basis={{ + id: `farm_contact`, + label: `${$ls(`common.farm_contact`)}`, + input: { + placeholder: `${$ls(`icu.enter_*`, { + value: `${$ls(`common.contact_name`)}`.toLowerCase(), + })}`, + }, + }} + /> +</div> diff --git a/apps-lib-pwa/src/lib/components/farm/farms-add-map.svelte b/apps-lib-pwa/src/lib/components/farm/farms-add-map.svelte @@ -0,0 +1,87 @@ +<script lang="ts"> + import WrapBorder from "$lib/components/lib/wrap-border.svelte"; + import MapMarkerArea from "$lib/components/map/map-marker-area.svelte"; + import Map from "$lib/components/map/map.svelte"; + import { app_lo } from "$lib/stores/app"; + import { focus_map_marker } from "$lib/utils/map"; + import { Fade, geop_is_valid, get_context } from "@radroots/apps-lib"; + import { + handle_err, + type GeocoderReverseResult, + type GeolocationPoint, + } from "@radroots/utils"; + import { onMount } from "svelte"; + + const { lc_geop_current, lc_geocode } = get_context(`lib`); + + let { + map_geoc = $bindable(undefined), + map_geop = $bindable(undefined), + farm_geop_lat, + farm_geop_lng, + }: { + map_geoc: GeocoderReverseResult | undefined; + map_geop: GeolocationPoint | undefined; + farm_geop_lat: string; + farm_geop_lng: string; + } = $props(); + + let map: maplibregl.Map | undefined = $state(undefined); + + const is_valid_geop = $derived(geop_is_valid(map_geop)); + + onMount(async () => { + try { + const geop = await lc_geop_current(); + if (!geop) return; + map_geop = { ...geop }; + const geoc = await lc_geocode(geop); + if (!geoc) return; + map_geoc = geoc; + if (map && map_geop) map.setCenter([map_geop.lng, map_geop.lat]); + focus_map_marker(); + } catch (e) { + handle_err(e, `on_mount`); + } + }); +</script> + +<div + class={`flex flex-col h-[100vh] w-full px-6 gap-4 justify-start items-center`} +> + <WrapBorder basis={{ classes: `h-lo_view_main_${$app_lo}` }}> + <Map bind:map> + {#if map_geop} + <MapMarkerArea + bind:map_geop + bind:map_geoc + basis={{ + show_display: true, + }} + /> + {/if} + </Map> + </WrapBorder> + {#if is_valid_geop} + <Fade> + <div + class={`flex flex-col w-full gap-1 justify-center items-center`} + > + <div + class={`flex flex-row w-full gap-2 justify-center items-center`} + > + <p + class={`font-sans font-[500] text-ly0-gl tracking-tightest`} + > + {farm_geop_lat} + </p> + <p + class={`font-sans font-[500] text-ly0-gl tracking-tightest`} + > + {farm_geop_lng} + </p> + </div> + </div> + </Fade> + {/if} +</div> diff --git a/apps-lib-pwa/src/lib/components/farm/farms-display-li-el.svelte b/apps-lib-pwa/src/lib/components/farm/farms-preview-card.svelte diff --git a/apps-lib-pwa/src/lib/components/form/form-line-ledger-label-select-label.svelte b/apps-lib-pwa/src/lib/components/form/form-line-ledger-label-select-label.svelte @@ -0,0 +1,36 @@ +<script lang="ts"> + import { symbols } from "@radroots/apps-lib"; + + let { + basis, + }: { + basis: { + label: string; + }; + } = $props(); +</script> + +<div class={`flex flex-row justify-start items-center`}> + <p + class={`pr-[13px] font-sansd text-trellis_ti text-ly0-gl-label uppercase`} + > + {`(${basis.label}`} + </p> + <div + class={`relative flex flex-row justify-start items-center -translate-x-[10px] -translate-y-[1px]`} + > + <p + class={`absolute font-sansd text-trellis_ti text-ly0-gl-label uppercase scale-y-[70%] scale-x-[80%] -translate-y-[1px]`} + > + {`${symbols.up}`} + </p> + <p + class={`absolute font-sansd text-trellis_ti text-ly0-gl-label uppercase scale-y-[70%] scale-x-[80%] translate-y-[2px]`} + > + {`${symbols.down}`} + </p> + </div> + <p class={`font-sansd text-trellis_ti text-ly0-gl-label uppercase`}> + {`)`} + </p> +</div> diff --git a/apps-lib-pwa/src/lib/components/form/form-line-ledger-select.svelte b/apps-lib-pwa/src/lib/components/form/form-line-ledger-select.svelte @@ -0,0 +1,86 @@ +<script lang="ts"> + import { + type ElementCallbackValueKeydown, + type IIdOpt, + type ISelectCallback, + type ISelectOption, + fmt_id, + } from "@radroots/apps-lib"; + import InputPwa from "../lib/input-pwa.svelte"; + import SelectPwa from "../lib/select-pwa.svelte"; + + let { + basis, + value_input = $bindable(``), + value_sel = $bindable(``), + }: { + basis: IIdOpt & { + display_value?: string; + label?: string; + input: { + placeholder?: string; + callback_keydown?: + | ElementCallbackValueKeydown<HTMLInputElement> + | undefined; + }; + select: { + entries: ISelectOption<string>[]; + callback?: ISelectCallback; + }; + }; + value_input?: string; + value_sel?: string; + } = $props(); + + const id = $derived(basis.id || ``); +</script> + +<div class={`flex flex-col w-full gap-2 justify-start items-start`}> + {#if basis.label} + <div class={`flex flex-row w-full justify-start items-center`}> + <p class={`font-sansd text-trellis_ti text-ly0-gl-label uppercase`}> + {basis.label} + </p> + </div> + {/if} + <div + class={`relative flex flex-row h-12 w-full justify-start items-center border-y-line border-ly0-edge`} + > + {#if basis.display_value} + <p class={`font-sans font-[400] text-ly0-gl text-form_base`}> + {basis.display_value} + </p> + {:else} + <InputPwa + bind:value={value_input} + basis={{ + id: id ? fmt_id(`${id}_input`) : undefined, + layer: 0, + classes: `h-10 placeholder:text-[1.1rem]`, + placeholder: basis.input.placeholder || ``, + callback_keydown: basis.input.callback_keydown, + }} + /> + <div + class={`absolute right-0 flex flex-row justify-center items-center`} + > + <SelectPwa + bind:value={value_sel} + basis={{ + classes: `w-fit text-ly1-gl`, + id: id ? fmt_id(`${id}_sel`) : undefined, + sync: true, + layer: 1, + show_arrows: `r`, + options: [ + { + entries: basis.select.entries, + }, + ], + callback: basis.select.callback, + }} + /> + </div> + {/if} + </div> +</div> diff --git a/apps-lib-pwa/src/lib/components/form/form-line-ledger.svelte b/apps-lib-pwa/src/lib/components/form/form-line-ledger.svelte @@ -0,0 +1,88 @@ +<script lang="ts"> + import { + type ElementCallbackValueKeydown, + type IIdOpt, + type ISelectOption, + fmt_id, + } from "@radroots/apps-lib"; + import { type FormField } from "@radroots/utils"; + import InputPwa from "../lib/input-pwa.svelte"; + import SelectMenu from "../lib/select-menu.svelte"; + import FormLineLedgerLabelSelectLabel from "./form-line-ledger-label-select-label.svelte"; + + let { + basis, + value = $bindable(``), + value_label_sel = $bindable(``), + }: { + basis: IIdOpt & { + display_value?: string; + label?: string; + label_select?: { + label: string; + entries: ISelectOption<string>[]; + }; + input?: { + placeholder?: string; + field?: FormField; + callback_keydown?: + | ElementCallbackValueKeydown<HTMLInputElement> + | undefined; + }; + }; + value?: string; + value_label_sel?: string; + } = $props(); + + const id = $derived(basis.id || ``); +</script> + +<div class={`flex flex-col w-full gap-2 justify-start items-start`}> + {#if basis.label} + <div class={`flex flex-row w-full justify-start gap-1 items-center`}> + <p class={`font-sansd text-trellis_ti text-ly0-gl-label uppercase`}> + {basis.label} + </p> + {#if basis.label_select} + <SelectMenu + bind:value={value_label_sel} + basis={{ + layer: 0, + options: [ + { + entries: basis.label_select.entries, + }, + ], + }} + > + <FormLineLedgerLabelSelectLabel + basis={{ + label: basis.label_select.label, + }} + /> + </SelectMenu> + {/if} + </div> + {/if} + <div + class={`flex flex-row h-12 w-full justify-start items-center border-y-line border-ly0-edge`} + > + {#if basis.display_value} + <p class={`font-sans font-[400] text-ly1-gl text-form_base`}> + {basis.display_value} + </p> + {:else if basis.input} + <InputPwa + bind:value + basis={{ + id: id ? fmt_id(id) : undefined, + layer: 0, + classes: `h-10 placeholder:text-[1.1rem]`, + field: basis.input?.field || undefined, + placeholder: basis.input?.placeholder || ``, + callback_keydown: basis.input?.callback_keydown, + }} + /> + {/if} + </div> +</div> diff --git a/apps-lib-pwa/src/lib/components/lib/carousel-container.svelte b/apps-lib-pwa/src/lib/components/lib/carousel-container.svelte @@ -0,0 +1,21 @@ +<script lang="ts"> + import type { ICarouselContainer } from "$lib/types/components/lib"; + import { fmt_cl } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + + let { + basis, + children, + }: { + basis: ICarouselContainer<string>; + children: Snippet; + } = $props(); + + const classes = $derived( + `${fmt_cl(basis.classes)} carousel-container flex h-full w-full`, + ); +</script> + +<div data-carousel-container={basis.view} class={classes}> + {@render children()} +</div> diff --git a/apps-lib-pwa/src/lib/components/lib/carousel-item.svelte b/apps-lib-pwa/src/lib/components/lib/carousel-item.svelte @@ -0,0 +1,45 @@ +<script lang="ts"> + import type { + CarouselKeyboardEvent, + CarouselMouseEvent, + ICarouselItem, + } from "$lib/types/components/lib"; + import { fmt_cl } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + + let { + basis, + children, + }: { + basis: ICarouselItem<string>; + children: Snippet; + } = $props(); + + const classes = $derived( + `${fmt_cl(basis.classes)} carousel-item flex flex-col h-full w-full`, + ); + + const handle_click = async (ev: MouseEvent): Promise<void> => { + if (!basis.callback_click) return; + const event_cast = ev as CarouselMouseEvent; + await basis.callback_click(event_cast); + }; + + const handle_keydown = async (ev: KeyboardEvent): Promise<void> => { + if (!basis.callback_keydown) return; + const event_cast = ev as CarouselKeyboardEvent; + await basis.callback_keydown(event_cast); + }; +</script> + +<!-- svelte-ignore a11y_no_noninteractive_tabindex --> +<div + data-carousel-item={basis.view} + class={classes} + role={basis.role ?? undefined} + tabindex={basis.tabindex ?? undefined} + onclick={handle_click} + onkeydown={handle_keydown} +> + {@render children()} +</div> diff --git a/apps-lib-pwa/src/lib/components/lib/input-pwa.svelte b/apps-lib-pwa/src/lib/components/lib/input-pwa.svelte @@ -0,0 +1,120 @@ +<script lang="ts"> + import { browser } from "$app/environment"; + import { + fmt_cl, + idb_kv, + type IInput, + parse_layer, + value_constrain, + } from "@radroots/apps-lib"; + import { handle_err } from "@radroots/utils"; + import { onMount } from "svelte"; + + let { + basis, + el = $bindable(null), + value = $bindable(``), + }: { + basis: IInput<string>; + el?: HTMLInputElement | null; + value?: string; + } = $props(); + + const id = $derived(basis?.id ? basis.id : null); + const layer = $derived( + typeof basis?.layer === `boolean` ? 0 : parse_layer(basis?.layer), + ); + const classes_layer = $derived( + typeof basis?.layer === `boolean` || typeof basis?.layer === `undefined` + ? `` + : `bg-ly${layer} text-ly${layer}-gl_d placeholder:text-ly${layer}-gl_pl caret-ly${layer}-gl`, + ); + + const sync_from_idb = async (): Promise<void> => { + if (!browser || !id) return; + try { + const kv_val = await idb_kv.get(id); + if (kv_val !== null && kv_val !== undefined && kv_val !== value) { + value = kv_val; + } else if (kv_val === null || kv_val === undefined) { + value = ``; + await idb_kv.set(id, ``); + } + } catch (e) { + handle_err(e, `sync_from_idb`); + } + }; + + const sync_to_idb = async (): Promise<void> => { + if (!browser || !id) return; + try { + await idb_kv.set(id, value || ``); + } catch (e) { + handle_err(e, `input_idb_sync`); + } + }; + + onMount(async () => { + await sync_from_idb(); + if (basis?.callback_mount && el) { + try { + await basis.callback_mount({ el }); + } catch (e) { + handle_err(e, `callback_mount`); + } + } + }); + + $effect(() => { + if (id && basis?.sync && browser) { + (async () => { + await sync_to_idb(); + })(); + } + }); + + const handle_on_input = async (): Promise<void> => { + try { + let val_cur = value; + let pass = true; + if (basis?.field) { + val_cur = value_constrain(basis.field?.charset, val_cur); + if (val_cur !== value) { + value = val_cur; + } + pass = basis.field?.validate.test(val_cur); + } + if (basis?.callback) { + await basis.callback({ value: val_cur, pass }); + } + } catch (e) { + handle_err(e, `handle_on_input`); + } + }; +</script> + +<input + bind:this={el} + bind:value + disabled={!!basis.disabled} + oninput={handle_on_input} + onblur={async ({ currentTarget: el }) => { + if (basis.callback_blur) await basis.callback_blur({ el }); + }} + onfocus={async ({ currentTarget: el }) => { + if (id && basis.sync && browser) await sync_from_idb(); + if (basis.callback_focus) await basis.callback_focus({ el }); + }} + onkeydown={async (ev) => { + if (basis?.callback_keydown) + await basis.callback_keydown({ + key: ev.key, + key_s: ev.key === `Enter`, + el: ev.currentTarget, + }); + }} + {id} + type="text" + class={`${fmt_cl(basis?.classes)} el-input ${classes_layer} el-re`} + placeholder={basis?.placeholder || ``} +/> diff --git a/apps-lib-pwa/src/lib/components/lib/select-pwa.svelte b/apps-lib-pwa/src/lib/components/lib/select-pwa.svelte @@ -0,0 +1,126 @@ +<script lang="ts"> + import { browser } from "$app/environment"; + import { + Glyph, + type ISelect, + fmt_cl, + idb_kv, + parse_layer, + } from "@radroots/apps-lib"; + import { handle_err } from "@radroots/utils"; + import { onMount } from "svelte"; + + let { + basis, + value = $bindable(``), + el = $bindable(null), + }: { + basis: ISelect; + value: string; + el?: HTMLSelectElement | null; + } = $props(); + + const id = $derived(basis?.id ? basis.id : null); + + const layer = $derived( + typeof basis?.layer === `boolean` + ? parse_layer(0) + : parse_layer(basis.layer), + ); + + const classes_layer = $derived( + typeof basis?.layer === `boolean` + ? `` + : !value + ? `text-ly${layer}-gl/60` + : `text-ly${layer}-gl_d`, + ); + + onMount(async () => { + try { + if (id && basis?.sync_init && browser) { + const sync_val = await idb_kv.get(id); + await idb_kv.set(id, sync_val || ``); + } + } catch (e) { + handle_err(e, `on_mount`); + } + }); + + $effect(() => { + if (browser && id && basis?.sync) { + (async () => { + await idb_kv.set(id, value); + })(); + } + }); + + const handle_on_change = async (el: HTMLSelectElement): Promise<void> => { + try { + const opt = basis.options + .map((i) => i.entries) + .reduce((_, j) => j, []) + .find((k) => k.value === el?.value); + if (el) el.value = value; + if (basis?.sync && id && browser) await idb_kv.set(id, value); + if (basis.callback && opt) await basis.callback(opt); + } catch (e) { + console.log(`(error) handle_on_change `, e); + } + }; +</script> + +{#if basis?.show_arrows === "l"} + <div class={`flex flex-row pr-[2px] justify-center items-center`}> + <Glyph + basis={{ + key: `caret-up-down`, + dim: `xs`, + + classes: `text-ly${layer}-gl translate-y-[1px]`, + }} + /> + </div> +{/if} +<select + bind:this={el} + bind:value + onchange={async ({ currentTarget: el }) => { + handle_on_change(el); + }} + {id} + class={`${fmt_cl(basis.classes)} z-10 el-select ${classes_layer}`} +> + {#each basis.options as opt_g} + {#if opt_g.group} + <optgroup> + {#each opt_g.entries as opt} + <option + label={opt_g.group === true + ? `-`.repeat(21) + : opt_g.group || ``} + > + {opt.label} + </option> + {/each} + </optgroup> + {:else} + {#each opt_g.entries as opt} + <option value={opt.value} disabled={!!opt.disabled}> + {opt.label} + </option> + {/each} + {/if} + {/each} +</select> +{#if basis?.show_arrows === "r"} + <div class={`flex flex-row pl-[2px] justify-center items-center`}> + <Glyph + basis={{ + key: `caret-up-down`, + dim: `xs`, + classes: `text-ly${layer}-gl`, + }} + /> + </div> +{/if} diff --git a/apps-lib-pwa/src/lib/components/lib/wrap-border.svelte b/apps-lib-pwa/src/lib/components/lib/wrap-border.svelte @@ -0,0 +1,22 @@ +<script lang="ts"> + import { type IClOpt, fmt_cl } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + + let { + basis, + children, + }: { + basis: IClOpt; + children: Snippet; + } = $props(); +</script> + +<div + class={`${fmt_cl(basis.classes)} relative flex flex-col w-full py-[4px] px-[4px] justify-start items-center rounded-[32px] overflow-hidden bg-white shadow-sm`} +> + <div + class={`flex flex-row h-full w-full justify-center items-center bg-white/30 overflow-hidden rounded-[28px]`} + > + {@render children()} + </div> +</div> diff --git a/apps-lib-pwa/src/lib/components/map/map.svelte b/apps-lib-pwa/src/lib/components/map/map.svelte @@ -33,7 +33,7 @@ bind:map class="{fmt_cl(basis?.classes)} relative h-full w-full" zoom={10} - style={cfg_map.styles.base[$theme_mode]} + style={cfg_map.styles.base[$theme_mode || "light"]} attributionControl={false} {interactive} {zoomOnDoubleClick} diff --git a/apps-lib-pwa/src/lib/index.ts b/apps-lib-pwa/src/lib/index.ts @@ -1,26 +1,38 @@ export { default as ButtonGlyphSimple } from "./components/button/button-glyph-simple.svelte"; export { default as ButtonLabelDashed } from "./components/button/button-label-dashed.svelte"; +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 ButtonSimple } from "./components/button/button-simple.svelte"; -export { default as FarmsDisplayLiEl } from "./components/farm/farms-display-li-el.svelte"; +export { default as FarmsAddDetail } from "./components/farm/farms-add-detail.svelte"; +export { default as FarmsAddMap } from "./components/farm/farms-add-map.svelte"; +export { default as FarmsPreviewCard } from "./components/farm/farms-preview-card.svelte"; export { default as EntryLine } from "./components/form/entry-line.svelte"; export { default as EntryWrap } from "./components/form/entry-wrap.svelte"; +export { default as FormLineLedgerLabelSelectLabel } from "./components/form/form-line-ledger-label-select-label.svelte"; +export { default as FormLineLedgerSelect } from "./components/form/form-line-ledger-select.svelte"; +export { default as FormLineLedger } from "./components/form/form-line-ledger.svelte"; export { default as LayoutPage } from "./components/layout/layout-page.svelte"; export { default as LayoutView } from "./components/layout/layout-view.svelte"; export { default as LayoutWindow } from "./components/layout/layout-window.svelte"; +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 InputPwa } from "./components/lib/input-pwa.svelte"; export { default as InputValue } from "./components/lib/input-value.svelte"; export { default as LoadSymbol } from "./components/lib/load-symbol.svelte"; export { default as LogoCircleSm } from "./components/lib/logo-circle-sm.svelte"; export { default as LogoCircle } from "./components/lib/logo-circle.svelte"; export { default as LogoLetters } from "./components/lib/logo-letters.svelte"; export { default as SelectMenu } from "./components/lib/select-menu.svelte"; +export { default as SelectPwa } from "./components/lib/select-pwa.svelte"; +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 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 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 @@ -1,5 +1,5 @@ -import type { CallbackRoute, GeometryScreenPositionHorizontal, ICb, ICbOpt, IDisabledOpt, IGlyph, IGlyphKey, ILoadingOpt, ILyOpt } from "@radroots/apps-lib"; -import type { CallbackPromise } from "@radroots/utils"; +import type { CallbackRoute, GeometryScreenPositionHorizontal, ICb, ICbOpt, IClOpt, IDisabledOpt, IGlyph, IGlyphKey, ILoadingOpt, ILyOpt } from "@radroots/apps-lib"; +import type { CallbackPromise, CallbackPromiseGeneric } from "@radroots/utils"; export type IButtonSimple = ILyOpt & { label: string; @@ -31,3 +31,23 @@ export type IFloatPage = { }; export type IButtonNavRound = ICb & IDisabledOpt & ILoadingOpt & IGlyphKey; + +export type CarouselMouseEvent = MouseEvent & { + currentTarget: EventTarget & HTMLDivElement; +}; + +export type CarouselKeyboardEvent = KeyboardEvent & { + currentTarget: EventTarget & HTMLDivElement; +}; + +export type ICarouselContainer<T extends string> = IClOpt & { + view: T; +}; + +export type ICarouselItem<T extends string> = IClOpt & { + view: T; + role?: string; + tabindex?: number; + callback_click?: CallbackPromiseGeneric<CarouselMouseEvent>; + callback_keydown?: CallbackPromiseGeneric<CarouselKeyboardEvent>; +}; diff --git a/apps-lib-pwa/src/lib/types/views/farm.ts b/apps-lib-pwa/src/lib/types/views/farm.ts @@ -1,5 +1,5 @@ import type { Farm } from "@radroots/tangle-schema-bindings"; -import type { LocationBasis } from "@radroots/utils"; +import type { GeocoderReverseResult, GeolocationPoint, LocationBasis } from "@radroots/utils"; export type FarmExtended = { farm: Farm; @@ -14,4 +14,28 @@ export type FarmLotBasis = { }; export type IViewFarmsData = { list: FarmExtended[]; +}; + +export type IViewFarmsAddSubmission = { + farm_name: string; + farm_area?: number; + farm_area_unit?: string; + farm_contact_name?: string; + geolocation_point: GeolocationPoint; + geocode_result: GeocoderReverseResult; +}; + +export type IViewFarmsProductsAddSubmitPayload = { + product: string; + process: string; + description: string; + price_amount: number; + price_currency: string; + price_quantity_unit: string; + photos: string[]; + quantity_amount: number; + quantity_unit: string; + quantity_label: string; + geolocation_point: GeolocationPoint; + geocode_result: GeocoderReverseResult; }; \ No newline at end of file diff --git a/apps-lib-pwa/src/lib/utils/farm/schema.ts b/apps-lib-pwa/src/lib/utils/farm/schema.ts @@ -0,0 +1,28 @@ +import { dev } from "$app/environment"; +import type { IViewFarmsAddSubmission, IViewFarmsProductsAddSubmitPayload } from "$lib/types/views/farm"; +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"; + +export const schema_view_farms_add_submission: z.ZodSchema<IViewFarmsAddSubmission> = z.object({ + farm_name: z.string().regex(form_fields.farm_name.validate), + farm_area: zf_numf_pos.optional(), + farm_area_unit: z.string().regex(form_fields.area_unit.validate).optional(), + farm_contact_name: z.string().regex(form_fields.contact_name.validate).optional(), + geolocation_point: schema_geolocation_point, + geocode_result: schema_geocode_result, +}); + +export const schema_view_farms_products_add_submission: z.ZodSchema<IViewFarmsProductsAddSubmitPayload> = z.object({ + product: z.string().regex(form_fields.product_key.validate), + process: z.string().regex(form_fields.product_process.validate), + description: z.string().regex(form_fields.product_description.validate), + price_amount: zf_price, + price_currency: z.string().regex(form_fields.price_currency.validate), + price_quantity_unit: z.string().regex(form_fields.quantity_unit.validate), + photos: z.array(z.string().regex(dev ? util_rxp.url_image_upload_dev : util_rxp.url_image_upload)), + quantity_amount: zf_numi_pos, + quantity_unit: z.string().regex(form_fields.quantity_unit.validate), + quantity_label: z.string().regex(form_fields.quantity_label.validate), + geolocation_point: schema_geolocation_point, + geocode_result: schema_geocode_result, +}); +\ No newline at end of file diff --git a/apps-lib-pwa/src/lib/views/farms/farms-add.svelte b/apps-lib-pwa/src/lib/views/farms/farms-add.svelte @@ -0,0 +1,244 @@ +<script lang="ts"> + import ButtonLayoutBottom from "$lib/components/button/button-layout-bottom.svelte"; + import ButtonLayoutPair from "$lib/components/button/button-layout-pair.svelte"; + import FarmsAddDetails from "$lib/components/farm/farms-add-detail.svelte"; + import FarmsAddMap from "$lib/components/farm/farms-add-map.svelte"; + import LayoutView from "$lib/components/layout/layout-view.svelte"; + import CarouselContainer from "$lib/components/lib/carousel-container.svelte"; + 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 { schema_view_farms_add_submission } from "$lib/utils/farm/schema"; + import { focus_map_marker } from "$lib/utils/map"; + import { + carousel_dec, + carousel_inc, + carousel_init, + casl_i, + el_id, + fmt_id, + geop_init, + geop_is_valid, + get_context, + type CallbackRoute, + } from "@radroots/apps-lib"; + import { + geol_lat_fmt, + geol_lng_fmt, + handle_err, + parse_float, + parse_geocode_address, + type CallbackPromiseGeneric, + type GeocoderReverseResult, + type GeolocationAddress, + type GeolocationPoint, + } from "@radroots/utils"; + import { onMount } from "svelte"; + + const { ls, locale, lc_gui_alert, lc_geop_current, lc_geocode } = + get_context(`lib`); + + let { + basis, + }: { + basis: { + callback_route?: CallbackRoute<string>; + on_submit: CallbackPromiseGeneric<{ + payload: IViewFarmsAddSubmission; + }>; + }; + } = $props(); + + let map_geop: GeolocationPoint = $state(geop_init()); + let map_geoc: GeocoderReverseResult | undefined = $state(undefined); + + let val_farmname = $state(``); + let val_farmaddress = $state(``); + let val_farmcontact = $state(``); + let val_farmarea = $state(``); + let val_farmarea_unit = $state(`ac`); + + const carousel_view: "farms_add" = "farms_add"; + + const disabled_submit = $derived($casl_i === 1 && !val_farmname); + + onMount(async () => { + try { + await carousel_init(carousel_view, 1); + } catch (e) { + handle_err(e, `on_mount`); + } + }); + + const farm_geop_lat = $derived( + geop_is_valid(map_geop) + ? geol_lat_fmt(map_geop.lat, `dms`, $locale, 3) + : ``, + ); + + const farm_geop_lng = $derived( + geop_is_valid(map_geop) + ? geol_lng_fmt(map_geop.lng, `dms`, $locale, 3) + : ``, + ); + + const farm_geolocation_address: GeolocationAddress | undefined = $derived( + parse_geocode_address(map_geoc), + ); + + $effect(() => { + if (farm_geolocation_address) + val_farmaddress = `${farm_geolocation_address.primary}, ${farm_geolocation_address.admin}, ${farm_geolocation_address.country}`; + }); + + const handle_enter_location = async (): Promise<void> => { + map_geoc = undefined; + map_geop = geop_init(); + val_farmaddress = ``; + await handle_continue(); + el_id(fmt_id(`farm_location`))?.focus(); + }; + + const handle_continue_1 = async (): Promise<void> => { + if (!map_geop || !map_geoc) + return void lc_gui_alert(`No farm location provided.`); //@todo + const farms_add_submission = schema_view_farms_add_submission.safeParse( + { + farm_name: val_farmname, + farm_area: val_farmarea ? parse_float(val_farmarea) : undefined, + farm_area_unit: + val_farmarea && val_farmarea_unit + ? val_farmarea_unit + : undefined, + farm_contact_name: val_farmcontact + ? val_farmcontact + : undefined, + geolocation_point: map_geop, + geocode_result: map_geoc, + } satisfies IViewFarmsAddSubmission, + ); + + if (!farms_add_submission.success) { + return void lc_gui_alert( + `Request invalid: ${farms_add_submission.error}`, + ); //@todo + } + await basis.on_submit({ payload: farms_add_submission.data }); + }; + + const handle_continue = async (): Promise<void> => { + switch ($casl_i) { + case 1: + return await handle_continue_1(); + default: + await carousel_inc(carousel_view); + } + }; + + const handle_back = async (): Promise<void> => { + switch ($casl_i) { + case 1: { + if (!geop_is_valid(map_geop)) { + const geop_cur = await lc_geop_current(); + if (geop_cur) { + map_geop = geop_cur; + const geoc_cur = await lc_geocode(geop_cur); + if (geoc_cur) map_geoc = geoc_cur; + focus_map_marker(); + } + } + } + default: + return await carousel_dec(carousel_view); + } + }; +</script> + +<LayoutView> + <PageToolbar + basis={{ + header: { + label: `${$ls(`common.farms`)} / ${`${$ls(`common.add`)}`}`, + callback_route: basis.callback_route, + }, + }} + > + {#snippet header_option()} + <!-- @todo {#if $casl_i === 0} + <button + class={`flex flex-row justify-center items-center`} + onclick={async () => { + await handle_enter_location(); + }} + > + <p + class={`font-sans font-[600] text-[18px] text-ly0-gl-hl`} + > + {`${$ls(`common.enter_location`)}`} + </p> + <Glyph + basis={{ + classes: `text-ly0-gl-hl`, + dim: `md`, + key: `caret-right`, + }} + /> + </button> + {/if}--> + {/snippet} + </PageToolbar> + <CarouselContainer + basis={{ + view: carousel_view, + }} + > + <CarouselItem + basis={{ + view: carousel_view, + classes: `justify-start items-center`, + }} + > + <FarmsAddMap + bind:map_geop + bind:map_geoc + {farm_geop_lat} + {farm_geop_lng} + /> + </CarouselItem> + <CarouselItem + basis={{ + view: carousel_view, + classes: `justify-start items-center`, + }} + > + <FarmsAddDetails + bind:val_farmname + bind:val_farmaddress + bind:val_farmcontact + bind:val_farmarea + bind:val_farmarea_unit + {farm_geop_lat} + {farm_geop_lng} + /> + </CarouselItem> + </CarouselContainer> +</LayoutView> +{#if $app_platform?.browser !== `safari`} + <ButtonLayoutBottom> + <ButtonLayoutPair + basis={{ + continue: { + label: `${$ls(`common.continue`)}`, + disabled: disabled_submit, + callback: handle_continue, + }, + back: { + label: `${$ls(`common.back`)}`, + visible: $casl_i > 0, + callback: handle_back, + }, + }} + /> + </ButtonLayoutBottom> +{/if} diff --git a/apps-lib-pwa/src/lib/views/farms/farms.svelte b/apps-lib-pwa/src/lib/views/farms/farms.svelte @@ -1,7 +1,7 @@ <script lang="ts"> import ButtonGlyphSimple from "$lib/components/button/button-glyph-simple.svelte"; import ButtonLabelDashed from "$lib/components/button/button-label-dashed.svelte"; - import FarmsDisplayLiEl from "$lib/components/farm/farms-display-li-el.svelte"; + import FarmsPreviewCard from "$lib/components/farm/farms-preview-card.svelte"; import LayoutPage from "$lib/components/layout/layout-page.svelte"; import LayoutView from "$lib/components/layout/layout-view.svelte"; import PageToolbar from "$lib/components/navigation/page-toolbar.svelte"; @@ -72,7 +72,7 @@ {#if basis.data} {#if basis.data?.list.length} {#each basis.data?.list || [] as li} - <FarmsDisplayLiEl + <FarmsPreviewCard basis={li} on_handle_farm_view={basis.on_handle_farm_view} />