web


git clone https://radroots.dev/git/web.git
Log | Files | Refs | Submodules | README | LICENSE

commit 54f8936cfa1df45c2c0d1d8395ca1abc606a0008
parent e5e683513d3891eed7ea61ae58a856cf739bcf2a
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Sat, 16 Nov 2024 09:01:57 +0000

Edit crates `core` add `media_upload` and `trade_product_media` models, `tauri` add models handlers, SQL up migrations. Edit `/models/trade-product/add` update submit handler to save media upload models, set trade product media relation, upload media files via hosting endpoint. Edit `/models/trade-product` update load data handler for media upload model. Add nostr event sign attest util to pass serialized \"X-Nostr-Event\" fetch request header. Edit routes. Edit lib components, types, utils. Add/edit styles.

Diffstat:
Acrates/core/src/models/media_upload.rs | 275+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/core/src/models/mod.rs | 2++
Acrates/core/src/models/trade_product_media.rs | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/tauri/migrations/0005_media_upload.sql | 12++++++++++++
Rcrates/tauri/migrations/0005_nostr_profile_relay.sql -> crates/tauri/migrations/0006_nostr_profile_relay.sql | 0
Rcrates/tauri/migrations/0006_trade_product_location.sql -> crates/tauri/migrations/0007_trade_product_location.sql | 0
Acrates/tauri/migrations/0008_trade_product_media.sql | 8++++++++
Mcrates/tauri/src/lib.rs | 17++++++++++++++++-
Acrates/tauri/src/models/media_upload.rs | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/tauri/src/models/mod.rs | 2++
Acrates/tauri/src/models/trade_product_media.rs | 45+++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/components/image_upload_control.svelte | 4----
Msrc/lib/components/trade_product_list_card.svelte | 70++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/lib/types.ts | 5+++--
Msrc/lib/utils/fetch.ts | 93++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/routes/(app)/+page.svelte | 9+++------
Msrc/routes/(app)/models/trade-product/+page.svelte | 21+++++++++++++++++----
Msrc/routes/(app)/models/trade-product/add/+page.svelte | 302++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/routes/(app)/models/trade-product/view/+page.svelte | 12+++++++-----
Msrc/routes/(app)/test/+page.svelte | 49++++++++++++-------------------------------------
Msrc/routes/+layout.svelte | 4+++-
Mtailwind.config.ts | 6++++--
22 files changed, 848 insertions(+), 221 deletions(-)

