diff --git a/Cargo.lock b/Cargo.lock index d9ffcc7..312bb25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -816,7 +816,7 @@ dependencies = [ [[package]] name = "file-service" -version = "0.1.0" +version = "0.1.1" dependencies = [ "base64ct", "build_html", diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index 3be7912..0a841e3 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "file-service" -version = "0.1.0" +version = "0.1.1" authors = ["savanni@luminescent-dreams.com"] edition = "2018" diff --git a/file-service/src/handlers.rs b/file-service/src/handlers.rs index 699451a..96478df 100644 --- a/file-service/src/handlers.rs +++ b/file-service/src/handlers.rs @@ -9,6 +9,8 @@ use warp::{filters::multipart::FormData, http::Response, multipart::Part}; use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken}; +const CSS: &str = include_str!("../templates/style.css"); + pub async fn handle_index( app: App, token: Option, @@ -22,6 +24,13 @@ pub async fn handle_index( } } +pub async fn handle_css() -> Result, Error> { + Response::builder() + .header("content-type", "text/css") + .status(StatusCode::OK) + .body(CSS.to_owned()) +} + pub fn render_auth_page(message: Option) -> Result, Error> { Response::builder() .status(StatusCode::OK) diff --git a/file-service/src/html.rs b/file-service/src/html.rs index 1d2199a..d38fb21 100644 --- a/file-service/src/html.rs +++ b/file-service/src/html.rs @@ -1,5 +1,38 @@ use build_html::{self, Html, HtmlContainer}; +#[derive(Clone, Debug, Default)] +pub struct Attributes(Vec<(String, String)>); + +/* +impl FromIterator<(String, String)> for Attributes { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + Attributes(iter.collect::>()) + } +} + +impl FromIterator<(&str, &str)> for Attributes { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + unimplemented!() + } +} +*/ + +impl ToString for Attributes { + fn to_string(&self) -> String { + self.0 + .iter() + .map(|(key, value)| format!("{}=\"{}\"", key, value)) + .collect::>() + .join(" ") + } +} + #[derive(Clone, Debug)] pub struct Form { path: String, @@ -41,11 +74,11 @@ impl Html for Form { None => "".to_owned(), }; format!( - "
\n{elements}\n
\n", + "
\n", path = self.path, method = self.method, encoding = encoding, - elements = self.elements.to_html_string() + elements = self.elements.to_html_string(), ) } } @@ -62,7 +95,7 @@ pub struct Input { name: String, id: Option, value: Option, - content: Option, + attributes: Attributes, } impl Html for Input { @@ -75,14 +108,15 @@ impl Html for Input { Some(ref value) => format!("value=\"{}\"", value), None => "".to_owned(), }; + let attrs = self.attributes.to_string(); format!( - "{content}\n", + "\n", ty = self.ty, name = self.name, id = id, value = value, - content = self.content.clone().unwrap_or("".to_owned()), + attrs = attrs, ) } } @@ -94,7 +128,7 @@ impl Input { name: name.to_owned(), id: None, value: None, - content: None, + attributes: Attributes::default(), } } @@ -108,12 +142,18 @@ impl Input { self } - /* - pub fn with_content(mut self, val: &str) -> Self { - self.content = Some(val.to_owned()); + pub fn with_attributes<'a>( + mut self, + values: impl IntoIterator, + ) -> Self { + self.attributes = Attributes( + values + .into_iter() + .map(|(a, b)| (a.to_owned(), b.to_owned())) + .collect::>(), + ); self } - */ } #[derive(Clone, Debug)] @@ -146,6 +186,7 @@ pub struct Button { ty: Option, name: Option, label: String, + attributes: Attributes, } impl Button { @@ -154,6 +195,7 @@ impl Button { ty: None, name: None, label: label.to_owned(), + attributes: Attributes::default(), } } @@ -161,6 +203,19 @@ impl Button { self.ty = Some(ty.to_owned()); self } + + pub fn with_attributes<'a>( + mut self, + values: impl IntoIterator, + ) -> Self { + self.attributes = Attributes( + values + .into_iter() + .map(|(a, b)| (a.to_owned(), b.to_owned())) + .collect::>(), + ); + self + } } impl Html for Button { @@ -174,9 +229,10 @@ impl Html for Button { None => "".to_owned(), }; format!( - "", + "", name = name, - label = self.label + label = self.label, + attrs = self.attributes.to_string() ) } } @@ -184,18 +240,37 @@ impl Html for Button { #[derive(Clone, Debug)] pub struct Image { path: String, + attributes: Attributes, } impl Image { pub fn new(path: &str) -> Self { Self { path: path.to_owned(), + attributes: Attributes::default(), } } + + pub fn with_attributes<'a>( + mut self, + values: impl IntoIterator, + ) -> Self { + self.attributes = Attributes( + values + .into_iter() + .map(|(a, b)| (a.to_owned(), b.to_owned())) + .collect::>(), + ); + self + } } impl Html for Image { fn to_html_string(&self) -> String { - format!("", path = self.path,) + format!( + "", + path = self.path, + attrs = self.attributes.to_string() + ) } } diff --git a/file-service/src/main.rs b/file-service/src/main.rs index c70f7ff..88b69cc 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -1,7 +1,7 @@ extern crate log; use cookie::Cookie; -use handlers::{file, handle_auth, handle_upload, thumbnail}; +use handlers::{file, handle_auth, handle_css, handle_upload, thumbnail}; use std::{ collections::{HashMap, HashSet}, convert::Infallible, @@ -119,6 +119,8 @@ pub async fn main() { .and(maybe_with_session()) .then(handle_index); + let styles = warp::path!("css").and(warp::get()).then(handle_css); + let auth = warp::path!("auth") .and(warp::post()) .and(with_app(app.clone())) @@ -145,7 +147,8 @@ pub async fn main() { .then(move |id, old_etags, app: App| file(app, id, old_etags)); let server = warp::serve( - root.or(auth) + root.or(styles) + .or(auth) .or(upload_via_form) .or(thumbnail) .or(file) diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index ba1b586..18dd832 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -5,35 +5,48 @@ use file_service::{FileHandle, FileId, ReadFileError}; pub fn auth(_message: Option) -> build_html::HtmlPage { build_html::HtmlPage::new() .with_title("Authentication") - .with_html( - Form::new() - .with_path("/auth") - .with_method("post") + .with_stylesheet("/css") + .with_container( + Container::new(ContainerType::Div) + .with_attributes([("class", "authentication-page")]) .with_container( Container::new(ContainerType::Div) - .with_html(Input::new("token", "token").with_id("for-token-input")) - .with_html(Label::new("for-token-input", "Authentication Token")), + .with_attributes([("class", "card authentication-form")]) + .with_html( + Form::new() + .with_path("/auth") + .with_method("post") + .with_container( + Container::new(ContainerType::Div) + .with_attributes([("class", "authentication-form__label")]) + .with_html(Label::new("for-token-input", "Authentication")), + ) + .with_container( + Container::new(ContainerType::Div) + .with_attributes([("class", "authentication-form__input")]) + .with_html( + Input::new("token", "token") + .with_id("for-token-input") + .with_attributes([("size", "50")]), + ), + ), + ), ), ) } pub fn gallery(handles: Vec>) -> build_html::HtmlPage { let mut page = build_html::HtmlPage::new() - .with_title("Admin list of files") - .with_header(1, "Admin list of files") - .with_html( - Form::new() - .with_path("/upload") - .with_method("post") - .with_encoding("multipart/form-data") - .with_container( - Container::new(ContainerType::Div) - .with_html(Input::new("file", "file").with_id("for-selector-input")) - .with_html(Label::new("for-selector-input", "Select a file")), - ) - .with_html(Button::new("Upload file").with_type("submit")), + .with_title("Gallery") + .with_stylesheet("/css") + .with_container( + Container::new(ContainerType::Div) + .with_attributes([("class", "gallery-page")]) + .with_header(1, "Gallery") + .with_html(upload_form()), ); + let mut gallery = Container::new(ContainerType::Div).with_attributes([("class", "gallery")]); for handle in handles { let container = match handle { Ok(ref handle) => thumbnail(&handle.id).with_html( @@ -48,19 +61,42 @@ pub fn gallery(handles: Vec>) -> build_html::H .with_attributes(vec![("class", "file")]) .with_paragraph(format!("{:?}", err)), }; - page.add_container(container) + gallery.add_container(container); } + page.add_container(gallery); page } -pub fn thumbnail(id: &FileId) -> Container { - let mut container = Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]); - let tn = Container::new(ContainerType::Div) - .with_attributes(vec![("class", "thumbnail")]) - .with_link( - format!("/{}", **id), - Image::new(&format!("{}/tn", **id)).to_html_string(), - ); - container.add_html(tn); - container +pub fn upload_form() -> Form { + Form::new() + .with_path("/upload") + .with_method("post") + .with_encoding("multipart/form-data") + .with_container( + Container::new(ContainerType::Div) + .with_attributes([("class", "card upload-form")]) + .with_html(Input::new("file", "file").with_attributes([ + ("id", "for-selector-input"), + ("placeholder", "select file"), + ("class", "upload-form__selector"), + ])) + .with_html( + Button::new("Upload file") + .with_attributes([("class", "upload-form__button")]) + .with_type("submit"), + ), + ) +} + +pub fn thumbnail(id: &FileId) -> Container { + Container::new(ContainerType::Div) + .with_attributes(vec![("class", "card thumbnail")]) + .with_html( + Container::new(ContainerType::Div).with_link( + format!("/{}", **id), + Image::new(&format!("{}/tn", **id)) + .with_attributes([("class", "thumbnail__image")]) + .to_html_string(), + ), + ) } diff --git a/file-service/templates/style.css b/file-service/templates/style.css index eeb6f8b..ae81cdf 100644 --- a/file-service/templates/style.css +++ b/file-service/templates/style.css @@ -1,29 +1,83 @@ -body { - font-family: 'Ariel', sans-serif; +:root { + --main-bg-color: #e5f0fc; + --fg-color: #449dfc; + + --space-small: 4px; + --space-medium: 8px; + --space-large: 12px; + + --hover-low: 4px 4px 4px gray; } -.files { +body { + font-family: 'Ariel', sans-serif; + background-color: var(--main-bg-color); +} + +.card { + border: 1px solid black; + border-radius: 5px; + box-shadow: var(--hover-low); + margin: var(--space-large); + padding: var(--space-medium); + +} + +.authentication-page { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 200px; +} + +.authentication-form { +} + +.authentication-form__label { + margin: var(--space-small); +} + +.authentication-form__input { + margin: var(--space-small); +} + +.gallery-page { + display: flex; + flex-direction: column; +} + +.upload-form { + display: flex; + flex-direction: column; +} + +.upload-form__selector { + margin: var(--space-small); +} + +.upload-form__button { + margin: var(--space-small); +} + +.gallery { display: flex; flex-wrap: wrap; } -.file { - display: flex; - margin: 1em; - border: 1px solid #449dfc; - border-radius: 5px; - padding: 1em; -} - .thumbnail { - max-width: 320px; - margin: 1em; + width: 300px; + display: flex; + flex-direction: column; + justify-content: space-between; } -img { +.thumbnail__image { max-width: 100%; + border: none; } +/* [type="submit"] { border-radius: 1em; margin: 1em; @@ -31,15 +85,10 @@ img { } .uploadform { - display: flex; - margin: 1em; - background-color: #e5f0fc; - border: 1px solid #449dfc; - border-radius: 5px; - padding: 1em; } +*/ -/* https://benmarshall.me/styling-file-inputs/ */ + /* [type="file"] { border: 0; clip: rect(0, 0, 0, 0); @@ -71,12 +120,30 @@ img { outline: 1px dotted #000; outline: -webkit-focus-ring-color auto 5px; } +*/ -@media screen and (max-width: 980px) { /* This is the screen width of a OnePlus 5t */ +@media screen and (max-width: 1080px) { /* This is the screen width of a OnePlus 8 */ body { font-size: xx-large; } + .authentication-form { + width: 100%; + } + + .upload-form__selector { + font-size: larger; + } + + .upload-form__button { + font-size: larger; + } + + .thumbnail { + width: 100%; + } + + /* [type="submit"] { font-size: xx-large; width: 100%; @@ -100,4 +167,5 @@ img { flex-direction: column; width: 100%; } + */ }