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",
"tracing",
], 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 }
[features]
default = ["ssr"]
hydrate = ["leptos/hydrate", "thaw/csr"]
ssr = [
"dep:axum",
"dep:tower",
@@ -102,9 +106,14 @@ ssr = [
"dep:leptos_axum",
"thaw/ssr",
"lettre",
"aws-config",
"aws-sdk-s3",
"tera",
]
default = ["ssr"]
hydrate = ["leptos/hydrate", "thaw/csr"]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
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)
);
-- 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 (
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,
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,
visible BOOLEAN NOT NULL DEFAULT TRUE,
upvotes INTEGER NOT NULL,

View File

@@ -10,7 +10,7 @@ use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::{components::*, path};
use thaw::{ConfigProvider, Layout, LayoutHeader, LoadingBarProvider, Theme, ToasterProvider};
use thaw::{ConfigProvider, LayoutHeader, LoadingBarProvider, Theme, ToasterProvider};
#[cfg(feature = "ssr")]
pub mod ssr {
@@ -28,6 +28,17 @@ pub mod ssr {
.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> {
let auth = leptos_axum::extract().await?;
Ok(auth)

View File

@@ -15,3 +15,6 @@ pub use file_preview::*;
mod 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::{
app::{
components::{FilePreview, ImageUpload, Upload1},
models::Category,
components::{FilePreview, ImageUpload, ProgressBar1, Upload1},
pages::get_categories,
},
auth::{Logout, User},
@@ -9,7 +8,10 @@ use crate::{
};
use chrono::prelude::*;
use leptos::prelude::*;
use leptos::{logging::log, task::spawn_local};
use leptos::{
logging::log,
task::{spawn, spawn_local},
};
use thaw::*;
use web_sys::js_sys::Uint8Array;
@@ -19,10 +21,37 @@ pub async fn go_upload(
date: NaiveDate,
photo: ImageUpload,
) -> Result<(), ServerFnError> {
// ..
log!("{}", category);
log!("{}", date);
log!("{}", photo.name);
use crate::app::ssr::{pool, s3};
let client = s3()?;
let pool = pool()?;
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(())
}
@@ -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 upload = move |_| {
// todo: rewrite this so it doesn't block frontend... idk why this happens
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);
});
}
spawn(async move {
for file in photos {
current.set(file.name.clone());
let _ = go_upload(category, date, file).await;
progress.update(|f| *f += 1);
}
});
};
view! {
@@ -140,7 +176,7 @@ pub fn Account(
}>
{ move || if files().is_empty() {
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()
} else {
files().into_iter().map(move |file| {
@@ -154,6 +190,10 @@ pub fn Account(
</Flex>
</Upload1>
</div>
<div>
<p>{progress}/{file_count}" - "{current}</p>
<ProgressBar1 max=file_count value=progress />
</div>
<Button button_type=ButtonType::Button disabled on_click=upload>
"Upload"

View File

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

View File

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

View File

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