Style the file-service #76

Merged
savanni merged 7 commits from file-service/mobile-format into main 2023-10-19 01:53:07 +00:00
3 changed files with 118 additions and 54 deletions
Showing only changes of commit 07b4cb31ce - Show all commits

View File

@ -1,8 +1,40 @@
use build_html::{self, Html, HtmlContainer}; 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<T>(iter: T) -> Self
where
T: IntoIterator<Item = (String, String)>,
{
Attributes(iter.collect::<Vec<(String, String)>>())
}
}
impl FromIterator<(&str, &str)> for Attributes {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = (&str, &str)>,
{
unimplemented!()
}
}
*/
impl ToString for Attributes {
fn to_string(&self) -> String {
self.0
.iter()
.map(|(key, value)| format!("{}=\"{}\"", key, value))
.collect::<Vec<String>>()
.join(" ")
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Form { pub struct Form {
classes: Option<String>,
path: String, path: String,
method: String, method: String,
encoding: Option<String>, encoding: Option<String>,
@ -12,7 +44,6 @@ pub struct Form {
impl Form { impl Form {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
classes: None,
path: "/".to_owned(), path: "/".to_owned(),
method: "get".to_owned(), method: "get".to_owned(),
encoding: None, encoding: None,
@ -38,21 +69,16 @@ impl Form {
impl Html for Form { impl Html for Form {
fn to_html_string(&self) -> String { fn to_html_string(&self) -> String {
let classes = match self.classes {
Some(ref classes) => format!("class=\"{}\"", classes),
None => "".to_owned(),
};
let encoding = match self.encoding { let encoding = match self.encoding {
Some(ref encoding) => format!("enctype=\"{encoding}\"", encoding = encoding), Some(ref encoding) => format!("enctype=\"{encoding}\"", encoding = encoding),
None => "".to_owned(), None => "".to_owned(),
}; };
format!( format!(
"<form action=\"{path}\" method=\"{method}\" {encoding} {classes}>\n{elements}\n</form>\n", "<form action=\"{path}\" method=\"{method}\" {encoding}\n{elements}\n</form>\n",
path = self.path, path = self.path,
method = self.method, method = self.method,
encoding = encoding, encoding = encoding,
elements = self.elements.to_html_string(), elements = self.elements.to_html_string(),
classes = classes,
) )
} }
} }
@ -69,7 +95,7 @@ pub struct Input {
name: String, name: String,
id: Option<String>, id: Option<String>,
value: Option<String>, value: Option<String>,
attributes: Vec<(String, String)>, attributes: Attributes,
} }
impl Html for Input { impl Html for Input {
@ -82,12 +108,7 @@ impl Html for Input {
Some(ref value) => format!("value=\"{}\"", value), Some(ref value) => format!("value=\"{}\"", value),
None => "".to_owned(), None => "".to_owned(),
}; };
let attrs = self let attrs = self.attributes.to_string();
.attributes
.iter()
.map(|(key, value)| format!("{}=\"{}\"", key, value))
.collect::<Vec<String>>()
.join(" ");
format!( format!(
"<input type=\"{ty}\" name=\"{name}\" {id} {value} {attrs} />\n", "<input type=\"{ty}\" name=\"{name}\" {id} {value} {attrs} />\n",
@ -107,7 +128,7 @@ impl Input {
name: name.to_owned(), name: name.to_owned(),
id: None, id: None,
value: None, value: None,
attributes: vec![], attributes: Attributes::default(),
} }
} }
@ -125,10 +146,12 @@ impl Input {
mut self, mut self,
values: impl IntoIterator<Item = (&'a str, &'a str)>, values: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> Self { ) -> Self {
self.attributes = values self.attributes = Attributes(
.into_iter() values
.map(|(a, b)| (a.to_owned(), b.to_owned())) .into_iter()
.collect::<Vec<(String, String)>>(); .map(|(a, b)| (a.to_owned(), b.to_owned()))
.collect::<Vec<(String, String)>>(),
);
self self
} }
} }
@ -201,18 +224,37 @@ impl Html for Button {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Image { pub struct Image {
path: String, path: String,
attributes: Attributes,
} }
impl Image { impl Image {
pub fn new(path: &str) -> Self { pub fn new(path: &str) -> Self {
Self { Self {
path: path.to_owned(), path: path.to_owned(),
attributes: Attributes::default(),
} }
} }
pub fn with_attributes<'a>(
mut self,
values: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> Self {
self.attributes = Attributes(
values
.into_iter()
.map(|(a, b)| (a.to_owned(), b.to_owned()))
.collect::<Vec<(String, String)>>(),
);
self
}
} }
impl Html for Image { impl Html for Image {
fn to_html_string(&self) -> String { fn to_html_string(&self) -> String {
format!("<img src={path} />", path = self.path,) format!(
"<img src={path} {attrs} />",
path = self.path,
attrs = self.attributes.to_string()
)
} }
} }

View File

@ -37,22 +37,27 @@ pub fn auth(_message: Option<String>) -> build_html::HtmlPage {
pub fn gallery(handles: Vec<Result<FileHandle, ReadFileError>>) -> build_html::HtmlPage { pub fn gallery(handles: Vec<Result<FileHandle, ReadFileError>>) -> build_html::HtmlPage {
let mut page = build_html::HtmlPage::new() let mut page = build_html::HtmlPage::new()
.with_title("Admin list of files") .with_title("Gallery")
.with_stylesheet("/css") .with_stylesheet("/css")
.with_header(1, "Admin list of files") .with_container(
.with_html( Container::new(ContainerType::Div)
Form::new() .with_attributes([("class", "gallery-page")])
.with_path("/upload") .with_header(1, "Gallery")
.with_method("post") .with_html(
.with_encoding("multipart/form-data") Form::new()
.with_container( .with_path("/upload")
Container::new(ContainerType::Div) .with_method("post")
.with_html(Input::new("file", "file").with_id("for-selector-input")) .with_encoding("multipart/form-data")
.with_html(Label::new("for-selector-input", "Select a file")), .with_container(
) Container::new(ContainerType::Div)
.with_html(Button::new("Upload file").with_type("submit")), .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")),
),
); );
let mut gallery = Container::new(ContainerType::Div).with_attributes([("class", "gallery")]);
for handle in handles { for handle in handles {
let container = match handle { let container = match handle {
Ok(ref handle) => thumbnail(&handle.id).with_html( Ok(ref handle) => thumbnail(&handle.id).with_html(
@ -67,19 +72,21 @@ pub fn gallery(handles: Vec<Result<FileHandle, ReadFileError>>) -> build_html::H
.with_attributes(vec![("class", "file")]) .with_attributes(vec![("class", "file")])
.with_paragraph(format!("{:?}", err)), .with_paragraph(format!("{:?}", err)),
}; };
page.add_container(container) gallery.add_container(container);
} }
page.add_container(gallery);
page page
} }
pub fn thumbnail(id: &FileId) -> Container { pub fn thumbnail(id: &FileId) -> Container {
let mut container = Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]); let mut container =
let tn = Container::new(ContainerType::Div) Container::new(ContainerType::Div).with_attributes(vec![("class", "thumbnail")]);
.with_attributes(vec![("class", "thumbnail")]) let tn = Container::new(ContainerType::Div).with_link(
.with_link( format!("/{}", **id),
format!("/{}", **id), Image::new(&format!("{}/tn", **id))
Image::new(&format!("{}/tn", **id)).to_html_string(), .with_attributes([("class", "thumbnail__image")])
); .to_html_string(),
);
container.add_html(tn); container.add_html(tn);
container container
} }

View File

@ -2,9 +2,9 @@
--main-bg-color: #e5f0fc; --main-bg-color: #e5f0fc;
--fg-color: #449dfc; --fg-color: #449dfc;
--px-4: 4px; --space-small: 4px;
--px-8: 8px; --space-medium: 8px;
--px-12: 12px; --space-large: 12px;
--hover-low: 4px 4px 4px gray; --hover-low: 4px 4px 4px gray;
} }
@ -16,6 +16,7 @@ body {
.authentication-page { .authentication-page {
display: flex; display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 200px; height: 200px;
@ -29,35 +30,46 @@ body {
} }
.authentication-form__label { .authentication-form__label {
margin: var(--px-4); margin: var(--space-small);
} }
.authentication-form__input { .authentication-form__input {
margin: var(--px-4); margin: var(--space-small);
} }
.files { .gallery-page {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.file { .gallery {
display: flex; display: flex;
margin: 1em;
border: 1px solid #449dfc;
border-radius: 5px;
padding: 1em;
} }
.thumbnail {
border: 1px solid black;
box-shadow: var(--hover-low);
max-width: 300px;
margin: var(--space-large);
padding: var(--space-medium);
display: flex;
flex-direction: column;
justify-content: space-between;
}
/*
.thumbnail { .thumbnail {
max-width: 320px; max-width: 320px;
margin: 1em; margin: 1em;
} }
*/
img { .thumbnail__image {
max-width: 100%; max-width: 100%;
border: none;
} }
/*
[type="submit"] { [type="submit"] {
border-radius: 1em; border-radius: 1em;
margin: 1em; margin: 1em;
@ -69,8 +81,10 @@ img {
margin: 1em; margin: 1em;
padding: 1em; padding: 1em;
} }
*/
/* https://benmarshall.me/styling-file-inputs/ */ /* https://benmarshall.me/styling-file-inputs/ */
/*
[type="file"] { [type="file"] {
border: 0; border: 0;
clip: rect(0, 0, 0, 0); clip: rect(0, 0, 0, 0);
@ -102,6 +116,7 @@ img {
outline: 1px dotted #000; outline: 1px dotted #000;
outline: -webkit-focus-ring-color auto 5px; 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: 980px) { /* This is the screen width of a OnePlus 5t