diff --git a/jirs-client/js/css/aside.css b/jirs-client/js/css/aside.css index 9ef9609d..ccbe4e43 100644 --- a/jirs-client/js/css/aside.css +++ b/jirs-client/js/css/aside.css @@ -95,12 +95,11 @@ aside#navbar-left:hover .item > .itemText { opacity: 1; } -aside#navbar-left .styledTooltip { - z-index: calc(var(--modal) + 1); - position: fixed; - width: 300px; - border-radius: 3px; - background: #fff; - transform: translateZ(0); - box-shadow: rgba(9, 30, 66, 0.25) 0 4px 8px -2px, rgba(9, 30, 66, 0.31) 0 0 1px; +.styledTooltip.aboutTooltipPopup { + bottom: 48px; + left: 120px; +} + +.styledTooltip.aboutTooltipPopup #about-github-button { + margin-left: 10px; } diff --git a/jirs-client/js/css/shared.css b/jirs-client/js/css/shared.css index 7c7d5824..e90fe8f8 100644 --- a/jirs-client/js/css/shared.css +++ b/jirs-client/js/css/shared.css @@ -3,3 +3,104 @@ padding-top: 18px; border-top: 1px solid var(--borderLight); } + +.styledButton { + display: inline-flex; + align-items: center; + justify-content: center; + height: 32px; + vertical-align: middle; + line-height: 1; + white-space: nowrap; + border-radius: 3px; + transition: all 0.1s; + appearance: none; + cursor: pointer; + user-select: none; + font-size: 14.5px; +} + +.styledButton.withIcon > span.text { + margin-left: 7px; +} + +.styledButton:disabled { + opacity: 0.6; + cursor: default; +} + +.styledButton:not(.onlyIcon) { + padding: 0 12px; +} + +.styledButton.onlyIcon { + padding: 0 9px; +} + +.styledButton.primary { + color: #fff; + background: var(--primary); + font-family: var(--font-medium); +} + +.styledButton.primary:not(:disabled):hover { + filter: brightness(115%); +} + +.styledButton.primary:not(:disabled):active { + filter: brightness(110%); +} + +.styledButton.primary:not(:disabled).isActive { + filter: brightness(110%); +} + +.styledButton.success { + color: #fff; + background: var(--success); +} + +.styledButton.danger { + color: #fff; + background: var(--danger); +} + +.styledButton.secondary { + color: var(--textDark); + background: var(--secondary); + font-family: var(--font-regular); +} + +.styledButton.secondary:not(:disabled):hover { + background: var(--backgroundLight); +} + +.styledButton.secondary:not(:disabled):active { + color: var(--primary); + background: var(--backgroundLightPrimary); +} + +.styledButton.secondary:not(:disabled).isActive { + color: var(--primary); + background: var(--backgroundLightPrimary); +} + +.styledButton.empty { + background: #fff; + color: var(--textDark); + font-family: var(--font-regular); +} + +.styledButton.empty:not(:disabled):hover { + background: var(--backgroundLight); +} + +.styledButton.empty:not(:disabled):active { + color: var(--primary); + background: var(--backgroundLightPrimary); +} + +.styledButton.empty:not(:disabled).isActive { + color: var(--primary); + background: var(--backgroundLightPrimary); +} diff --git a/jirs-client/js/css/styledTooltip.css b/jirs-client/js/css/styledTooltip.css new file mode 100644 index 00000000..7ce66296 --- /dev/null +++ b/jirs-client/js/css/styledTooltip.css @@ -0,0 +1,30 @@ +.styledTooltip { + z-index: calc(var(--modal) + 1); + position: fixed; + width: 300px; + border-radius: 3px; + background: #fff; + transform: translateZ(0); + box-shadow: rgba(9, 30, 66, 0.25) 0 4px 8px -2px, rgba(9, 30, 66, 0.31) 0 0 1px; +} + +.styledTooltip .feedbackDropdown { + padding: 16px 24px 24px; +} + +.styledTooltip .feedbackImageCont { + padding: 24px 56px 20px; +} + +.styledTooltip .feedbackImage { + width: 100%; +} + +.styledTooltip .feedbackParagraph { + margin-bottom: 12px; + font-size: 15px; +} + +.styledTooltip .feedbackParagraph:last-of-type { + margin-bottom: 22px; +} diff --git a/jirs-client/js/styles.css b/jirs-client/js/styles.css index c5cf28e6..96f72504 100644 --- a/jirs-client/js/styles.css +++ b/jirs-client/js/styles.css @@ -6,4 +6,5 @@ @import "css/aside.css"; @import "css/icon.css"; @import "css/shared.css"; +@import "css/styledTooltip.css"; @import "css/app.css"; diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 34b49e76..dbc12fc1 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -1,7 +1,8 @@ -use crate::model::Page; use seed::fetch::FetchObject; use seed::{prelude::*, *}; +use crate::model::Page; + mod api; mod login; mod model; @@ -16,6 +17,7 @@ pub enum Msg { CurrentProjectResult(FetchObject), CurrentUserResult(FetchObject), InternalFailure(String), + ToggleAboutTooltip, } fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index ca781b7a..d347fa7f 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -59,6 +59,8 @@ pub struct Model { pub comments_by_project_id: HashMap>, pub page: Page, pub host_url: String, + // + pub about_tooltip_visible: bool, } impl Default for Model { @@ -75,6 +77,7 @@ impl Default for Model { comments_by_project_id: Default::default(), page: Page::Project, host_url, + about_tooltip_visible: false, } } } diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index 88963dc2..fb37ee81 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -1,7 +1,8 @@ +use seed::{prelude::*, *}; + use crate::model::Page; use crate::shared::{host_client, inner_layout}; use crate::Msg; -use seed::{prelude::*, *}; pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { match msg { @@ -13,6 +14,9 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order .skip() .perform_cmd(crate::api::fetch_current_user(model.host_url.clone())); } + Msg::ToggleAboutTooltip => { + model.about_tooltip_visible = !model.about_tooltip_visible; + } _ => (), } } diff --git a/jirs-client/src/shared.rs b/jirs-client/src/shared/mod.rs similarity index 57% rename from jirs-client/src/shared.rs rename to jirs-client/src/shared/mod.rs index 24581b1d..93ea6dad 100644 --- a/jirs-client/src/shared.rs +++ b/jirs-client/src/shared/mod.rs @@ -1,27 +1,33 @@ -use seed::fetch::{FetchObject, FetchResult, ResponseWithDataResult}; +use seed::fetch::{FetchObject, ResponseWithDataResult}; use seed::{prelude::*, *}; -use serde::Deserialize; use jirs_data::FullProjectResponse; +use styled_button::*; use crate::model::{Icon, Model, Page}; use crate::Msg; -pub fn navbar_left(model: &Model) -> Node { - let mut logo_svg = Node::from_html(include_str!("../static/logo.svg")); +pub mod styled_button; +pub mod styled_tooltip; - aside![ - id!["navbar-left"], - a![ - attrs![At::Class => "logoLink", At::Href => "/"], - div![attrs![At::Class => "styledLogo"], logo_svg] +pub fn navbar_left(model: &Model) -> Vec> { + let logo_svg = Node::from_html(include_str!("../../static/logo.svg")); + + vec![ + about_tooltip_popup(model), + aside![ + id!["navbar-left"], + a![ + attrs![At::Class => "logoLink", At::Href => "/"], + div![attrs![At::Class => "styledLogo"], logo_svg] + ], + navbar_left_item(model, "Search issues", Icon::Search), + navbar_left_item(model, "Create Issue", Icon::Plus), + div![ + attrs![At::Class => "bottom"], + about_tooltip(model, navbar_left_item(model, "About", Icon::Help)), + ], ], - navbar_left_item(model, "Search issues", Icon::Search), - navbar_left_item(model, "Create Issue", Icon::Plus), - div![ - attrs![At::Class => "bottom"], - about_tooltip(model, navbar_left_item(model, "About", Icon::Help)) - ] ] } @@ -33,16 +39,74 @@ fn navbar_left_item(_model: &Model, text: &str, logo: Icon) -> Node { ] } -pub fn about_tooltip(_model: &Model, children: Node) -> Node { - div![attrs![At::Class => "aboutTooltip"], children] +pub fn about_tooltip(model: &Model, children: Node) -> Node { + div![ + attrs![At::Class => "aboutTooltip"], + ev(Ev::Click, |_| Msg::ToggleAboutTooltip), + children + ] } -pub fn styled_tooltip() -> Node { - div![attrs![At::Class => "styledTooltip"]] +fn about_tooltip_popup(model: &Model) -> Node { + styled_tooltip::StyledTooltip { + visible: model.about_tooltip_visible, + class_name: "aboutTooltipPopup".to_string(), + children: div![ + ev(Ev::Click, |_| Msg::ToggleAboutTooltip), + attrs![At::Class => "feedbackDropdown"], + div![ + attrs![At::Class => "feedbackImageCont"], + img![attrs![At::Src => "/feedback.png", At::Class => "feedbackImage"]] + ], + div![ + attrs![At::Class => "feedbackParagraph"], + "This simplified Jira clone is built with Seed.rs on the front-end and Actix-Web on the back-end." + ], + div![ + attrs![At::Class => "feedbackParagraph"], + "Read more on my website or reach out via ", + a![ + attrs![At::Href => "mailto:adrian.wozniak@ita-prog.pl"], + strong!["adrian.wozniak@ita-prog.pl"] + ] + ], + a![ + attrs![ + At::Href => "https://gitlab.com/adrian.wozniak/jirs", + At::Target => "_blank", + At::Rel => "noreferrer noopener", + ], + StyledButton { + text: Some("Visit Website".to_string()), + variant: Variant::Primary, + disabled: false, + active: false, + icon_only: false, + icon: None, + }.into_node(), + ], + a![ + id!["about-github-button"], + attrs![ + At::Href => "https://gitlab.com/adrian.wozniak/jirs", + At::Target => "_blank", + At::Rel => "noreferrer noopener", + ], + StyledButton { + text: Some("Github Repo".to_string()), + variant: Variant::Secondary, + disabled: false, + active: false, + icon_only: false, + icon: Some(Icon::Github), + }.into_node() + ] + ], + }.into_node() } pub fn sidebar(model: &Model) -> Node { - let project_icon = Node::from_html(include_str!("../static/project-avatar.svg")); + let project_icon = Node::from_html(include_str!("../../static/project-avatar.svg")); let project_info = match model.project.as_ref() { Some(project) => li![ id!["projectInfo"], @@ -96,12 +160,16 @@ fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option) attrs![At::Class => class_list.join(" ")], a![ attrs![At::Href => path], - i![attrs![At::Class => format!("styledIcon {}", icon)], ""], + styled_icon(icon), div![attrs![At::Class => "linkText"], name], ] ] } +pub fn styled_icon(icon: Icon) -> Node { + i![attrs![At::Class => format!("styledIcon {}", icon)], ""] +} + pub fn divider() -> Node { div![attrs![At::Class => "divider"], ""] } diff --git a/jirs-client/src/shared/styled_button.rs b/jirs-client/src/shared/styled_button.rs new file mode 100644 index 00000000..e9c23a85 --- /dev/null +++ b/jirs-client/src/shared/styled_button.rs @@ -0,0 +1,85 @@ +use seed::{prelude::*, *}; + +use crate::model::Icon; +use crate::shared::styled_icon; +use crate::Msg; + +pub enum Variant { + Primary, + Success, + Danger, + Secondary, + Empty, +} + +impl ToString for Variant { + fn to_string(&self) -> String { + match self { + Variant::Primary => "primary", + Variant::Success => "success", + Variant::Danger => "danger", + Variant::Secondary => "secondary", + Variant::Empty => "empty", + } + .to_string() + } +} + +pub struct StyledButton { + pub variant: Variant, + pub icon_only: bool, + pub disabled: bool, + pub active: bool, + pub text: Option, + pub icon: Option, +} + +impl Into> for StyledButton { + fn into(self) -> Node { + styled_button(self) + } +} + +impl StyledButton { + pub fn into_node(self) -> Node { + self.into() + } +} + +pub fn styled_button(values: StyledButton) -> Node { + let StyledButton { + text, + variant, + icon_only, + disabled, + active, + icon, + } = values; + let mut class_list = vec!["styledButton".to_string(), variant.to_string()]; + if icon_only { + class_list.push("iconOnly".to_string()); + } + if active { + class_list.push("isActive".to_string()); + } + if icon.is_some() { + class_list.push("withIcon".to_string()); + } + + let icon_node = match icon { + None => empty![], + Some(i) => styled_icon(i), + }; + + button![ + attrs![ + At::Class => class_list.join(" "), + ], + match disabled { + true => vec![attrs![At::Disabled => true]], + false => vec![], + }, + icon_node, + span![attrs![At::Class => "text"], text.unwrap_or_default()], + ] +} diff --git a/jirs-client/src/shared/styled_tooltip.rs b/jirs-client/src/shared/styled_tooltip.rs new file mode 100644 index 00000000..1e5b5119 --- /dev/null +++ b/jirs-client/src/shared/styled_tooltip.rs @@ -0,0 +1,37 @@ +use seed::{prelude::*, *}; + +use crate::Msg; + +pub struct StyledTooltip { + pub visible: bool, + pub class_name: String, + pub children: Node, +} + +impl Into> for StyledTooltip { + fn into(self) -> Node { + styled_tooltip(self) + } +} + +impl StyledTooltip { + pub fn into_node(self) -> Node { + self.into() + } +} + +pub fn styled_tooltip(values: StyledTooltip) -> Node { + let StyledTooltip { + visible, + class_name, + children, + } = values; + if visible { + div![ + attrs![At::Class => format!("styledTooltip {}", class_name)], + children + ] + } else { + empty!() + } +} diff --git a/jirs-server/src/schema.rs b/jirs-server/src/schema.rs index b6d52824..614476f1 100644 --- a/jirs-server/src/schema.rs +++ b/jirs-server/src/schema.rs @@ -83,11 +83,4 @@ joinable!(issues -> users (reporter_id)); joinable!(tokens -> users (user_id)); joinable!(users -> projects (project_id)); -allow_tables_to_appear_in_same_query!( - comments, - issue_assignees, - issues, - projects, - tokens, - users, -); +allow_tables_to_appear_in_same_query!(comments, issue_assignees, issues, projects, tokens, users,);