web_lib

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

commit aeed3ec102306f07ba47808842790d76021a34f0
parent 53b46820cad03092a5a373805c8fd4dd46930c7a
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Mon, 10 Feb 2025 10:19:29 +0000

utils: refactor library for app/0.0.1, migrate from direct index.ts entry to tsup compilation

Diffstat:
Mutils/.gitignore | 26++++++++++++++++++--------
Dutils/index.ts | 1-
Mutils/package.json | 23++++++++++++++++++-----
Autils/src/*regex.ts | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/*validation.ts | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/_env.ts | 3+++
Autils/src/app/document.ts | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/app/i18n.ts | 23+++++++++++++++++++++++
Autils/src/app/lib.ts | 31+++++++++++++++++++++++++++++++
Autils/src/app/styles.ts | 45+++++++++++++++++++++++++++++++++++++++++++++
Autils/src/app/types/app.ts | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/app/types/basis.ts | 14++++++++++++++
Autils/src/app/types/component.ts | 247+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/app/types/element.ts | 7+++++++
Autils/src/app/types/geometry.ts | 16++++++++++++++++
Autils/src/app/types/glyph.ts | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/app/types/interface.ts | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/app/types/resolve.ts | 29+++++++++++++++++++++++++++++
Autils/src/app/types/view.ts | 33+++++++++++++++++++++++++++++++++
Autils/src/app/util.ts | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/app/util/resolve-enum.ts | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/app/util/resolve.ts | 6++++++
Autils/src/app/util/search.ts | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/app/validation/view.ts | 27+++++++++++++++++++++++++++
Dutils/src/ascii.ts | 3---
Autils/src/client/geo.ts | 12++++++++++++
Dutils/src/client/geolocation.ts | 16----------------
Mutils/src/client/gui.ts | 4+---
Autils/src/config.ts | 20++++++++++++++++++++
Mutils/src/currency.ts | 154++++++++++---------------------------------------------------------------------
Mutils/src/error.ts | 29+++++++++++++++--------------
Dutils/src/file.ts | 41-----------------------------------------
Dutils/src/format.ts | 5-----
Autils/src/geo.ts | 231+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dutils/src/geolocation.ts | 88-------------------------------------------------------------------------------
Dutils/src/guard.ts | 33---------------------------------
Dutils/src/hash.ts | 28----------------------------
Mutils/src/http.ts | 15++++++++++-----
Autils/src/image.ts | 7+++++++
Mutils/src/index.ts | 83+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Autils/src/lib.ts | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Mutils/src/list.ts | 23++---------------------
Dutils/src/math.ts | 8--------
Dutils/src/nostr-event-util/lib.ts | 99-------------------------------------------------------------------------------
Dutils/src/nostr-event-util/types.ts | 80-------------------------------------------------------------------------------
Dutils/src/nostr-key-util/lib.ts | 122-------------------------------------------------------------------------------
Dutils/src/nostr-relay-util/lib.ts | 46----------------------------------------------
Dutils/src/nostr-relay-util/types.ts | 21---------------------
Dutils/src/nostr/event.ts | 46----------------------------------------------
Dutils/src/nostr/ndk.ts | 75---------------------------------------------------------------------------
Autils/src/nostr/nostr-event-util/lib.ts | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/nostr/nostr-event-util/types.ts | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/src/nostr/nostr-key-util/lib.ts | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rutils/src/nostr-key-util/types.ts -> utils/src/nostr/nostr-key-util/types.ts | 0
Autils/src/nostr/nostr/event.ts | 46++++++++++++++++++++++++++++++++++++++++++++++
Rutils/src/nostr/key.ts -> utils/src/nostr/nostr/key.ts | 0
Autils/src/nostr/nostr/ndk.ts | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rutils/src/nostr/types.ts -> utils/src/nostr/nostr/types.ts | 0
Autils/src/number.ts | 13+++++++++++++
Dutils/src/regex.ts | 33---------------------------------
Autils/src/response.ts | 33+++++++++++++++++++++++++++++++++
Mutils/src/trade.ts | 18+++++++++++++-----
Mutils/src/types.ts | 36++++++++++++++++--------------------
Autils/src/unit.ts | 41+++++++++++++++++++++++++++++++++++++++++
Dutils/src/units.ts | 56--------------------------------------------------------
Dutils/src/window.ts | 32--------------------------------
Mutils/tsconfig.json | 16+++++++++++-----
Autils/tsup.config.ts | 11+++++++++++
68 files changed, 2473 insertions(+), 1089 deletions(-)

diff --git a/utils/.gitignore b/utils/.gitignore @@ -18,20 +18,30 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local - # turbo .turbo + +# Output +.output +/build +dist + +# local env files +.env* +!.env.example + + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# local .tmp* +.backup* .dev* .vscode notes*.txt notes*.md -justfile -dist git-diff.txt +justfile diff --git a/utils/index.ts b/utils/index.ts @@ -1 +0,0 @@ -export * from "./src"; diff --git a/utils/package.json b/utils/package.json @@ -1,17 +1,28 @@ { - "name": "@radroots/utils", + "name": "@radroots/util", "version": "0.0.0", "private": true, "license": "GPLv3", "type": "module", - "module": "index.ts", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, "scripts": { - "build": "just build && tsc", - "dev": "tsc -w" + "build": "tsup", + "dev": "tsup --watch", + "watch": "tsc -w" }, "devDependencies": { "@types/ngeohash": "0.6.8", + "@types/node": "^22.13.1", "@types/uuid": "^10.0.0", + "tsup": "^6.2.3", "typescript": "^5.3.3" }, "publishConfig": { @@ -20,7 +31,9 @@ "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.4.0", - "@nostr-dev-kit/ndk": "^2.10.7", + "@nostr-dev-kit/ndk": "^2.11.0", + "@sveltekit-i18n/base": "^1.3.7", + "@sveltekit-i18n/parser-icu": "^1.0.8", "convert": "^5.5.1", "geohashing": "^2.0.1", "nostr-geotags": "^0.7.1", diff --git a/utils/src/*regex.ts b/utils/src/*regex.ts @@ -0,0 +1,132 @@ +import { FormField } from "$root"; + +export const util_rxp = { + product_key: /^[A-Za-z_]+$/, + product_key_ch: /^[A-Za-z_]$/, + product_title: /[A-Za-z0-9 ]+$/, + product_title_ch: /[A-Za-z0-9 ]$/, + float: /^[+-]?(\d+(\.\d*)?|\.\d+)$/, + float_ch: /^[0-9\.\+\-]$/, + float_pos: /^\d+(\.\d+)?$/, + float_pos_ch: /^[0-9\.]$/, + description: /^(?:\S+(?:\s+\S+)*)$/, + description_ch: /[^a-zA-Z0-9.,!?;:'"(){}[]\s\u0600-\u06FF\u0900-\u097F\u0400-\u04FF\u0500-\u052F\u1F00-\u1FFF\u4E00-\u9FFF\uAC00-\uD7AF\u3040-\u309F\u30A0-\u30FF ]+/, + nbsp: /[\u00A0]/g, + nbsp_rp: /[\u00A0]+/g, + rtlm: /[\u200F]/g, + rtlm_rp: /[\u200F]+/g, + commas: /[,]+/g, + periods: /[.]+/g, + word_only: /^[a-zA-Z]+$/, + alpha: /[a-zA-Z ]$/, + alpha_ch: /[a-zA-Z ]$/, + num: /^[0-9]+$/, + lat: /^[-+]?([1-8]?[0-9](\.\d{1,6})?|90(\.0{1,6})?)$/, + lat_ch: /^[\d\.\+\-]$/, + lng: /^[-+]?((1[0-7]?[0-9]|180)(\.\d{1,6})?|(\d{1,2})(\.\d{1,6})?)$/, + lng_ch: /^[\d\.\+\-]$/, + alphanum: /[a-zA-Z0-9., ]$/, + alphanum_ch: /[a-zA-Z0-9.,\s\u0600-\u06FF\u0900-\u097F\u0400-\u04FF\u0500-\u052F\u1F00-\u1FFF\u4E00-\u9FFF\uAC00-\uD7AF\u3040-\u309F\u30A0-\u30FF ]+/, + price: /^\d+(\.\d+)?$/, + price_ch: /[0-9.]$/, + price_cur: /^[A-Za-z]{3}$/, + price_cur_ch: /[A-Za-z]$/, + profile_name: /^[a-zA-Z0-9._]{3,30}$/, + profile_name_ch: /[a-zA-Z0-9._]/, + trade_product_key: /^(?:[a-zA-Z0-9]+(?:\s+[a-zA-Z0-9]+){0,2})$/, + trade_product_category: /^(?:[a-zA-Z0-9]+(?:\s+[a-zA-Z0-9]+){0,2})$/, + currency_symbol: /(?:[A-Za-z]{3,5}\$|\p{Sc})/u, + currency_marker: /(?:[A-Za-z]{2,4}[^\d\s]+|[^\d\s]{1,3}[A-Za-z]{2,4})/, + ws_proto: /^(wss:\/\/|ws:\/\/)/, + quantity_unit: /^(kg|lb|g)$/, + quantity_unit_ch: /[A-Za-z]$/, + url_image_upload: /^blob:https:\/\/domain\.tld\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/, + url_image_upload_dev: /^blob:http:\/\/localhost:\d+\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/, + country_code_a2: /^[A-Za-z]{2}$/, + addr_primary: /[a-zA-Z0-9., ]$/, + addr_admin: /[a-zA-Z0-9., ]$/, + num_int: /^[0-9]$/, + area_unit: /^(ac|ha|ft2|m2)$/, + area_unit_ch: /[A-Za-z2]$/, +}; + +export type FormFieldsKey = + | `product_title` + | `product_key` + | `product_process` + | `product_description` + | `price` + | `price_currency` + | `quantity_unit` + | `quantity` + | `quantity_label` + | `farm_name` + | `farm_size` + | `area` + | `area_unit` + | `contact_name` + | `profile_name` + + +export const form_fields: Record<FormFieldsKey, FormField> = { + profile_name: { + charset: util_rxp.profile_name_ch, + validate: util_rxp.profile_name, + }, + product_description: { + charset: util_rxp.alpha_ch, + validate: util_rxp.alpha, + }, + product_key: { + charset: util_rxp.product_key_ch, + validate: util_rxp.product_key, + }, + product_title: { + charset: util_rxp.product_title_ch, + validate: util_rxp.product_title, + }, + product_process: { + charset: util_rxp.alphanum_ch, + validate: util_rxp.alphanum, + }, + price: { + charset: util_rxp.price_ch, + validate: util_rxp.price, + }, + price_currency: { + charset: util_rxp.price_cur_ch, + validate: util_rxp.price_cur, + }, + quantity: { + charset: util_rxp.num, + validate: util_rxp.num, + }, + quantity_unit: { + charset: util_rxp.quantity_unit_ch, + validate: util_rxp.quantity_unit, + }, + quantity_label: { + charset: util_rxp.alphanum_ch, + validate: util_rxp.alphanum, + }, + area: { + charset: util_rxp.float_ch, + validate: util_rxp.float, + }, + area_unit: { + charset: util_rxp.area_unit_ch, + validate: util_rxp.area_unit, + }, + farm_name: { + charset: util_rxp.alpha_ch, + validate: util_rxp.alpha, + }, + farm_size: { + charset: util_rxp.num_int, + validate: util_rxp.num_int, + }, + contact_name: { + charset: util_rxp.alpha_ch, + validate: util_rxp.alpha, + }, +}; diff --git a/utils/src/*validation.ts b/utils/src/*validation.ts @@ -0,0 +1,49 @@ +import { GeolocationAddress, GeolocationPoint, parse_int, ResolveEnumArea_Unit, ResolveEnumQuantity_Unit, util_rxp } from "$root"; +import { z } from "zod"; + +export const vunion_area_unit: z.ZodUnion<[ + z.ZodLiteral<ResolveEnumArea_Unit>, + z.ZodLiteral<ResolveEnumArea_Unit>, + z.ZodLiteral<ResolveEnumArea_Unit>, + z.ZodLiteral<ResolveEnumArea_Unit>, +]> = z.union([ + z.literal(`ac`), + z.literal(`ft2`), + z.literal(`ha`), + z.literal(`m2`), +]); + +export const vunion_mass_unit: z.ZodUnion<[ + z.ZodLiteral<ResolveEnumQuantity_Unit>, + z.ZodLiteral<ResolveEnumQuantity_Unit>, + z.ZodLiteral<ResolveEnumQuantity_Unit>, +]> = z.union([ + z.literal(`kg`), + z.literal(`lb`), + z.literal(`g`), +]); + +export const vs_geolocation_address: z.ZodSchema<GeolocationAddress> = z.object({ + primary: z.string().regex(util_rxp.addr_primary), + admin: z.string().regex(util_rxp.addr_admin), + country: z.string().regex(util_rxp.country_code_a2) +}); + +export const vs_geolocation_point: z.ZodSchema<GeolocationPoint> = z.object({ + lat: z.number().min(-90).max(90), + lng: z.number().min(-180).max(180), +}); + +export const ve_price_amount = z.preprocess((input) => { + return parse_int(String(input), 1.00); +}, z.number().positive().multipleOf(0.01)); + +export const ve_quantity_amount = z.preprocess((input) => { + return parse_int(String(input), 1); +}, z.number().int().positive()); + +export const zod_numf_price = z.number().positive().multipleOf(0.01); + +export const zod_numi_pos = z.number().int().positive(); + +export const zod_numf_pos = z.number().positive(); +\ No newline at end of file diff --git a/utils/src/_env.ts b/utils/src/_env.ts @@ -0,0 +1,3 @@ +export const _env = { + PROD: process.env.NODE_ENV === `production` +}; diff --git a/utils/src/app/document.ts b/utils/src/app/document.ts @@ -0,0 +1,51 @@ +import { sleep, ThemeLayer } from "$root"; + +export const el_id = (id: string): HTMLElement | undefined => { + const el = document.getElementById(id); + return el ? el : undefined; +}; + +export const el_toggle = (id: string, toggle_class: string): void => { + const el = document.getElementById(id); + if (el) el.classList.toggle(toggle_class); +}; + +export const els_id_pref = (id_pref: string): Element[] | undefined => { + const els = document.querySelectorAll(`[id^="${id_pref}"]`); + if (els && els.length) return Array.from(els); + return undefined; +}; + +export const els_id_pref_index = (id_pref: string, num_index: number, orientation: `greater` | `lesser` | `not` = `greater`, inclusive: boolean = true): Element[] | undefined => { + const els = document.querySelectorAll(`[id^="${`${id_pref}-`.replaceAll(`--`, `-`)}"]`); + if (els && els.length) return Array.from(els).filter(el => { + const match = el.id.match(/(?<=^|\-)[0-9]\d*(?=\-)/) + if (match) { + const num = parseInt(match[0], 10); + switch (orientation) { + case `greater`: { + if (inclusive) return num >= num_index; + else return num > num_index; + } + case `lesser`: { + if (inclusive) return num <= num_index; + else return num < num_index; + } + case `not`: { + return num !== num_index; + } + } + } + return false; + }); + return undefined; +}; + +export const el_focus = async (id: string, callback: () => Promise<void>, layer: ThemeLayer = 1): Promise<void> => { + const el = el_id(id); + el?.classList.add(`entry-layer-${layer}-highlight`); + el?.focus(); + await sleep(1200); + await callback(); + el?.classList.remove(`entry-layer-${layer}-highlight`); +}; diff --git a/utils/src/app/i18n.ts b/utils/src/app/i18n.ts @@ -0,0 +1,22 @@ +import i18n, { type Loader } from "@sveltekit-i18n/base"; +import type { Config, Parser } from "@sveltekit-i18n/parser-icu"; +import parser from "@sveltekit-i18n/parser-icu"; + +type LanguageConfig = { + default?: string; + value?: string; +}; +export const i18n_conf = <T extends string>(opts: { + default_locale: T; + translations: Record<T, any>; + loaders: Loader.LoaderModule[] +}): i18n<Parser.Params<LanguageConfig>> => { + const { default_locale: initLocale, translations, loaders } = opts; + const config: Config<LanguageConfig> = { + initLocale, + translations, + parser: parser(), + loaders, + }; + return new i18n(config); +}; +\ No newline at end of file diff --git a/utils/src/app/lib.ts b/utils/src/app/lib.ts @@ -0,0 +1,30 @@ +import type { AppLayoutKey } from "$root"; + +type ConfigWindow = { + layout: Record<AppLayoutKey, { + h: number; + }>; + debounce: { + search: number; + } +}; + +export const cfg_app: ConfigWindow = { + layout: { + ios0: { + h: 600 + }, + ios1: { + h: 750 + }, + webm0: { + h: 600 + }, + webm1: { + h: 750 + } + }, + debounce: { + search: 200 + }, +}; +\ No newline at end of file diff --git a/utils/src/app/styles.ts b/utils/src/app/styles.ts @@ -0,0 +1,44 @@ +import type { AppLayoutKey, GeometryGlyphDimension, IToastKind, LoadingDimension } from "$root"; + +export const glyph_style_map: Map<GeometryGlyphDimension, { gl_1: number; dim_1?: number; }> = new Map([ + ["xs--", { gl_1: 12 }], + ["xs-", { gl_1: 12, dim_1: 17 }], + ["xs", { gl_1: 15, dim_1: 18 }], + ["xs+", { gl_1: 18, dim_1: 20 }], + ["sm-", { gl_1: 19, dim_1: 22 }], + ["sm", { gl_1: 20, dim_1: 24 }], + ["sm+", { gl_1: 21 }], + ["md-", { gl_1: 23 }], + ["md", { gl_1: 24 }], + ["md+", { gl_1: 26 }], + ["lg-", { gl_1: 27 }], + ["lg", { gl_1: 28 }], + ["xl", { gl_1: 30 }], + ["xl+", { gl_1: 40 }], +]); + +export const loading_style_map: Map<LoadingDimension, { dim_1: number; gl_2: number }> = new Map([ + ["glyph-send-button", { dim_1: 20, gl_2: 20 }], + ["xs", { dim_1: 12, gl_2: 12 }], + ["sm", { dim_1: 16, gl_2: 16 }], + ["md", { dim_1: 20, gl_2: 20 }], + ["lg", { dim_1: 28, gl_2: 28 }], + ["xl", { dim_1: 36, gl_2: 36 }], +]); + +export const toast_layout_map: Map<AppLayoutKey, string> = new Map([ + [`ios0`, `pt-8`], + [`ios1`, `pt-16`], + [`webm0`, `pt-8`], + [`webm1`, `pt-16`], +]); + +export const toast_style_map: Map<IToastKind, { inner: string; outer: string }> = new Map([ + [ + `simple`, + { + inner: `justify-center`, + outer: `min-h-toast_min w-full px-4 rounded-2xl shadow-sm`, + }, + ], +]); +\ No newline at end of file diff --git a/utils/src/app/types/app.ts b/utils/src/app/types/app.ts @@ -0,0 +1,78 @@ +import type { GeocoderReverseResult, GeolocationPoint, GeometryDimension, IClientGeolocationPosition, INavigationRoute, ISelectOption } from "$root"; + +export type ThemeLayer = 0 | 1 | 2; + +export type AppConfigType = `farmer` | `personal` + +export type AppLayoutKeyIOS = `ios0` | `ios1`; +export type AppLayoutKeyWeb = `webm0` | `webm1`; +export type AppLayoutKey = AppLayoutKeyIOS | AppLayoutKeyWeb; + +export type AppLayoutIOS<T extends string> = `${T}_${AppLayoutKeyIOS}`; +export type AppLayoutWeb<T extends string> = `${T}_${AppLayoutKeyWeb}`; + +export type AppLayoutKeyHeight = + | `lo_bottom_button` + | `nav_tabs` + | `nav_page_header` + | `nav_page_toolbar`; + +export type AppLayoutKeyWidth = + | `lo` + | `lo_textdesc`; + +export type AppHeightsResponsiveIOS = AppLayoutIOS<AppLayoutKeyHeight>; +export type AppHeightsResponsiveWeb = AppLayoutWeb<AppLayoutKeyHeight>; + +export type AppWidthsResponsiveIOS = AppLayoutIOS<AppLayoutKeyWidth>; +export type AppWidthsResponsiveWeb = AppLayoutWeb<AppLayoutKeyWidth>; + +export type CallbackPromiseFigureResult<Ti, Tr> = (value: Ti) => Promise<Tr | undefined>; +export type CallbackPromiseFull<Ti, Tr> = (value: Ti) => Promise<Tr>; +export type CallbackPromiseGeneric<T> = (value: T) => Promise<void>; +export type CallbackPromiseReturn<T> = () => Promise<T>; +export type CallbackPromiseResult<Tr> = () => Promise<Tr | undefined>; + +export type CallbackPromise = () => Promise<void>; +export type CallbackRoute<T extends string> = CallbackPromise | INavigationRoute<T>; + +export type ElementCallbackSelect = CallbackPromiseGeneric<ISelectOption<string>> + +export type EntryStyle = `guide` | `line`; + + +export type LoadingBlades = 8 | 12; +export type LoadingDimension = GeometryDimension | `glyph-send-button`; //@todo remove + +export type LayerGlyphBasisKind = `_a` | `_d` | `_pl`; + +export type NavigationRouteParamNostrPublicKey = `nostr_pk`; +export type NavigationRouteParamRecordKey = `rkey`; +export type NavigationRouteParamId = `id`; +export type NavigationRouteParamLat = `lat`; +export type NavigationRouteParamLng = `lng`; +export type NavigationRouteParamKey = NavigationRouteParamNostrPublicKey | NavigationRouteParamId | NavigationRouteParamRecordKey | NavigationRouteParamLat | NavigationRouteParamLng; +export type NavigationParamTuple = [NavigationRouteParamKey, string]; +export type NavigationPreviousParam<T extends string> = { route: T, label?: string; params?: NavigationParamTuple[] } + +export type EasingFunction = (t: number) => number; + +export interface SvelteTransitionConfig { + delay?: number; + duration?: number; + easing?: EasingFunction; + css?: (t: number, u: number) => string; + tick?: (t: number, u: number) => void; +} + +export type LcGuiAlertCallback = CallbackPromiseFull<string, boolean>; +export type LcGuiConfirmCallback = CallbackPromiseFull<string | { message: string; ok?: string; cancel?: string }, boolean>; + +//export type LcGeocodeCurrentCallback = CallbackPromiseFull<boolean | string, IClientGeolocationPosition | undefined>; +export type LcGeocodeCurrentCallback = CallbackPromiseResult<IClientGeolocationPosition>; + +export type LcGeocodeCallback = CallbackPromiseFull<GeolocationPoint, GeocoderReverseResult | undefined>; +export type LcPhotoAddCallback = CallbackPromiseResult<string>; +export type LcPhotoAddMultipleCallback = CallbackPromiseResult<string[]>; + +export type ImageAspectRatio = `auto` | `1/1` | `4/3` | `16/9` | `3/4`; +\ No newline at end of file diff --git a/utils/src/app/types/basis.ts b/utils/src/app/types/basis.ts @@ -0,0 +1,13 @@ +import type { CallbackPromise, CallbackPromiseReturn } from "$root"; + +export type IBasisOpt<T extends object> = T | undefined; + +export type IViewBasis<T extends object> = { + kv_init_prevent?: boolean; + lc_on_mount?: CallbackPromise; + lc_on_destroy?: CallbackPromise; +} & T; + +export type IViewBasisLoad<Tv extends object, Tl extends object> = IViewBasis<Tv> & { + lc_load: CallbackPromiseReturn<Tl | undefined>; +}; +\ No newline at end of file diff --git a/utils/src/app/types/component.ts b/utils/src/app/types/component.ts @@ -0,0 +1,247 @@ +import type { CallbackPromise, CallbackRoute, ElementCallbackMount, ElementCallbackSelect, ElementCallbackValue, ElementCallbackValueBlur, ElementCallbackValueKeydown, FormField, GeometryGlyphDimension, GeometryScreenPosition, GlyphKey, GlyphWeight, ICb, ICbG, ICbGOpt, ICbOpt, IClOpt, IDisabledOpt, IEntryWrap, IGl, IGlOpt, IIdGOpt, IIdOpt, ILabel, ILabelOpt, ILabelTup, ILoadingOpt, ILy, ILyOpt, INavigationRoutePreventRouteNav, LcGeocodeCallback, NavigationParamTuple } from "$root"; + +export type IToastKind = `simple`; + +export type IToast = IClOpt & + ILabel & IGlOpt & ILyOpt & { + styles?: IToastKind[]; + position?: GeometryScreenPosition; + }; + +export type IGlyph = ICbOpt & IIdOpt & ILyOpt & IClOpt & { + weight?: GlyphWeight; + key: GlyphKey; + dim?: GeometryGlyphDimension; +}; + +export type IInput<T extends string> = IIdGOpt<T> & IClOpt & ILyOpt & IDisabledOpt & { + placeholder?: string; + label?: string; + hidden?: boolean; + validate?: RegExp; + sync?: boolean; + field?: FormField; + field_constrain?: boolean; + callback?: ElementCallbackValue, + callback_keydown?: ElementCallbackValueKeydown<HTMLInputElement>, + callback_blur?: ElementCallbackValueBlur<HTMLInputElement>; + callback_focus?: ElementCallbackValueBlur<HTMLInputElement>; + callback_mount?: ElementCallbackMount<HTMLInputElement>; +}; + +export type IInputValue<T extends string> = Omit<IInput<T>, `id` | `sync`>; + +export type ISelectOption<T extends string> = IDisabledOpt & { + value: T; + label: string; +}; + +export type ISelect = IIdOpt & IClOpt & ILyOpt & { + callback?: ElementCallbackSelect; + sync?: boolean; + sync_init?: boolean; + options: { group?: string | true; entries: ISelectOption<string>[] }[]; + show_arrows?: 'l' | 'r'; +}; + +export type ITextArea = IIdOpt & IClOpt & ILyOpt & { + placeholder?: string; + label?: string; + hidden?: boolean; + validate?: RegExp; + sync?: true; + field?: FormField; + field_constrain?: boolean; + callback?: ElementCallbackValue, + callback_keydown?: ElementCallbackValueKeydown<HTMLTextAreaElement>, + callback_blur?: ElementCallbackValueBlur<HTMLTextAreaElement>; + callback_focus?: ElementCallbackValueBlur<HTMLTextAreaElement>; + callback_mount?: ElementCallbackMount<HTMLTextAreaElement>; +}; + +//export type IBasisOpt<T extends object> = T | undefined; + +export type INavigationRoute<T extends string> = { + route: T | [T, NavigationParamTuple[]]; +}; + +export type IPageToolbar<T extends string> = ICbOpt & { + header?: IPageHeader<T>; +}; + +export type IPageHeader<T extends string> = { + label: string; + callback_route?: CallbackRoute<T>; +}; + +export type IGlyphCircle = { + classes_wrap: string; + glyph: IGlyph +}; + +export type ITrellis = ILy & + IClOpt & + ITrellisStyles & { + id?: string; + view?: string; + title?: ITrellisTitle; + description?: ITrellisDescription; + default_el?: ITrellisDefault; + list?: (ITrellisKind | undefined)[]; + hide_offset?: true; + }; + +export type ITrellisStyles = { + hide_rounded?: boolean; + hide_border_top?: boolean; + hide_border_bottom?: boolean; + set_title_background?: boolean; + set_default_background?: boolean; +}; + +export type ITrellisTitle = ICbOpt & + IClOpt & { + mod?: ITrellisBasisOffsetMod, + value: string | true; + link?: ICbOpt & + IClOpt & + IGlOpt & ILabelOpt; + }; + +export type ITrellisDescription = string | true; + +export type ITrellisBasisOffsetModKey = 'sm' | 'glyph'; +export type ITrellisBasisOffsetMod = ITrellisBasisOffsetModKey | (({ glyph: IGlyph } | { glyph_circle: IGlyphCircle }) & { + loading?: boolean; +}); + +export type ITrellisDefault = { + labels?: ITrellisDefaultLabel[]; + show_title?: boolean; +}; + +export type ITrellisDefaultLabel = ICbOpt & { + label: string; + classes?: string; +}; + +export type ITrellisKind = ( + | ITrellisKindTouch + | ITrellisKindInput + | ITrellisKindSelect +); + +export type ITrellisBasis = { + loading?: boolean; + hide_active?: boolean; + hide_field?: boolean; + offset?: ITrellisBasisOffset; + full_rounded?: boolean; +}; + +export type ITrellisBasisOffset = ICbGOpt<MouseEvent> & + IClOpt & { + mod?: ITrellisBasisOffsetMod; + classes?: string; + hide_space?: boolean; + hide_offset?: boolean; + }; + +export type ITrellisKindDisplay = { + display?: ITrellisKindDisplayValue; +} +export type ITrellisKindDisplayValue = ICbGOpt<MouseEvent> & ILoadingOpt & + (ITrellisKindDisplayValueIcon | ILabel); + + +export type ITrellisKindDisplayValueIcon = { + icon: { + classes?: string; + key: GlyphKey; + }; +}; +export type ITrellisKindTouch = ITrellisBasis & { + touch: ITrellisBasisTouch; +}; + +export type ITrellisBasisTouch = ICbGOpt<MouseEvent> & + ILabelTup & ITrellisKindDisplay & { + end?: ITrellisBasisTouchEnd; + }; + +export type ITrellisKindInput = ITrellisBasis & { + input: ITrellisBasisInput; +}; + +export type ITrellisBasisInput = { + basis: IInput<string>; + line_label?: { + classes?: string; + value: string; + }; + action?: { + visible: boolean; + loading?: boolean; + callback?: CallbackPromise; + glyph?: IGlyph + }; +}; + +export type ITrellisKindSelect = ITrellisBasis & { + select: ITrellisBasisSelect; +}; + +export type ITrellisBasisSelect = ICbGOpt<MouseEvent> & + ILabelTup & ITrellisKindDisplay & ILoadingOpt & { + end?: ITrellisBasisTouchEnd; + el: ISelect & { value: string; }; + }; + +export type ITrellisBasisTouchEnd = ICbGOpt<MouseEvent> & IGl; + +export type INavBasisPrev = IClOpt & ICbG< + HTMLLabelElement | null +> & IGlOpt & ILabelOpt & IDisabledOpt & { + loading?: boolean; +}; +export type INavBasisOption = IClOpt & ICbG< + HTMLLabelElement | null +> & IGlOpt & ILabelOpt & IDisabledOpt & { + loading?: boolean; +}; +export type INavBasis<T extends string> = { + prev: ICbOpt & ILoadingOpt & INavigationRoute<T> & INavigationRoutePreventRouteNav & { + label?: string; + kind?: 'arrow' + }; + title?: ICbOpt & ILabel; + option?: INavBasisOption; +}; + +export type IEntrySelect = ILoadingOpt & { + wrap: IEntryWrap; + el: ISelect; + hide_arrows?: boolean; +}; + +export type IButtonSimple = ILyOpt & { + label: string; + callback: CallbackPromise; + allow_propogation?: boolean; +}; + +export type IMapMarkerArea = { + show_display?: boolean; + no_drag?: boolean; + lc_geocode: LcGeocodeCallback; +} + +export type ILayoutTrellisLine = ILabelOpt & + IClOpt & { + notify?: IClOpt & + ICb & + ILabelOpt & + IGlOpt & { + glyph_first?: boolean; + }; + }; diff --git a/utils/src/app/types/element.ts b/utils/src/app/types/element.ts @@ -0,0 +1,7 @@ +import type { CallbackPromiseGeneric } from "$root"; + +export type ElementCallbackValue = CallbackPromiseGeneric<{ value: string; pass: boolean; }>; +export type ElementCallbackValueKeydown<T extends HTMLElement> = CallbackPromiseGeneric<{ key: string; key_s: boolean; el: T }>; +export type ElementCallbackValueBlur<T extends HTMLElement> = CallbackPromiseGeneric<{ el: T }>; +export type ElementCallbackValueFocus<T extends HTMLElement> = CallbackPromiseGeneric<{ el: T }>; +export type ElementCallbackMount<T extends HTMLElement> = CallbackPromiseGeneric<{ el: T }>; diff --git a/utils/src/app/types/geometry.ts b/utils/src/app/types/geometry.ts @@ -0,0 +1,15 @@ +export type GeometryScreenPositionHorizontal = `left` | `center` | `right`; +export type GeometryScreenPositionVertical = `top` | `center` | `bottom`; +export type GeometryScreenPosition = `${GeometryScreenPositionVertical}-${GeometryScreenPositionHorizontal}`; +export type GeometryCardinalDirection = `up` | `down` | `left` | `right`; +export type GeometryDimension = + `xs` | + `sm` | + `md` | + `lg` | + `xl`; +export type GeometryGlyphDimension = + | `${GeometryDimension}` + | `${GeometryDimension}-` + | `${GeometryDimension}--` + | `${GeometryDimension}+`; +\ No newline at end of file diff --git a/utils/src/app/types/glyph.ts b/utils/src/app/types/glyph.ts @@ -0,0 +1,125 @@ +import type { GeometryCardinalDirection } from "$root"; + +export type GlyphKey = | + `crop` | + `map-trifold` | + `trash-simple` | + `backspace` | + `user-circle-check` | + `images-square` | + `bell` | + `columns` | + `bold` | + `article` | + `grid-four` | + `link-simple` | + `seal-check` | + `selection-foreground` | + `image-square` | + `image-broken` | + `funnel` | + `users-three` | + `note-blank` | + `user-circle-plus` | + `user-circle` | + `receipt` | + `invoice` | + `note` | + `arrow-left` | + `arrows-down-up` | + `basket` | + `arrow-right` | + `upload-simple` | + `printer` | + `download-simple` | + `list` | + `asterisk` | + `asterisk-simple` | + `subtitles-slash` | + `cardholder` | + `globe-x` | + `exclamation-mark` | + `network-x` | + `x-circle` | + `address-book-tabs` | + `paper-plane-tilt` | + `note-pencil` | + `share-fat` | + `folder` | + `trash` | + `plus-circle` | + `currency-${GlyphKeyCurrency}` | + `arrow-down` | + `caret-circle-down` | + `caret-circle-up` | + `shopping-bag-open` | + `coffee-bean` | + `compass` | + `map-pin-simple` | + `handbag-simple` | + `devices` | + `lock-key` | + `gear` | + `gear-fine` | + `bell-simple` | + `envelope` | + `house-line` | + `arrows-left-right` | + `list-plus` | + `squares-four` | + `list-plus` | + `app-window` | + `circle-notch` | + `subtract-square` | + `device-tablet-speaker` | + `weather-cloud` | + `warning` | + `circle-notch` | + `minus` | + `key` | + `arrow-u-up-left` | + `arrow-counter-clockwise` | + `circle` | + `check-circle` | + `circle-dashed` | + `dots-three` | + `cards-three` | + `lightning` | + `cards` | + `note-pencil` | + `tray` | + `calendar-dots` | + `notepad` | + `network` | + `calendar-blank` | + `chats-circle` | + `plant` | + `farm` | + `magnifying-glass` | + `chat-circle-dots` | + `dots-three-outline` | + `copy` | + `circles-four` | + `waveform` | + `film-strip` | + `arrow-up` | + `arrow-circle-up` | + `plus` | + `funnel-simple` | + `user` | + `camera` | + `check` | + `file` | + `share-network` | + `question` | + `minus-circle` | + `globe-simple` | + `globe` | + `warning-circle` | + `x` | + `info` | + `caret-${GeometryCardinalDirection}` | + `caret-up-down`; + +export type GlyphKeyCurrency = `dollar` | `eur`; +export type GlyphWeight = `bold` | `fill` diff --git a/utils/src/app/types/interface.ts b/utils/src/app/types/interface.ts @@ -0,0 +1,214 @@ +import type { CallbackPromise, CallbackPromiseGeneric, EntryStyle, GlyphKey, IGlyph, IInput, IInputValue, ITextArea, LayerGlyphBasisKind, LoadingBlades, LoadingDimension, SvelteTransitionConfig, ThemeLayer } from "$root"; + +export type IDisabled = { + disabled: boolean | never; +}; + +export type IDisabledOpt = Partial<IDisabled>; + +export type IBasis<T> = { + basis: T; +}; + +export type ICb = { + callback: CallbackPromise | never; +}; + +export type ICbOpt = Partial<ICb>; + +export type ICbG<T> = { + callback: CallbackPromiseGeneric<T> | never; +}; + +export type ICbGOpt<T> = Partial<ICbG<T>>; + +export type ICl = { + classes: string | never; +}; + +export type IClOpt = Partial<ICl>; + +export type IClWrap = { + classes_wr: string | never; +}; + +export type IClOptWrap = Partial<IClWrap>; + +export type IId = { + id: string | never; +}; + +export type IIdOpt = Partial<IId>; + +export type IGl = { + glyph: IGlyph | never; +}; + +export type IGlOpt = Partial<IGl>; + +export type IGlyphKey = { + glyph: GlyphKey +}; + +export type ILy = { + layer: ThemeLayer | never; +}; + +export type ILyOpt = Partial<ILy>; + +export type ILableFieldsSwap = { + toggle: boolean; + on: IClOpt & { + value: string; + }, + off: IClOpt & { + value: string; + }, +}; + +export type ILabelSwap = { + swap: ILableFieldsSwap; +} + +export type ILabelTupFields = { + left?: ILableFields[]; + right?: ILableFields[]; +}; + +export type ILabelTup = { + label: ILabelTupFields; +}; + +export type LabelFieldKind = `link` | `on` | `shade`; + +export type ILableFields = & { + classes_wrap?: string + classes?: string; + kind?: LayerGlyphBasisKind; + hide_truncate?: boolean; + hide_active?: boolean; +} & ( + ({ + value: string; + } | ILabelSwap) + | IGl + ); + +export type ILabel = { + label: ILableFields; +}; + +export type ILabelOpt = Partial<ILabel>; + +export type ILoadSymbol = IClOpt & { + color?: 'white'; + blades?: LoadingBlades; + dim?: LoadingDimension; +}; + +export type IIdG<T extends string> = { + id: T | never; +}; + +export type FormField = { + validate: RegExp; + charset: RegExp; +}; + +export type IIdGOpt<T extends string> = Partial<IIdG<T>>; + +export type IIdWrap = { + id_wrap: string | never; +}; + +export type IIdWrapOpt = Partial<IIdWrap>; + +export type ILabelValue = { + label: IClOpt & { + value: string; + }; +}; + +export type ILabelDisplay = IIdWrapOpt & IClOpt & ILabelValue & ILyOpt & { + style?: EntryStyle; +}; + + +export type ILoading = { + loading: boolean | never; +}; + +export type ILoadingOpt = Partial<ILoading>; + +export type IEntryWrap = IClOpt & IIdOpt & ILyOpt & { + style?: EntryStyle; + style_a?: true; + no_pad?: true; + fade?: { + in?: SvelteTransitionConfig; + out?: SvelteTransitionConfig; + }; +} + +export type IEntryLine = ILoadingOpt & { + wrap?: IEntryWrap; + el: IInputValue<string>; + notify_inline?: { + glyph: GlyphKey | IGlyph; + }; +}; + +export type IEntryLineIdb = ILoadingOpt & { + wrap?: IEntryWrap; + el: IInput<string>; + notify_inline?: { + glyph: GlyphKey | IGlyph; + }; +}; + +export type IEntryMultiLine = { + wrap?: IEntryWrap; + el: ITextArea; + notify_inline?: { + glyph: GlyphKey | IGlyph; + }; +} + +export type IEnvelopeLower = { + visible: boolean; + close: CallbackPromise; + full_cover?: boolean; + label_close?: string | true; +}; + +export type IButtonRound = IClOpt & ILoadingOpt & { + label: string; + callback: CallbackPromise; +}; + +export type INavigationRoutePreventRouteNav = { + prevent_route?: { + callback: CallbackPromise; + }; +}; + +export type INavigationRoutePreventRoute = { + prevent_route: CallbackPromise; +}; + +export type IImageBlob = IIdOpt & { + data: Uint8Array | undefined; + alt?: string; +}; + +export type IImagePath = IClOpt & + ICbGOpt< + MouseEvent & { + currentTarget: EventTarget & HTMLImageElement; + } + > & + IIdOpt & { + path?: string; + alt?: string; + }; + diff --git a/utils/src/app/types/resolve.ts b/utils/src/app/types/resolve.ts @@ -0,0 +1,29 @@ +export type ResolveEnumArea_Unit = 'ac' | 'ft2' | 'ha' | 'm2'; +export type ResolveEnumBudget_Item_Type = 'capital_investment' | 'equipment' | 'fees' | 'infrastructure' | 'insurance' | 'labor' | 'materials' | 'other' | 'supplies'; +export type ResolveEnumBudget_Spending_Type = 'equipment' | 'labor' | 'maintenance' | 'other' | 'supplies' | 'utilities'; +export type ResolveEnumCredential = 'email' | 'phone'; +export type ResolveEnumPayment_Method = 'cash'; +export type ResolveEnumPayment_Period = 'biweekly' | 'hourly' | 'monthly' | 'weekly'; +export type ResolveEnumPayment_Status = 'confirmed' | 'pending'; +export type ResolveEnumQuantity_Unit = 'g' | 'kg' | 'lb' | 'ton'; +export type ResolveEnumRole = 'admin' | 'guest' | 'internal' | 'member'; +export type ResolveEnumWorker_Type = 'contractor' | 'laborer'; +export type ResolveAccountInfo = { id: string, created_at: string, updated_at: string, role: ResolveEnumRole, auth_ref: { credential: ResolveEnumCredential, email: { id: string, created_at: string, updated_at: string, address: string } }, profiles?: Array<{ id: string, created_at: string, updated_at: string, name: string, display_name?: string | null, primary: boolean, about?: string | null, emails: Array<{ id: string, created_at: string, updated_at: string, address: string }>, profile_photos?: Array<{ id: string, created_at: string, updated_at: string, primary: boolean, title?: string | null, description?: string | null, media_image: { id: string, created_at: string, updated_at: string, url: string } }> | null, nostr_keys: Array<{ id: string, created_at: string, updated_at: string, public_key: string }> }> | null, farms?: Array<{ id: string, created_at: string, updated_at: string, name: string, area?: number | null, area_unit: ResolveEnumArea_Unit, geolocation: { id: string, created_at: string, updated_at: string, point: { type: string, coordinates: Array<number> }, polygon?: { type: string, coordinates: Array<Array<Array<number>>> } | null, address: { id: string, created_at: string, updated_at: string, primary: string, admin: string, country: string } }, farm_products?: Array<{ id: string, created_at: string, updated_at: string, name: string, farm_lot_products?: Array<{ id: string }> | null }> | null, farm_lots?: Array<{ id: string, created_at: string, updated_at: string, name?: string | null, area?: number | null, area_unit: ResolveEnumArea_Unit, geolocation: { id: string, created_at: string, updated_at: string, point: { type: string, coordinates: Array<number> }, polygon?: { type: string, coordinates: Array<Array<Array<number>>> } | null, address: { id: string, created_at: string, updated_at: string, primary: string, admin: string, country: string } }, farm_lot_products?: Array<{ id: string, created_at: string, updated_at: string, area_planted?: number | null, area_unit: ResolveEnumArea_Unit, date_planted?: string | null, days_to_maturity?: number | null, farm_product: { id: string, created_at: string, updated_at: string, name: string, farm_lot_products?: Array<{ id: string }> | null }, farm_trade_products?: Array<{ id: string, created_at: string, updated_at: string, title: string, description: string, process?: string | null, trade_product_prices?: Array<{ id: string, created_at: string, updated_at: string, amount: number, currency: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }> | null, trade_product_quantitys?: Array<{ id: string, created_at: string, updated_at: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }> | null }> | null, farm_lot_harvests?: Array<{ id: string, created_at: string, updated_at: string, quantity_unit: ResolveEnumQuantity_Unit, quantity_harvested: number }> | null }> | null }> | null }> | null }; +export type ResolveAuthRefInfo = { credential: ResolveEnumCredential, email: { id: string, created_at: string, updated_at: string, address: string } }; +export type ResolveEmailInfo = { id: string, created_at: string, updated_at: string, address: string }; +export type ResolveProfileInfo = { id: string, created_at: string, updated_at: string, name: string, display_name?: string | null, primary: boolean, about?: string | null, emails: Array<{ id: string, created_at: string, updated_at: string, address: string }>, profile_photos?: Array<{ id: string, created_at: string, updated_at: string, primary: boolean, title?: string | null, description?: string | null, media_image: { id: string, created_at: string, updated_at: string, url: string } }> | null, nostr_keys: Array<{ id: string, created_at: string, updated_at: string, public_key: string }> }; +export type ResolveProfilePhotoInfo = { id: string, created_at: string, updated_at: string, primary: boolean, title?: string | null, description?: string | null, media_image: { id: string, created_at: string, updated_at: string, url: string } }; +export type ResolveMediaImageInfo = { id: string, created_at: string, updated_at: string, url: string }; +export type ResolveNostrKeyInfo = { id: string, created_at: string, updated_at: string, public_key: string }; +export type ResolveFarmInfo = { id: string, created_at: string, updated_at: string, name: string, area?: number | null, area_unit: ResolveEnumArea_Unit, geolocation: { id: string, created_at: string, updated_at: string, point: { type: string, coordinates: Array<number> }, polygon?: { type: string, coordinates: Array<Array<Array<number>>> } | null, address: { id: string, created_at: string, updated_at: string, primary: string, admin: string, country: string } }, farm_products?: Array<{ id: string, created_at: string, updated_at: string, name: string, farm_lot_products?: Array<{ id: string }> | null }> | null, farm_lots?: Array<{ id: string, created_at: string, updated_at: string, name?: string | null, area?: number | null, area_unit: ResolveEnumArea_Unit, geolocation: { id: string, created_at: string, updated_at: string, point: { type: string, coordinates: Array<number> }, polygon?: { type: string, coordinates: Array<Array<Array<number>>> } | null, address: { id: string, created_at: string, updated_at: string, primary: string, admin: string, country: string } }, farm_lot_products?: Array<{ id: string, created_at: string, updated_at: string, area_planted?: number | null, area_unit: ResolveEnumArea_Unit, date_planted?: string | null, days_to_maturity?: number | null, farm_product: { id: string, created_at: string, updated_at: string, name: string, farm_lot_products?: Array<{ id: string }> | null }, farm_trade_products?: Array<{ id: string, created_at: string, updated_at: string, title: string, description: string, process?: string | null, trade_product_prices?: Array<{ id: string, created_at: string, updated_at: string, amount: number, currency: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }> | null, trade_product_quantitys?: Array<{ id: string, created_at: string, updated_at: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }> | null }> | null, farm_lot_harvests?: Array<{ id: string, created_at: string, updated_at: string, quantity_unit: ResolveEnumQuantity_Unit, quantity_harvested: number }> | null }> | null }> | null }; +export type ResolveGeolocationInfo = { id: string, created_at: string, updated_at: string, point: { type: string, coordinates: Array<number> }, polygon?: { type: string, coordinates: Array<Array<Array<number>>> } | null, address: { id: string, created_at: string, updated_at: string, primary: string, admin: string, country: string } }; +export type ResolveAddressInfo = { id: string, created_at: string, updated_at: string, primary: string, admin: string, country: string }; +export type ResolveFarmLotInfo = { id: string, created_at: string, updated_at: string, name?: string | null, area?: number | null, area_unit: ResolveEnumArea_Unit, geolocation: { id: string, created_at: string, updated_at: string, point: { type: string, coordinates: Array<number> }, polygon?: { type: string, coordinates: Array<Array<Array<number>>> } | null, address: { id: string, created_at: string, updated_at: string, primary: string, admin: string, country: string } }, farm_lot_products?: Array<{ id: string, created_at: string, updated_at: string, area_planted?: number | null, area_unit: ResolveEnumArea_Unit, date_planted?: string | null, days_to_maturity?: number | null, farm_product: { id: string, created_at: string, updated_at: string, name: string, farm_lot_products?: Array<{ id: string }> | null }, farm_trade_products?: Array<{ id: string, created_at: string, updated_at: string, title: string, description: string, process?: string | null, trade_product_prices?: Array<{ id: string, created_at: string, updated_at: string, amount: number, currency: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }> | null, trade_product_quantitys?: Array<{ id: string, created_at: string, updated_at: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }> | null }> | null, farm_lot_harvests?: Array<{ id: string, created_at: string, updated_at: string, quantity_unit: ResolveEnumQuantity_Unit, quantity_harvested: number }> | null }> | null }; +export type ResolveFarmLotProductInfo = { id: string, created_at: string, updated_at: string, area_planted?: number | null, area_unit: ResolveEnumArea_Unit, date_planted?: string | null, days_to_maturity?: number | null, farm_product: { id: string, created_at: string, updated_at: string, name: string, farm_lot_products?: Array<{ id: string }> | null }, farm_trade_products?: Array<{ id: string, created_at: string, updated_at: string, title: string, description: string, process?: string | null, trade_product_prices?: Array<{ id: string, created_at: string, updated_at: string, amount: number, currency: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }> | null, trade_product_quantitys?: Array<{ id: string, created_at: string, updated_at: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }> | null }> | null, farm_lot_harvests?: Array<{ id: string, created_at: string, updated_at: string, quantity_unit: ResolveEnumQuantity_Unit, quantity_harvested: number }> | null }; +export type ResolveFarmProductInfo = { id: string, created_at: string, updated_at: string, name: string, farm_lot_products?: Array<{ id: string }> | null }; +export type ResolveFarmTradeProductInfo = { id: string, created_at: string, updated_at: string, title: string, description: string, process?: string | null, trade_product_prices?: Array<{ id: string, created_at: string, updated_at: string, amount: number, currency: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }> | null, trade_product_quantitys?: Array<{ id: string, created_at: string, updated_at: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }> | null }; +export type ResolveTradeProductPriceInfo = { id: string, created_at: string, updated_at: string, amount: number, currency: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }; +export type ResolveTradeProductQuantityInfo = { id: string, created_at: string, updated_at: string, quantity_amount?: number | null, quantity_unit: ResolveEnumQuantity_Unit, quantity_label?: string | null }; +export type ResolveFarmLotHarvestInfo = { id: string, created_at: string, updated_at: string, quantity_unit: ResolveEnumQuantity_Unit, quantity_harvested: number }; +export type ResolveGeometryPoint = { type: string, coordinates: Array<number> }; +export type ResolveGeometryPolygon = { type: string, coordinates: Array<Array<Array<number>>> }; diff --git a/utils/src/app/types/view.ts b/utils/src/app/types/view.ts @@ -0,0 +1,33 @@ +import { GeolocationAddress, GeolocationPoint, ResolveGeolocationInfo, ResolveProfileInfo } from "$root"; + +export type IViewSearchData = { + geolocations: ResolveGeolocationInfo[]; + profiles: ResolveProfileInfo[]; + nostr_relay: { id: string }[]; + farm_products: { id: string }[]; +}; + +export type IViewFarmsProductsAddSubmission = { + 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; + geolocation_address: GeolocationAddress; +}; + +export type IViewFarmsAddSubmission = { + farm_name: string; + farm_area: number; + farm_area_unit: string; + farm_contact_name: string; + geolocation_point: GeolocationPoint; + geolocation_address: GeolocationAddress; +}; + diff --git a/utils/src/app/util.ts b/utils/src/app/util.ts @@ -0,0 +1,95 @@ +import type { AppLayoutKey, AppLayoutKeyIOS, AppLayoutKeyWeb, LabelFieldKind, NavigationParamTuple, ThemeLayer } from "$root"; + +export const fmt_cl = (classes?: string): string => { + return classes ? classes : ``; +}; + +export const get_layout = (val: string | false): AppLayoutKey => { + switch (val) { + case `ios0`: + case `ios1`: + case `webm0`: + case `webm1`: + return val; + default: + return `ios0`; + }; +}; + +export const get_ios_layout = (val: string | false): AppLayoutKeyIOS => { + switch (val) { + case `ios0`: + case `ios1`: + return val; + default: + return `ios0`; + }; +}; + +export const get_web_layout = (val: string | false): AppLayoutKeyWeb => { + switch (val) { + case `webm0`: + case `webm1`: + return val; + default: + return `webm0`; + }; +}; + +export const parse_layer = (layer?: number, layer_default?: ThemeLayer): ThemeLayer => { + switch (layer) { + case 0: + case 1: + case 2: + return layer; + default: + return layer_default ? layer_default : 0; + }; +}; + +export const value_constrain = (regex_charset: RegExp, value: string): string => { + return value + .split(``) + .filter((char) => regex_charset.test(char)) + .join(``); +}; + +export const value_constrain_textarea = (regex_charset: RegExp, value: string): string => { + return value + .replace(/\u00A0/g, ` `) + .split(/[\n]/) + .map(line => line + .split(``) + .filter((char) => regex_charset.test(char)) + .join(``) + ) + .join("\n"); +}; + +export const encode_qp = (params_list?: NavigationParamTuple[]): string => { + const params = (params_list || []).filter(i => i[0] && i[1]) + if (!params.length) return ``; + return params.map(([k, v], index) => `${index === 0 ? `?` : ``}&${k.trim()}=${encodeURI(v.trim())}`).join(``).trim(); +}; + +export const encode_qp_route = <T extends string>(route: T, params_list?: NavigationParamTuple[]): string => { + return `${route}/${encode_qp(params_list)}`.replaceAll(`//`, `/`) +}; + +export const fmt_trellis = (hide_border_t: boolean, hide_border_b: boolean): string => { + return `${hide_border_t ? `group-first:border-t-0` : `group-first:border-t-line`} ${hide_border_b ? `group-last:border-b-0` : `group-last:border-b-line`}`; +}; + +export const get_label_classes_kind = (layer: ThemeLayer, label_kind: LabelFieldKind | undefined, hide_active: boolean): string => { + return `text-layer-${layer}-glyph${label_kind ? `-${label_kind}` : ``} ${hide_active ? `` : `group-active:text-layer-${layer}-glyph${label_kind ? `-${label_kind}_a` : `_a`}`}` +}; + +export const fmt_textarea_value = (value: string): string => { + return value.replace(/ /g, `\u00A0`); +}; + +export const list_assign = (list_curr: string[], list_new: string[]): string[] => { + return Array.from( + new Set([...list_curr, ...list_new]), + ).filter((i) => !!i); +}; +\ No newline at end of file diff --git a/utils/src/app/util/resolve-enum.ts b/utils/src/app/util/resolve-enum.ts @@ -0,0 +1,165 @@ +export type ResolveEnumAreaUnitKey = `Ac` | `Ft2` | `Ha` | `M2`; + +export const parse_enum_area_unit_key = (val: string): ResolveEnumAreaUnitKey | undefined => { + switch (val) { + case `ac`: + return `Ac`; + case `ft2`: + return `Ft2`; + case `ha`: + return `Ha`; + case `m2`: + return `M2`; + default: + return undefined; + } +}; + +export type ResolveEnumBudgetItemTypeKey = `CapitalInvestment` | `Equipment` | `Fees` | `Infrastructure` | `Insurance` | `Labor` | `Materials` | `Other` | `Supplies`; + +export const parse_enum_budget_item_type_key = (val: string): ResolveEnumBudgetItemTypeKey | undefined => { + switch (val) { + case `capitalinvestment`: + return `CapitalInvestment`; + case `equipment`: + return `Equipment`; + case `fees`: + return `Fees`; + case `infrastructure`: + return `Infrastructure`; + case `insurance`: + return `Insurance`; + case `labor`: + return `Labor`; + case `materials`: + return `Materials`; + case `other`: + return `Other`; + case `supplies`: + return `Supplies`; + default: + return undefined; + } +}; + +export type ResolveEnumBudgetSpendingTypeKey = `Equipment` | `Labor` | `Maintenance` | `Other` | `Supplies` | `Utilities`; + +export const parse_enum_budget_spending_type_key = (val: string): ResolveEnumBudgetSpendingTypeKey | undefined => { + switch (val) { + case `equipment`: + return `Equipment`; + case `labor`: + return `Labor`; + case `maintenance`: + return `Maintenance`; + case `other`: + return `Other`; + case `supplies`: + return `Supplies`; + case `utilities`: + return `Utilities`; + default: + return undefined; + } +}; + +export type ResolveEnumCredentialKey = `Email` | `Phone`; + +export const parse_enum_credential_key = (val: string): ResolveEnumCredentialKey | undefined => { + switch (val) { + case `email`: + return `Email`; + case `phone`: + return `Phone`; + default: + return undefined; + } +}; + +export type ResolveEnumPaymentMethodKey = `Cash`; + +export const parse_enum_payment_method_key = (val: string): ResolveEnumPaymentMethodKey | undefined => { + switch (val) { + case `cash`: + return `Cash`; + default: + return undefined; + } +}; + +export type ResolveEnumPaymentPeriodKey = `Biweekly` | `Hourly` | `Monthly` | `Weekly`; + +export const parse_enum_payment_period_key = (val: string): ResolveEnumPaymentPeriodKey | undefined => { + switch (val) { + case `biweekly`: + return `Biweekly`; + case `hourly`: + return `Hourly`; + case `monthly`: + return `Monthly`; + case `weekly`: + return `Weekly`; + default: + return undefined; + } +}; + +export type ResolveEnumPaymentStatusKey = `Confirmed` | `Pending`; + +export const parse_enum_payment_status_key = (val: string): ResolveEnumPaymentStatusKey | undefined => { + switch (val) { + case `confirmed`: + return `Confirmed`; + case `pending`: + return `Pending`; + default: + return undefined; + } +}; + +export type ResolveEnumQuantityUnitKey = `G` | `Kg` | `Lb` | `Ton`; + +export const parse_enum_quantity_unit_key = (val: string): ResolveEnumQuantityUnitKey | undefined => { + switch (val) { + case `g`: + return `G`; + case `kg`: + return `Kg`; + case `lb`: + return `Lb`; + case `ton`: + return `Ton`; + default: + return undefined; + } +}; + +export type ResolveEnumRoleKey = `Admin` | `Guest` | `Internal` | `Member`; + +export const parse_enum_role_key = (val: string): ResolveEnumRoleKey | undefined => { + switch (val) { + case `admin`: + return `Admin`; + case `guest`: + return `Guest`; + case `internal`: + return `Internal`; + case `member`: + return `Member`; + default: + return undefined; + } +}; + +export type ResolveEnumWorkerTypeKey = `Contractor` | `Laborer`; + +export const parse_enum_worker_type_key = (val: string): ResolveEnumWorkerTypeKey | undefined => { + switch (val) { + case `contractor`: + return `Contractor`; + case `laborer`: + return `Laborer`; + default: + return undefined; + } +}; +\ No newline at end of file diff --git a/utils/src/app/util/resolve.ts b/utils/src/app/util/resolve.ts @@ -0,0 +1,5 @@ +import { ResolveAddressInfo } from "../types/resolve"; + +export const lib_address_fmt = (addr: ResolveAddressInfo): string => { + return `${addr.primary}, ${addr.admin}, ${addr.country}` +}; +\ No newline at end of file diff --git a/utils/src/app/util/search.ts b/utils/src/app/util/search.ts @@ -0,0 +1,72 @@ +export type SearchServiceResult = Record<string, any> & { id: string, result_k: string; result_v: string; }; +export type SearchServiceFlattenedData = Record<string, any> & { list: string; }; + +export type ISearchService = { + search(input: string): SearchServiceResult[] +}; + +export class SearchService implements ISearchService { + private _flattened_data: SearchServiceFlattenedData[] = []; + private _index_map: Map<string, Record<string, any>[]> = new Map(); + + constructor(data: Record<string, any[]>) { + this.flatten_data(data); + this.index_data(); + } + + private get flattened_data() { + return this._flattened_data; + } + + private flatten_data(data: Record<string, any[]>) { + Object.keys(data).forEach((list_group) => { + const list = data[list_group]; + list.forEach((item) => { + const flattened_item: SearchServiceFlattenedData = { ...item, group: list_group }; + this._flattened_data.push(flattened_item); + }); + }); + } + + private index_data(): void { + this.flattened_data.forEach((item) => { + Object.keys(item).forEach((key) => { + const key_lower = key.toLowerCase(); + const value = item[key]; + if (value != null) { + if (!this._index_map.has(key_lower)) this._index_map.set(key_lower, []); + this._index_map.get(key_lower)?.push(item); + } + }); + }); + } + + public search(query: string): SearchServiceResult[] { + if (!query) return []; + const search_query = query.toLowerCase().trim(); + let results: SearchServiceResult[] = []; + const results_seen = new Set<string>(); + this._index_map.forEach((items) => { + items.forEach((item) => { + for (const [key, value] of Object.entries(item)) { + if (key === `id` || key === `created_at` || key === `updated_at` || key === `public_key` || key === `group`) continue; + if (value && value.toString().replace(/[()_-]/gi, ` `).toLowerCase().includes(search_query)) { + const { group, ...rest } = item; + if (!(`id` in item)) continue; + const result_key = item.id; + if (results_seen.has(result_key)) continue; + results_seen.add(result_key); + const reshaped_result: SearchServiceResult = { + id: item.id, + result_k: key, + result_v: value, + [group]: { ...rest }, + }; + results.push(reshaped_result); + } + }; + }); + }); + return results; + } +} diff --git a/utils/src/app/validation/view.ts b/utils/src/app/validation/view.ts @@ -0,0 +1,27 @@ +import { form_fields, IViewFarmsAddSubmission, IViewFarmsProductsAddSubmission, util_rxp, vs_geolocation_address, vs_geolocation_point, zod_numf_pos, zod_numf_price, zod_numi_pos } from "$root"; +import { _env } from "src/_env"; +import { z } from "zod"; + +export const vs_view_farms_products_add_submission: z.ZodSchema<IViewFarmsProductsAddSubmission> = 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: zod_numf_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(_env.PROD ? util_rxp.url_image_upload : util_rxp.url_image_upload_dev)), + quantity_amount: zod_numi_pos, + quantity_unit: z.string().regex(form_fields.quantity_unit.validate), + quantity_label: z.string().regex(form_fields.quantity_label.validate), + geolocation_point: vs_geolocation_point, + geolocation_address: vs_geolocation_address, +}); + +export const vs_view_farms_add_submission: z.ZodSchema<IViewFarmsAddSubmission> = z.object({ + farm_name: z.string().regex(form_fields.farm_name.validate), + farm_area: zod_numf_pos, + farm_area_unit: z.string().regex(form_fields.area_unit.validate), + farm_contact_name: z.string().regex(form_fields.contact_name.validate), + geolocation_point: vs_geolocation_point, + geolocation_address: vs_geolocation_address, +}); diff --git a/utils/src/ascii.ts b/utils/src/ascii.ts @@ -1,2 +0,0 @@ -export const root_symbol = "»-`--,"; -export const root_symbol_full = "»--`--,---"; -\ No newline at end of file diff --git a/utils/src/client/geo.ts b/utils/src/client/geo.ts @@ -0,0 +1,11 @@ +import type { ErrorMessage, IClientGeolocationPosition } from "$root"; + +export type IGeolocationErrorMessage = + | `error.client.geolocation.permission_denied` + | `error.client.geolocation.location_unavailable` + | `error.client.geolocation.timeout` + | `*`; + +export type IClientGeolocation = { + current(): Promise<IClientGeolocationPosition | ErrorMessage<IGeolocationErrorMessage>>; +}; +\ No newline at end of file diff --git a/utils/src/client/geolocation.ts b/utils/src/client/geolocation.ts @@ -1,15 +0,0 @@ -import { type ErrorMessage } from ".."; - -export type IGeolocationErrorMessage = `*-permissions` | `*`; - -export type IClientGeolocationPosition = { - lat: number; - lng: number; - accuracy?: number; - altitude?: number; - altitude_accuracy?: number; -}; - -export type IClientGeolocation = { - current(): Promise<IClientGeolocationPosition | ErrorMessage<IGeolocationErrorMessage>>; -}; -\ No newline at end of file diff --git a/utils/src/client/gui.ts b/utils/src/client/gui.ts @@ -1,12 +1,10 @@ -import { type ResultsList } from ".."; - export type IClientGuiDialogKind = "info" | "warning" | "error"; export type IClientGuiDialogAlertOpts = string; export type IClientGuiDialogConfirmOpts = string | { title?: string, kind?: IClientGuiDialogKind; message: string; cancel?: string; ok?: string; }; -export type IClientGuiDialogResolve = ResultsList<string>; +export type IClientGuiDialogResolve = { results: string[]; }; export type IClientGuiNotifyPermission = "default" | "denied" | "granted"; export type IClientGuiNotifySendOptions = { diff --git a/utils/src/config.ts b/utils/src/config.ts @@ -0,0 +1,19 @@ +export const cfg_map = { + styles: { + base: { + light: `https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json`, + dark: `https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json` + } + }, + popup: { + dot: { + offset: [0, -10] as [number, number] + } + }, + coords: { + default: { + lat: 0, + lng: 0, + } + } +}; +\ No newline at end of file diff --git a/utils/src/currency.ts b/utils/src/currency.ts @@ -1,162 +1,43 @@ -import { regex } from "./regex"; +import { util_rxp } from "$root"; export type FiatCurrency = `usd` | `eur`; export const fiat_currencies: FiatCurrency[] = [`usd`, `eur`] as const; -export type FiatCurrencyGlyphs = `dollar` | `eur`; - -export type CurrencyPriceFmt = [string, FiatCurrency, number, number] - -export function parse_currency(val?: string): FiatCurrency { - const _val = val?.trim().toLowerCase() - switch (_val) { +export const parse_currency = (val?: string): FiatCurrency => { + const cur = val?.trim().toLowerCase() + switch (cur) { case `usd`: case `eur`: - return _val; + return cur; default: return `usd`; }; }; -export function parse_currency_glyph_key(val?: string): | `currency-${FiatCurrencyGlyphs}` { - switch (val) { - case "usd": - return `currency-dollar`; - case "eur": - return `currency-eur`; - default: - return `currency-dollar`; - }; -}; - -export type CurrencyDecimalSeparator = `,` | `.`; - -export type CurrencyMetadata = { - cur: FiatCurrency; - /** - * currency symbol - */ - cur_s: string; - /** - * currency marker - */ - cur_m: string; - /** - * true if symbol is at the start - */ - cur_pos: boolean; - dec_s: CurrencyDecimalSeparator; -} - - -export type CurrencyPrice = CurrencyMetadata & { - /** - * integer num - */ - num_i: number; - /** - * fractional num - */ - num_f: number; - /** - * integer value - */ - val_i: string; - /** - * fractional value - */ - val_f: string; -}; - -export const locale_fractional_decimal = (locale: string): CurrencyDecimalSeparator => { - const formatter = new Intl.NumberFormat(locale); - const formatted = formatter.format(1.1); - return formatted.includes(',') ? `,` : `.`; +export const fmt_price = (locale: string, value: string, currency: string): string => { + const fmt = new Intl.NumberFormat(locale, { + style: 'currency', + currency: currency.toUpperCase(), + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + return fmt.format(parseFloat(value)); }; export const parse_currency_marker = (locale: string, currency: string): string => { const cur = parse_currency(currency); - const fmt = new Intl.NumberFormat(`en-US`, { + const fmt = new Intl.NumberFormat(locale, { style: 'currency', currency: cur.toUpperCase(), minimumFractionDigits: 2, }); const fmt_basis = fmt.format(1); let fmt_res: string | undefined = undefined; - fmt_res = fmt_basis.match(regex.currency_marker)?.[0]; + fmt_res = fmt_basis.match(util_rxp.currency_marker)?.[0]; if (fmt_res) return fmt_res; - fmt_res = fmt_basis.match(regex.currency_symbol)?.[0]; + fmt_res = fmt_basis.match(util_rxp.currency_symbol)?.[0]; if (fmt_res) return fmt_res; fmt_res = fmt_basis.match(new RegExp(cur, `i`))?.[0]; if (fmt_res) return fmt_res; return cur.toUpperCase(); -} - -export const parse_currency_price = (locale: string, currency: string, price: number | string): CurrencyPrice | undefined => { - const num_amt = Math.max(typeof price === `number` ? price : Number(price.replace(/[^0-9.]/g, ``)), 0); - const cur = parse_currency(currency); - const fmt = new Intl.NumberFormat(locale, { - style: 'currency', - currency: cur.toUpperCase(), - minimumFractionDigits: 2, - }); - const fmt_amt = fmt.format(num_amt); - const fmt_num = fmt_amt.replace(/[^0-9.,\s\u200F\u00A0]+/g, ``).trim() - const cur_s = fmt_amt.match(regex.currency_symbol)?.[0] || fmt_amt.match(new RegExp(cur, `i`))?.[0]; - if (!cur_s) return undefined; - const cur_m = fmt_amt.match(regex.currency_marker)?.[0] || cur_s; - const cur_pos = fmt_amt.startsWith(cur_m); - const dec_s = locale_fractional_decimal(locale); - const [_val_i, _val_f] = fmt_num.split(dec_s); - const val_i = _val_i?.replace(regex.nbsp_rp, ``).replace(regex.rtlm_rp, ``); - const val_f = _val_f?.replace(regex.nbsp_rp, ``).replace(regex.rtlm_rp, ``); - if (!val_i || !val_f) return undefined; - const num_i = Math.max(Math.min(parseInt(val_i?.replace(dec_s === `,` ? regex.periods : regex.commas, ``) || `0`), 0)); - const num_f = Math.max(Math.min(parseInt(val_f || `0`), 0)); - - return { - cur, - cur_s, - cur_m, - cur_pos, - dec_s, - num_i, - num_f, - val_i, - val_f, - }; -}; - -export const fmt_currency_price = (currency_price: CurrencyPrice, hide_currency_marker?: boolean): string => { - const cur_val = hide_currency_marker ? currency_price.cur_s : currency_price.cur_m; - return `${currency_price.cur_pos ? `${cur_val} ` : ``}${currency_price.val_i}${currency_price.dec_s}${currency_price.val_f}${currency_price.cur_pos ? `` : ` ${cur_val}`}` -}; - -export const sum_currency_price = (currency_price: CurrencyPrice): number => { - return currency_price.num_i + (currency_price.num_f / 100); -}; - -export const parse_currency_price_fmt = (locale: string, _currency: string, amount: number): CurrencyPriceFmt | undefined => { - const currency = parse_currency(_currency); - const fmt = new Intl.NumberFormat(locale, { - style: 'currency', - currency: currency.toUpperCase(), - minimumFractionDigits: 2, - }); - const fmt_amt = fmt.format(amount); - const [symbol_val_i, val_f] = fmt_amt.split('.'); - if (!symbol_val_i || !val_f) return undefined; - return [symbol_val_i.charAt(0), currency, Number(symbol_val_i.replaceAll(`,`, ``).slice(1)), Number(val_f)]; -}; - -export const price_fmt = (locale: string, _currency: string): Intl.NumberFormat => { - const currency = parse_currency(_currency); - const fmt = new Intl.NumberFormat(locale, { - style: 'currency', - currency: currency.toUpperCase(), - minimumFractionDigits: 2, - }); - return fmt; -}; - - +}; +\ No newline at end of file diff --git a/utils/src/error.ts b/utils/src/error.ts @@ -1,8 +1,14 @@ -import type { ErrorMessage, ErrorResponse } from "./types"; +export type IErrorCatchCallback = { + name: string; + message: string; + stack: string; + url: string; + func: string; +}; -export const handle_error = (e: unknown, append?: string): ErrorMessage<string> => { - const msg = (e as Error).message ? (e as Error).message : String(e); - const err = `${msg}${append ? ` ${append}` : ``}`; +export type ErrorMessage<T extends string> = { err: T }; + +export const err_msg = <T extends string>(err: T): ErrorMessage<T> => { return { err }; }; @@ -11,14 +17,8 @@ export const throw_err = (param: string | ErrorMessage<string>): undefined => { else throw new Error(param.err); }; -export const err_msg = <T extends string>(err: T): ErrorMessage<T> => { +export const handle_error = (e: unknown, append?: string): ErrorMessage<string> => { + const msg = (e as Error).message ? (e as Error).message : String(e); + const err = `${msg}${append ? ` ${append}` : ``}`; return { err }; -}; - -export const err_res = <T extends object>(error: T): ErrorResponse<T> => { - return { error }; -}; - -export const err_system = (message: string): boolean => { - return message.split(` `).length > 1 -}; +}; +\ No newline at end of file diff --git a/utils/src/file.ts b/utils/src/file.ts @@ -1,40 +0,0 @@ -import type { FileBytesFormat, FilePath } from "./types"; - -export const parse_file_name = (file_path: string): string => { - const file_path_dirs = file_path.split(`/`); - if (file_path_dirs.length) { - const res = file_path_dirs[file_path_dirs.length - 1]; - if (res) return res; - }; - return ``; -}; - -export const format_file_bytes = (num_bytes: number, format: FileBytesFormat): string => { - if (num_bytes < 0) throw new Error(`Number of bytes cannot be negative`); - let factor: number; - switch (format) { - case 'kb': - factor = 1024; - break; - case 'mb': - factor = 1024 ** 2; - break; - case 'gb': - factor = 1024 ** 3; - break; - } - const result = num_bytes / factor; - return `${result.toFixed(2)} ${format.toUpperCase()}`; -}; - -export const parse_file_path = (file_path: string): FilePath | undefined => { - const file_path_spl = file_path.split(`/`); - const file_path_file = file_path_spl[file_path_spl.length - 1] || ``; - const [file_name, mime_type] = file_path_file.split(`.`); - if (!file_name || !mime_type) return undefined; - return { - file_path, - file_name, - mime_type - }; -}; -\ No newline at end of file diff --git a/utils/src/format.ts b/utils/src/format.ts @@ -1,4 +0,0 @@ -export const fmt_plural_agreement = (num: number = 0, val_s: string, val_pl: string): string => { - if (num === 1) return val_s; - return val_pl; -}; -\ No newline at end of file diff --git a/utils/src/geo.ts b/utils/src/geo.ts @@ -0,0 +1,231 @@ +import { GeolocationPointTuple, GeometryPoint, ResolveAddressInfo, ResolveGeometryPoint } from "$root"; +import { decodeBase32, encodeBase32 } from "geohashing"; + +export type GeolocationAddress = Omit<ResolveAddressInfo, "id" | "created_at" | "updated_at">; + +export type GeolocationPoint = { + lat: number; + lng: number; +}; + +export type LocationPoint = GeolocationCoordinatesPoint & { + error: GeolocationCoordinatesPoint; +} + +export type GeocoderReverseResult = { + id: number; + name: string; + admin1_id: string | number; + admin1_name: string; + country_id: string; + country_name: string; + latitude: number; + longitude: number; +}; + +export type IClientGeolocationPosition = { + lat: number; + lng: number; + accuracy?: number; + altitude?: number; + altitude_accuracy?: number; +}; + +export type GeolocationCoordinatesPoint = { + lat: number; + lng: number; +} + +export type GeolocationLatitudeFmtOption = 'dms' | 'd' | 'dm'; + +export const geohash_encode = (opts: { + lat: string | number; + lng: string | number; +}): string => { + const lat = typeof opts.lat === `string` ? parseFloat(opts.lat) : opts.lat; + const lng = typeof opts.lng === `string` ? parseFloat(opts.lng) : opts.lng; + const geohash = encodeBase32(lat, lng); + return geohash; +}; + +export const geohash_decode = (geohash: string): LocationPoint => { + const { lat, lng, error: { lat: lat_err, lng: lng_err } } = decodeBase32(geohash); + return { + lat, + lng, + error: { + lat: lat_err, + lng: lng_err + } + }; +}; + +export const location_geohash = (point: GeolocationCoordinatesPoint): string => { + const { lat, lng } = point; + const res = geohash_encode({ lat, lng }); + return res; +}; + +export const parse_geop_point = (point: GeolocationCoordinatesPoint): GeolocationPoint => { + const { lat, lng } = point; + return { lat, lng }; +}; + +export const parse_geol_coords = (number: number): number => { + return Math.round(number * 1e7) / 1e7; +}; + +export const parse_geolocation_address = (addr?: ResolveAddressInfo): GeolocationAddress | undefined => { + if (!addr) return undefined; + const { primary, admin, country } = addr; + return { primary, admin, country }; +}; + +export const parse_geolocation_point = (point?: ResolveGeometryPoint): GeolocationPoint | undefined => { + if (!point) return undefined; + return { + lat: point.coordinates[1], + lng: point.coordinates[0], + }; +}; + +export const parse_geocode_address = (geoc?: GeocoderReverseResult): GeolocationAddress | undefined => { + if (!geoc) return undefined; + const { name: primary, admin1_name: admin, country_id: country } = geoc; + return { primary, admin, country }; +}; + +export const fmt_geocode_address = (geoc: GeocoderReverseResult): string => { + const addr = parse_geocode_address(geoc); + return addr ? `${addr.primary}, ${addr.admin}, ${addr.country}` : ``; +}; + +export const fmt_geolocation_address = (addr: ResolveAddressInfo): string => { + return `${addr.primary}, ${addr.admin}, ${addr.country}`; +}; + +export const fmt_geometry_point_coords = (point: ResolveGeometryPoint, locale: string): string => { + const lat = geol_lat_fmt(point.coordinates[0], `dms`, locale, 3); + const lng = geol_lng_fmt(point.coordinates[1], `dms`, locale, 3); + return `${lat}, ${lng}`; +}; + +export const parse_geom_point_tup = (point: GeometryPoint): GeolocationPointTuple => { + return [ + point.coordinates[0], + point.coordinates[1], + ]; +}; + +export const parse_geol_point_tup = (point: GeolocationPoint): GeolocationPointTuple => { + return [ + point.lng, + point.lat + ]; +}; + +export const parse_tup_geop_point = (map_center: GeolocationPointTuple): GeolocationPoint => { + return { + lat: map_center[1], + lng: map_center[0] + } +}; + +export const geol_lat_fmt = (lat: number, fmt_opt: GeolocationLatitudeFmtOption, locale: string, precision: number = 5): string => { + const options: Intl.NumberFormatOptions = { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }; + const fmt_deg = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }); + const fmt_min = new Intl.NumberFormat(locale, options); + const fmt_sec = new Intl.NumberFormat(locale, options); + if (fmt_opt === 'dms') { + const deg = Math.floor(Math.abs(lat)); + const min = Math.floor((Math.abs(lat) - deg) * 60); + const sec = ((Math.abs(lat) - deg - min / 60) * 3600); + return `${fmt_deg.format(deg)}° ${fmt_min.format(min)}' ${fmt_sec.format(sec)}" ${lat >= 0 ? 'N' : 'S'}`; + } else if (fmt_opt === 'dm') { + const deg = Math.floor(Math.abs(lat)); + const min = (Math.abs(lat) - deg) * 60; + return `${fmt_deg.format(deg)}° ${fmt_min.format(min)}' ${lat >= 0 ? 'N' : 'S'}`; + } else { + return `${lat.toLocaleString(locale, { maximumFractionDigits: precision })}° ${lat >= 0 ? 'N' : 'S'}`; + } +}; + +export const geol_lng_fmt = (lng: number, fmt_opt: GeolocationLatitudeFmtOption, locale: string, precision: number = 5): string => { + const options: Intl.NumberFormatOptions = { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }; + const fmt_deg = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }); + const fmt_min = new Intl.NumberFormat(locale, options); + const fmt_sec = new Intl.NumberFormat(locale, options); + if (fmt_opt === 'dms') { + const degrees = Math.floor(Math.abs(lng)); + const minutes = Math.floor((Math.abs(lng) - degrees) * 60); + const seconds = ((Math.abs(lng) - degrees - minutes / 60) * 3600); + return `${fmt_deg.format(degrees)}° ${fmt_min.format(minutes)}' ${fmt_sec.format(seconds)}" ${lng >= 0 ? 'E' : 'W'}`; + } else if (fmt_opt === 'dm') { + const degrees = Math.floor(Math.abs(lng)); + const minutes = (Math.abs(lng) - degrees) * 60; + return `${fmt_deg.format(degrees)}° ${fmt_min.format(minutes)}' ${lng >= 0 ? 'E' : 'W'}`; + } else { + return `${lng.toLocaleString(locale, { maximumFractionDigits: precision })}° ${lng >= 0 ? 'E' : 'W'}`; + } +}; + +export const compute_bounding_box = (lat: number, lng: number, distance_km: number): { nw: GeolocationPoint; ne: GeolocationPoint; se: GeolocationPoint; sw: GeolocationPoint; } => { + const deg_to_rad = (deg: number) => deg * (Math.PI / 180); + const rad_to_deg = (rad: number) => rad * (180 / Math.PI); + + const R = 6371; + + function destination_point(lat: number, lng: number, bearing: number, distance_km: number): GeolocationPoint { + const lat1 = deg_to_rad(lat); + const lon1 = deg_to_rad(lng); + const angular_distance = distance_km / R; + + const lat2 = Math.asin(Math.sin(lat1) * Math.cos(angular_distance) + Math.cos(lat1) * Math.sin(angular_distance) * Math.cos(deg_to_rad(bearing))); + const lon2 = lon1 + Math.atan2(Math.sin(deg_to_rad(bearing)) * Math.sin(angular_distance) * Math.cos(lat1), Math.cos(angular_distance) - Math.sin(lat1) * Math.sin(lat2)); + + return { lat: rad_to_deg(lat2), lng: rad_to_deg(lon2) }; + } + + const bearings = [0, 90, 180, 270]; + + const coords = bearings.map(bearing => destination_point(lat, lng, bearing, distance_km / Math.sqrt(2))); + + return { + nw: coords[0], + ne: coords[1], + se: coords[2], + sw: coords[3] + }; +}; + +export const geo_bounds_calc = (lat: number, lng: number, distance_km: number): { north: GeolocationPoint; south: GeolocationPoint; east: GeolocationPoint; west: GeolocationPoint; } => { + const deg_to_rad = (deg: number) => deg * (Math.PI / 180); + const rad_to_deg = (rad: number) => rad * (180 / Math.PI); + + const R = 6371; + + function destination_point(lat: number, lng: number, bearing: number, distance_km: number): GeolocationPoint { + const lat1 = deg_to_rad(lat); + const lon1 = deg_to_rad(lng); + const angular_distance = distance_km / R; + + const lat2 = Math.asin(Math.sin(lat1) * Math.cos(angular_distance) + Math.cos(lat1) * Math.sin(angular_distance) * Math.cos(deg_to_rad(bearing))); + const lon2 = lon1 + Math.atan2(Math.sin(deg_to_rad(bearing)) * Math.sin(angular_distance) * Math.cos(lat1), Math.cos(angular_distance) - Math.sin(lat1) * Math.sin(lat2)); + + return { lat: rad_to_deg(lat2), lng: rad_to_deg(lon2) }; + } + + return { + north: destination_point(lat, lng, 0, distance_km), + south: destination_point(lat, lng, 180, distance_km), + east: destination_point(lat, lng, 90, distance_km), + west: destination_point(lat, lng, 270, distance_km) + }; +}; + diff --git a/utils/src/geolocation.ts b/utils/src/geolocation.ts @@ -1,87 +0,0 @@ -//import * as ngeohash from "ngeohash"; -import { decodeBase32, encodeBase32 } from "geohashing"; -import type { LocationPoint } from "./types"; - -export type GeolocationCoordinatesPoint = { - lat: number; - lng: number; -} - -export type GeolocationLatitudeFmtOption = 'dms' | 'd' | 'dm'; - - -export const geohash_encode = (opts: { - lat: string | number; - lng: string | number; -}): string => { - const lat = typeof opts.lat === `string` ? parseFloat(opts.lat) : opts.lat; - const lng = typeof opts.lng === `string` ? parseFloat(opts.lng) : opts.lng; - const geohash = encodeBase32(lat, lng); - return geohash; -}; - -export const geohash_decode = (geohash: string): LocationPoint => { - const { lat, lng, error: { lat: lat_err, lng: lng_err } } = decodeBase32(geohash); - return { - lat, - lng, - error: { - lat: lat_err, - lng: lng_err - } - }; -}; - -export const location_geohash = (point: GeolocationCoordinatesPoint): string => { - const { lat, lng } = point; - const res = geohash_encode({ lat, lng }); - return res; -}; - -export const geol_lat_fmt = (lat: number, fmt_opt: GeolocationLatitudeFmtOption, locale: string, precision: number = 5): string => { - const options: Intl.NumberFormatOptions = { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }; - const fmt_deg = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }); - const fmt_min = new Intl.NumberFormat(locale, options); - const fmt_sec = new Intl.NumberFormat(locale, options); - if (fmt_opt === 'dms') { - const deg = Math.floor(Math.abs(lat)); - const min = Math.floor((Math.abs(lat) - deg) * 60); - const sec = ((Math.abs(lat) - deg - min / 60) * 3600); - return `${fmt_deg.format(deg)}° ${fmt_min.format(min)}' ${fmt_sec.format(sec)}" ${lat >= 0 ? 'N' : 'S'}`; - } else if (fmt_opt === 'dm') { - const deg = Math.floor(Math.abs(lat)); - const min = (Math.abs(lat) - deg) * 60; - return `${fmt_deg.format(deg)}° ${fmt_min.format(min)}' ${lat >= 0 ? 'N' : 'S'}`; - } else { - return `${lat.toLocaleString(locale, { maximumFractionDigits: precision })}° ${lat >= 0 ? 'N' : 'S'}`; - } -}; - -export const geol_lng_fmt = (lng: number, fmt_opt: GeolocationLatitudeFmtOption, locale: string, precision: number = 5): string => { - const options: Intl.NumberFormatOptions = { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }; - const fmt_deg = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }); - const fmt_min = new Intl.NumberFormat(locale, options); - const fmt_sec = new Intl.NumberFormat(locale, options); - if (fmt_opt === 'dms') { - const degrees = Math.floor(Math.abs(lng)); - const minutes = Math.floor((Math.abs(lng) - degrees) * 60); - const seconds = ((Math.abs(lng) - degrees - minutes / 60) * 3600); - return `${fmt_deg.format(degrees)}° ${fmt_min.format(minutes)}' ${fmt_sec.format(seconds)}" ${lng >= 0 ? 'E' : 'W'}`; - } else if (fmt_opt === 'dm') { - const degrees = Math.floor(Math.abs(lng)); - const minutes = (Math.abs(lng) - degrees) * 60; - return `${fmt_deg.format(degrees)}° ${fmt_min.format(minutes)}' ${lng >= 0 ? 'E' : 'W'}`; - } else { - return `${lng.toLocaleString(locale, { maximumFractionDigits: precision })}° ${lng >= 0 ? 'E' : 'W'}`; - } -}; - -export const parse_geol_coords = (number: number): number => { - return Math.round(number * 1e7) / 1e7; -}; -\ No newline at end of file diff --git a/utils/src/guard.ts b/utils/src/guard.ts @@ -1,32 +0,0 @@ -import { type NotifyMessage } from "."; - -export const is_err_response = (response: any): response is { err: string } => { - return "err" in response && typeof response.err === "string"; -} - -export const is_pass_response = (response: any): response is { pass: true } => { - return "pass" in response && response.pass === true; -} - -export const is_result_response = (response: any): response is { result: string } => { - return "result" in response && typeof response.result === "string"; -} - -export const is_results_response = (response: any): response is { results: string[] } => { - return "results" in response && Array.isArray(response.results); -} - -export const is_error_response = (response: any): response is { error: string } => { - return "error" in response && typeof response.result === "string"; -} - -export const is_message_response = (response: any): response is NotifyMessage => { - return ( - typeof response === "object" && - response !== null && - "message" in response && - typeof response.message === "string" && - (response.ok === undefined || typeof response.ok === "string") && - (response.cancel === undefined || typeof response.cancel === "string") - ); -}; -\ No newline at end of file diff --git a/utils/src/hash.ts b/utils/src/hash.ts @@ -1,28 +0,0 @@ -import { hmac } from '@noble/hashes/hmac'; -import { sha256 } from '@noble/hashes/sha2'; - -export const hash_sha256 = (msg: string): Uint8Array => { - const result = sha256(msg); - return result; -}; - -export const hash_sha256_bin = (msg_data: number[]): Uint8Array => { - const result = sha256 - .create() - .update(Uint8Array.from(msg_data)) - .digest(); - return result; -}; - -export const hash_hmac = (key: string, msg: string): Uint8Array => { - const result = hmac(sha256, key, msg); - return result; -}; - -export const hash_hmac_bin = (key_data: number[], msg_data: number[]): Uint8Array => { - const result = hmac - .create(sha256, Uint8Array.from(key_data)) - .update(Uint8Array.from(msg_data)) - .digest(); - return result; -}; diff --git a/utils/src/http.ts b/utils/src/http.ts @@ -1,4 +1,8 @@ -import { type FieldRecord, is_error_response, is_message_response, type NotifyMessage } from "."; +import { is_error_response, is_message_response, type ErrorMessage, type FieldRecord, type NotifyMessage } from "$root"; + +export type IClientHttp = { + fetch(opts: IHttpOpts): Promise<IHttpResponse | ErrorMessage<string>>; +}; export type IHttpImageResponse = { status: number; @@ -85,20 +89,21 @@ export const http_parse_response = async (res: Response): Promise<Promise<IHttpR export const http_fetch = async (opts: IHttpOpts): Promise<IHttpResponse> => { const { url, options } = http_fetch_opts(opts); const response = await fetch(url, options); - let response_data: any = null; + let data: any = null; try { const res_json = await response.json(); - response_data = typeof res_json === `string` ? JSON.parse(res_json) : res_json; + data = typeof res_json === `string` ? JSON.parse(res_json) : res_json; } catch { } - if (!response_data) { + if (!data) { try { const res_text = await response.text(); - response_data = res_text; + data = res_text; } catch { } } return { status: response.status, url: response.url, + data, headers: lib_http_parse_headers(response.headers) }; }; diff --git a/utils/src/image.ts b/utils/src/image.ts @@ -0,0 +1,7 @@ +export type MediaImageUploadResult = { + base_url: string; + file_hash: string; + file_ext: string; +}; + +export const fmt_media_image_upload_result_url = (res: MediaImageUploadResult): string => `${res.base_url}/${res.file_hash}.${res.file_ext}`; diff --git a/utils/src/index.ts b/utils/src/index.ts @@ -1,34 +1,49 @@ -export * from "./ascii" -export * from "./client/database" -export * from "./client/datastore" -export * from "./client/geolocation" -export * from "./client/gui" -export * from "./currency" -export * from "./error" -export * from "./file" -export * from "./format" -export * from "./geolocation" -export * from "./guard" -export * from "./hash" -export * from "./http" -export * from "./list" -export * from "./math" -export * from "./model" -export * from "./nostr/event" -export * from "./nostr/key" -export * from "./nostr/ndk" -export * from "./nostr/types" -export * from "./nostr-event-util/lib" -export * from "./nostr-event-util/types" -export * from "./nostr-key-util/lib" -export * from "./nostr-key-util/types" -export * from "./nostr-relay-util/lib" -export * from "./nostr-relay-util/types" -export * from "./object" -export * from "./regex" -export * from "./time" -export * from "./trade" -export * from "./types" -export * from "./units" -export * from "./uuid" -export * from "./window" +export * from "./*regex.js" +export * from "./*validation.js" +export * from "./app/document.js" +export * from "./app/i18n.js" +export * from "./app/lib.js" +export * from "./app/styles.js" +export * from "./app/types/app.js" +export * from "./app/types/basis.js" +export * from "./app/types/component.js" +export * from "./app/types/element.js" +export * from "./app/types/geometry.js" +export * from "./app/types/glyph.js" +export * from "./app/types/interface.js" +export * from "./app/types/resolve.js" +export * from "./app/types/view.js" +export * from "./app/util/resolve-enum.js" +export * from "./app/util/resolve.js" +export * from "./app/util/search.js" +export * from "./app/util.js" +export * from "./app/validation/view.js" +export * from "./client/database.js" +export * from "./client/datastore.js" +export * from "./client/geo.js" +export * from "./client/gui.js" +export * from "./config.js" +export * from "./currency.js" +export * from "./error.js" +export * from "./geo.js" +export * from "./http.js" +export * from "./image.js" +export * from "./lib.js" +export * from "./list.js" +export * from "./model.js" +export * from "./nostr/nostr/event.js" +export * from "./nostr/nostr/key.js" +export * from "./nostr/nostr/ndk.js" +export * from "./nostr/nostr/types.js" +export * from "./nostr/nostr-event-util/lib.js" +export * from "./nostr/nostr-event-util/types.js" +export * from "./nostr/nostr-key-util/lib.js" +export * from "./nostr/nostr-key-util/types.js" +export * from "./number.js" +export * from "./object.js" +export * from "./response.js" +export * from "./time.js" +export * from "./trade.js" +export * from "./types.js" +export * from "./unit.js" +export * from "./uuid.js" diff --git a/utils/src/lib.ts b/utils/src/lib.ts @@ -0,0 +1,49 @@ +import { CallbackPromise } from "$root"; + +export const ascii = { + bullet: '•', + dash: `—`, + up: `↑`, + down: `↓` +} + +export const root_symbol = "»--`--,---"; + +export const sleep = async (ms: number): Promise<void> => { + await new Promise((resolve) => setTimeout(resolve, ms)); +}; + +export const obj_keys_maxnum = (obj: any): number => Math.max( + ...Object.keys(obj).map(Number), +); + +export const debounce_callback = (func: Function, delay: number) => { + let timer: ReturnType<typeof setTimeout>; + return function (this: any, ...args: any) { + clearTimeout(timer); + timer = setTimeout(() => func.apply(this, args), delay); + }; +}; + +export const str_trunc = (val: string, max_length: number = 28): string => { + if (val.length <= max_length) return val; + return `${val.slice(0, max_length - 3)}...`; +}; + +export const exe_iter = async (callback: CallbackPromise, num: number = 1, delay: number = 400): Promise<void> => { + try { + const iter_fn = (count: number) => { + if (count > 0) { + callback(); + if (count > 1) { + setTimeout(() => { + iter_fn(count - 1); + }, delay); + } + } + }; + iter_fn(num); + } catch (e) { + console.log(`(error) exe_iter `, e); + } +}; +\ No newline at end of file diff --git a/utils/src/list.ts b/utils/src/list.ts @@ -1,21 +1 @@ -export const list_remove_index = <T>(array: T[], index: number): T[] => { - if (index < 0 || index >= array.length) throw new Error(`Index out of bounds`); - return [...array.slice(0, index), ...array.slice(index + 1)]; -}; - -export const list_move_index = <T>(array: T[], index_start: number, index_end: number): T[] => { - if (index_start < 0 || index_start >= array.length || index_end < 0 || index_end >= array.length) throw new Error(`Index out of bounds`); - const item = array[index_start]; - const arr_new = array.filter((_, index) => index !== index_start); - const adjusted_final_index = index_end > index_start ? index_end - 1 : index_end; - const arr_res: T[] = arr_new.slice(0, adjusted_final_index); - if (item) arr_res.push(item) - arr_res.push(...arr_new.slice(adjusted_final_index)) - return arr_res; -}; - -export const list_assign = (list_curr: string[], list_new: string[]): string[] => { - return Array.from( - new Set([...list_curr, ...list_new]), - ).filter((i) => !!i); -}; +export const list_defined = (i: any): boolean => typeof i !== `undefined`; +\ No newline at end of file diff --git a/utils/src/math.ts b/utils/src/math.ts @@ -1,7 +0,0 @@ -export const round_to_5 = (num: number): number => { - return Math.round(num / 5) * 5; -}; - -export const num_str = (num: number): string => num.toString(); - -export const num_min = (num: number = 0, num_min: number = 0): number => Math.max(num, num_min); -\ No newline at end of file diff --git a/utils/src/nostr-event-util/lib.ts b/utils/src/nostr-event-util/lib.ts @@ -1,98 +0,0 @@ -import { type NDKEvent } from "@nostr-dev-kit/ndk"; -import ngeotags, { type GeoTags as NostrGeotagsGeotags, type InputData as NostrGeotagsInputData } from "nostr-geotags"; -import { type NostrEvent as NostrToolsEvent } from "nostr-tools"; -import type { INostrEventEventSign } from ".."; -import { lib_nostr_event_sign, lib_nostr_event_sign_attest, lib_nostr_event_verify, lib_nostr_event_verify_serialized, lib_nostr_nevent_encode } from '../nostr/event'; -import type { INostrEventUtil, INostrEventUtilFormatTagsBasisNip99, INostrEventUtilNeventEncode, NostrEventTagClient, NostrEventTagLocation, NostrEventTagMediaUpload, NostrEventTagPrice, NostrEventTagQuantity } from "./types"; - -export class NostrEventUtil implements INostrEventUtil { - public first_tag_value = (event: NDKEvent, tag_name: string): string => { - const tag = event.getMatchingTags(tag_name)[0]; - return tag ? tag[1] : ""; - } - - private fmt_tag_price = (opts: NostrEventTagPrice): string[] => { - const tag = [`price`, opts.amt, opts.currency, opts.qty_amt, opts.qty_unit]; - return tag; - }; - - - private fmt_tag_quantity = (opts: NostrEventTagQuantity): string[] => { - const tag = [`quantity`, opts.amt, opts.unit]; - if (opts.label) tag.push(opts.label); - return tag; - }; - - private fmt_tag_location = (opts: NostrEventTagLocation): string[] => { - const tag = [`location`]; - if (opts.city) tag.push(opts.city); - if (opts.region_code && !isNaN(parseInt(opts.region_code))) tag.push(opts.region_code); - else if (opts.region) tag.push(opts.region); //@todo - if (opts.country_code) tag.push(opts.country_code); - return tag; - }; - - private fmt_tag_image = (opts: NostrEventTagMediaUpload): string[] => { - const tag = [`image`, opts.url]; - if (opts.size) tag.push(`${opts.size.w}x${opts.size.h}`) - return tag; - }; - - private fmt_tag_client = (opts: NostrEventTagClient, d_tag?: string): string[] => { - const tag = [`client`, opts.name]; - if (d_tag) tag.push(`31990:${opts.pubkey}:${d_tag}`); - tag.push(opts.relay); - return tag; - }; - - private fmt_tag_geotags = (opts: NostrEventTagLocation): NostrGeotagsGeotags[] => { - const data: NostrGeotagsInputData = { - lat: opts.lat, - lon: opts.lng, - city: opts.city, - regionName: opts.region, - countryName: opts.country, - countryCode: opts.country_code - }; - return ngeotags(data, { - geohash: true, - gps: true, - city: true, - iso31662: true, - }); - }; - - public fmt_tags_basis_nip99 = (opts: INostrEventUtilFormatTagsBasisNip99): string[][] => { - const { d_tag, listing, quantity, price, location } = opts; - const tags: string[][] = [[`d`, d_tag]]; - if (opts.client) tags.push(this.fmt_tag_client(opts.client, opts.d_tag)); - for (const [k, v] of Object.entries(listing)) if (v) tags.push([k, v]); - tags.push(this.fmt_tag_quantity(quantity)); - tags.push(this.fmt_tag_price(price)); - tags.push(this.fmt_tag_location(location)); - if (opts.images) for (const image of opts.images) tags.push(this.fmt_tag_image(image)); - tags.push(...this.fmt_tag_geotags(location)); - return tags; - }; - - public nostr_event_sign = (opts: INostrEventEventSign): NostrToolsEvent => { - return lib_nostr_event_sign(opts); - }; - - public nostr_event_sign_attest = (secret_key: string): NostrToolsEvent => { - return lib_nostr_event_sign_attest(secret_key); - }; - - public nostr_event_verify = (event: NostrToolsEvent): boolean => { - return lib_nostr_event_verify(event); - }; - - public nostr_event_verify_serialized = (event_serialized: string): boolean => { - const result = lib_nostr_event_verify_serialized(event_serialized); - return !!result; - }; - - public nevent_encode = (opts: INostrEventUtilNeventEncode): string => { - return lib_nostr_nevent_encode(opts); - }; -} -\ No newline at end of file diff --git a/utils/src/nostr-event-util/types.ts b/utils/src/nostr-event-util/types.ts @@ -1,80 +0,0 @@ -import { NDKEvent } from "@nostr-dev-kit/ndk"; -import { type NostrEvent as NostrToolsEvent } from "nostr-tools"; -import type { INostrEventEventSign } from ".."; - -export type INostrEventUtilFormatTagsBasisNip99 = { - d_tag: string; - listing: NostrEventTagListing; - quantity: NostrEventTagQuantity; - price: NostrEventTagPrice; - location: NostrEventTagLocation; - images?: NostrEventTagMediaUpload[]; - client?: NostrEventTagClient; -}; - -export type INostrEventUtilNeventEncode = { - id: string; - relays: string[]; - author: string; - kind: number; -}; - -export type INostrEventUtil = { - first_tag_value(event: NDKEvent, tag_name: string): string; - fmt_tags_basis_nip99: (opts: INostrEventUtilFormatTagsBasisNip99) => string[][]; - nostr_event_sign: (opts: INostrEventEventSign) => NostrToolsEvent; - nostr_event_sign_attest: (secret_key: string) => NostrToolsEvent; - nostr_event_verify_serialized: (event_serialized: string) => boolean; - nostr_event_verify: (event: NostrToolsEvent) => boolean; - nevent_encode: (opts: INostrEventUtilNeventEncode) => string; -}; - -export type NostrEventTagListing = { - key: string; - title: string; - category: string; - summary?: string; - process?: string; - lot?: string; - location?: string; - profile?: string; - year?: string; -}; - -export type NostrEventTagPrice = { - amt: string; - currency: string; - qty_amt: string; - qty_unit: string; -}; - -export type NostrEventTagQuantity = { - amt: string; - unit: string; - label?: string; -}; - -export type NostrEventTagLocation = { - city?: string; - region?: string; - region_code?: string; - country?: string; - country_code?: string; - lat: number; - lng: number; - geohash: string; -}; - -export type NostrEventTagMediaUpload = { - url: string; - size?: { - w: number; - h: number; - }; -}; - -export type NostrEventTagClient = { - name: string; - pubkey: string; - relay: string; -}; diff --git a/utils/src/nostr-key-util/lib.ts b/utils/src/nostr-key-util/lib.ts @@ -1,122 +0,0 @@ -import { getPublicKey, nip19 } from 'nostr-tools'; -import { lib_nostr_get_key_bytes, lib_nostr_key_generate, lib_nostr_nsec_decode, lib_nostr_nsec_encode } from '../nostr/key'; -import type { INostrKeyUtil } from './types'; - -export class NostrKeyUtil implements INostrKeyUtil { - /** - * - * @returns nostr secret key hex - */ - public generate_key(): string { - return lib_nostr_key_generate(); - }; - - - /** - * - * @returns nostr public key hex from secret key - */ - public public_key(secret_key_hex: string | undefined): string { - try { - if (!secret_key_hex) return ``; - const bytes = lib_nostr_get_key_bytes(secret_key_hex); - const hex = getPublicKey(bytes) - return hex; - } catch (e) { - return `` - } - } - - /** - * - * @returns nostr secret key to public key hex - */ - public publickey_decode(secret_key_hex?: string): string | undefined { - try { - if (secret_key_hex) { - return getPublicKey(lib_nostr_get_key_bytes(secret_key_hex)) - } - return undefined; - } catch (e) { - return undefined; - } - } - - /** - * - * @returns nostr public key npub - */ - public npub(public_key_hex: string | undefined, fallback_to_hex?: boolean): string { - if (!public_key_hex) return ``; - const npub = nip19.npubEncode(public_key_hex); - return npub ? npub : fallback_to_hex ? public_key_hex : ``; - } - - /** - * - * @returns public key hex from npub - */ - public npub_decode(npub: string): string { - const decode = nip19.decode(npub); - console.log(`decode `, decode) - if (decode && decode.type === `npub` && decode.data) return decode.data - return ``; - } - - /** - * - * @returns nostr secret key nsec - */ - public nsec(secret_key_hex?: string): string | undefined { - return lib_nostr_nsec_encode(secret_key_hex); - } - - /** - * - * @returns nostr secret key hex from nsec - */ - public nsec_decode(nsec: string): string | undefined { - return lib_nostr_nsec_decode(nsec); - } - - /** - * - * @returns - */ - public nevent(event_pointer: nip19.EventPointer, relays: string[]): string { - return nip19.neventEncode(event_pointer) - } - - /** - * - * @returns nostr public key nprofile - */ - public nprofile(public_key_hex: string, relays: string[]): string { - if (!public_key_hex || !relays.length) return ``; - return nip19.nprofileEncode({ pubkey: public_key_hex, relays }) - } - - /** - * - * @returns nostr public key nprofile - */ - public nprofile_decode(nprofile: string): [string, string[]] | undefined { - if (!nprofile) return undefined; - const decode = nip19.decode(nprofile); - if (decode && decode.type === `nprofile` && decode.data && decode.data.pubkey && decode.data.relays) return [decode.data.pubkey, decode.data.relays] - return undefined; - } - - /** - * - * @returns - */ - public secretkey_to_publickey(nsec_or_hex: string): string | undefined { - if (nsec_or_hex.startsWith(`nsec1`)) { - return this.nsec_decode(nsec_or_hex); - } else if (nsec_or_hex.length === 64) { - return this.publickey_decode(nsec_or_hex) - } - return undefined; - } -}; diff --git a/utils/src/nostr-relay-util/lib.ts b/utils/src/nostr-relay-util/lib.ts @@ -1,45 +0,0 @@ -import { type NDKEvent } from "@nostr-dev-kit/ndk"; -import type { INostrRealyUtil, NostrRelayInformationDocument, NostrRelayInformationDocumentFormFields } from "./types"; - -export class NostrRelayUtil implements INostrRealyUtil { - public first_tag_value(event: NDKEvent, tag_name: string): string { - const tag = event.getMatchingTags(tag_name)[0]; - return tag ? tag[1] : ""; - } - - private parse_nostr_relay_information_document = (data: any): NostrRelayInformationDocument | undefined => { - const obj = JSON.parse(data); - return { - id: typeof obj.id === 'string' ? obj.id : undefined, - name: typeof obj.name === 'string' ? obj.name : undefined, - description: typeof obj.description === 'string' ? obj.description : undefined, - pubkey: typeof obj.pubkey === 'string' ? obj.pubkey : undefined, - contact: typeof obj.contact === 'string' ? obj.contact : undefined, - supported_nips: Array.isArray(obj.supported_nips) && obj.supported_nips.every((nip: any) => typeof nip === 'number') - ? obj.supported_nips - : undefined, - software: typeof obj.software === 'string' ? obj.software : undefined, - version: typeof obj.version === 'string' ? obj.version : undefined, - limitation_payment_required: obj.limitation && typeof obj.limitation === 'object' && typeof obj.limitation.payment_required === 'string' ? obj.limitation.payment_required : undefined, - limitation_restricted_writes: obj.limitation && typeof obj.limitation === 'object' && typeof obj.limitation.restricted_writes === 'boolean' ? obj.limitation.restricted_writes : undefined, - }; - }; - - public parse_nostr_relay_information_document_fields = (data: any): NostrRelayInformationDocumentFormFields | undefined => { - const info_doc = this.parse_nostr_relay_information_document(data); - if (!info_doc) return; - const result: Partial<NostrRelayInformationDocumentFormFields> = {}; - Object.entries(info_doc).forEach(([key, value]) => { - if (typeof value === 'boolean') { - result[key as keyof NostrRelayInformationDocument] = value ? '1' : '0'; - } else if (Array.isArray(value)) { - result[key as keyof NostrRelayInformationDocument] = value.join(', '); - } else if (value === null || value === undefined) { - result[key as keyof NostrRelayInformationDocument] = ''; - } else { - result[key as keyof NostrRelayInformationDocument] = String(value); - } - }); - return result; - }; -} -\ No newline at end of file diff --git a/utils/src/nostr-relay-util/types.ts b/utils/src/nostr-relay-util/types.ts @@ -1,21 +0,0 @@ -import { NDKEvent } from "@nostr-dev-kit/ndk"; - -export type INostrRealyUtil = { - first_tag_value(event: NDKEvent, tag_name: string): string; - parse_nostr_relay_information_document_fields: (data: any) => NostrRelayInformationDocumentFormFields | undefined; -}; - -export type NostrRelayInformationDocument = { - id?: string; - name?: string; - description?: string; - pubkey?: string; - contact?: string; - supported_nips?: number[]; - software?: string; - version?: string; - limitation_payment_required?: string; - limitation_restricted_writes?: boolean; -} - -export type NostrRelayInformationDocumentFormFields = { [K in keyof NostrRelayInformationDocument]: string; }; diff --git a/utils/src/nostr/event.ts b/utils/src/nostr/event.ts @@ -1,45 +0,0 @@ -import { schnorr } from "@noble/curves/secp256k1"; -import { hexToBytes } from "@noble/hashes/utils"; -import { finalizeEvent, getEventHash, nip19, type NostrEvent as NostrToolsEvent } from "nostr-tools"; -import { type INostrEventEventSign, type INostrEventUtilNeventEncode, uuidv4 } from ".."; - -export const lib_nostr_event_verify = (event: NostrToolsEvent): boolean => { - const hash = getEventHash(event); - if (hash !== event.id) return false - const valid = schnorr.verify(event.sig, hash, event.pubkey); - return valid; -}; - -export const lib_nostr_event_sign = (opts: INostrEventEventSign): NostrToolsEvent => { - return finalizeEvent(opts.event, hexToBytes(opts.secret_key)) -}; - -export const lib_nostr_event_sign_attest = (secret_key: string): NostrToolsEvent => { - return lib_nostr_event_sign({ - secret_key, - event: { - kind: 1, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: uuidv4(), - }, - }); -}; - - -export const lib_nostr_event_verify_serialized = async (event_serialized: string): Promise<{ public_key: string } | undefined> => { - try { - const event = JSON.parse(event_serialized); - const hash = getEventHash(event); - if (hash !== event.id) return undefined; - const valid = schnorr.verify(event.sig, hash, event.pubkey); - if (valid) return { public_key: String(event.pubkey) }; - return undefined; - } catch { - return undefined; - } -}; - -export const lib_nostr_nevent_encode = (opts: INostrEventUtilNeventEncode): string => { - return nip19.neventEncode(opts); -}; -\ No newline at end of file diff --git a/utils/src/nostr/ndk.ts b/utils/src/nostr/ndk.ts @@ -1,75 +0,0 @@ -import NDK, { NDKEvent, NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk'; -import { time_now_ms } from '..'; -import type { NostrMetadataTmp } from './types'; - -export const ndk_init = async (opts: { - $ndk: NDK; - secret_key: string; -}): Promise<NDKUser | undefined> => { - try { - const { $ndk: ndk, secret_key } = opts; - const signer = new NDKPrivateKeySigner(secret_key); - ndk.signer = signer; - - const user = await signer.user(); - if (user) { - user.ndk = ndk; - return user; - } - } catch (e) { - console.log(`(error) ndk_init `, e); - }; -}; - -export const ndk_event_metadata = async (opts: { - $ndk: NDK; - $ndk_user: NDKUser; - metadata: NostrMetadataTmp -}): Promise<NDKEvent | undefined> => { - try { - const { $ndk, $ndk_user } = opts; - const ev = await ndk_event({ - $ndk, - $ndk_user, - basis: { - kind: 0, - content: JSON.stringify(opts.metadata), - }, - }); - return ev; - } catch (e) { - console.log(`(error) ndk_event_metadata `, e); - } -}; - -export const ndk_event = async (opts: { - $ndk: NDK; - $ndk_user: NDKUser; - basis: { - kind: number; - content: string; - tags?: string[][]; - } -}): Promise<NDKEvent | undefined> => { - try { - const { $ndk: ndk, $ndk_user: ndk_user, basis } = opts; - const time_now = time_now_ms(); - - const tags: string[][] = [ - ['published_at', time_now.toString()], - ]; - - if (basis.tags && basis.tags?.length > 0) for (const tag of basis.tags) tags.push(tag); - - const event: NDKEvent = new NDKEvent(ndk, { - kind: basis.kind, - pubkey: ndk_user.pubkey, - content: basis.content, - created_at: time_now, - tags - }); - return event; - } catch (e) { - console.log(`(error) ndk_event `, e); - }; -}; diff --git a/utils/src/nostr/nostr-event-util/lib.ts b/utils/src/nostr/nostr-event-util/lib.ts @@ -0,0 +1,96 @@ +import { lib_nostr_event_sign, lib_nostr_event_sign_attest, lib_nostr_event_verify, lib_nostr_event_verify_serialized, lib_nostr_nevent_encode, type INostrEventEventSign, type INostrEventUtil, type INostrEventUtilFormatTagsBasisNip99, type INostrEventUtilNeventEncode, type NostrEventTagClient, type NostrEventTagLocation, type NostrEventTagMediaUpload, type NostrEventTagPrice, type NostrEventTagQuantity } from "$root"; +import { type NDKEvent } from "@nostr-dev-kit/ndk"; +import ngeotags, { type GeoTags as NostrGeotagsGeotags, type InputData as NostrGeotagsInputData } from "nostr-geotags"; +import { type NostrEvent as NostrToolsEvent } from "nostr-tools"; + +export class NostrEventUtil implements INostrEventUtil { + public first_tag_value = (event: NDKEvent, tag_name: string): string => { + const tag = event.getMatchingTags(tag_name)[0]; + return tag ? tag[1] : ""; + } + + private fmt_tag_price = (opts: NostrEventTagPrice): string[] => { + const tag = [`price`, opts.amt, opts.currency, opts.qty_amt, opts.qty_unit]; + return tag; + }; + + + private fmt_tag_quantity = (opts: NostrEventTagQuantity): string[] => { + const tag = [`quantity`, opts.amt, opts.unit]; + if (opts.label) tag.push(opts.label); + return tag; + }; + + private fmt_tag_location = (opts: NostrEventTagLocation): string[] => { + const tag = [`location`]; + if (opts.city) tag.push(opts.city); + if (opts.region_code && !isNaN(parseInt(opts.region_code))) tag.push(opts.region_code); + else if (opts.region) tag.push(opts.region); //@todo + if (opts.country_code) tag.push(opts.country_code); + return tag; + }; + + private fmt_tag_image = (opts: NostrEventTagMediaUpload): string[] => { + const tag = [`image`, opts.url]; + if (opts.size) tag.push(`${opts.size.w}x${opts.size.h}`) + return tag; + }; + + private fmt_tag_client = (opts: NostrEventTagClient, d_tag?: string): string[] => { + const tag = [`client`, opts.name]; + if (d_tag) tag.push(`31990:${opts.pubkey}:${d_tag}`); + tag.push(opts.relay); + return tag; + }; + + private fmt_tag_geotags = (opts: NostrEventTagLocation): NostrGeotagsGeotags[] => { + const data: NostrGeotagsInputData = { + lat: opts.lat, + lon: opts.lng, + city: opts.city, + regionName: opts.region, + countryName: opts.country, + countryCode: opts.country_code + }; + return ngeotags(data, { + geohash: true, + gps: true, + city: true, + iso31662: true, + }); + }; + + public fmt_tags_basis_nip99 = (opts: INostrEventUtilFormatTagsBasisNip99): string[][] => { + const { d_tag, listing, quantity, price, location } = opts; + const tags: string[][] = [[`d`, d_tag]]; + if (opts.client) tags.push(this.fmt_tag_client(opts.client, opts.d_tag)); + for (const [k, v] of Object.entries(listing)) if (v) tags.push([k, v]); + tags.push(this.fmt_tag_quantity(quantity)); + tags.push(this.fmt_tag_price(price)); + tags.push(this.fmt_tag_location(location)); + if (opts.images) for (const image of opts.images) tags.push(this.fmt_tag_image(image)); + tags.push(...this.fmt_tag_geotags(location)); + return tags; + }; + + public nostr_event_sign = (opts: INostrEventEventSign): NostrToolsEvent => { + return lib_nostr_event_sign(opts); + }; + + public nostr_event_sign_attest = (secret_key: string): NostrToolsEvent => { + return lib_nostr_event_sign_attest(secret_key); + }; + + public nostr_event_verify = (event: NostrToolsEvent): boolean => { + return lib_nostr_event_verify(event); + }; + + public nostr_event_verify_serialized = (event_serialized: string): boolean => { + const result = lib_nostr_event_verify_serialized(event_serialized); + return !!result; + }; + + public nevent_encode = (opts: INostrEventUtilNeventEncode): string => { + return lib_nostr_nevent_encode(opts); + }; +} +\ No newline at end of file diff --git a/utils/src/nostr/nostr-event-util/types.ts b/utils/src/nostr/nostr-event-util/types.ts @@ -0,0 +1,80 @@ +import type { INostrEventEventSign } from "$root"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import { type NostrEvent as NostrToolsEvent } from "nostr-tools"; + +export type INostrEventUtilFormatTagsBasisNip99 = { + d_tag: string; + listing: NostrEventTagListing; + quantity: NostrEventTagQuantity; + price: NostrEventTagPrice; + location: NostrEventTagLocation; + images?: NostrEventTagMediaUpload[]; + client?: NostrEventTagClient; +}; + +export type INostrEventUtilNeventEncode = { + id: string; + relays: string[]; + author: string; + kind: number; +}; + +export type INostrEventUtil = { + first_tag_value(event: NDKEvent, tag_name: string): string; + fmt_tags_basis_nip99: (opts: INostrEventUtilFormatTagsBasisNip99) => string[][]; + nostr_event_sign: (opts: INostrEventEventSign) => NostrToolsEvent; + nostr_event_sign_attest: (secret_key: string) => NostrToolsEvent; + nostr_event_verify_serialized: (event_serialized: string) => boolean; + nostr_event_verify: (event: NostrToolsEvent) => boolean; + nevent_encode: (opts: INostrEventUtilNeventEncode) => string; +}; + +export type NostrEventTagListing = { + key: string; + title: string; + category: string; + summary?: string; + process?: string; + lot?: string; + location?: string; + profile?: string; + year?: string; +}; + +export type NostrEventTagPrice = { + amt: string; + currency: string; + qty_amt: string; + qty_unit: string; +}; + +export type NostrEventTagQuantity = { + amt: string; + unit: string; + label?: string; +}; + +export type NostrEventTagLocation = { + city?: string; + region?: string; + region_code?: string; + country?: string; + country_code?: string; + lat: number; + lng: number; + geohash: string; +}; + +export type NostrEventTagMediaUpload = { + url: string; + size?: { + w: number; + h: number; + }; +}; + +export type NostrEventTagClient = { + name: string; + pubkey: string; + relay: string; +}; diff --git a/utils/src/nostr/nostr-key-util/lib.ts b/utils/src/nostr/nostr-key-util/lib.ts @@ -0,0 +1,121 @@ +import { lib_nostr_get_key_bytes, lib_nostr_key_generate, lib_nostr_nsec_decode, lib_nostr_nsec_encode, type INostrKeyUtil } from '$root'; +import { getPublicKey, nip19 } from 'nostr-tools'; + +export class NostrKeyUtil implements INostrKeyUtil { + /** + * + * @returns nostr secret key hex + */ + public generate_key(): string { + return lib_nostr_key_generate(); + }; + + + /** + * + * @returns nostr public key hex from secret key + */ + public public_key(secret_key_hex: string | undefined): string { + try { + if (!secret_key_hex) return ``; + const bytes = lib_nostr_get_key_bytes(secret_key_hex); + const hex = getPublicKey(bytes) + return hex; + } catch (e) { + return `` + } + } + + /** + * + * @returns nostr secret key to public key hex + */ + public publickey_decode(secret_key_hex?: string): string | undefined { + try { + if (secret_key_hex) { + return getPublicKey(lib_nostr_get_key_bytes(secret_key_hex)) + } + return undefined; + } catch (e) { + return undefined; + } + } + + /** + * + * @returns nostr public key npub + */ + public npub(public_key_hex: string | undefined, fallback_to_hex?: boolean): string { + if (!public_key_hex) return ``; + const npub = nip19.npubEncode(public_key_hex); + return npub ? npub : fallback_to_hex ? public_key_hex : ``; + } + + /** + * + * @returns public key hex from npub + */ + public npub_decode(npub: string): string { + const decode = nip19.decode(npub); + console.log(`decode `, decode) + if (decode && decode.type === `npub` && decode.data) return decode.data + return ``; + } + + /** + * + * @returns nostr secret key nsec + */ + public nsec(secret_key_hex?: string): string | undefined { + return lib_nostr_nsec_encode(secret_key_hex); + } + + /** + * + * @returns nostr secret key hex from nsec + */ + public nsec_decode(nsec: string): string | undefined { + return lib_nostr_nsec_decode(nsec); + } + + /** + * + * @returns + */ + public nevent(event_pointer: nip19.EventPointer, relays: string[]): string { + return nip19.neventEncode(event_pointer) + } + + /** + * + * @returns nostr public key nprofile + */ + public nprofile(public_key_hex: string, relays: string[]): string { + if (!public_key_hex || !relays.length) return ``; + return nip19.nprofileEncode({ pubkey: public_key_hex, relays }) + } + + /** + * + * @returns nostr public key nprofile + */ + public nprofile_decode(nprofile: string): [string, string[]] | undefined { + if (!nprofile) return undefined; + const decode = nip19.decode(nprofile); + if (decode && decode.type === `nprofile` && decode.data && decode.data.pubkey && decode.data.relays) return [decode.data.pubkey, decode.data.relays] + return undefined; + } + + /** + * + * @returns + */ + public secretkey_to_publickey(nsec_or_hex: string): string | undefined { + if (nsec_or_hex.startsWith(`nsec1`)) { + return this.nsec_decode(nsec_or_hex); + } else if (nsec_or_hex.length === 64) { + return this.publickey_decode(nsec_or_hex) + } + return undefined; + } +}; diff --git a/utils/src/nostr-key-util/types.ts b/utils/src/nostr/nostr-key-util/types.ts diff --git a/utils/src/nostr/nostr/event.ts b/utils/src/nostr/nostr/event.ts @@ -0,0 +1,45 @@ +import { uuidv4, type INostrEventEventSign, type INostrEventUtilNeventEncode } from "$root"; +import { schnorr } from "@noble/curves/secp256k1"; +import { hexToBytes } from "@noble/hashes/utils"; +import { finalizeEvent, getEventHash, nip19, type NostrEvent as NostrToolsEvent } from "nostr-tools"; + +export const lib_nostr_event_verify = (event: NostrToolsEvent): boolean => { + const hash = getEventHash(event); + if (hash !== event.id) return false + const valid = schnorr.verify(event.sig, hash, event.pubkey); + return valid; +}; + +export const lib_nostr_event_sign = (opts: INostrEventEventSign): NostrToolsEvent => { + return finalizeEvent(opts.event, hexToBytes(opts.secret_key)) +}; + +export const lib_nostr_event_sign_attest = (secret_key: string): NostrToolsEvent => { + return lib_nostr_event_sign({ + secret_key, + event: { + kind: 1, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: uuidv4(), + }, + }); +}; + + +export const lib_nostr_event_verify_serialized = async (event_serialized: string): Promise<{ public_key: string } | undefined> => { + try { + const event = JSON.parse(event_serialized); + const hash = getEventHash(event); + if (hash !== event.id) return undefined; + const valid = schnorr.verify(event.sig, hash, event.pubkey); + if (valid) return { public_key: String(event.pubkey) }; + return undefined; + } catch { + return undefined; + } +}; + +export const lib_nostr_nevent_encode = (opts: INostrEventUtilNeventEncode): string => { + return nip19.neventEncode(opts); +}; +\ No newline at end of file diff --git a/utils/src/nostr/key.ts b/utils/src/nostr/nostr/key.ts diff --git a/utils/src/nostr/nostr/ndk.ts b/utils/src/nostr/nostr/ndk.ts @@ -0,0 +1,74 @@ +import { time_now_ms, type NostrMetadataTmp } from '$root'; +import NDK, { NDKEvent, NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk'; + +export const ndk_init = async (opts: { + $ndk: NDK; + secret_key: string; +}): Promise<NDKUser | undefined> => { + try { + const { $ndk: ndk, secret_key } = opts; + const signer = new NDKPrivateKeySigner(secret_key); + ndk.signer = signer; + + const user = await signer.user(); + if (user) { + user.ndk = ndk; + return user; + } + } catch (e) { + console.log(`(error) ndk_init `, e); + }; +}; + +export const ndk_event_metadata = async (opts: { + $ndk: NDK; + $ndk_user: NDKUser; + metadata: NostrMetadataTmp +}): Promise<NDKEvent | undefined> => { + try { + const { $ndk, $ndk_user } = opts; + const ev = await ndk_event({ + $ndk, + $ndk_user, + basis: { + kind: 0, + content: JSON.stringify(opts.metadata), + }, + }); + return ev; + } catch (e) { + console.log(`(error) ndk_event_metadata `, e); + } +}; + +export const ndk_event = async (opts: { + $ndk: NDK; + $ndk_user: NDKUser; + basis: { + kind: number; + content: string; + tags?: string[][]; + } +}): Promise<NDKEvent | undefined> => { + try { + const { $ndk: ndk, $ndk_user: ndk_user, basis } = opts; + const time_now = time_now_ms(); + + const tags: string[][] = [ + ['published_at', time_now.toString()], + ]; + + if (basis.tags && basis.tags?.length > 0) for (const tag of basis.tags) tags.push(tag); + + const event: NDKEvent = new NDKEvent(ndk, { + kind: basis.kind, + pubkey: ndk_user.pubkey, + content: basis.content, + created_at: time_now, + tags + }); + return event; + } catch (e) { + console.log(`(error) ndk_event `, e); + }; +}; diff --git a/utils/src/nostr/types.ts b/utils/src/nostr/nostr/types.ts diff --git a/utils/src/number.ts b/utils/src/number.ts @@ -0,0 +1,13 @@ +export const parse_int = (val: string, fallback: number = 0): number => { + const num = parseInt(val); + return isNaN(num) ? fallback : num; +}; + +export const parse_float = (val: string, fallback: number = 0): number => { + const num = parseFloat(val); + return isNaN(num) ? fallback : num; +}; + +export const num_str = (num: number): string => num.toString(); + +export const num_interval_range = (min: number, max: number): number => Math.floor(Math.random() * (max - min + 1)) + min; diff --git a/utils/src/regex.ts b/utils/src/regex.ts @@ -1,33 +0,0 @@ -export const regex = { - float: /^[+-]?(\d+(\.\d*)?|\.\d+)$/, - float_ch: /^[0-9\.\+\-]$/, - float_pos: /^\d+(\.\d+)?$/, - float_pos_ch: /^[0-9\.]$/, - description: /^(?:\S+(?:\s+\S+)*)$/, - description_ch: /[^a-zA-Z0-9.,!?;:'"(){}[]\s\u0600-\u06FF\u0900-\u097F\u0400-\u04FF\u0500-\u052F\u1F00-\u1FFF\u4E00-\u9FFF\uAC00-\uD7AF\u3040-\u309F\u30A0-\u30FF ]+/, - nbsp: /[\u00A0]/g, - nbsp_rp: /[\u00A0]+/g, - rtlm: /[\u200F]/g, - rtlm_rp: /[\u200F]+/g, - commas: /[,]+/g, - periods: /[.]+/g, - word_only: /^[a-zA-Z]+$/, - alpha: /[a-zA-Z ]$/, - alpha_ch: /[a-zA-Z ]$/, - num: /^[0-9]+$/, - lat: /^[-+]?([1-8]?[0-9](\.\d{1,6})?|90(\.0{1,6})?)$/, - lat_ch: /^[\d\.\+\-]$/, - lng: /^[-+]?((1[0-7]?[0-9]|180)(\.\d{1,6})?|(\d{1,2})(\.\d{1,6})?)$/, - lng_ch: /^[\d\.\+\-]$/, - alphanum: /[a-zA-Z0-9., ]$/, - alphanum_ch: /[a-zA-Z0-9.,\s\u0600-\u06FF\u0900-\u097F\u0400-\u04FF\u0500-\u052F\u1F00-\u1FFF\u4E00-\u9FFF\uAC00-\uD7AF\u3040-\u309F\u30A0-\u30FF ]+/, - price: /^\d+(\.\d+)?$/, - price_ch: /[0-9.]$/, - profile_name: /^[a-zA-Z0-9._]{3,30}$/, - profile_name_ch: /[a-zA-Z0-9._]/, - trade_product_key: /^(?:[a-zA-Z0-9]+(?:\s+[a-zA-Z0-9]+){0,2})$/, - trade_product_category: /^(?:[a-zA-Z0-9]+(?:\s+[a-zA-Z0-9]+){0,2})$/, - currency_symbol: /(?:[A-Za-z]{3,5}\$|\p{Sc})/u, - currency_marker: /(?:[A-Za-z]{2,4}[^\d\s]+|[^\d\s]{1,3}[A-Za-z]{2,4})/, - ws_proto: /^(wss:\/\/|ws:\/\/)/, -}; diff --git a/utils/src/response.ts b/utils/src/response.ts @@ -0,0 +1,32 @@ +import type { NotifyMessage } from "$root"; + +export const is_err_response = (response: any): response is { err: string } => { + return "err" in response && typeof response.err === "string"; +} + +export const is_pass_response = (response: any): response is { pass: true } => { + return "pass" in response && response.pass === true; +} + +export const is_result_response = (response: any): response is { result: string } => { + return "result" in response && typeof response.result === "string"; +} + +export const is_results_response = (response: any): response is { results: string[] } => { + return "results" in response && Array.isArray(response.results); +} + +export const is_error_response = (response: any): response is { error: string } => { + return "error" in response && typeof response.result === "string"; +} + +export const is_message_response = (response: any): response is NotifyMessage => { + return ( + typeof response === "object" && + response !== null && + "message" in response && + typeof response.message === "string" && + (response.ok === undefined || typeof response.ok === "string") && + (response.cancel === undefined || typeof response.cancel === "string") + ); +}; +\ No newline at end of file diff --git a/utils/src/trade.ts b/utils/src/trade.ts @@ -1,4 +1,4 @@ -import { type MassUnit, parse_mass_unit } from "./units"; +import { parse_int, parse_mass_unit, type MassUnit } from "$root"; export type TradeKey = `coffee` | `cacao` | `maca`; @@ -14,15 +14,15 @@ export const fmt_trade_quantity_tup = (obj: TradeQuantity): string => `${obj.mas export const parse_trade_quantity_tup = (sel_key: string): TradeQuantity | undefined => { const [qty_mass, qty_mass_u, qty_label] = sel_key.split(`-`); - if (!qty_mass || !qty_mass_u || !qty_label) return undefined; + const mass_unit = parse_mass_unit(qty_mass_u); + if (!qty_mass || !qty_mass_u || !qty_label || !mass_unit) return undefined; return { - mass: parseInt(qty_mass), - mass_unit: parse_mass_unit(qty_mass_u), //@todo + mass: parse_int(qty_mass, 1), + mass_unit, label: qty_label } }; - export const trade_keys: TradeKey[] = [`coffee`, `cacao`, `maca`] as const; const trade_quantity_default: TradeQuantity[] = [ @@ -43,9 +43,16 @@ const trade_quantity_default: TradeQuantity[] = [ }, ]; +const trade_process_default: string[] = [ + `natural`, + `dried`, + `roasted` +]; + export type TradeParam = { default: { quantity: TradeQuantity[]; + process: string[]; }, key: Record<TradeKey, { quantity: TradeQuantity[]; @@ -56,6 +63,7 @@ export type TradeParam = { export const trade: TradeParam = { default: { quantity: trade_quantity_default, + process: trade_process_default, }, key: { coffee: { diff --git a/utils/src/types.ts b/utils/src/types.ts @@ -1,9 +1,21 @@ -import type { GeolocationCoordinatesPoint } from "./geolocation"; +export type FieldRecord = Record<string, string>; +export type NotifyMessage = { + message: string; + ok?: string; + cancel?: string; +}; -export type ErrorResponse<T extends object> = { error: T; }; -export type ErrorMessage<T extends string> = { err: T }; +export type GeometryPoint = { + type: string; + coordinates: number[]; +}; -export type FieldRecord = Record<string, string>; +export type GeometryPolygon = { + type: string; + coordinates: number[][][]; +}; + +export type GeolocationPointTuple = [number, number]; export type ResultId = { id: string; }; export type ResultPass = { pass: true; }; @@ -12,21 +24,6 @@ export type ResultObj<T> = { result: T; }; export type ResultPublicKey = { public_key: string; }; export type ResultSecretKey = { secret_key: string; }; -export type LocationPoint = GeolocationCoordinatesPoint & { - lat: number; - lng: number; - error: GeolocationCoordinatesPoint; -} - -export type NumberTuple = [number, number]; - export type FileBytesFormat = `kb` | `mb` | `gb`; export type FileMimeType = string; export type FilePath = { file_path: string; file_name: string; mime_type: FileMimeType; } - - -export type NotifyMessage = { - message: string; - ok?: string; - cancel?: string; -}; -\ No newline at end of file diff --git a/utils/src/unit.ts b/utils/src/unit.ts @@ -0,0 +1,41 @@ +import type { vunion_area_unit, vunion_mass_unit } from "$root"; +import { z } from "zod"; + +export type AreaUnit = z.infer<typeof vunion_area_unit>; +export const area_units: AreaUnit[] = [`ac`, `ha`, `ft2`, `m2`] as const; + +export type MassUnit = z.infer<typeof vunion_mass_unit>; +export const mass_units: MassUnit[] = [`kg`, `lb`, `g`] as const; + +export function parse_mass_unit_default(val?: string): MassUnit { + const unit = parse_mass_unit(val); + return unit ?? `kg` +} + +export function parse_mass_unit(val?: string): MassUnit | undefined { + switch (val) { + case `kg`: + case `lb`: + case `g`: + return val; + default: + return undefined; + }; +}; + +export function parse_area_unit_default(val?: string): AreaUnit { + const unit = parse_area_unit(val); + return unit ?? `ac` +} + +export function parse_area_unit(val?: string): AreaUnit | undefined { + switch (val) { + case `ac`: + case `ha`: + case `ft2`: + case `m2`: + return val; + default: + return undefined; + }; +}; diff --git a/utils/src/units.ts b/utils/src/units.ts @@ -1,55 +0,0 @@ -import convert from "convert"; -import { z } from "zod"; - -export const MassUnitSchema = z.union([ - z.literal(`kg`), - z.literal(`lb`), - z.literal(`g`), -]); - -export const mass_units: MassUnit[] = [`kg`, `lb`, `g`] as const; - -export type MassUnit = z.infer<typeof MassUnitSchema>; - -export function parse_mass_unit(val?: string): MassUnit { - switch (val) { - case `kg`: - case `lb`: - case `g`: - return val; - default: - return `kg`; - }; -}; - -export function parse_mass_unit_u(val?: string): MassUnit | undefined { - switch (val) { - case `kg`: - case `lb`: - case `g`: - return val; - default: - return undefined; - }; -}; - -export const mass_g = (units: MassUnit, amt: number): number => { - return convert(amt, units).to(`g`); -}; - -export const mass_tf = (units_from: MassUnit, units_to: MassUnit, amt: number): number => { - return convert(amt, units_from).to(units_to); -}; - -export const mass_tf_u = (units_from: MassUnit, units_to: string, amt: number): number => { - const _units_to = parse_mass_unit_u(units_to); - if (!_units_to) throw new Error(`Malformed units.`) - return convert(amt, units_from).to(_units_to); -}; - -export const mass_tf_str = (units_from: string, units_to: string, amt: number): number => { - const _units_from = parse_mass_unit_u(units_from); - const _units_to = parse_mass_unit_u(units_to); - if (!_units_from || !_units_to) throw new Error(`Malformed units.`) - return convert(amt, _units_from).to(_units_to); -}; -\ No newline at end of file diff --git a/utils/src/window.ts b/utils/src/window.ts @@ -1,31 +0,0 @@ -export type AppLayoutKey = 'mobile_base' | 'mobile_y'; -export type AppLayout<T1 extends string, T2 extends string> = `${T1}_${T2}`; -export type AppHeightsResponsiveKey = - | `tabs` - | `nav` - | `lo_view` - | `view` - | `view_offset` - | `trellis_centered`; -export type AppHeightsResponsive = AppLayout<AppHeightsResponsiveKey, AppLayoutKey>; - -type ClientWindow = { - app: { - layout: Record<AppLayoutKey, { - h: number; - }>; - } -}; - -export const wind: ClientWindow = { - app: { - layout: { - mobile_base: { - h: 600 - }, - mobile_y: { - h: 750 - } - } - } -}; -\ No newline at end of file diff --git a/utils/tsconfig.json b/utils/tsconfig.json @@ -6,17 +6,23 @@ "es2021", "dom" ], - "module": "CommonJS", + "module": "ESNext", + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, "outDir": "./dist", - "rootDir": "./src", - "noEmit": true, "esModuleInterop": true, - "skipLibCheck": true + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "$root": ["src/index.js"] + } }, "include": [ "src" ], "exclude": [ - "node_modules" + "node_modules", + "dist" ], } \ No newline at end of file diff --git a/utils/tsup.config.ts b/utils/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + dts: true, + outDir: 'dist', + splitting: false, + clean: true, + sourcemap: true, +});