Add some filters

This commit is contained in:
Adrian Woźniak 2020-03-31 11:11:06 +02:00
parent 55a8904210
commit 1f382d9ac6
17 changed files with 326 additions and 89 deletions

View File

@ -37,6 +37,33 @@
width: 160px; width: 160px;
} }
#projectPage > #projectBoardFilters > #avatars {
display: flex;
flex-direction: row-reverse;
margin: 0 12px 0 2px;
}
#projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder {
display: inline-flex;
margin-left: -2px;
border-radius: 50%;
transition: transform 0.1s;
cursor: pointer;
user-select: none;
}
#projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder.isActive {
box-shadow: 0 0 0 4px var(--primary);
}
#projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder:hover {
transform: translateY(-5px);
}
#projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder > .styledAvatar {
box-shadow: 0 0 0 2px #fff;
}
@media (max-width: 1100px) { @media (max-width: 1100px) {
#projectPage { #projectPage {
padding: 25px 20px 50px calc(var(--appNavBarLeftWidth) + var(--secondarySideBarWidth) + 20px); padding: 25px 20px 50px calc(var(--appNavBarLeftWidth) + var(--secondarySideBarWidth) + 20px);

View File

@ -0,0 +1,29 @@
.styledAvatar.image {
display: inline-block;
border-radius: 100%;
/*background-image: url("${imageURL}");*/
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: cover;
background-color: var(--backgroundLight);
}
.styledAvatar.letter {
display: inline-block;
/*width: ${props => props.size} px;*/
/*height: ${props => props.size} px;*/
border-radius: 100%;
text-transform: uppercase;
color: #fff;
/*background: ${props => props.color};*/
font-family: var(--font-medium);
font-weight: normal;
/*${props => font.size(Math.round(props.size / 1.7))}*/
}
.styledAvatar.letter > span {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}

View File

@ -63,3 +63,14 @@
--font-bold: "CircularStdBold"; --font-bold: "CircularStdBold";
--font-black: "CircularStdBlack"; --font-black: "CircularStdBlack";
} }
:root {
--avatar-color-1: #DA7657;
--avatar-color-2: #6ADA57;
--avatar-color-3: #5784DA;
--avatar-color-4: #AA57DA;
--avatar-color-5: #DA5757;
--avatar-color-6: #DA5792;
--avatar-color-7: #57DACA;
--avatar-color-8: #57A5DA;
}

View File

@ -7,5 +7,6 @@
@import "css/icon.css"; @import "css/icon.css";
@import "css/shared.css"; @import "css/shared.css";
@import "css/styledTooltip.css"; @import "css/styledTooltip.css";
@import "css/styledAvatar.css";
@import "css/app.css"; @import "css/app.css";
@import "css/project.css"; @import "css/project.css";

View File