diff --git a/crates/core/src/models/media_upload.rs b/crates/core/src/models/media_upload.rs @@ -0,0 +1,275 @@ +use crate::{ + error::ModelError, + types::{IModelsId, IModelsQueryBindValue, IModelsQueryBindValueTuple, IModelsResults}, + utils::{time_created_on, uuidv4}, +}; +use futures::TryStreamExt; + +#[derive(Debug, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct MediaUpload { + id: String, + created_at: String, + updated_at: String, + file_path: String, + mime_type: String, + res_base: String, + res_path: String, + label: Option<String>, + description: Option<String>, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct IMediaUploadFields { + pub file_path: String, + pub mime_type: String, + pub res_base: String, + pub res_path: String, + pub label: Option<String>, + pub description: Option<String>, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct IMediaUploadFieldsUpdate { + pub file_path: Option<String>, + pub mime_type: Option<String>, + pub res_base: Option<String>, + pub res_path: Option<String>, + pub label: Option<String>, + pub description: Option<String>, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum MediaUploadSort { + Newest, + Oldest, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum MediaUploadQueryBindValues { + Id(IModelsQueryBindValue), +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum MediaUploadQueryListOf { + All(IModelsQueryBindValue), + OnTradeProduct(IModelsQueryBindValue), + OffTradeProduct(IModelsQueryBindValue), +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct IMediaUploadQueryGetList { + pub of: MediaUploadQueryListOf, + pub sort: Option<MediaUploadSort>, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct IMediaUploadQueryGet { + pub on: Option<MediaUploadQueryBindValues>, + pub list: Option<IMediaUploadQueryGetList>, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct IMediaUploadQueryUpdate { + pub on: MediaUploadQueryBindValues, + pub fields: IMediaUploadFieldsUpdate, +} + +pub type IMediaUploadAdd = IMediaUploadFields; +pub type IMediaUploadAddResolve = IModelsId; +pub type IMediaUploadGet = IMediaUploadQueryGet; +pub type IMediaUploadGetResolve = IModelsResults<MediaUpload>; +pub type IMediaUploadDelete = MediaUploadQueryBindValues; +pub type IMediaUploadDeleteResolve = (); +pub type IMediaUploadUpdate = IMediaUploadQueryUpdate; +pub type IMediaUploadUpdateResolve = (); + +pub fn media_upload_query_bind_values(opts: MediaUploadQueryBindValues) -> IModelsQueryBindValueTuple { + match opts { + MediaUploadQueryBindValues::Id(id) => ("id".to_string(), id), + } +} + +pub fn media_upload_query_get_list(opts: IMediaUploadQueryGetList) -> IModelsQueryBindValueTuple { + let query_sort = match opts.sort { + Some(MediaUploadSort::Newest) => " ORDER BY mu.created_at DESC", + Some(MediaUploadSort::Oldest) => " ORDER BY mu.created_at ASC", + None => "", + }; + match opts.of { + MediaUploadQueryListOf::All(_) => (format!("SELECT mu.* FROM media_upload mu{}", query_sort), "".to_string()), + MediaUploadQueryListOf::OnTradeProduct(id) => (format!("SELECT mu.* FROM media_upload mu JOIN trade_product_media tp_lg ON mu.id = tp_lg.tb_mu WHERE tp_lg.tb_tp = ?1{}", query_sort), id), + MediaUploadQueryListOf::OffTradeProduct(id) => (format!("SELECT mu.* FROM media_upload mu WHERE NOT EXISTS (SELECT 1 FROM trade_product_media tp_lg WHERE tp_lg.tb_mu = mu.id AND tp_lg.tb_tp = ?1){}", query_sort), id), + } +} + +fn media_upload_fields_bind_values( + opts: IMediaUploadFields, +) -> Result<Vec<IModelsQueryBindValueTuple>, ModelError> { + let bind_values = serde_json::to_value(&opts) + .map_err(|err| ModelError::SerializationError(err.to_string()))? + .as_object() + .ok_or_else(|| ModelError::InvalidArgument("model.error.object_invalid".to_string()))? + .iter() + .filter_map(|(key, value)| value.as_str().map(|v| (key.clone(), v.to_string()))) + .collect::<Vec<_>>(); + Ok(bind_values) +} + +fn media_upload_fields_update_bind_values( + opts: IMediaUploadFieldsUpdate, +) -> Result<Vec<IModelsQueryBindValueTuple>, ModelError> { + let bind_values = serde_json::to_value(&opts) + .map_err(|err| ModelError::SerializationError(err.to_string()))? + .as_object() + .ok_or_else(|| ModelError::InvalidArgument("model.error.object_invalid".to_string()))? + .iter() + .filter_map(|(key, value)| value.as_str().map(|v| (key.clone(), v.to_string()))) + .collect::<Vec<_>>(); + Ok(bind_values) +} + +pub async fn lib_model_media_upload_add( + db: &sqlx::Pool<sqlx::Sqlite>, + opts: IMediaUploadAdd, +) -> Result<IMediaUploadAddResolve, ModelError> { + let id = uuidv4(); + let created_at = time_created_on(); + let updated_at = created_at.clone(); + let bind_values = media_upload_fields_bind_values(opts) + .map_err(|e| ModelError::InvalidArgument(e.to_string()))?; + let mut query_col = vec![ + "id".to_string(), + "created_at".to_string(), + "updated_at".to_string(), + ]; + let mut query_pl = vec!["?1".to_string(), "?2".to_string(), "?3".to_string()]; + let mut query_vals: Vec<String> = vec![id.to_string(), created_at.clone(), updated_at.clone()]; + for (k, v) in bind_values.iter() { + query_col.push(k.clone()); + query_pl.push(format!("?{}", query_col.len())); + query_vals.push(v.clone()); + } + let query = format!( + "INSERT INTO media_upload ({}) VALUES ({});", + query_col.join(", "), + query_pl.join(", ") + ); + let mut query_builder = sqlx::query(&query); + for value in query_vals.iter() { + query_builder = query_builder.bind(value); + } + query_builder + .execute(db) + .await + .map_err(|e| ModelError::InvalidQuery(e.to_string()))?; + Ok(IModelsId { id }) +} + +fn media_upload_query_get( + opts: IMediaUploadGet, +) -> Result<(String, Vec<IModelsQueryBindValue>), ModelError> { + match opts { + IMediaUploadQueryGet { + list: Some(opts_list), + .. + } => { + let (query, bv) = media_upload_query_get_list(opts_list); + Ok((query, vec![bv])) + } + IMediaUploadQueryGet { + on: Some(opts_on), .. + } => { + let (bv_k, bv) = media_upload_query_bind_values(opts_on); + let query = format!("SELECT * FROM media_upload WHERE {} = ?1;", bv_k); + Ok((query, vec![bv])) + } + _ => Err(ModelError::InvalidQuery( + "model.media_upload.error.query_invalid".to_string(), + )), + } +} + +pub async fn lib_model_media_upload_get( + db: &sqlx::Pool<sqlx::Sqlite>, + opts: IMediaUploadQueryGet, +) -> Result<IMediaUploadGetResolve, ModelError> { + let (query, bind_values) = media_upload_query_get(opts)?; + let mut query_builder = sqlx::query_as::<_, MediaUpload>(&query); + for value in bind_values.iter() { + query_builder = query_builder.bind(value); + } + let results = query_builder + .fetch(db) + .try_collect() + .await + .map_err(|e: sqlx::Error| ModelError::InvalidQuery(e.to_string()))?; + Ok(IModelsResults { results }) +} + +pub async fn lib_model_media_upload_delete( + db: &sqlx::Pool<sqlx::Sqlite>, + opts: IMediaUploadDelete, +) -> Result<IMediaUploadDeleteResolve, ModelError> { + let (bv_k, bv) = media_upload_query_bind_values(opts); + let query = format!("DELETE FROM media_upload WHERE {} = ?1;", bv_k); + let result = sqlx::query(&query) + .bind(bv) + .execute(db) + .await + .map_err(|e: sqlx::Error| ModelError::InvalidQuery(e.to_string()))?; + println!("{:?}", result); + if result.rows_affected() > 0 { + Ok(()) + } else { + Err(ModelError::InvalidQuery( + "models.error.model_not_found".to_string(), + )) + } +} + +pub async fn lib_model_media_upload_update( + db: &sqlx::Pool<sqlx::Sqlite>, + opts: IMediaUploadUpdate, +) -> Result<IMediaUploadUpdateResolve, ModelError> { + let (bv_k, bv) = media_upload_query_bind_values(opts.on); + let bind_values = media_upload_fields_update_bind_values(opts.fields) + .map_err(|e| ModelError::InvalidArgument(e.to_string()))?; + let updated_at = time_created_on(); + let mut query_col = vec!["updated_at".to_string()]; + let mut query_pl = vec!["?2".to_string()]; + let mut query_vals = vec![bv, updated_at]; + for (k, v) in bind_values.iter() { + query_col.push(k.clone()); + query_pl.push(format!("?{}", query_col.len() + 1)); + query_vals.push(v.clone()); + } + let query = format!( + "UPDATE media_upload SET {} WHERE {} = ?1;", + query_col + .iter() + .enumerate() + .map(|(i, col)| format!("{} = {}", col, query_pl[i])) + .collect::<Vec<_>>() + .join(", "), + bv_k + ); + let mut query_builder = sqlx::query(&query); + for value in query_vals.iter() { + query_builder = query_builder.bind(value); + } + let result = query_builder + .execute(db) + .await + .map_err(|e| ModelError::InvalidQuery(e.to_string()))?; + println!("{:?}", result); + if result.rows_affected() > 0 { + Ok(()) + } else { + Err(ModelError::InvalidQuery( + "models.error.model_not_found".to_string(), + )) + } +} diff --git a/crates/core/src/models/mod.rs b/crates/core/src/models/mod.rs @@ -1,6 +1,8 @@ pub mod location_gcs; +pub mod media_upload; pub mod nostr_profile; pub mod nostr_profile_relay; pub mod nostr_relay; pub mod trade_product; pub mod trade_product_location; +pub mod trade_product_media; diff --git a/crates/core/src/models/trade_product_media.rs b/crates/core/src/models/trade_product_media.rs @@ -0,0 +1,73 @@ +use crate::{ + error::ModelError, + types::IModelsResults, + models::trade_product::{trade_product_query_bind_values, TradeProductQueryBindValues}, + models::media_upload::{media_upload_query_bind_values, MediaUploadQueryBindValues}, +}; +use futures::TryStreamExt; + +#[derive(Debug, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct TradeProductMedia { + tb_tp: String, + tb_mu: String, +} + +pub type ITradeProductMediaRelationResolve = bool; +pub type ITradeProductMediaRelationResolveGetAll = IModelsResults<TradeProductMedia>; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct ITradeProductMediaRelation { + pub trade_product: TradeProductQueryBindValues, + pub media_upload: MediaUploadQueryBindValues, +} + +pub async fn lib_model_trade_product_media_set( + db: &sqlx::Pool<sqlx::Sqlite>, + opts: ITradeProductMediaRelation, +) -> Result<ITradeProductMediaRelationResolve, ModelError> { + let (bv_tp_k, bv_tp) = trade_product_query_bind_values(opts.trade_product); + let (bv_mu_k, bv_mu) = media_upload_query_bind_values(opts.media_upload); + let query_vals = vec![bv_tp, bv_mu]; + let query = format!("INSERT INTO trade_product_media (tb_tp, tb_mu) VALUES ((SELECT id FROM trade_product WHERE {} = ?1), (SELECT id FROM media_upload WHERE {} = ?2));", bv_tp_k, bv_mu_k); + let mut query_builder = sqlx::query(&query); + for value in query_vals.iter() { + query_builder = query_builder.bind(value); + } + query_builder + .execute(db) + .await + .map_err(|e| ModelError::InvalidQuery(e.to_string()))?; + Ok(true) +} + +pub async fn lib_model_trade_product_media_unset( + db: &sqlx::Pool<sqlx::Sqlite>, + opts: ITradeProductMediaRelation, +) -> Result<ITradeProductMediaRelationResolve, ModelError> { + let (bv_tp_k, bv_tp) = trade_product_query_bind_values(opts.trade_product); + let (bv_mu_k, bv_mu) = media_upload_query_bind_values(opts.media_upload); + let query_vals = vec![bv_tp, bv_mu]; + let query = format!("DELETE FROM trade_product_media WHERE tb_tp = (SELECT id FROM trade_product WHERE {} = ?1) AND tb_mu = (SELECT id FROM media_upload WHERE {} = ?2);", bv_tp_k, bv_mu_k); + let mut query_builder = sqlx::query(&query); + for value in query_vals.iter() { + query_builder = query_builder.bind(value); + } + query_builder + .execute(db) + .await + .map_err(|e| ModelError::InvalidQuery(e.to_string()))?; + Ok(true) +} + +pub async fn lib_model_trade_product_media_get_all( + db: &sqlx::Pool<sqlx::Sqlite>, +) -> Result<ITradeProductMediaRelationResolveGetAll, ModelError> { + let query = format!("SELECT * FROM trade_product_media;"); + let query_builder = sqlx::query_as::<_, TradeProductMedia>(&query); + let results = query_builder + .fetch(db) + .try_collect() + .await + .map_err(|e: sqlx::Error| ModelError::InvalidQuery(e.to_string()))?; + Ok(IModelsResults { results }) +} diff --git a/crates/tauri/migrations/0005_media_upload.sql b/crates/tauri/migrations/0005_media_upload.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS media_upload ( + id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), + created_at DATETIME NOT NULL CHECK(length(created_at) = 24), + updated_at DATETIME NOT NULL CHECK(length(created_at) = 24), + file_path TEXT NOT NULL, + mime_type TEXT NOT NULL, + res_base TEXT NOT NULL, + res_path TEXT NOT NULL, + label TEXT, + description TEXT +); +\ No newline at end of file diff --git a/crates/tauri/migrations/0005_nostr_profile_relay.sql b/crates/tauri/migrations/0006_nostr_profile_relay.sql diff --git a/crates/tauri/migrations/0006_trade_product_location.sql b/crates/tauri/migrations/0007_trade_product_location.sql diff --git a/crates/tauri/migrations/0008_trade_product_media.sql b/crates/tauri/migrations/0008_trade_product_media.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS trade_product_media ( + tb_tp CHAR(36), + tb_mu CHAR(36), + FOREIGN KEY (tb_tp) REFERENCES trade_product(id) ON DELETE CASCADE, + FOREIGN KEY (tb_mu) REFERENCES media_upload(id) ON DELETE CASCADE, + PRIMARY KEY (tb_tp, tb_mu) +); +\ No newline at end of file diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs @@ -8,6 +8,10 @@ use models::{ model_location_gcs_add, model_location_gcs_delete, model_location_gcs_get, model_location_gcs_update, }, + media_upload::{ + model_media_upload_add, model_media_upload_delete, model_media_upload_get, + model_media_upload_update, + }, nostr_profile::{ model_nostr_profile_add, model_nostr_profile_delete, model_nostr_profile_get, model_nostr_profile_update, @@ -28,6 +32,10 @@ use models::{ model_trade_product_location_get_all, model_trade_product_location_set, model_trade_product_location_unset, }, + trade_product_media::{ + model_trade_product_media_get_all, model_trade_product_media_set, + model_trade_product_media_unset, + }, }; use radroots::Radroots; use tauri::Manager; @@ -82,7 +90,14 @@ pub fn run() { model_nostr_profile_relay_get_all, model_trade_product_location_set, model_trade_product_location_unset, - model_trade_product_location_get_all + model_trade_product_location_get_all, + model_media_upload_add, + model_media_upload_get, + model_media_upload_delete, + model_media_upload_update, + model_trade_product_media_set, + model_trade_product_media_unset, + model_trade_product_media_get_all, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/crates/tauri/src/models/media_upload.rs b/crates/tauri/src/models/media_upload.rs @@ -0,0 +1,60 @@ +use crate::radroots::Radroots; +use radroots_core::{ + models::media_upload::{lib_model_media_upload_add, IMediaUploadAdd, IMediaUploadAddResolve, lib_model_media_upload_get, IMediaUploadGet, IMediaUploadGetResolve, lib_model_media_upload_delete, IMediaUploadDelete, IMediaUploadDeleteResolve, lib_model_media_upload_update, IMediaUploadUpdate, IMediaUploadUpdateResolve}, +}; + +#[tauri::command] +pub async fn model_media_upload_add( + state: tauri::State<'_, Radroots>, + opts: IMediaUploadAdd, +) -> Result<IMediaUploadAddResolve, String> { + match lib_model_media_upload_add(&state.db, opts).await { + Ok(result) => Ok(result), + Err(e) => { + println!("ERROR {}", e); + Err(e.to_string()) + } + } +} + +#[tauri::command] +pub async fn model_media_upload_get( + state: tauri::State<'_, Radroots>, + opts: IMediaUploadGet, +) -> Result<IMediaUploadGetResolve, String> { + match lib_model_media_upload_get(&state.db, opts).await { + Ok(result) => Ok(result), + Err(e) => { + println!("ERROR {}", e); + Err(e.to_string()) + } + } +} + +#[tauri::command] +pub async fn model_media_upload_delete( + state: tauri::State<'_, Radroots>, + opts: IMediaUploadDelete, +) -> Result<IMediaUploadDeleteResolve, String> { + match lib_model_media_upload_delete(&state.db, opts).await { + Ok(result) => Ok(result), + Err(e) => { + println!("ERROR {}", e); + Err(e.to_string()) + } + } +} + +#[tauri::command] +pub async fn model_media_upload_update( + state: tauri::State<'_, Radroots>, + opts: IMediaUploadUpdate, +) -> Result<IMediaUploadUpdateResolve, String> { + match lib_model_media_upload_update(&state.db, opts).await { + Ok(result) => Ok(result), + Err(e) => { + println!("ERROR {}", e); + Err(e.to_string()) + } + } +} diff --git a/crates/tauri/src/models/mod.rs b/crates/tauri/src/models/mod.rs @@ -1,6 +1,8 @@ pub(crate) mod location_gcs; +pub(crate) mod media_upload; pub(crate) mod nostr_profile; pub(crate) mod nostr_profile_relay; pub(crate) mod nostr_relay; pub(crate) mod trade_product; pub(crate) mod trade_product_location; +pub(crate) mod trade_product_media; diff --git a/crates/tauri/src/models/trade_product_media.rs b/crates/tauri/src/models/trade_product_media.rs @@ -0,0 +1,45 @@ +use crate::radroots::Radroots; +use radroots_core::{ + models::trade_product_media::{lib_model_trade_product_media_set, lib_model_trade_product_media_unset, lib_model_trade_product_media_get_all, ITradeProductMediaRelation, ITradeProductMediaRelationResolve, ITradeProductMediaRelationResolveGetAll}, +}; + +#[tauri::command] +pub async fn model_trade_product_media_set( + state: tauri::State<'_, Radroots>, + opts: ITradeProductMediaRelation, +) -> Result<ITradeProductMediaRelationResolve, String> { + match lib_model_trade_product_media_set(&state.db, opts).await { + Ok(result) => Ok(result), + Err(e) => { + println!("ERROR {}", e); + Err(e.to_string()) + } + } +} + +#[tauri::command] +pub async fn model_trade_product_media_unset( + state: tauri::State<'_, Radroots>, + opts: ITradeProductMediaRelation, +) -> Result<ITradeProductMediaRelationResolve, String> { + match lib_model_trade_product_media_unset(&state.db, opts).await { + Ok(result) => Ok(result), + Err(e) => { + println!("ERROR {}", e); + Err(e.to_string()) + } + } +} + +#[tauri::command] +pub async fn model_trade_product_media_get_all( + state: tauri::State<'_, Radroots>, +) -> Result<ITradeProductMediaRelationResolveGetAll, String> { + match lib_model_trade_product_media_get_all(&state.db).await { + Ok(result) => Ok(result), + Err(e) => { + println!("ERROR {}", e); + Err(e.to_string()) + } + } +} diff --git a/src/lib/components/image_upload_control.svelte b/src/lib/components/image_upload_control.svelte @@ -33,10 +33,6 @@ try { const photo_paths_select = await dialog.open_photos(); if (!photo_paths_select) return; - console.log( - `photo_paths_select.results[0] `, - photo_paths_select.results[0], - ); photo_paths = list_assign(photo_paths, photo_paths_select.results); } catch (e) { console.log(`(error) handle_photo_add `, e); diff --git a/src/lib/components/trade_product_list_card.svelte b/src/lib/components/trade_product_list_card.svelte @@ -8,6 +8,7 @@ fmt_geol_latitude, fmt_geol_longitude, Glyph, + ImagePath, locale, route, t, @@ -32,7 +33,7 @@ result: TradeProductBundle; }; $: ({ - result: { trade_product, location_gcs }, + result: { trade_product, location_gcs, media_uploads }, } = basis); $: tradeproduct_qty_sold = 0; @@ -51,7 +52,6 @@ <button class={`flex flex-row px-5 py-1 justify-center items-center bg-layer-1-surface active-layer-1 rounded-full`} on:click|stopPropagation={async () => { - console.log(`hi`); await route(`/models/trade-product/view`, [ [`id`, trade_product.id], ]); @@ -94,48 +94,62 @@ <div class={`flex flex-row h-[10rem] w-full justify-center items-center border-b-line border-b-layer-1-surface-edge`} > - <button - class={`group flex flex-row w-20 justify-center items-center`} - on:click|stopPropagation={async () => {}} - > + {#if media_uploads && media_uploads.length} <div - class={`relative flex flex-col w-full justify-start items-center`} + class={`flex flex-row h-full w-full justify-center items-center`} > - <div - class={`relative flex flex-row py-2 px-[0.8rem] justify-center items-center`} - > - <Glyph + {#each media_uploads as media_upload} + <ImagePath basis={{ - classes: `text-layer-0-glyph group-active:text-layer-0-glyph_a el-re`, - dim: `xl`, - weight: `bold`, - key: `camera`, + path: `${media_upload.res_base}/${media_upload.res_path}.${media_upload.mime_type}`, }} /> + {/each} + </div> + {:else} + <button + class={`group flex flex-row w-20 justify-center items-center`} + on:click|stopPropagation={async () => {}} + > + <div + class={`relative flex flex-col w-full justify-start items-center`} + > <div - class={`absolute top-0 right-0 flex flex-row justify-center items-center`} + class={`relative flex flex-row py-2 px-[0.8rem] justify-center items-center`} > <Glyph basis={{ classes: `text-layer-0-glyph group-active:text-layer-0-glyph_a el-re`, - dim: `xs`, + dim: `xl`, weight: `bold`, - key: `plus`, + key: `camera`, }} /> + <div + class={`absolute top-0 right-0 flex flex-row justify-center items-center`} + > + <Glyph + basis={{ + classes: `text-layer-0-glyph group-active:text-layer-0-glyph_a el-re`, + dim: `xs`, + weight: `bold`, + key: `plus`, + }} + /> + </div> </div> - </div> - <div - class={`absolute -bottom-4 left-0 flex flex-row w-full justify-center items-center`} - > - <p - class={`font-sans font-[500] text-[1rem] text-layer-0-glyph group-active:text-layer-0-glyph_a el-re`} + <div + class={`absolute -bottom-4 left-0 flex flex-row w-full justify-center items-center`} > - {`${$t(`icu.no_*`, { value: `${$t(`common.photos`)}`.toLowerCase() })}`} - </p> + <p + class={`font-sans font-[500] text-[1rem] text-layer-0-glyph group-active:text-layer-0-glyph_a el-re`} + > + {`${$t(`icu.no_*`, { value: `${$t(`common.photos`)}`.toLowerCase() })}`} + </p> + </div> </div> - </div> - </button> + </button> + {/if} </div> {#if location_gcs} <div diff --git a/src/lib/types.ts b/src/lib/types.ts @@ -1,6 +1,7 @@ -import type { LocationGcs, TradeProduct } from "@radroots/models"; +import type { LocationGcs, MediaUpload, TradeProduct } from "@radroots/models"; export type TradeProductBundle = { trade_product: TradeProduct; - location_gcs?: LocationGcs; + location_gcs: LocationGcs; + media_uploads?: MediaUpload[]; }; \ No newline at end of file diff --git a/src/lib/utils/fetch.ts b/src/lib/utils/fetch.ts @@ -1,67 +1,58 @@ -import { PUBLIC_RADROOTS_URL } from "$env/static/public"; -import { http } from "$lib/client"; -import type { UploadFilePresignedUrl } from "@radroots/svelte-lib"; -import { err_msg, type ErrorMessage, type FilePath, type ResultPass } from "@radroots/utils"; - -export const fetch_uploads_presigned_url = async (opts: FilePath): Promise< - | UploadFilePresignedUrl - | ErrorMessage<string> -> => { - try { - const { file_name, mime_type } = opts; - const res = await http.fetch({ - url: `${PUBLIC_RADROOTS_URL}/public/image/upload`, - method: `post`, - data: { - file_name, - mime_type, - }, - }); - if (`err` in res) return res; - else if ( - res.data && - `url` in res.data && - typeof res.data.url === `string` && - `storage_key` in res.data && - typeof res.data.storage_key === `string` && - `file_name` in res.data && - typeof res.data.file_name === `string` - ) { - return { - url: res.data.url, - storage_key: res.data.storage_key, - file_name: res.data.file_name, - }; - } - return err_msg(`request_failure`); - } catch (e) { - console.log(`(error) fetch_uploads_presigned_url `, e); - return err_msg(`network_failure`); - } -}; +import { fs, http, keystore } from "$lib/client"; +import { ks } from "$lib/conf"; +import type { IClientHttpResponseError } from "@radroots/client"; +import { app_nostr_key } from "@radroots/svelte-lib"; +import { err_msg, err_res, nostr_event_sign_attest, type ErrorMessage, type ErrorResponse, type FilePath } from "@radroots/utils"; +import { get as get_store } from "svelte/store"; export const fetch_put_upload = async (opts: { url: string; - file_data: Uint8Array; - mime_type: string; -}): Promise<ResultPass | ErrorMessage<string>> => { + file_path: FilePath; +}): Promise<{ + res_base: string; + res_path: string; +} | ErrorResponse<IClientHttpResponseError> | ErrorMessage<string>> => { try { - const { url, file_data, mime_type } = opts; + const nostr_public_key = get_store(app_nostr_key); + const secret_key = await keystore.get( + ks.keys.nostr_secretkey(nostr_public_key), + ); + if (`err` in secret_key) return err_msg(`error.client.keystore_nostr_secretkey`); + const { url, file_path } = opts; + const file_data = await fs.read_bin(file_path.file_path); + if (!file_data) return err_msg(`error.client.file_path_read_bin_undefined`);; const res = await http.fetch({ url, method: `put`, headers: { - "Content-Type": mime_type, + "Content-Type": file_path.mime_type, + "X-Nostr-Event": JSON.stringify(nostr_event_sign_attest(secret_key.result)), }, + authorization: nostr_public_key, data_bin: file_data, }); - if (`err` in res) return res; - else if (res && res.status === 200) { - return { pass: true }; + console.log(JSON.stringify(res, null, 4), `res`) + if (`err` in res) err_msg(`error.client.request_failure`); + else if (res.error) { + return err_res(res.error); + } + else if ( + res.status === 200 && + res.data && + `pass` in res.data && + `res_base` in res.data && + typeof res.data.res_base === `string` && + `res_path` in res.data && + typeof res.data.res_path === `string` + ) { + return { + res_base: res.data.res_base, + res_path: res.data.res_path, + }; } - return err_msg(`request_failure`); + return err_msg(`error.client.request_unhandled`); } catch (e) { console.log(`(error) fetch_put_upload `, e); - return err_msg(`network_failure`); + return err_msg(`error.client.network_failure`); } }; diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte @@ -46,7 +46,7 @@ ], farmer: [ { - route: `/`, + route: `/test`, label: `Post New`, key: `note-blank`, }, @@ -67,7 +67,7 @@ return; } } - await route(`/models/trade-product`); + await route(`/models/trade-product/add`); }, }, { @@ -224,10 +224,7 @@ icon: `arrows-down-up`, label: `Transactions`, callback: async () => { - const res = await db.nostr_relay_get({ - list: [`on_profile`, { public_key: $app_nostr_key }], - }); - console.log(JSON.stringify(res, null, 4), `res`); + await route(`/models/trade-product`); }, }, { diff --git a/src/routes/(app)/models/trade-product/+page.svelte b/src/routes/(app)/models/trade-product/+page.svelte @@ -39,14 +39,24 @@ const results: TradeProductBundle[] = []; for (const trade_product of trade_products.results) { - const location_gcs = await db.location_gcs_get({ + const location_gcs_res = await db.location_gcs_get({ list: [`on_trade_product`, { id: trade_product.id }], }); + if (`err` in location_gcs_res) { + //@todo + return; + } + const location_gcs = location_gcs_res.results[0]; + const media_uploads_res = await db.media_upload_get({ + list: [`on_trade_product`, { id: trade_product.id }], + }); + results.push({ trade_product, - location_gcs: - `results` in location_gcs - ? location_gcs.results[0] + location_gcs, + media_uploads: + `results` in media_uploads_res + ? media_uploads_res.results : undefined, }); } @@ -54,11 +64,14 @@ const data: LoadData = { results, }; + console.log(JSON.stringify(data, null, 4), `data`); return data; } catch (e) { console.log(`(error) load_data `, e); } }; + + console.log(JSON.stringify(ld, null, 4), `ld`); </script> {#if ld && ld.results.length > 0} diff --git a/src/routes/(app)/models/trade-product/add/+page.svelte b/src/routes/(app)/models/trade-product/add/+page.svelte @@ -1,5 +1,6 @@ <script lang="ts"> - import { db, dialog, fs, geol } from "$lib/client"; + import { PUBLIC_RADROOTS_URL } from "$env/static/public"; + import { db, dialog, geol } from "$lib/client"; import ImageUploadControl from "$lib/components/image_upload_control.svelte"; import ImageUploadEditEnvelope from "$lib/components/image_upload_edit_envelope.svelte"; import MapPointSelectEnvelope from "$lib/components/map_point_select_envelope.svelte"; @@ -7,10 +8,7 @@ import TradeFieldDisplayKv from "$lib/components/trade_field_display_kv.svelte"; import { ascii } from "$lib/conf"; import { el_focus } from "$lib/utils/client"; - import { - fetch_put_upload, - fetch_uploads_presigned_url, - } from "$lib/utils/fetch"; + import { fetch_put_upload } from "$lib/utils/fetch"; import { location_gcs_to_geoc } from "$lib/utils/geocode"; import { kv_init_page, kv_sync } from "$lib/utils/kv"; import { model_location_gcs_add_geocode } from "$lib/utils/models"; @@ -19,6 +17,7 @@ tradeproduct_init_kv, tradeproduct_validate_fields, } from "$lib/utils/trade_product"; + import type { IClientHttpResponseError } from "@radroots/client"; import type { GeocoderReverseResult } from "@radroots/geocoder"; import { trade_product_form_fields, @@ -46,14 +45,17 @@ LayoutTrellis, LayoutTrellisLine, LayoutView, + Loading, locale, Nav, route, SelectElement, + sleep, t, view_effect, } from "@radroots/svelte-lib"; import { + err_system, fiat_currencies, fmt_currency_price, fmt_trade_quantity_tup, @@ -109,6 +111,14 @@ }, ], ]), + success: new Map<number, CarouselParam>([ + [ + 0, + { + label_next: ``, + }, + ], + ]), }, default: { tradepr_key: `coffee`, @@ -116,7 +126,7 @@ }; let view_init: View = `c_1`; - type View = `c_1`; + type View = `c_1` | `success`; let view: View = view_init; $: { view_effect<View>(view); @@ -125,6 +135,7 @@ let load_page = false; let load_submit = false; + let tradepr_success_id = ``; let tradepr_photo_paths: string[] = []; let tradepr_photo_edit: { index: number; file_path: string } | undefined = undefined; @@ -434,6 +445,8 @@ const submit = async (): Promise<void> => { try { + if (load_submit) return; + load_submit = true; if (!tradepr_photo_paths.length) { const confirm = await dialog.confirm({ message: `${`${$t(`icu.the_listing_will_be_created_without_a_*`, { value: `${$t(`common.photo`)}`.toLowerCase() })}`}. ${$t(`common.do_you_want_to_continue_q`)}`, @@ -455,6 +468,7 @@ if (`result` in location_gcs_get_i) { location_gcs_id = location_gcs_get_i.result.id; } else { + //@todo add check for existing geohash if (tradepr_lgc_map_point && tradepr_lgc_map_geoc) { const location_gcs_add_geocode = await model_location_gcs_add_geocode({ @@ -467,7 +481,9 @@ ) { await dialog.alert( `err` in location_gcs_add_geocode - ? `${$t(`icu.the_*_is_incomplete`, { value: `${$t(`models.location_gcs.fields.${location_gcs_add_geocode.err}.label`)}`.toLowerCase() })}` + ? err_system(location_gcs_add_geocode.err) + ? `${$t(`error.client.database_read_failure`)}` + : `${$t(`icu.the_*_is_incomplete`, { value: `${$t(`models.location_gcs.fields.${location_gcs_add_geocode.err}.label`)}`.toLowerCase() })}` : `${$t(`${location_gcs_add_geocode.err_s[0]}`)}`, ); return; @@ -491,93 +507,60 @@ } return; } - const trade_product_fields = await trade_product_fields_validate({ - field_defaults: [ - [`price_qty_amt`, num_str(1)], - [`profile`, `natural`], - [`qty_avail`, num_str($tradepr_qty_avail)], - [`year`, year_curr()], - ], - }); - console.log( - JSON.stringify(trade_product_fields, null, 4), - `trade_product_fields`, - ); - if (`err` in trade_product_fields) { - await dialog.alert( - `${$t(`icu.the_*_is_incomplete`, { value: `${$t(`models.trade_product.fields.${trade_product_fields.err}.label`)}`.toLowerCase() })}`, - ); - return; - } - const trade_product_add = - await db.trade_product_add(trade_product_fields); - if (`err` in trade_product_add || `err_s` in trade_product_add) { - await dialog.alert( - `err` in trade_product_add - ? `${$t(`icu.the_*_is_incomplete`, { value: `${$t(`models.trade_product.fields.${trade_product_add.err}.label`)}`.toLowerCase() })}` - : `${$t(`${trade_product_add.err_s[0]}`)}`, - ); - return; - } - const trade_product_location_set = - await db.trade_product_location_set({ - trade_product: { - id: trade_product_add.id, - }, - location_gcs: { - id: location_gcs_get_c.result.id, - }, - }); - if (!(`pass` in trade_product_location_set)) { - await dialog.alert( - `${$t(`common.failure_to_process_the_request`)}`, - ); - return; - } + // photos const photo_path_uploads: { - file_name: string; - storage_key: string; + file_path: FilePath; + res_base: string; + res_path: string; }[] = []; const photo_path_uploads_err: { file_path: FilePath; - err_trace: `url` | `put`; err_msg: string; }[] = []; + const photo_path_uploads_error: IClientHttpResponseError[] = []; if (tradepr_photo_paths.length) { for (const photo_path of tradepr_photo_paths) { const file_path = parse_file_path(photo_path); if (!file_path) continue; - const file_data = await fs.read_bin(photo_path); - if (!file_data) continue; - const uploads_presigned_url = - await fetch_uploads_presigned_url(file_path); - if (`err` in uploads_presigned_url) { - photo_path_uploads_err.push({ - file_path, - err_trace: `url`, - err_msg: uploads_presigned_url.err, - }); - continue; - } + const url = `${PUBLIC_RADROOTS_URL}/public/upload/image`; //@todo const put_upload = await fetch_put_upload({ - url: uploads_presigned_url.url, - file_data, - mime_type: file_path.mime_type, + url, + file_path, }); if (`err` in put_upload) { photo_path_uploads_err.push({ file_path, - err_trace: `put`, err_msg: put_upload.err, }); continue; + } else if (`error` in put_upload) { + photo_path_uploads_error.push(put_upload.error); + continue; } photo_path_uploads.push({ - file_name: uploads_presigned_url.file_name, - storage_key: uploads_presigned_url.storage_key, + file_path, + res_base: put_upload.res_base, + res_path: put_upload.res_path, }); } } + if (photo_path_uploads_error.length) { + const confirm = await dialog.confirm({ + message: `${$t(photo_path_uploads_error[0].message)}`, //@todo + ok_label: photo_path_uploads_error[0].label_ok + ? `${$t(photo_path_uploads_error[0].label_ok)}` || + undefined + : undefined, + cancel_label: photo_path_uploads_error[0].label_cancel + ? `${$t(photo_path_uploads_error[0].label_cancel)}` || + undefined + : undefined, + }); + if (confirm) { + console.log(`@todo add profile name`); + return; + } + } if (photo_path_uploads_err.length) { await dialog.alert( `${$t(`icu.there_was_a_failure_while_*`, { @@ -590,14 +573,97 @@ })}`.toLowerCase(), })}`, ); + return; } + const media_upload_added: string[] = []; if (photo_path_uploads.length) { - console.log(`@todo add photo models`); + for (const photo_path_upload of photo_path_uploads) { + const media_upload_add = await db.media_upload_add({ + file_path: photo_path_upload.file_path.file_path, + mime_type: photo_path_upload.file_path.mime_type, + res_base: photo_path_upload.res_base, + res_path: photo_path_upload.res_path, + }); + if ( + `err` in media_upload_add || + `err_s` in media_upload_add + ) + continue; //@todo + media_upload_added.push(media_upload_add.id); + } } - await route(`/models/trade-product`); + // trade product + const trade_product_fields = await trade_product_fields_validate({ + field_defaults: [ + [`price_qty_amt`, num_str(1)], + [`profile`, `natural`], + [`qty_avail`, num_str(Math.max($tradepr_qty_avail, 1))], + [`year`, year_curr()], + ], + }); + if (`err` in trade_product_fields) { + await dialog.alert( + `${$t(`icu.the_*_is_incomplete`, { value: `${$t(`models.trade_product.fields.${trade_product_fields.err}.label`)}`.toLowerCase() })}`, + ); + return; + } + const trade_product_add = + await db.trade_product_add(trade_product_fields); + if (`err` in trade_product_add || `err_s` in trade_product_add) { + await dialog.alert( + `err` in trade_product_add + ? `${$t(`icu.the_*_is_incomplete`, { value: `${$t(`models.trade_product.fields.${trade_product_add.err}.label`)}`.toLowerCase() })}` + : `${$t(`${trade_product_add.err_s[0]}`)}`, + ); + return; + } + let trade_product_location_set_err: string = ``; + const trade_product_location_set = + await db.trade_product_location_set({ + trade_product: { + id: trade_product_add.id, + }, + location_gcs: { + id: location_gcs_get_c.result.id, + }, + }); + if (`err` in trade_product_location_set) { + trade_product_location_set_err = trade_product_location_set.err; + } + const trade_product_media_set_err: string[] = []; + for (const media_upload of media_upload_added) { + const trade_product_media_set = + await db.trade_product_media_set({ + trade_product: { + id: trade_product_add.id, + }, + media_upload: { + id: media_upload, + }, + }); + if (`err` in trade_product_media_set) { + trade_product_media_set_err.push( + trade_product_media_set.err, + ); + } + } + + if (trade_product_location_set_err) { + //@todo + } + + if (trade_product_media_set_err.length) { + //@todo + } + + handle_view(`success`); + await sleep(2000); + await route(`/`); } catch (e) { console.log(`(error) submit `, e); + } finally { + load_submit = false; } }; </script> @@ -1589,21 +1655,94 @@ await submit(); }} > - {`${$t(`common.post`)}`} + {#if load_submit} + <Loading /> + {:else} + {`${$t(`common.post`)}`} + {/if} </button> </LayoutTrellis> </div> </div> </div> + <div + data-view={`success`} + class={`hidden flex flex-col h-full w-full justify-start items-center`} + > + <div + data-carousel-container={`success`} + class={`carousel-container flex h-full w-full`} + > + <div + data-carousel-item={`success`} + class={`carousel-item flex flex-col w-full justify-start items-center ${view === `success` ? `fade-in-long` : ``}`} + > + <LayoutTrellis> + <div + class={`flex flex-col h-trellis_centered_${$app_layout} w-full justify-center items-center`} + > + <Glyph + basis={{ + classes: `text-success text-[72px]`, + weight: `bold`, + key: `seal-check`, + }} + /> + <div + class={`flex flex-col pt-1 justify-start items-center`} + > + <div + class={`flex flex-row w-full justify-center items-center`} + > + <p + class={`font-sans font-[400] text-[1.3rem] text-layer-0-glyph`} + > + {`${$t(`common.complete`)}`} + </p> + </div> + <div + class={`flex flex-row w-full justify-center items-center`} + > + <button + class={`flex flex-row justify-center items-center`} + on:click={async () => { + if (tradepr_success_id) + await route( + `/models/trade-product/view`, + [ + [ + `id`, + tradepr_success_id, + ], + ], + ); + }} + > + <p + class={`font-sans font-[400] text-[1.1rem] text-layer-0-glyph`} + > + {`${$t(`icu.click_to_*`, { value: `${$t(`icu.view_the_*`, { value: `${$t(`common.product`)}` })}`.toLowerCase() })}`} + </p> + </button> + </div> + </div> + </div> + </LayoutTrellis> + </div> + </div> + </div> </LayoutView> {/if} <Nav basis={{ prev: { - label: `${$t(`common.back`)}`, - route: `/models/trade-product`, + label: + view === `success` + ? `${$t(`common.home`)}` + : `${$t(`common.back`)}`, + route: view === `success` ? `/` : `/models/trade-product`, prevent_route: - view === `c_1` && $carousel_index === 0 + (view === `c_1` && $carousel_index === 0) || view === `success` ? undefined : { callback: async () => { @@ -1616,12 +1755,14 @@ }, title: { label: { - value: `${$t(`icu.new_*`, { value: `${$t(`common.product`)}` })}`, + value: + view === `success` + ? `` + : `${$t(`icu.new_*`, { value: `${$t(`common.product`)}` })}`, }, callback: async () => {}, }, option: { - loading: load_submit, label: { value: $carousel_num > 1 @@ -1629,7 +1770,8 @@ : page_param.carousel[view].get($carousel_index) ?.label_next || ``, glyph: - $carousel_index === $carousel_index_max + $carousel_index === $carousel_index_max || + view === `success` ? undefined : { key: `caret-right`, diff --git a/src/routes/(app)/models/trade-product/view/+page.svelte b/src/routes/(app)/models/trade-product/view/+page.svelte @@ -34,16 +34,18 @@ } const { result: trade_product } = _trade_product; - const location_gcs = await db.location_gcs_get({ + const location_gcs_res = await db.location_gcs_get({ list: [`on_trade_product`, { id: trade_product.id }], }); + if (`err` in location_gcs_res) { + //@todo + return; + } + const location_gcs = location_gcs_res.results[0]; const data: LoadData = { trade_product, - location_gcs: - `results` in location_gcs - ? location_gcs.results[0] - : undefined, + location_gcs, }; return data; } catch (e) { diff --git a/src/routes/(app)/test/+page.svelte b/src/routes/(app)/test/+page.svelte @@ -1,49 +1,24 @@ <script lang="ts"> - import { db } from "$lib/client"; - import type { LocationGcs } from "@radroots/models"; - import { LayoutView, Nav } from "@radroots/svelte-lib"; - import { onMount } from "svelte"; + import { LayoutView, Nav, t } from "@radroots/svelte-lib"; - let list1: LocationGcs[] = []; - - onMount(async () => { + const test1 = async (): Promise<void> => { try { - const res1 = await db.location_gcs_get({ list: [`all`] }); - if (!(`err` in res1)) list1 = res1.results; } catch (e) { - } finally { + console.log(`(error) test1 `, e); } - }); + }; </script> <LayoutView> <div class={`flex flex-col w-full justify-center items-center`}> - <p class={`font-sans font-[400] text-layer-0-glyph`}> - {`Models`} - </p> - <div class={`flex flex-col w-full px-4 justify-center items-center`}> - {#each list1 as li} - <div class={`flex flex-col justify-start items-center`}> - <p - class={`font-sans font-[400] text-layer-0-glyph break-all`} - > - {JSON.stringify(li, null, 4)} - </p> - <div - class={`flex flex-row w-full justify-center items-center`} - > - <button - class={`flex flex-row justify-center items-center`} - on:click={async () => { - await db.location_gcs_delete({ id: li.id }); - }} - > - {`del`} - </button> - </div> - </div> - {/each} - </div> + <button + class={`flex flex-row justify-center items-center`} + on:click={async () => { + await test1(); + }} + > + test1 + </button> </div> </LayoutView> <Nav diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte @@ -108,4 +108,6 @@ <Controls /> <CssStatic /> <CssStyles /> -<div class="hidden h-entry_guide h-entry_line h-[calc(100vh-12%)]" /> +<div + class="hidden h-entry_guide h-entry_line h-[calc(100vh-12%)] h-trellis_centered_mobile_base h-trellis_centered_mobile_y" +/> diff --git a/tailwind.config.ts b/tailwind.config.ts @@ -18,7 +18,9 @@ const heights_responsive = { view_mobile_base: `31rem`, view_mobile_y: `42rem`, view_offset_mobile_base: `1rem`, - view_offset_mobile_y: `2rem` + view_offset_mobile_y: `2rem`, + trellis_centered_mobile_base: `32rem`, + trellis_centered_mobile_y: `35rem` }; const heights = { @@ -32,7 +34,7 @@ const heights = { envelope_top: `56px`, toast_min: `56px`, envelope_button: `50px`, - line_label: `1.75rem` + line_label: `1.75rem`, };