prep for actual upload

This commit is contained in:
2025-09-03 16:52:39 +02:00
parent d06c2f6568
commit 1155743a99
11 changed files with 407 additions and 59 deletions

View File

@@ -118,11 +118,11 @@ site-root = "target/site"
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./styles/style.scss"
style-file = "./styles/style.css"
tailwind-input-file = "./styles/tailwind.css"
# The locales files
watch-additional-files = ["locales"]
watch-additional-files = ["locales", "styles"]
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"

Binary file not shown.

View File

@@ -81,11 +81,11 @@ pub fn App() -> impl IntoView {
<LoadingBarProvider>
<ToasterProvider>
<Router>
<Layout>
<div>
<LayoutHeader attr:style="background-color: #0078ffaa; padding: 20px;">
<NavBar user=user />
</LayoutHeader>
<Layout attr:style="padding: 20px;">
<div style="padding: 20px;">
<main>
<FlatRoutes fallback=|| "Not found.">
// Route
@@ -105,8 +105,8 @@ pub fn App() -> impl IntoView {
/>
</FlatRoutes>
</main>
</Layout>
</Layout>
</div>
</div>
</Router>
<BackTop1 />
</ToasterProvider>

View File

@@ -0,0 +1,22 @@
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use thaw::*;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ImageUpload {
pub name: String,
pub blob_url: String,
pub data: Vec<u8>,
}
#[component]
pub fn FilePreview(file: ImageUpload) -> impl IntoView {
view! {
<Flex vertical=true inline=false>
<div style="margin: 10px;">
<div style="align: center; height: 194px;"><img src={file.blob_url} style="max-height: 194px" /></div>
<div style="align: center"><p>{file.name}</p></div>
</div>
</Flex>
}
}

View File

@@ -9,3 +9,9 @@ pub use ad::*;
mod category;
pub use category::*;
mod file_preview;
pub use file_preview::*;
mod upload;
pub use upload::*;

View File

@@ -0,0 +1,150 @@
.thaw-select {
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: center;
box-sizing: border-box;
font-family: var(--fontFamilyBase);
}
.thaw-select::after {
content: "";
position: absolute;
right: 0px;
left: 0px;
bottom: 0px;
height: var(--borderRadiusMedium);
background-image: linear-gradient(
0deg,
var(--colorCompoundBrandStroke) 0%,
var(--colorCompoundBrandStroke) 50%,
transparent 50%,
transparent 100%
);
transition-delay: var(--curveAccelerateMid);
transition-duration: var(--durationUltraFast);
transition-property: transform;
transform: scaleX(0);
box-sizing: border-box;
border-radius: 0 0 var(--borderRadiusMedium) var(--borderRadiusMedium);
}
.thaw-select:focus-within::after {
transition-delay: var(--curveDecelerateMid);
transition-duration: var(--durationNormal);
transition-property: transform;
transform: scaleX(1);
}
.thaw-select__select {
flex-grow: 1;
padding-right: calc(
var(--spacingHorizontalMNudge) + 20px + var(--spacingHorizontalXXS) +
var(--spacingHorizontalXXS)
);
padding-left: calc(
var(--spacingHorizontalMNudge) + var(--spacingHorizontalXXS)
);
padding-top: 0px;
padding-bottom: 0px;
max-width: 100%;
height: 32px;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
line-height: var(--lineHeightBase300);
font-weight: var(--fontWeightRegular);
font-size: var(--fontSizeBase300);
font-family: var(--fontFamilyBase);
border-radius: var(--borderRadiusMedium);
border: 1px solid var(--colorNeutralStroke1);
border-bottom-color: var(--colorNeutralStrokeAccessible);
box-shadow: none;
appearance: none;
box-sizing: border-box;
cursor: pointer;
}
.thaw-select--small .thaw-select__select {
height: 24px;
padding-right: calc(
var(--spacingHorizontalSNudge) + 16px + var(--spacingHorizontalXXS) +
var(--spacingHorizontalXXS)
);
padding-left: calc(
var(--spacingHorizontalSNudge) + var(--spacingHorizontalXXS)
);
}
.thaw-select--large .thaw-select__select {
height: 40px;
padding-right: calc(
var(--spacingHorizontalM) + 24px + var(--spacingHorizontalSNudge) +
var(--spacingHorizontalSNudge)
);
padding-left: calc(
var(--spacingHorizontalM) + var(--spacingHorizontalSNudge)
);
}
.thaw-select__select:focus {
outline-color: transparent;
outline-style: solid;
outline-width: 2px;
}
.thaw-select:hover {
border-bottom-color: var(--colorNeutralStrokeAccessible);
border-left-color: var(--colorNeutralStroke1Hover);
border-right-color: var(--colorNeutralStroke1Hover);
border-top-color: var(--colorNeutralStroke1Hover);
}
.thaw-select:active {
border-bottom-color: var(--colorNeutralStrokeAccessible);
border-left-color: var(--colorNeutralStroke1Pressed);
border-right-color: var(--colorNeutralStroke1Pressed);
border-top-color: var(--colorNeutralStroke1Pressed);
}
.thaw-select__icon {
position: absolute;
width: 20px;
height: 20px;
right: var(--spacingHorizontalMNudge);
display: block;
pointer-events: none;
color: var(--colorNeutralStrokeAccessible);
box-sizing: border-box;
font-size: 20px;
}
.thaw-select--small .thaw-select__icon {
width: 16px;
height: 16px;
right: var(--spacingHorizontalSNudge);
font-size: 16px;
}
.thaw-select--large .thaw-select__icon {
width: 24px;
height: 24px;
right: var(--spacingHorizontalM);
font-size: 24px;
}
.thaw-select__icon svg {
display: block;
line-height: 0;
}
.thaw-select--disabled > .thaw-select__select {
border-color: var(--colorNeutralStrokeDisabled);
border-bottom-color: var(--colorNeutralStrokeDisabled);
background-color: var(--colorTransparentBackground);
color: var(--colorNeutralForegroundDisabled);
cursor: not-allowed;
}
.thaw-select--disabled > .thaw-select__icon {
color: var(--colorNeutralForegroundDisabled);
}

View File

@@ -0,0 +1,109 @@
pub use web_sys::FileList;
use leptos::{ev, html, prelude::*};
use thaw_utils::{ArcOneCallback, add_event_listener, class_list, mount_style};
#[component]
pub fn Upload1(
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] id: MaybeProp<String>,
/// A string specifying a name for the input control.
/// This name is submitted along with the control's value when the form data is submitted.
#[prop(optional, into)]
name: MaybeProp<String>,
/// The accept type of upload.
#[prop(optional, into)]
accept: Signal<String>,
/// Allow multiple files to be selected.
#[prop(optional, into)]
multiple: Signal<bool>,
/// Customize upload request.
#[prop(optional, into)]
custom_request: Option<ArcOneCallback<FileList>>,
children: Children,
) -> impl IntoView {
mount_style("upload", include_str!("./upload.css"));
let input_ref = NodeRef::<html::Input>::new();
let trigger_ref = NodeRef::<html::Div>::new();
Effect::new(move |_| {
let Some(trigger_el) = trigger_ref.get() else {
return;
};
let handle = add_event_listener(trigger_el, ev::click, move |_| {
if let Some(input_ref) = input_ref.get_untracked() {
input_ref.click();
}
});
on_cleanup(move || {
handle.remove();
});
});
let on_file_addition = move |files: FileList| {
if let Some(custom_request) = custom_request.as_ref() {
custom_request(files);
}
};
let on_change = {
let on_file_addition = on_file_addition.clone();
move |_| {
if let Some(input_ref) = input_ref.get_untracked() {
if let Some(files) = input_ref.files() {
on_file_addition(files);
}
input_ref.set_value("");
}
}
};
let is_trigger_dragover = RwSignal::new(false);
let on_trigger_drop = move |event: ev::DragEvent| {
event.prevent_default();
if let Some(data) = event.data_transfer()
&& let Some(files) = data.files()
{
on_file_addition(files);
}
is_trigger_dragover.set(false);
};
let on_trigger_dragover = move |event: ev::DragEvent| {
event.prevent_default();
is_trigger_dragover.set(true);
};
let on_trigger_dragenter = move |event: ev::DragEvent| {
event.prevent_default();
};
let on_trigger_dragleave = move |event: ev::DragEvent| {
event.prevent_default();
is_trigger_dragover.set(false);
};
view! {
<div
node_ref=trigger_ref
class=class_list![
"thaw-upload",
("thaw-upload--drag-over", move || is_trigger_dragover.get()),
class
]
on:drop=on_trigger_drop
on:dragover=on_trigger_dragover
on:dragenter=on_trigger_dragenter
on:dragleave=on_trigger_dragleave>
{children()}
</div>
<input
class="thaw-upload__input"
id=move || id.get()
name=move || name.get()
node_ref=input_ref
type="file"
accept=move || accept.get()
multiple=move || multiple.get()
on:change=on_change
/>
}
}

View File

@@ -0,0 +1,16 @@
.thaw-upload__input {
width: 0;
height: 0;
opacity: 0;
}
.thaw-upload {
flex: 1;
overflow-x: scroll;
overflow-y: hidden;
border: var(--strokeWidthThin) dashed var(--colorNeutralStroke1);
}
.thaw-upload--drag-over {
border: var(--strokeWidthThin) dashed var(--colorBrandForeground1);
}

View File

@@ -1,20 +1,29 @@
use crate::{
app::pages::get_categories,
app::{
components::{FilePreview, ImageUpload, Upload1},
models::Category,
pages::get_categories,
},
auth::{Logout, User},
error_template::ErrorTemplate,
};
use chrono::prelude::*;
use leptos::prelude::*;
use leptos::{logging::log, task::spawn_local};
use serde::{Deserialize, Serialize};
use thaw::*;
use web_sys::js_sys::Uint8Array;
#[derive(Clone, Serialize, Deserialize)]
pub struct ImageUpload {
pub name: String,
pub blob_url: String,
pub data: Vec<u8>,
#[server]
pub async fn go_upload(
category: i64,
date: NaiveDate,
photo: ImageUpload,
) -> Result<(), ServerFnError> {
// ..
log!("{}", category);
log!("{}", date);
log!("{}", photo.name);
Ok(())
}
#[component]
@@ -22,8 +31,6 @@ pub fn Account(
user: Resource<Result<Option<User>, ServerFnError>>,
action: ServerAction<Logout>,
) -> impl IntoView {
let toaster = ToasterInjection::expect_context();
let user = move || user.get().unwrap().unwrap().unwrap();
let categories = Resource::new(move || (), move |_| get_categories());
@@ -48,35 +55,31 @@ pub fn Account(
let arr: Uint8Array = Uint8Array::new(&jsval);
let data: Vec<u8> = arr.to_vec();
// verify data is an image, if not, reject
// todo: verify data is an image, if not, reject
files.write().push(ImageUpload {
name: file.name(),
blob_url,
data,
});
log!("{}", file.name());
});
}
// toaster.dispatch_toast(
// move || {
// view! {
// <Toast>
// <ToastBody>
// {format!("Number of uploaded files: {len}")}
// </ToastBody>
// </Toast>
// }
// },
// Default::default(),
// );
};
Effect::new(move || {
let cat = selected_category.get();
log!("{}", cat);
});
let disabled = RwSignal::new(false);
let upload = move |_| {
disabled.set(true);
let category = selected_category.get().parse().unwrap();
let date = date.get();
let photos = files.get();
for file in photos {
spawn_local(async move {
let _ = dbg!(go_upload(category, date, file).await);
});
}
};
view! {
<Flex vertical=false inline=true>
@@ -92,13 +95,7 @@ pub fn Account(
<Divider />
</div>
// preview list of uploaded images
// with a max amount probably and a ...
// dropdown for which field <- which is the category
// date picker for which day <- which will become the album, or add to if it already exists
<Flex vertical=true inline=true>
<Flex vertical=true inline=false>
<Transition fallback=move || view! { <p>"Loading..."</p> }>
<ErrorBoundary fallback=|errors| {
@@ -118,7 +115,7 @@ pub fn Account(
} else {
categories.into_iter().map(move |category| {
view! {
<option>{category.name}</option>
<option value={category.id.to_string()}>{category.name}</option>
}.into_any()
})
.collect_view()
@@ -133,25 +130,35 @@ pub fn Account(
</Transition>
<DatePicker value=date/>
<Upload custom_request>
<UploadDragger>"Sleep fotos naar hier om te uploaden."</UploadDragger>
</Upload>
<div class="">
<Transition fallback=move || view! { <p>"Loading..."</p> }>
<ErrorBoundary fallback=|errors| {
view! { <ErrorTemplate errors=errors/> }
}>
{ move || files().into_iter().map(move |file| {
view! {
<img src={file.blob_url} style="width: 100px" />
<p>{file.name}</p>
}.into_any()
})
.collect_view() }
</ErrorBoundary>
</Transition>
<div style="display: flex; height: 250px; width: 100%;">
<Upload1 custom_request multiple=RwSignal::new(true)>
<Flex vertical=false inline=false gap=FlexGap::WH(0, 0)>
<Transition fallback=move || view! { <p>"Loading..."</p> }>
<ErrorBoundary fallback=|errors| {
view! { <ErrorTemplate errors=errors/> }
}>
{ move || if files().is_empty() {
view! {
<div style="margin: auto;">"Sleep hier de fotos om te uploaden."</div>
}.into_any()
} else {
files().into_iter().map(move |file| {
view! {
<FilePreview file />
}.into_any()
}).collect_view().into_any()
}}
</ErrorBoundary>
</Transition>
</Flex>
</Upload1>
</div>
<Button button_type=ButtonType::Button disabled on_click=upload>
"Upload"
</Button>
</Flex>
}
}

View File

@@ -18,4 +18,22 @@ body {
.card_preview {
background: #333333;
}
}
.parent {
background: skyblue;
width: 350px;
overflow-x: auto;
padding: 40px 20px;
}
.parent>.content-wrapper {
display: inline-block;
white-space: nowrap;
min-width: 100%;
}
.parent>.content-wrapper>.child {
background: springgreen;
white-space: normal;
}

View File

@@ -1,2 +1,22 @@
@import "tailwindcss" source("../src");
.parent {
background: skyblue;
width: 350px;
overflow-x: auto;
padding: 40px 20px;
}
.parent>.content-wrapper {
display: inline-block;
white-space: nowrap;
min-width: 100%;
}
.parent>.content-wrapper>.child {
background: springgreen;
white-space: normal;
}