@ -11,6 +11,9 @@ mod project_settings;
mod register; mod register;
mod shared; mod shared;
pub type UserId = i32;
pub type AvatarFilterActive = bool;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Msg { pub enum Msg {
ChangePage(model::Page), ChangePage(model::Page),
@ -21,6 +24,8 @@ pub enum Msg {
// project // project
ProjectTextFilterChanged(String), ProjectTextFilterChanged(String),
ProjectAvatarFilterChanged(UserId, AvatarFilterActive),
ProjectToggleOnlyMy,
} }
fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) { fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {

View File

@ -51,6 +51,8 @@ pub struct UpdateProjectForm {
pub struct ProjectPage { pub struct ProjectPage {
pub about_tooltip_visible: bool, pub about_tooltip_visible: bool,
pub text_filter: String, pub text_filter: String,
pub active_avatar_filters: Vec<i32>,
pub only_my_filter: bool,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -85,6 +87,8 @@ impl Default for Model {
project_page: ProjectPage { project_page: ProjectPage {
about_tooltip_visible: false, about_tooltip_visible: false,
text_filter: "".to_string(), text_filter: "".to_string(),
active_avatar_filters: vec![],
only_my_filter: false,
}, },
} }
} }

View File

@ -1,9 +1,10 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use crate::model::{Icon, Model, Page}; use crate::model::{Icon, Model, Page};
use crate::shared::styled_avatar::StyledAvatar;
use crate::shared::styled_button::{StyledButton, Variant}; use crate::shared::styled_button::{StyledButton, Variant};
use crate::shared::styled_input::StyledInput; use crate::shared::styled_input::StyledInput;
use crate::shared::{host_client, inner_layout}; use crate::shared::{host_client, inner_layout, ToNode};
use crate::Msg; use crate::Msg;
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
@ -22,6 +23,23 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
Msg::ProjectTextFilterChanged(text) => { Msg::ProjectTextFilterChanged(text) => {
model.project_page.text_filter = text; model.project_page.text_filter = text;
} }
Msg::ProjectAvatarFilterChanged(user_id, active) => match active {
true => {
model.project_page.active_avatar_filters = model
.project_page
.active_avatar_filters
.iter()
.filter(|id| **id != user_id)
.map(|id| *id)
.collect();
}
false => {
model.project_page.active_avatar_filters.push(user_id);
}
},
Msg::ProjectToggleOnlyMy => {
model.project_page.only_my_filter = !model.project_page.only_my_filter;
}
_ => (), _ => (),
} }
} }
@ -56,6 +74,7 @@ fn header() -> Node<Msg> {
active: false, active: false,
text: Some("Github Repo".to_string()), text: Some("Github Repo".to_string()),
icon: Some(Icon::Github), icon: Some(Icon::Github),
on_click: None,
} }
.into_node(); .into_node();
div![ div![
@ -68,7 +87,7 @@ fn header() -> Node<Msg> {
] ]
} }
fn project_board_filters(_model: &Model) -> Node<Msg> { fn project_board_filters(model: &Model) -> Node<Msg> {
let search_input = StyledInput { let search_input = StyledInput {
icon: Some(Icon::Search), icon: Some(Icon::Search),
id: Some("searchInput".to_string()), id: Some("searchInput".to_string()),
@ -76,5 +95,54 @@ fn project_board_filters(_model: &Model) -> Node<Msg> {
on_change: input_ev(Ev::Change, |value| Msg::ProjectTextFilterChanged(value)), on_change: input_ev(Ev::Change, |value| Msg::ProjectTextFilterChanged(value)),
} }
.into_node(); .into_node();
div![id!["projectBoardFilters"], search_input]
let only_my = StyledButton {
variant: Variant::Empty,
icon_only: false,
disabled: false,
active: model.project_page.only_my_filter,
text: Some("Only my".to_string()),
icon: None,
on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy)),
}
.into_node();
div![
id!["projectBoardFilters"],
search_input,
avatars_filters(model),
only_my,
]
}
fn avatars_filters(model: &Model) -> Node<Msg> {
let project = match model.project.as_ref() {
Some(p) => p,
_ => return empty![],
};
let active_avatar_filters = &model.project_page.active_avatar_filters;
let avatars: Vec<Node<Msg>> = project
.users
.iter()
.map(|user| {
let mut class_list = vec!["avatarIsActiveBorder"];
let user_id = user.id;
let active = active_avatar_filters.contains(&user_id);
if active {
class_list.push("isActive");
}
let styled_avatar = StyledAvatar {
avatar_url: user.avatar_url.clone(),
size: 32,
name: user.name.clone(),
on_click: Some(mouse_ev(Ev::Click, move |_| {
Msg::ProjectAvatarFilterChanged(user_id, active)
})),
}
.into_node();
div![attrs![At::Class => class_list.join(" ")], styled_avatar]
})
.collect();
div![id!["avatars"], avatars]
} }

View File

@ -8,10 +8,15 @@ use crate::Msg;
pub mod aside; pub mod aside;
pub mod navbar_left; pub mod navbar_left;
pub mod styled_avatar;
pub mod styled_button; pub mod styled_button;
pub mod styled_input; pub mod styled_input;
pub mod styled_tooltip; pub mod styled_tooltip;
pub trait ToNode {
fn into_node(self) -> Node<Msg>;
}
pub fn styled_icon(icon: Icon) -> Node<Msg> { pub fn styled_icon(icon: Icon) -> Node<Msg> {
i![attrs![At::Class => format!("styledIcon {}", icon)], ""] i![attrs![At::Class => format!("styledIcon {}", icon)], ""]
} }

View File

