add toast and start adding postgres
This commit is contained in:
@ -5,3 +5,4 @@ pub use hero::Hero;
|
||||
mod echo;
|
||||
pub use echo::Echo;
|
||||
pub mod progress;
|
||||
pub mod toast;
|
||||
|
||||
15
src/components/toast/component.rs
Normal file
15
src/components/toast/component.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_primitives::toast::{self, ToastProviderProps};
|
||||
|
||||
#[component]
|
||||
pub fn ToastProvider(props: ToastProviderProps) -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: asset!("./style.css") }
|
||||
toast::ToastProvider {
|
||||
default_duration: props.default_duration,
|
||||
max_toasts: props.max_toasts,
|
||||
render_toast: props.render_toast,
|
||||
{props.children}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/components/toast/mod.rs
Normal file
2
src/components/toast/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
mod component;
|
||||
pub use component::*;
|
||||
185
src/components/toast/style.css
Normal file
185
src/components/toast/style.css
Normal file
@ -0,0 +1,185 @@
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.toast-list {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.toast-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.toast {
|
||||
z-index: calc(var(--toast-count) - var(--toast-index));
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 18rem;
|
||||
height: 4rem;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--light, var(--primary-color-6))
|
||||
var(--dark, var(--primary-color-7));
|
||||
border-radius: 0.5rem;
|
||||
margin-top: -4rem;
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
||||
filter: var(--light, none)
|
||||
var(
|
||||
--dark,
|
||||
brightness(calc(0.5 + 0.5 * (1 - ((var(--toast-index) + 1) / 4))))
|
||||
);
|
||||
opacity: calc(1 - var(--toast-hidden));
|
||||
transform: scale(
|
||||
calc(100% - var(--toast-index) * 5%),
|
||||
calc(100% - var(--toast-index) * 2%)
|
||||
);
|
||||
transition: transform 0.2s ease, margin-top 0.2s ease, opacity 0.2s ease;
|
||||
|
||||
--toast-hidden: calc(min(max(0, var(--toast-index) - 2), 1));
|
||||
}
|
||||
|
||||
.toast-container:not(:hover, :focus-within)
|
||||
.toast[data-toast-even]:not([data-top]) {
|
||||
animation: slide-up-even 0.2s ease-out;
|
||||
}
|
||||
|
||||
.toast-container:not(:hover, :focus-within)
|
||||
.toast[data-toast-odd]:not([data-top]) {
|
||||
animation: slide-up-odd 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slide-up-even {
|
||||
from {
|
||||
transform: translateY(0.5rem)
|
||||
scale(
|
||||
calc(100% - var(--toast-index) * 5%),
|
||||
calc(100% - var(--toast-index) * 2%)
|
||||
);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0)
|
||||
scale(
|
||||
calc(100% - var(--toast-index) * 5%),
|
||||
calc(100% - var(--toast-index) * 2%)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-up-odd {
|
||||
from {
|
||||
transform: translateY(0.5rem)
|
||||
scale(
|
||||
calc(100% - var(--toast-index) * 5%),
|
||||
calc(100% - var(--toast-index) * 2%)
|
||||
);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0)
|
||||
scale(
|
||||
calc(100% - var(--toast-index) * 5%),
|
||||
calc(100% - var(--toast-index) * 2%)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.toast[data-top] {
|
||||
animation: slide-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
.toast-container:hover .toast[data-top],
|
||||
.toast-container:focus-within .toast[data-top] {
|
||||
animation: slide-in 0 ease-out;
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(100%)
|
||||
scale(
|
||||
calc(110% - var(--toast-index) * 5%),
|
||||
calc(110% - var(--toast-index) * 2%)
|
||||
);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0)
|
||||
scale(
|
||||
calc(100% - var(--toast-index) * 5%),
|
||||
calc(100% - var(--toast-index) * 2%)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.toast-container:hover .toast,
|
||||
.toast-container:focus-within .toast {
|
||||
margin-top: var(--toast-padding);
|
||||
filter: brightness(1);
|
||||
opacity: 1;
|
||||
transform: scale(calc(100%));
|
||||
}
|
||||
|
||||
.toast[data-type="success"] {
|
||||
background-color: var(--primary-success-color);
|
||||
color: var(--secondary-success-color);
|
||||
}
|
||||
|
||||
.toast[data-type="error"] {
|
||||
background-color: var(--primary-error-color);
|
||||
color: var(--contrast-error-color);
|
||||
}
|
||||
|
||||
.toast[data-type="warning"] {
|
||||
background-color: var(--primary-warning-color);
|
||||
color: var(--secondary-warning-color);
|
||||
}
|
||||
|
||||
.toast[data-type="info"] {
|
||||
background-color: var(--primary-info-color);
|
||||
color: var(--secondary-info-color);
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
transition: filter 0.2s ease;
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
margin-bottom: 4px;
|
||||
color: var(--secondary-color-4);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toast-description {
|
||||
color: var(--secondary-color-3);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
align-self: flex-start;
|
||||
padding: 0;
|
||||
border: none;
|
||||
margin: 0;
|
||||
background: none;
|
||||
color: var(--secondary-color-3);
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
color: var(--secondary-color-1);
|
||||
}
|
||||
25
src/config.rs
Normal file
25
src/config.rs
Normal file
@ -0,0 +1,25 @@
|
||||
#[cfg(feature = "server")]
|
||||
pub struct ServerConfig {
|
||||
pub upload_folder: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
impl ServerConfig {
|
||||
pub fn load() -> Self {
|
||||
Self {
|
||||
upload_folder: "./test/".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientConfig {
|
||||
pub upload_max_size: usize
|
||||
}
|
||||
|
||||
impl ClientConfig {
|
||||
pub fn load () -> Self {
|
||||
Self {
|
||||
upload_max_size: 1024 * 1024 * 1024 * 1 //1GB for testing
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/main.rs
13
src/main.rs
@ -3,6 +3,10 @@ use dioxus::prelude::*;
|
||||
use views::{Upload};
|
||||
use tracing::Level;
|
||||
|
||||
pub mod config;
|
||||
|
||||
use crate::components::toast::ToastProvider;
|
||||
|
||||
mod components;
|
||||
mod views;
|
||||
|
||||
@ -25,9 +29,14 @@ fn main() {
|
||||
fn App() -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
// document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
||||
div {
|
||||
class: "h-screen w-screen bg-zinc-900 text-white",
|
||||
ToastProvider {
|
||||
Upload {}
|
||||
}
|
||||
}
|
||||
|
||||
Router::<Route> {}
|
||||
// Router::<Route> {}
|
||||
}
|
||||
}
|
||||
|
||||
0
src/models/file.rs
Normal file
0
src/models/file.rs
Normal file
@ -1,58 +1,62 @@
|
||||
use dioxus::{
|
||||
fullstack::{ByteStream, FileStream},
|
||||
fullstack::{FileStream},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
use dioxus_html::{FileData, HasFileData};
|
||||
#[cfg(feature = "server")]
|
||||
use crate::config::ServerConfig;
|
||||
|
||||
use crate::config::ClientConfig;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use futures::StreamExt;
|
||||
|
||||
use std::{ env, fs, io::Write };
|
||||
|
||||
|
||||
use random_string;
|
||||
use dioxus_primitives::toast::{
|
||||
ToastOptions,
|
||||
consume_toast
|
||||
};
|
||||
|
||||
pub fn byte_to_human_size(size: u64) -> String {
|
||||
let sizes = vec!["B", "KB", "MB", "GB", "TB", "WTFAREYOUDOINGB"];
|
||||
let mut current = 0;
|
||||
|
||||
let mut res_size: f64 = size as f64;
|
||||
let mut res: String;
|
||||
|
||||
while (res_size >= 1000.0 && current < sizes.len()) {
|
||||
while res_size >= 1000.0 && current < sizes.len() {
|
||||
res_size /= 1000.0;
|
||||
current += 1;
|
||||
}
|
||||
|
||||
res = ((res_size * 100.0).round() / 100.0).to_string() + " " + sizes[current];
|
||||
res
|
||||
((res_size * 100.0).round() / 100.0).to_string() + " " + sizes[current]
|
||||
}
|
||||
|
||||
#[post("/api/upload")]
|
||||
async fn upload_file(mut upload: FileStream) -> Result<String, HttpError> {
|
||||
let UPLOAD_SIZE_LIMIT: u64 = 1024 * 1024 * 1024 * 1;
|
||||
let UPLOAD_FOLDER: String = match env::var("UPLOAD_FOLDER") {
|
||||
Ok(value) => { value },
|
||||
Err(_) => { return HttpError::internal_server_error("UPLOAD_FOLDER not set"); }
|
||||
};
|
||||
let s_config = ServerConfig::load();
|
||||
let c_config = ClientConfig::load();
|
||||
|
||||
let mut total_len: u64 = 0;
|
||||
let mut error: Option<&str> = None;
|
||||
|
||||
let filename: String = loop {
|
||||
let cur = random_string::generate(20, "ABCDEFGHIJKLMNOPQRTSTUVWXYZ0123456789");
|
||||
if (!fs::exists(UPLOAD_FOLDER.clone() + &cur).expect("can't check if file exists")) {
|
||||
if (!fs::exists(s_config.upload_folder.clone() + &cur).expect("can't check if file exists")) {
|
||||
break cur;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(size) = upload.size() {
|
||||
if size > UPLOAD_SIZE_LIMIT {
|
||||
if size > c_config.upload_max_size.try_into().unwrap() {
|
||||
return HttpError::payload_too_large("this file is too large");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let mut file = match fs::File::create(UPLOAD_FOLDER.clone() + &filename) {
|
||||
let mut file = match fs::File::create(s_config.upload_folder.clone() + &filename) {
|
||||
Ok(val) => { val },
|
||||
Err(_) => { return HttpError::internal_server_error("failed to open the output file") }
|
||||
};
|
||||
@ -61,7 +65,7 @@ async fn upload_file(mut upload: FileStream) -> Result<String, HttpError> {
|
||||
match chunk {
|
||||
Ok(bytes) => {
|
||||
total_len += bytes.len() as u64;
|
||||
if total_len > UPLOAD_SIZE_LIMIT {
|
||||
if total_len > c_config.upload_max_size.try_into().unwrap() {
|
||||
error = Some("Uploaded file too large");
|
||||
break;
|
||||
}
|
||||
@ -75,7 +79,7 @@ async fn upload_file(mut upload: FileStream) -> Result<String, HttpError> {
|
||||
match error {
|
||||
Some(err)=> {
|
||||
file.sync_data();
|
||||
fs::remove_file(UPLOAD_FOLDER.clone() + &filename);
|
||||
fs::remove_file(s_config.upload_folder.clone() + &filename);
|
||||
HttpError::internal_server_error(err)?
|
||||
}
|
||||
None => { Ok(filename) }
|
||||
@ -101,7 +105,18 @@ pub fn build_table(files: Vec<(String, String, Option<Result<String, HttpError>>
|
||||
Some(res) => { match res {
|
||||
Ok(file_url) => {let url = file_url.clone(); rsx! { input {
|
||||
type:"button",
|
||||
onclick: move |_| { web_sys::window().unwrap().navigator().clipboard().write_text(&url); },
|
||||
onclick: move |_| {
|
||||
let toast_api = consume_toast();
|
||||
toast_api
|
||||
.success(
|
||||
"Success".to_string(),
|
||||
ToastOptions::new()
|
||||
.description("The url has been copied successfully")
|
||||
.duration(Duration::from_secs(5))
|
||||
.permanent(false),
|
||||
);
|
||||
let _ = web_sys::window().unwrap().navigator().clipboard().write_text(&url);
|
||||
},
|
||||
value: "{file_url}"
|
||||
} } },
|
||||
Err(e) => {
|
||||
@ -120,13 +135,12 @@ pub fn build_table(files: Vec<(String, String, Option<Result<String, HttpError>>
|
||||
|
||||
#[component]
|
||||
pub fn Upload() -> Element {
|
||||
//TODO: global config struct
|
||||
let UPLOAD_SIZE_LIMIT: u64 = 1024 * 1024 * 1024 * 1;
|
||||
let config = ClientConfig::load();
|
||||
|
||||
let mut selected : Signal<Vec<(String, String, Option<Result<String, HttpError>>)>> = use_signal(|| vec![]);
|
||||
|
||||
rsx! {
|
||||
div { class : "p-2",
|
||||
div { class : "p-2 h-full w-full",
|
||||
p { class: "text-4xl font-bold p-1", "Upload a file" }
|
||||
|
||||
form {
|
||||
@ -144,7 +158,7 @@ pub fn Upload() -> Element {
|
||||
selected.with_mut(|files| {files.push((file.name(), byte_to_human_size(file.size()), None)); } );
|
||||
let idx = selected().len() - 1;
|
||||
|
||||
if (file.size() > UPLOAD_SIZE_LIMIT) {
|
||||
if file.size() > config.upload_max_size as u64 {
|
||||
//messy but firefox can't handle when server returns early
|
||||
selected.with_mut(|files| { files[idx].2 = Some(HttpError::payload_too_large("This file is too large")) });
|
||||
return ;
|
||||
@ -152,8 +166,10 @@ pub fn Upload() -> Element {
|
||||
|
||||
let res = match upload_file(file.clone().into()).await {
|
||||
Ok(file_id) => {
|
||||
let host = web_sys::window().unwrap().location().host().expect("unknown_host");
|
||||
Ok(host + "/upload/" + &file_id)
|
||||
let location = web_sys::window().unwrap().location();
|
||||
let host = location.host().expect("unknown host");
|
||||
let protocol = location.protocol().expect("unknown protocol");
|
||||
Ok(protocol + "//" + &host + "/upload/" + &file_id)
|
||||
},
|
||||
Err(err) => { Err(err) }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user