This commit is contained in:
2025-09-06 17:22:47 +02:00
parent 1155743a99
commit ba69ce50ee
13 changed files with 1376 additions and 79 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
dotenv

1274
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -79,11 +79,15 @@ lettre = { version = "0.11.18", default-features = false, features = [
"tokio1-rustls-tls", "tokio1-rustls-tls",
"tracing", "tracing",
], optional = true } ], optional = true }
aws-config = { version = "1.1", features = [
"behavior-version-latest",
], optional = true }
aws-sdk-s3 = { version = "1.104", optional = true }
tera = { version = "1.20.0", default-features = false, optional = true } tera = { version = "1.20.0", default-features = false, optional = true }
[features] [features]
default = ["ssr"]
hydrate = ["leptos/hydrate", "thaw/csr"]
ssr = [ ssr = [
"dep:axum", "dep:axum",
"dep:tower", "dep:tower",
@@ -102,9 +106,14 @@ ssr = [
"dep:leptos_axum", "dep:leptos_axum",
"thaw/ssr", "thaw/ssr",
"lettre", "lettre",
"aws-config",
"aws-sdk-s3",
"tera", "tera",
] ]
default = ["ssr"]
hydrate = ["leptos/hydrate", "thaw/csr"]
[package.metadata.cargo-all-features] [package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"] denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]] skip_feature_sets = [["ssr", "hydrate"]]

Binary file not shown.

View File

@@ -9,11 +9,15 @@ CREATE TABLE IF NOT EXISTS albums (
FOREIGN KEY(category_id) REFERENCES categories(id) FOREIGN KEY(category_id) REFERENCES categories(id)
); );
-- s3://airsoftfotos/{category:1}/{event-date:3-9-2025}/{id:1}/airsoftfotos.nl_{uuid}.webp -- processed
-- s3://airsoftfotos/{category:1}/{event-date:3-9-2025}/{id:1}/{uuid}_{filename} -- original file
CREATE TABLE IF NOT EXISTS photos ( CREATE TABLE IF NOT EXISTS photos (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- might just store id as the filename in S3... or at least a folder with the og image, and a processed image (lower res and watermarked)
album_id INTEGER NOT NULL, album_id INTEGER NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
filename TEXT NOT NULL, filename TEXT NOT NULL, -- is this the OG filename, or the filename we need to request from S3
description TEXT NULL, description TEXT NULL,
visible BOOLEAN NOT NULL DEFAULT TRUE, visible BOOLEAN NOT NULL DEFAULT TRUE,
upvotes INTEGER NOT NULL, upvotes INTEGER NOT NULL,

View File

@@ -10,7 +10,7 @@ use leptos::prelude::*;
use leptos_meta::*; use leptos_meta::*;
use leptos_router::{components::*, path}; use leptos_router::{components::*, path};
use thaw::{ConfigProvider, Layout, LayoutHeader, LoadingBarProvider, Theme, ToasterProvider}; use thaw::{ConfigProvider, LayoutHeader, LoadingBarProvider, Theme, ToasterProvider};
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub mod ssr { pub mod ssr {
@@ -28,6 +28,17 @@ pub mod ssr {
.ok_or_else(|| ServerFnError::ServerError("Lettre missing".into())) .ok_or_else(|| ServerFnError::ServerError("Lettre missing".into()))
} }
pub fn s3() -> Result<aws_sdk_s3::Client, ServerFnError> {
use aws_sdk_s3::Client;
let Some(shared_config) =
with_context::<AppState, _>(|state| state.aws_shared_config.clone())
else {
return Err(ServerFnError::ServerError("s3 init error".into()));
};
Ok(Client::new(&shared_config))
}
pub async fn auth() -> Result<AuthSession, ServerFnError> { pub async fn auth() -> Result<AuthSession, ServerFnError> {
let auth = leptos_axum::extract().await?; let auth = leptos_axum::extract().await?;
Ok(auth) Ok(auth)

View File

@@ -15,3 +15,6 @@ pub use file_preview::*;
mod upload; mod upload;
pub use upload::*; pub use upload::*;
mod progress_bar;
pub use progress_bar::*;

View File

@@ -0,0 +1,29 @@
.thaw-progress-bar {
display: block;
width: 100%;
height: 2px;
background-color: var(--colorNeutralBackground6);
overflow: hidden;
border-radius: var(--borderRadiusMedium);
}
.thaw-progress-bar__bar {
transition-timing-function: ease;
transition-duration: 0.3s;
transition-property: width;
height: 100%;
background-color: var(--colorCompoundBrandBackground);
border-radius: inherit;
}
.thaw-progress-bar--error .thaw-progress-bar__bar {
background-color: var(--colorPaletteRedBackground3);
}
.thaw-progress-bar--warning .thaw-progress-bar__bar {
background-color: var(--colorPaletteDarkOrangeBackground3);
}
.thaw-progress-bar--success .thaw-progress-bar__bar {
background-color: var(--colorPaletteGreenBackground3);
}

View File

@@ -0,0 +1,43 @@
use leptos::prelude::*;
use thaw::ProgressBarColor;
use thaw_utils::{class_list, mount_style};
#[component]
pub fn ProgressBar1(
#[prop(optional, into)] class: MaybeProp<String>,
/// A decimal number between 0 and 1 (or between 0 and max if given),
/// which specifies how much of the task has been completed.
#[prop(into, optional)]
value: Signal<i64>,
/// The maximum value, which indicates the task is complete.
/// The ProgressBar bar will be full when value equals max.
#[prop(default = 100.into(), optional)]
max: Signal<i64>,
/// ProgressBar color.
#[prop(into, optional)]
color: Signal<ProgressBarColor>,
) -> impl IntoView {
mount_style("progress-bar", include_str!("./progress-bar.css"));
let style = move || {
let max = max.get();
let value = value.get().max(0).min(max);
format!("width: {:.02}%;", (value as f64 / max as f64 * 100.0))
};
view! {
<div
class=class_list![
"thaw-progress-bar",
move || format!("thaw-progress-bar--{}", color.get().as_str()),
class
]
role="progressbar"
aria_valuemax=move || max.get()
aria-valuemin="0"
aria-valuenow=move || value.get()
>
<div class="thaw-progress-bar__bar" style=style></div>
</div>
}
}

View File

@@ -1,7 +1,6 @@
use crate::{ use crate::{
app::{ app::{
components::{FilePreview, ImageUpload, Upload1}, components::{FilePreview, ImageUpload, ProgressBar1, Upload1},
models::Category,
pages::get_categories, pages::get_categories,
}, },
auth::{Logout, User}, auth::{Logout, User},
@@ -9,7 +8,10 @@ use crate::{
}; };
use chrono::prelude::*; use chrono::prelude::*;
use leptos::prelude::*; use leptos::prelude::*;
use leptos::{logging::log, task::spawn_local}; use leptos::{
logging::log,
task::{spawn, spawn_local},
};
use thaw::*; use thaw::*;
use web_sys::js_sys::Uint8Array; use web_sys::js_sys::Uint8Array;
@@ -19,10 +21,37 @@ pub async fn go_upload(
date: NaiveDate, date: NaiveDate,
photo: ImageUpload, photo: ImageUpload,
) -> Result<(), ServerFnError> { ) -> Result<(), ServerFnError> {
// .. use crate::app::ssr::{pool, s3};
log!("{}", category); let client = s3()?;
log!("{}", date); let pool = pool()?;
log!("{}", photo.name);
spawn(async move {
// SHOULD ALL THIS CRAP BE MICROSERVICES?
// upload the original image
// post process the image
// upload post processed image
// store progress, probably in db or something?
// so the user can come back to it later
// this really need to be a seperate process from this point forward
// not a sub-thread that gets aborted on a reload
let object = match client.list_objects_v2().bucket("airsoftfotos").send().await {
Ok(obj) => obj,
Err(e) => {
log!("{:#?}", e);
return;
}
};
dbg!(object);
log!("{}", photo.name);
log!("{}", category);
log!("{}", date);
log!("{}", photo.name);
});
Ok(()) Ok(())
} }
@@ -66,19 +95,26 @@ pub fn Account(
} }
}; };
let file_count = Signal::derive(move || files.get().len() as i64);
let progress = RwSignal::new(0);
let current = RwSignal::new(String::new());
let disabled = RwSignal::new(false); let disabled = RwSignal::new(false);
let upload = move |_| { let upload = move |_| {
// todo: rewrite this so it doesn't block frontend... idk why this happens
disabled.set(true); disabled.set(true);
let category = selected_category.get().parse().unwrap(); let category = selected_category.get().parse().unwrap();
let date = date.get(); let date = date.get();
let photos = files.get(); let photos = files.get();
for file in photos { spawn(async move {
spawn_local(async move { for file in photos {
let _ = dbg!(go_upload(category, date, file).await); current.set(file.name.clone());
}); let _ = go_upload(category, date, file).await;
} progress.update(|f| *f += 1);
}
});
}; };
view! { view! {
@@ -140,7 +176,7 @@ pub fn Account(
}> }>
{ move || if files().is_empty() { { move || if files().is_empty() {
view! { view! {
<div style="margin: auto;">"Sleep hier de fotos om te uploaden."</div> <div style="margin: auto;">"Sleep naar hier de fotos om deze voor te bereiden om te uploaden."</div>
}.into_any() }.into_any()
} else { } else {
files().into_iter().map(move |file| { files().into_iter().map(move |file| {
@@ -154,6 +190,10 @@ pub fn Account(
</Flex> </Flex>
</Upload1> </Upload1>
</div> </div>
<div>
<p>{progress}/{file_count}" - "{current}</p>
<ProgressBar1 max=file_count value=progress />
</div>
<Button button_type=ButtonType::Button disabled on_click=upload> <Button button_type=ButtonType::Button disabled on_click=upload>
"Upload" "Upload"

View File

@@ -26,6 +26,7 @@ pub fn Login(action: ServerAction<Login>) -> impl IntoView {
name="email" name="email"
/> />
<Input input_type=InputType::Password placeholder="Wachtwoord" name="password"/> <Input input_type=InputType::Password placeholder="Wachtwoord" name="password"/>
<input type="checkbox" name="remember" /> Onthouden
<Button button_type=ButtonType::Submit class="button"> <Button button_type=ButtonType::Submit class="button">
"Inloggen" "Inloggen"
</Button> </Button>

View File

@@ -103,11 +103,14 @@ async fn main() -> anyhow::Result<()> {
let addr = leptos_options.site_addr; let addr = leptos_options.site_addr;
let routes = generate_route_list(App); let routes = generate_route_list(App);
let shared_config = aws_config::from_env().load().await;
let app_state = AppState { let app_state = AppState {
leptos_options, leptos_options,
pool: pool.clone(), pool: pool.clone(),
routes: routes.clone(), routes: routes.clone(),
lettre: dangerous_lettre, lettre: dangerous_lettre,
aws_shared_config: shared_config,
}; };
// build our application with a route // build our application with a route

View File

@@ -13,4 +13,5 @@ pub struct AppState {
pub pool: SqlitePool, pub pool: SqlitePool,
pub routes: Vec<AxumRouteListing>, pub routes: Vec<AxumRouteListing>,
pub lettre: DangerousLettre, pub lettre: DangerousLettre,
pub aws_shared_config: aws_config::SdkConfig,
} }