@ -2,7 +2,7 @@ use seed::{prelude::*, *};
use crate::model::{Icon, Model}; use crate::model::{Icon, Model};
use crate::shared::styled_button::{StyledButton, Variant}; use crate::shared::styled_button::{StyledButton, Variant};
use crate::shared::styled_tooltip; use crate::shared::{styled_tooltip, ToNode};
use crate::Msg; use crate::Msg;
pub fn render(model: &Model) -> Vec<Node<Msg>> { pub fn render(model: &Model) -> Vec<Node<Msg>> {
@ -43,6 +43,27 @@ pub fn about_tooltip(_model: &Model, children: Node<Msg>) -> Node<Msg> {
} }
fn about_tooltip_popup(model: &Model) -> Node<Msg> { fn about_tooltip_popup(model: &Model) -> Node<Msg> {
let visit_website = StyledButton {
text: Some("Visit Website".to_string()),
variant: Variant::Primary,
disabled: false,
active: false,
icon_only: false,
icon: None,
on_click: None,
}
.into_node();
let github_repo = StyledButton {
text: Some("Github Repo".to_string()),
variant: Variant::Secondary,
disabled: false,
active: false,
icon_only: false,
icon: Some(Icon::Github),
on_click: None,
}
.into_node();
styled_tooltip::StyledTooltip { styled_tooltip::StyledTooltip {
visible: model.project_page.about_tooltip_visible, visible: model.project_page.about_tooltip_visible,
class_name: "aboutTooltipPopup".to_string(), class_name: "aboutTooltipPopup".to_string(),
@ -71,14 +92,7 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
At::Target => "_blank", At::Target => "_blank",
At::Rel => "noreferrer noopener", At::Rel => "noreferrer noopener",
], ],
StyledButton { visit_website,
text: Some("Visit Website".to_string()),
variant: Variant::Primary,
disabled: false,
active: false,
icon_only: false,
icon: None,
}.into_node(),
], ],
a![ a![
id!["about-github-button"], id!["about-github-button"],
@ -87,14 +101,7 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
At::Target => "_blank", At::Target => "_blank",
At::Rel => "noreferrer noopener", At::Rel => "noreferrer noopener",
], ],
StyledButton { github_repo
text: Some("Github Repo".to_string()),
variant: Variant::Secondary,
disabled: false,
active: false,
icon_only: false,
icon: Some(Icon::Github),
}.into_node()
] ]
], ],
}.into_node() }.into_node()

View File

@ -0,0 +1,52 @@
use crate::shared::ToNode;
use crate::Msg;
use seed::{prelude::*, *};
pub struct StyledAvatar {
pub avatar_url: Option<String>,
pub size: u32,
pub name: String,
pub on_click: Option<EventHandler<Msg>>,
}
impl Default for StyledAvatar {
fn default() -> Self {
Self {
avatar_url: None,
size: 32,
name: "".to_string(),
on_click: None,
}
}
}
impl ToNode for StyledAvatar {
fn into_node(self) -> Node<Msg> {
render(self)
}
}
pub fn render(values: StyledAvatar) -> Node<Msg> {
let StyledAvatar {
avatar_url,
size,
name,
on_click,
} = values;
let shared_style = format!("width: {size}px; height: {size}px", size = size);
let handler = match on_click {
None => vec![],
Some(h) => vec![h],
};
match avatar_url {
Some(url) => div![
attrs![At::Class => "styledAvatar image", At::Style => format!("{shared}; background-image: url({url});", shared = shared_style, url = url)],
handler,
],
_ => div![
attrs![At::Class => "styledAvatar letter", At::Style => shared_style],
span![name],
handler
],
}
}

View File

@ -1,7 +1,7 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use crate::model::Icon; use crate::model::Icon;
use crate::shared::styled_icon; use crate::shared::{styled_icon, ToNode};
use crate::Msg; use crate::Msg;
pub enum Variant { pub enum Variant {
@ -32,21 +32,16 @@ pub struct StyledButton {
pub active: bool, pub active: bool,
pub text: Option<String>, pub text: Option<String>,
pub icon: Option<Icon>, pub icon: Option<Icon>,
pub on_click: Option<EventHandler<Msg>>,
} }
impl Into<Node<Msg>> for StyledButton { impl ToNode for StyledButton {
fn into(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
styled_button(self) render(self)
} }
} }
impl StyledButton { pub fn render(values: StyledButton) -> Node<Msg> {
pub fn into_node(self) -> Node<Msg> {
self.into()
}
}
pub fn styled_button(values: StyledButton) -> Node<Msg> {
let StyledButton { let StyledButton {
text, text,
variant, variant,
@ -54,6 +49,7 @@ pub fn styled_button(values: StyledButton) -> Node<Msg> {
disabled, disabled,
active, active,
icon, icon,
on_click,
} = values; } = values;
let mut class_list = vec!["styledButton".to_string(), variant.to_string()]; let mut class_list = vec!["styledButton".to_string(), variant.to_string()];
if icon_only { if icon_only {
@ -65,6 +61,10 @@ pub fn styled_button(values: StyledButton) -> Node<Msg> {
if icon.is_some() { if icon.is_some() {
class_list.push("withIcon".to_string()); class_list.push("withIcon".to_string());
} }
let handler = match on_click {
Some(h) if !disabled => vec![h],
_ => vec![],
};
let icon_node = match icon { let icon_node = match icon {
None => empty![], None => empty![],
@ -75,6 +75,7 @@ pub fn styled_button(values: StyledButton) -> Node<Msg> {
attrs![ attrs![
At::Class => class_list.join(" "), At::Class => class_list.join(" "),
], ],
handler,
match disabled { match disabled {
true => vec![attrs![At::Disabled => true]], true => vec![attrs![At::Disabled => true]],
false => vec![], false => vec![],

View File

@ -1,7 +1,7 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use crate::model::Icon; use crate::model::Icon;
use crate::shared::styled_icon; use crate::shared::{styled_icon, ToNode};
use crate::Msg; use crate::Msg;
pub struct StyledInput { pub struct StyledInput {
@ -11,18 +11,12 @@ pub struct StyledInput {
pub on_change: EventHandler<Msg>, pub on_change: EventHandler<Msg>,
} }
impl Into<Node<Msg>> for StyledInput { impl ToNode for StyledInput {
fn into(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
impl StyledInput {
pub fn into_node(self) -> Node<Msg> {
self.into()
}
}
pub fn render(values: StyledInput) -> Node<Msg> { pub fn render(values: StyledInput) -> Node<Msg> {
let StyledInput { let StyledInput {
id, id,

View File

@ -1,5 +1,6 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use crate::shared::ToNode;
use crate::Msg; use crate::Msg;
pub struct StyledTooltip { pub struct StyledTooltip {
@ -8,19 +9,13 @@ pub struct StyledTooltip {
pub children: Node<Msg>, pub children: Node<Msg>,
} }
impl Into<Node<Msg>> for StyledTooltip { impl ToNode for StyledTooltip {
fn into(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
styled_tooltip(self) render(self)
} }
} }
impl StyledTooltip { pub fn render(values: StyledTooltip) -> Node<Msg> {
pub fn into_node(self) -> Node<Msg> {
self.into()
}
}
pub fn styled_tooltip(values: StyledTooltip) -> Node<Msg> {
let StyledTooltip { let StyledTooltip {
visible, visible,
class_name, class_name,

0
migrations/.gitkeep Normal file
View File

View File

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -1,55 +1,51 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Image, Letter } from './Styles'; import { Image, Letter } from './Styles';
const propTypes = {
className: PropTypes.string,
avatarUrl: PropTypes.string,
name: PropTypes.string,
size: PropTypes.number,
};
const defaultProps = {
className: undefined,
avatarUrl: null,
name: '',
size: 32,
};
const Avatar = ({ className, avatarUrl, name, size, ...otherProps }) => { const Avatar = ({ className, avatarUrl, name, size, ...otherProps }) => {
const sharedProps = { const sharedProps = {
className, className,
size, size,
'data-testid': name ? `avatar:${name}` : 'avatar', 'data-testid': name ? `avatar:${name}` : 'avatar',
...otherProps, ...otherProps,
}; };
if (avatarUrl) { if (avatarUrl) {
return <Image avatarUrl={avatarUrl} {...sharedProps} />; return <Image avatarUrl={avatarUrl} {...sharedProps} />;
} }
return ( return (
<Letter color={getColorFromName(name)} {...sharedProps}> <Letter color={getColorFromName(name)} {...sharedProps}>
<span>{name.charAt(0)}</span> <span>{name.charAt(0)}</span>
</Letter> </Letter>
); );
}; };
const colors = [ const colors = [
'#DA7657', '#DA7657',
'#6ADA57', '#6ADA57',
'#5784DA', '#5784DA',
'#AA57DA', '#AA57DA',
'#DA5757', '#DA5757',
'#DA5792', '#DA5792',
'#57DACA', '#57DACA',
'#57A5DA', '#57A5DA',
]; ];
const getColorFromName = name => colors[name.toLocaleLowerCase().charCodeAt(0) % colors.length]; const getColorFromName = name => colors[name.toLocaleLowerCase().charCodeAt(0) % colors.length];
Avatar.propTypes = propTypes; Avatar.propTypes = {
Avatar.defaultProps = defaultProps; className: PropTypes.string,
avatarUrl: PropTypes.string,
name: PropTypes.string,
size: PropTypes.number,
};
Avatar.defaultProps = {
className: undefined,
avatarUrl: null,
name: '',
size: 32,
};
export default Avatar; export default Avatar;