Remove yewprint dependency

This commit is contained in:
Gabriel Tofvesson 2023-01-15 08:34:59 +01:00
parent 23f23c4936
commit eda5b62650
25 changed files with 1320 additions and 274 deletions

30
Cargo.lock generated
View File

@ -748,15 +748,6 @@ dependencies = [
"rayon", "rayon",
] ]
[[package]]
name = "id_tree"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcd9db8dd5be8bde5a2624ed4b2dfb74368fe7999eb9c4940fd3ca344b61071a"
dependencies = [
"snowflake",
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.3.0" version = "0.3.0"
@ -1460,12 +1451,6 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "snowflake"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27207bb65232eda1f588cf46db2fee75c0808d557f6b3cf19a75f5d6d7c94df1"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.5.2" version = "0.5.2"
@ -2306,20 +2291,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "yewprint"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29de9263e650aab7a0ccf49c53ae6b122082b1451762ca2d0e64367e6c8448b2"
dependencies = [
"gloo",
"id_tree",
"implicit-clone",
"wasm-bindgen",
"web-sys",
"yew",
]
[[package]] [[package]]
name = "yewprint-app" name = "yewprint-app"
version = "0.1.0" version = "0.1.0"
@ -2338,7 +2309,6 @@ dependencies = [
"wee_alloc", "wee_alloc",
"yew", "yew",
"yew-router", "yew-router",
"yewprint",
] ]
[[package]] [[package]]

View File

@ -17,7 +17,7 @@ crate-type = ["cdylib"]
console_error_panic_hook = { version = "0.1.6", optional = true } console_error_panic_hook = { version = "0.1.6", optional = true }
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["Window", "MediaQueryList"] } web-sys = { version = "0.3", features = ["Window", "MediaQueryList", "DomTokenList"] }
js-sys = "0.3" js-sys = "0.3"
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
@ -29,7 +29,7 @@ wee_alloc = { version = "0.4.5", optional = false }
yew = { version = "0.20.0", features = ["csr"] } yew = { version = "0.20.0", features = ["csr"] }
yew-router = { version = "0.17.0" } yew-router = { version = "0.17.0" }
yewprint = "0.4.0" #yewprint = "0.4.0"
gloo = "0.8.0" gloo = "0.8.0"
gloo-events = "0.1" gloo-events = "0.1"
futures = "0.3.25" futures = "0.3.25"

View File

@ -3,7 +3,7 @@ use yew::prelude::*;
use yew_router::Routable; use yew_router::Routable;
use yew_router::prelude::Navigator; use yew_router::prelude::Navigator;
use yew_router::prelude::use_navigator; use yew_router::prelude::use_navigator;
use yewprint::Icon; use crate::component::fa_icon::FontawesomeIcon;
use crate::page::Page; use crate::page::Page;
use crate::page::PageRouter; use crate::page::PageRouter;
use crate::page::Pages; use crate::page::Pages;
@ -11,6 +11,7 @@ use crate::theme::ThemeContext;
use crate::theme::ThemeMsg; use crate::theme::ThemeMsg;
use crate::theme::ThemeProvider; use crate::theme::ThemeProvider;
use crate::component::actionbar::{Actionbar, ActionbarOption}; use crate::component::actionbar::{Actionbar, ActionbarOption};
use crate::theme::ThemeState;
#[function_component] #[function_component]
@ -29,16 +30,16 @@ fn ThemedApp() -> Html {
let navigator = use_navigator().unwrap(); let navigator = use_navigator().unwrap();
// Helper function for generating tabs // Helper function for generating tabs
fn page_option(current: &Page, page: Page, navigator: Navigator, state: UseStateHandle<Page>) -> ActionbarOption { fn page_option(current: &Page, page: Page, navigator: Navigator, state: UseStateSetter<Page>) -> ActionbarOption {
let is_current = *current == page; let is_current = *current == page;
ActionbarOption::new( ActionbarOption::new(
page.name(), page.name(),
match page { match page {
Page::Home => Icon::Home, Page::Home => FontawesomeIcon::House,
Page::Projects => Icon::Code, Page::Projects => FontawesomeIcon::Code,
Page::Socials => Icon::SocialMedia, Page::Socials => FontawesomeIcon::ShareNodes,
Page::Gallery => Icon::Camera, Page::Gallery => FontawesomeIcon::Camera,
Page::Contact => Icon::Envelope, Page::Contact => FontawesomeIcon::Envelope,
}, },
Callback::from(move |_| { Callback::from(move |_| {
navigator.push(&page); navigator.push(&page);
@ -49,6 +50,7 @@ fn ThemedApp() -> Html {
} }
let ctx = use_context::<ThemeContext>().expect("No theme context supplied"); let ctx = use_context::<ThemeContext>().expect("No theme context supplied");
let theme_icon = if ctx.theme == ThemeState::Dark { FontawesomeIcon::Sun } else { FontawesomeIcon::Moon };
let page_state = use_state_eq(|| let page_state = use_state_eq(||
window() window()
.location() .location()
@ -65,23 +67,21 @@ fn ThemedApp() -> Html {
<div class="main"> <div class="main">
<Pages /> <Pages />
</div> </div>
<Actionbar> <Actionbar mobile_title="Menu" items={vec![
{vec![ page_option(&current_page, Page::Home, navigator.clone(), page_state.setter()),
page_option(&current_page, Page::Home, navigator.clone(), page_state.clone()), page_option(&current_page, Page::Projects, navigator.clone(), page_state.setter()),
page_option(&current_page, Page::Projects, navigator.clone(), page_state.clone()), page_option(&current_page, Page::Socials, navigator.clone(), page_state.setter()),
page_option(&current_page, Page::Socials, navigator.clone(), page_state.clone()), page_option(&current_page, Page::Gallery, navigator.clone(), page_state.setter()),
page_option(&current_page, Page::Gallery, navigator.clone(), page_state.clone()), page_option(&current_page, Page::Contact, navigator, page_state.setter()),
page_option(&current_page, Page::Contact, navigator, page_state), ActionbarOption::new_opt(
ActionbarOption::new_opt( None,
None, Some(theme_icon),
Some(Icon::Flash), Some(Callback::from(move |_| ctx.dispatch(ThemeMsg::Toggle))),
Some(Callback::from(move |_| ctx.dispatch(ThemeMsg::Toggle))), true,
true, false,
false, false
false )
) ]}/>
]}
</Actionbar>
</> </>
} }
} }

View File

@ -1,14 +1,12 @@
use yew::html::ChildrenRenderer;
use yew::prelude::*; use yew::prelude::*;
use yew::virtual_dom::VNode; use yew::virtual_dom::VNode;
use yewprint::{Collapse, Icon, ButtonGroup};
use yewprint::Button; use super::fa_icon::{FAIcon, FontawesomeIcon};
use yewprint::Icon::{MenuClosed, MenuOpen};
#[derive(PartialEq, Clone)] #[derive(PartialEq, Clone)]
pub struct ActionbarOption { pub struct ActionbarOption {
name: Option<String>, name: Option<String>,
icon: Option<Icon>, icon: Option<FontawesomeIcon>,
onclick: Option<Callback<MouseEvent>>, onclick: Option<Callback<MouseEvent>>,
center_content: bool, center_content: bool,
selected: bool, selected: bool,
@ -18,7 +16,7 @@ pub struct ActionbarOption {
#[derive(PartialEq, Clone)] #[derive(PartialEq, Clone)]
struct ActionbarOptionState { struct ActionbarOptionState {
inner: ActionbarOption, inner: ActionbarOption,
callback: UseStateHandle<bool> callback: UseStateSetter<bool>
} }
impl Into<VNode> for ActionbarOption { impl Into<VNode> for ActionbarOption {
@ -34,57 +32,69 @@ impl Into<VNode> for ActionbarOption {
} }
impl ActionbarOption { impl ActionbarOption {
pub fn new_opt(name: Option<String>, icon: Option<Icon>, onclick: Option<Callback<MouseEvent>>, center_content: bool, selected: bool, fill: bool) -> ActionbarOption { pub fn new_opt(name: Option<String>, icon: Option<FontawesomeIcon>, onclick: Option<Callback<MouseEvent>>, center_content: bool, selected: bool, fill: bool) -> ActionbarOption {
ActionbarOption { name, icon, onclick, center_content, selected, fill } ActionbarOption { name, icon, onclick, center_content, selected, fill }
} }
pub fn new(name: &str, icon: Icon, onclick: Callback<MouseEvent>, selected: bool) -> ActionbarOption { pub fn new(name: &str, icon: FontawesomeIcon, onclick: Callback<MouseEvent>, selected: bool) -> ActionbarOption {
ActionbarOption::new_opt(Some(name.to_owned()), Some(icon), Some(onclick), false, selected, true) ActionbarOption::new_opt(Some(name.to_owned()), Some(icon), Some(onclick), false, selected, true)
} }
} }
impl Into<VNode> for ActionbarOptionState { impl Into<VNode> for ActionbarOptionState {
fn into(self) -> VNode { fn into(self) -> VNode {
let classes = if self.inner.center_content { classes!() } else { classes!("ab-navbutton") };
let onclick = self.inner.onclick.unwrap_or_default(); let onclick = self.inner.onclick.unwrap_or_default();
html! { html! {
<Button <div
fill={self.inner.fill} class={classes!(
large=true if self.inner.selected { "current" } else { "" },
class={classes} if self.inner.icon.is_some() && self.inner.name.is_some() { "" } else { "small" }
icon={self.inner.icon} )}
onclick={Callback::from(move |e| { self.callback.set(false); onclick.emit(e) })} onclick={Callback::from(move |e| {
active={self.inner.selected}> self.callback.set(false);
{self.inner.name} onclick.emit(e)
</Button> })}>
<div>
{if let Some(icon) = self.inner.icon { html!{ <FAIcon icon={icon} /> } } else { html!{} }}
<span>{self.inner.name}</span>
</div>
</div>
} }
} }
} }
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct ActionbarProps { pub struct ActionbarProps {
pub children: ChildrenRenderer<ActionbarOption> #[prop_or_default]
pub mobile_title: Option<String>,
pub items: Vec<ActionbarOption>
} }
#[function_component] #[function_component]
pub fn Actionbar(props: &ActionbarProps) -> Html { pub fn Actionbar(props: &ActionbarProps) -> Html {
let expanded = use_state_eq(|| false); let expanded = use_state_eq(|| false);
let expanded_setter = expanded.setter();
let is_expanded = *expanded; let is_expanded = *expanded;
let children = props.children.iter().map(|opt| ActionbarOptionState { inner: opt, callback: expanded.clone() }).collect::<Vec<ActionbarOptionState>>();
html! { html! {
<div class="actionbar"> <div class="navbar">
<div class="ab-portrait"> <div style={if props.mobile_title.is_some() { "gap: 5px;" } else { "" }} onclick={Callback::from(move |_| expanded.set(!*expanded))}>
<Button onclick={Callback::from(move |_| expanded.set(!is_expanded))} icon={if is_expanded { MenuOpen } else { MenuClosed }} large=true /> <FAIcon
<Collapse is_open={is_expanded}> icon={if is_expanded { FontawesomeIcon::ChevronUp } else { FontawesomeIcon::ChevronDown }} />
<ButtonGroup class="ab-portrait-content" vertical=true> {
{children.clone()} if let Some(ref title) = props.mobile_title {
</ButtonGroup> html! {
</Collapse> <span>{title}</span>
}
} else { html! {} }
}
</div>
<div class={if is_expanded { "open" } else { "" }}></div>
<div>
{props.items.iter()
.map(move |opt| ActionbarOptionState { inner: (*opt).clone(), callback: expanded_setter.clone() })
.collect::<Html>()}
</div> </div>
<ButtonGroup class="ab-landscape" fill=true>
{children}
</ButtonGroup>
</div> </div>
} }
} }

0
src/component/cache.rs Normal file
View File

30
src/component/card.rs Normal file
View File

@ -0,0 +1,30 @@
use web_sys::MouseEvent;
use yew::{Html, function_component, html, Children, Properties, Classes, Callback, classes};
#[derive(Properties, PartialEq)]
pub struct CardProps {
#[prop_or_default]
pub children: Children,
#[prop_or_default]
pub style: String,
#[prop_or_default]
pub class: Classes,
#[prop_or_default]
pub onclick: Callback<MouseEvent>,
}
#[function_component]
pub fn Card(props: &CardProps) -> Html {
html! {
<div
class={classes!("card", props.class.clone())}
style={props.style.clone()}
onclick={props.onclick.clone()}>
{props.children.clone()}
</div>
}
}

377
src/component/fa_icon.rs Normal file
View File

@ -0,0 +1,377 @@
use std::fmt::Display;
use web_sys::MouseEvent;
use yew::{Properties, function_component, Html, html, Callback, Classes, classes};
#[derive(PartialEq, Clone)]
pub enum FontawesomeIcon {
Rust,
GitHub,
Camera,
ShareNodes,
Sun,
Moon,
House,
Code,
Envelope,
ChevronDown,
ChevronUp,
ChevronLeft,
ChevronRight,
}
impl Display for FontawesomeIcon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Rust => "fa-brands fa-rust",
Self::GitHub => "fa-brands fa-github",
Self::Camera => "fa-camera",
Self::ShareNodes => "fa-share-nodes",
Self::Sun => "fa-sun",
Self::Moon => "fa-moon",
Self::House => "fa-house",
Self::Code => "fa-code",
Self::Envelope => "fa-envelope",
Self::ChevronDown => "fa-chevron-down",
Self::ChevronUp => "fa-chevron-up",
Self::ChevronLeft => "fa-chevron-left",
Self::ChevronRight => "fa-chevron-right",
})
}
}
#[derive(PartialEq, Default)]
pub enum FontawesomeStyle {
#[default]
//Regular,
Solid,
Light,
DuoTone,
Thin,
Sharp,
}
impl Display for FontawesomeStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
//Self::Regular => "fa-regular",
Self::Solid => "fa-solid",
Self::Light => "fa-light",
Self::DuoTone => "fa-duotone",
Self::Thin => "fa-thin",
Self::Sharp => "fa-sharp",
})
}
}
#[derive(PartialEq, Default)]
pub enum FontawesomeSize {
XXS,
XS,
S,
#[default]
Regular,
L,
XL,
XXL
}
impl Display for FontawesomeSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::XXS => "fa-2xs",
Self::XS => "fa-xs",
Self::S => "fa-sm",
Self::Regular => "",
Self::L => "fa-lg",
Self::XL => "fa-xl",
Self::XXL => "fa-2xl"
})
}
}
#[derive(PartialEq, Default)]
pub enum FontawesomeSpinType {
#[default]
Regular,
Pulse
}
#[derive(PartialEq, Default)]
pub enum FontawesomeSpinDirection {
#[default]
Clockwise,
CounterClockwise
}
#[derive(PartialEq)]
pub enum FontawesomeAnimationType {
Beat {
scale: f32,
},
Fade {
opacity: f32,
},
BeatFade {
scale: f32,
opacity: f32,
},
Bounce {
rebound: f32,
height: f32,
squish_scale_x: f32,
squish_scale_y: f32,
jump_scale_x: f32,
jump_scale_y: f32,
land_scale_x: f32,
land_scale_y: f32,
},
Flip {
x: f32,
y: f32,
z: f32,
angle: f32,
},
Shake,
Spin {
spin_type: FontawesomeSpinType,
direction: FontawesomeSpinDirection
}
}
impl FontawesomeAnimationType {
fn class(&self) -> &'static str {
match self {
Self::Beat { scale: _ } => "fa-beat",
Self::Fade { opacity: _ } => "fa-fade",
Self::BeatFade { scale: _, opacity: _ } => "fa-beat-fade",
Self::Bounce { rebound: _, height: _, squish_scale_x: _, squish_scale_y: _, jump_scale_x: _, jump_scale_y: _, land_scale_x: _, land_scale_y: _ } => "fa-bounce",
Self::Flip { x: _, y: _, z: _, angle: _ } => "fa-flip",
Self::Shake => "fa-shake",
Self::Spin { spin_type, direction } =>
match direction {
FontawesomeSpinDirection::Clockwise =>
match spin_type {
FontawesomeSpinType::Regular => "fa-spin",
FontawesomeSpinType::Pulse => "fa-spin-pulse",
},
FontawesomeSpinDirection::CounterClockwise =>
match spin_type {
FontawesomeSpinType::Regular => "fa-spin fa-spin-reverse",
FontawesomeSpinType::Pulse => "fa-spin-pulse fa-spin-reverse",
},
}
}
}
fn style(&self) -> String {
match self {
Self::Beat { scale } => format!("--fa-beat-scale: {scale};"),
Self::Fade { opacity } => format!("--fa-fade-opacity: {opacity};"),
Self::BeatFade { scale, opacity } => format!("--fa-beat-fade-opacity: {opacity}; --fa-beat-fade-scale: {scale};"),
Self::Bounce {
rebound,
height,
squish_scale_x,
squish_scale_y,
jump_scale_x,
jump_scale_y,
land_scale_x,
land_scale_y
} => format!("--fa-bounce-rebound: {rebound}; --fa-bounce-height: {height}; --fa-bounce-start-scale-x: {squish_scale_x}; --fa-bounce-start-scale-y: {squish_scale_y}; --fa-bounce-jump-scale-x: {jump_scale_x}; --fa-bounce-jump-scale-y: {jump_scale_y}; --fa-bounce-land-scale-x: {land_scale_x}; --fa-bounce-land-scale-y: {land_scale_y};"),
Self::Flip { x, y, z, angle } => format!("--fa-flip-x: {x}; --fa-flip-y: {y}; --fa-flip-z: {z}; --fa-flip-angle: {angle};"),
_ => "".to_owned(),
}
}
}
#[derive(PartialEq)]
pub enum AnimationTime {
Seconds(f32),
Milliseconds(f32),
}
impl AnimationTime {
fn style_unit(&self) -> String {
match self {
Self::Seconds(seconds) => format!("{seconds}s"),
Self::Milliseconds(millis) => format!("{millis}ms"),
}
}
}
impl Default for AnimationTime {
fn default() -> Self {
Self::Seconds(0.0)
}
}
impl Display for AnimationTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.style_unit())
}
}
#[derive(PartialEq)]
pub enum AnimationDuration {
Time(AnimationTime),
Infinite
}
impl Default for AnimationDuration {
fn default() -> Self {
Self::Time(Default::default())
}
}
impl Display for AnimationDuration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&match self {
Self::Time(time) => time.style_unit(),
Self::Infinite => "infinite".to_owned(),
})
}
}
#[derive(PartialEq, Default)]
pub enum AnimationDirection {
#[default]
Normal,
Reverse,
Alternate,
AlternateReverse,
}
impl Display for AnimationDirection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Normal => "normal",
Self::Reverse => "reverse",
Self::Alternate => "alternate",
Self::AlternateReverse => "alternate-reverse"
})
}
}
#[derive(PartialEq)]
pub enum AnimationStepType {
JumpStart,
JumpEnd,
JumpNone,
JumpBoth,
Start,
End
}
impl Display for AnimationStepType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::JumpStart => "jump-start",
Self::JumpEnd => "jump-end",
Self::JumpNone => "jump-none",
Self::JumpBoth => "jump-both",
Self::Start => "start",
Self::End => "end",
})
}
}
#[derive(PartialEq, Default)]
pub enum AnimationTiming {
#[default]
Ease,
Linear,
EaseIn,
EaseOut,
EaseInOut,
CubicBezier(f32, f32, f32, f32),
Steps(i32, AnimationStepType),
StepStart,
StepEnd,
}
impl Display for AnimationTiming {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&
match self {
Self::Ease => "ease".to_owned(),
Self::Linear => "linear".to_owned(),
Self::EaseIn => "ease-in".to_owned(),
Self::EaseOut => "ease-out".to_owned(),
Self::EaseInOut => "ease-in-out".to_owned(),
Self::CubicBezier(p1, p2, p3, p4) => format!("cubic-bezier({p1}, {p2}, {p3}, {p4})"),
Self::Steps(count, step_type) => format!("steps({count}, {step_type})"),
Self::StepStart => "step-start".to_owned(),
Self::StepEnd => "step-end".to_owned(),
})
}
}
#[derive(PartialEq)]
pub struct FontawesomeAnimation {
duration: AnimationDuration,
delay: AnimationTime,
direction: AnimationDirection,
iterations: AnimationDuration,
timing: AnimationTiming,
anim_type: FontawesomeAnimationType
}
impl FontawesomeAnimation {
fn class(&self) -> &'static str {
self.anim_type.class()
}
fn style(&self) -> String {
format!(
"--fa-animation-delay: {}; --fa-animation-direction: {}; --fa-animation-duration: {}; --fa-animation-iteration-count: {}; --fa-animation-timing: {}; {}",
self.delay,
self.direction,
self.duration,
self.iterations,
self.timing,
self.anim_type.style()
)
}
}
#[derive(Properties, PartialEq)]
pub struct FontawesomeProperties {
pub icon: FontawesomeIcon,
#[prop_or_default]
pub style: FontawesomeStyle,
#[prop_or_default]
pub size: FontawesomeSize,
#[prop_or_default]
pub animation: Option<FontawesomeAnimation>,
#[prop_or_default]
pub onclick: Callback<MouseEvent>,
#[prop_or_default]
pub class: Classes,
}
#[function_component]
pub fn FAIcon(props: &FontawesomeProperties) -> Html {
html! {
<i
onclick={props.onclick.clone()}
class={classes!(format!(
"{} {} {} {}",
props.icon,
props.style,
props.size,
match props.animation {
None => "",
Some(ref animation) => animation.class()
}
), props.class.clone())}
style={format!(
"{}",
match props.animation {
None => "".to_owned(),
Some(ref animation) => animation.style()
}
)}></i>
}
}

View File

@ -1,9 +1,7 @@
use web_sys::MouseEvent; use web_sys::MouseEvent;
use yew::{function_component, Html, html, Callback, Properties, use_state}; use yew::{function_component, Html, html, Callback, Properties, Component};
use yewprint::{Overlay, Icon, IconSize, Intent};
const CHEVRON_SIZE: f64 = 40.0; use crate::component::{overlay::Overlay, fa_icon::{FAIcon, FontawesomeIcon, FontawesomeSize}};
const CHEVRON_TYPE: Intent = Intent::Danger;
#[derive(PartialEq, Clone)] #[derive(PartialEq, Clone)]
pub struct ImageDescription { pub struct ImageDescription {
@ -36,20 +34,24 @@ pub struct DefaultImageViewerProps {
pub onclose: Callback<()>, pub onclose: Callback<()>,
} }
/*
#[function_component] #[function_component]
pub fn DefaultImageViewer(props: &DefaultImageViewerProps) -> Html { pub fn DefaultImageViewer(props: &DefaultImageViewerProps) -> Html {
let selected_image = use_state(|| 0); let selected_image = use_state_eq(|| 0);
let (select_next, select_prev) = (selected_image.clone(), selected_image.clone()); let select_next = selected_image.setter();
let select_prev = selected_image.setter();
let (has_next, has_prev) = (props.infinite || *selected_image < props.images.len() - 1, props.infinite || *selected_image > 0); let has_next = props.infinite || *selected_image < props.images.len() - 1;
let next = (*select_next + props.images.len() + 1) % props.images.len(); let has_prev = props.infinite || *selected_image > 0;
let prev = (*select_prev + props.images.len() - 1) % props.images.len(); let next = (*selected_image + props.images.len() + 1) % props.images.len();
let prev = (*selected_image + props.images.len() - 1) % props.images.len();
let onclose = props.onclose.clone();
html! { html! {
<ImageViewer <ImageViewer
image={props.images[*selected_image].clone()} image={props.images[*selected_image].clone()}
open={props.open && !props.images.is_empty()} open={props.open && !props.images.is_empty()}
onclose={props.onclose.clone()} onclose={Callback::from(move |_| onclose.emit(()))}
has_next={has_next} has_next={has_next}
has_prev={has_prev} has_prev={has_prev}
onnext={Callback::from(move |_| if has_next { onnext={Callback::from(move |_| if has_next {
@ -61,6 +63,70 @@ pub fn DefaultImageViewer(props: &DefaultImageViewerProps) -> Html {
/> />
} }
} }
*/
pub enum DefaultImageViewerMessage {
Next,
Prev
}
pub struct DefaultImageViewer {
current: usize,
}
impl Component for DefaultImageViewer {
type Message = DefaultImageViewerMessage;
type Properties = DefaultImageViewerProps;
fn create(_ctx: &yew::Context<Self>) -> Self {
Self { current: 0 }
}
fn view(&self, ctx: &yew::Context<Self>) -> Html {
let DefaultImageViewerProps {
images,
open,
infinite,
onclose,
} = ctx.props();
let has_next = *infinite || self.current < images.len() - 1;
let has_prev = *infinite || self.current > 0;
let onclose = onclose.clone();
html! {
<ImageViewer
image={images[self.current].clone()}
open={*open && !images.is_empty()}
onclose={Callback::from(move |_| onclose.emit(()))}
has_next={has_next}
has_prev={has_prev}
onnext={if has_next { ctx.link().callback(|_| DefaultImageViewerMessage::Next)} else { Default::default() }}
onprev={if has_prev { ctx.link().callback(|_| DefaultImageViewerMessage::Prev)} else { Default::default() }}
/>
}
}
fn update(&mut self, ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
let images = &ctx.props().images;
self.current = match msg {
DefaultImageViewerMessage::Next => {
if self.current == images.len() - 1 { 0 } else { self.current + 1 }
},
DefaultImageViewerMessage::Prev => {
if self.current == 0 { images.len() - 1 } else { self.current - 1 }
}
};
images.len() > 1
}
fn changed(&mut self, ctx: &yew::Context<Self>, old_props: &Self::Properties) -> bool {
ctx.props() != old_props
}
}
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct ImageViewerProps { pub struct ImageViewerProps {
@ -81,18 +147,27 @@ pub fn ImageViewer(props: &ImageViewerProps) -> Html {
html! { html! {
<> <>
<Overlay <Overlay
class="gallery" class="overlay-gallery"
open={props.open && props.image.is_some()} open={props.open && props.image.is_some()}
onclose={&props.onclose}> onclick={Callback::from(move |_| onclose.emit(()))}>
<> <>
<div onclick={Callback::from(move |_| onclose.emit(()))}></div> <div></div>
{ {
if let Some(image_desc) = props.image.as_ref() { if let Some(image_desc) = props.image.as_ref() {
let onclose = props.onclose.clone(); let onclose = props.onclose.clone();
Some(html! { Some(html! {
<> <>
<div onclick={Callback::from(move |_| onclose.emit(()))}> <div>
<img src={if image_desc.link.starts_with("https?://") { image_desc.link.to_string() } else { format!("/img/{}", image_desc.link) }} /> <img
onclick={Callback::from(move |_| onclose.emit(()))}
src={
if image_desc.link.starts_with("https?://") {
image_desc.link.to_string()
} else {
format!("/img/{}", image_desc.link)
}
}
/>
{ {
if let Some(description) = &image_desc.description { if let Some(description) = &image_desc.description {
Some(html! { Some(html! {
@ -106,13 +181,13 @@ pub fn ImageViewer(props: &ImageViewerProps) -> Html {
// Controls: Next // Controls: Next
if props.has_next { if props.has_next {
Some(html! { Some(html! {
<Icon <div class="overlay-gallery-next">
icon={yewprint::Icon::ChevronRight} <FAIcon
intent={CHEVRON_TYPE} icon={FontawesomeIcon::ChevronRight}
size={IconSize(CHEVRON_SIZE)} size={FontawesomeSize::XL}
onclick={props.onnext.clone()} onclick={props.onnext.clone()}
class="gallery-next" />
/> </div>
}) })
} else { None } } else { None }
} }
@ -121,13 +196,13 @@ pub fn ImageViewer(props: &ImageViewerProps) -> Html {
// Controls: Prev // Controls: Prev
if props.has_prev { if props.has_prev {
Some(html! { Some(html! {
<Icon <div class="overlay-gallery-prev">
icon={yewprint::Icon::ChevronLeft} <FAIcon
intent={CHEVRON_TYPE} icon={FontawesomeIcon::ChevronLeft}
size={IconSize(CHEVRON_SIZE)} size={FontawesomeSize::XL}
onclick={props.onprev.clone()} onclick={props.onprev.clone()}
class="gallery-prev" />
/> </div>
}) })
} else { None } } else { None }
} }

View File

@ -1,2 +1,7 @@
pub mod actionbar; pub mod actionbar;
pub mod image_viewer; pub mod image_viewer;
pub mod cache;
pub mod fa_icon;
pub mod overlay;
pub mod card;
pub mod tag;

92
src/component/overlay.rs Normal file
View File

@ -0,0 +1,92 @@
use web_sys::MouseEvent;
use yew::{Html, html, Properties, Children, Classes, classes, Callback, Component};
#[derive(Properties, PartialEq, Debug)]
pub struct OverlayProps {
pub open: bool,
#[prop_or_default]
pub noshade: bool,
#[prop_or_default]
pub center: bool,
#[prop_or_default]
pub children: Children,
#[prop_or_default]
pub class: Classes,
#[prop_or_default]
pub onclick: Callback<()>
}
#[derive(Debug)]
pub enum OverlayMessage {
ClickContent,
ClickRoot
}
pub struct Overlay {
should_notify: bool,
on_root_click: Callback<MouseEvent>,
on_child_click: Callback<MouseEvent>,
}
impl Component for Overlay {
type Message = OverlayMessage;
type Properties = OverlayProps;
fn create(ctx: &yew::Context<Self>) -> Self {
Self {
should_notify: true,
on_root_click: ctx.link().callback(|_| OverlayMessage::ClickRoot),
on_child_click: ctx.link().callback(|_| OverlayMessage::ClickContent)
}
}
fn view(&self, ctx: &yew::Context<Self>) -> Html {
let OverlayProps {
open,
noshade,
center,
children,
class,
onclick: _,
} = ctx.props();
html! {
<div
onclick={self.on_root_click.clone()}
class={format!(
"overlay{}{}",
if *noshade { "" } else { " shade" },
if *open { "" } else { " hidden" }
)}>
<div
onclick={self.on_child_click.clone()}
class={classes!(if *center { "center" } else { "" }, class.clone())}>{children.clone()}</div>
</div>
}
}
fn update(&mut self, ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
let mut notified = false;
match msg {
OverlayMessage::ClickContent => self.should_notify = false,
OverlayMessage::ClickRoot => {
if self.should_notify {
ctx.props().onclick.emit(());
notified = true;
}
self.should_notify = true;
}
}
notified
}
fn changed(&mut self, ctx: &yew::Context<Self>, old_props: &Self::Properties) -> bool {
ctx.props() != old_props
}
}

35
src/component/tag.rs Normal file
View File

@ -0,0 +1,35 @@
use yew::{function_component, Properties, Html, html, classes};
#[derive(PartialEq, Default)]
pub enum TagColor {
#[default]
Blue,
Red,
Orange,
Green
}
impl TagColor {
fn as_class(&self) -> &'static str {
match self {
Self::Blue => "blue",
Self::Red => "red",
Self::Orange => "orange",
Self::Green => "green",
}
}
}
#[derive(Properties, PartialEq)]
pub struct TagProps {
#[prop_or_default]
pub color: TagColor,
pub text: String,
}
#[function_component]
pub fn Tag(props: &TagProps) -> Html {
html! {
<span class={classes!("tag", props.color.as_class())}><span>{props.text.clone()}</span></span>
}
}

View File

@ -1,7 +1,7 @@
use web_sys::MouseEvent; use web_sys::MouseEvent;
use yew::{function_component, html, Html, use_state, Callback}; use yew::{function_component, html, Html, Callback, use_state_eq};
use crate::{component::image_viewer::{ImageDescription, ImageViewer}}; use crate::component::image_viewer::{ImageDescription, ImageViewer};
const GALLERY: [&str; 13] = [ const GALLERY: [&str; 13] = [
"gallery-1.jpg", "gallery-1.jpg",
@ -21,19 +21,19 @@ const GALLERY: [&str; 13] = [
fn gallery_entry(link: &&str, onclick: Callback<MouseEvent>) -> Html { fn gallery_entry(link: &&str, onclick: Callback<MouseEvent>) -> Html {
html! { html! {
<div><img src={format!("/img/{}", link)} onclick={onclick}/></div> <div><img src={format!("/img/{link}")} onclick={onclick}/></div>
} }
} }
#[function_component] #[function_component]
pub fn Gallery() -> Html { pub fn Gallery() -> Html {
let images = GALLERY.iter().map(|it| ImageDescription::new_blank(it.to_string())).collect::<Vec<ImageDescription>>(); let images = GALLERY.iter().map(|it| ImageDescription::new(it.to_string(), it)).collect::<Vec<ImageDescription>>();
let is_open = use_state(|| false); let is_open = use_state_eq(|| false);
let selected_image = use_state(|| 0); let selected_image = use_state_eq(|| 0);
let select_next = selected_image.clone(); let select_next = selected_image.setter();
let select_prev = selected_image.clone(); let select_prev = selected_image.setter();
let next = (*select_next + images.len() + 1) % images.len(); let next = (*selected_image + images.len() + 1) % images.len();
let prev = (*select_prev + images.len() - 1) % images.len(); let prev = (*selected_image + images.len() - 1) % images.len();
html! { html! {
<> <>

View File

@ -2,39 +2,24 @@ use comrak::{markdown_to_html, ComrakOptions};
use gloo::utils::document; use gloo::utils::document;
use gloo_net::http::Request; use gloo_net::http::Request;
use serde::Deserialize; use serde::Deserialize;
use yew::{function_component, html, Html, UseStateHandle, use_state, use_effect_with_deps, Properties, Children, use_context, Callback}; use yew::{function_component, html, Html, UseStateHandle, use_effect_with_deps, Properties, Children, use_context, Callback, use_state_eq};
use yewprint::{Divider, Elevation, Card, Tag, Intent, Icon}; use crate::{util::log, theme::{ThemeContext, ThemeState}, component::{card::Card, tag::{Tag, TagColor}}};
use crate::{util::log, theme::{ThemeContext, ThemeState}, component::image_viewer::{ImageDescription, DefaultImageViewer}};
#[derive(Debug)] #[derive(Debug)]
enum TagType { enum TagType {
NaturalLanguage, CodeLanguage, Interest NaturalLanguage, CodeLanguage, Interest
} }
#[derive(PartialEq)]
enum ImageSource {
Link(String),
Icon(Icon, Intent)
}
#[derive(PartialEq)] #[derive(PartialEq)]
struct ImageResource { struct ImageResource {
source: ImageSource, source: String,
clickable: bool clickable: bool
} }
impl ImageResource { impl ImageResource {
pub fn new_link(link: String, clickable: bool) -> Self { pub fn new_link(link: String, clickable: bool) -> Self {
Self { Self {
source: ImageSource::Link(link), source: link,
clickable
}
}
pub fn new_icon(icon: Icon, intent: Intent, clickable: bool) -> Self {
Self {
source: ImageSource::Icon(icon, intent),
clickable clickable
} }
} }
@ -65,7 +50,7 @@ pub fn Home() -> Html {
html! { html! {
<> <>
<HomeTitle /> <HomeTitle />
<Divider /> <hr />
<div class="home-content"> <div class="home-content">
<Profile /> <Profile />
</div> </div>
@ -109,14 +94,14 @@ fn get_json_resource<T>(file: &'static str, on_result: impl FnOnce(Vec<T>) -> ()
#[function_component] #[function_component]
fn ProfileTags() -> Html { fn ProfileTags() -> Html {
let natural_languages: UseStateHandle<Vec<String>> = use_state(|| vec![]); let natural_languages: UseStateHandle<Vec<String>> = use_state_eq(|| vec![]);
let code_languages: UseStateHandle<Vec<String>> = use_state(|| vec![]); let code_languages: UseStateHandle<Vec<String>> = use_state_eq(|| vec![]);
let interests: UseStateHandle<Vec<String>> = use_state(|| vec![]); let interests: UseStateHandle<Vec<String>> = use_state_eq(|| vec![]);
// TODO: Cache results // TODO: Cache results
{ {
let natural_languages = natural_languages.clone(); let natural_languages = natural_languages.setter();
use_effect_with_deps( use_effect_with_deps(
move |_| get_json_resource( move |_| get_json_resource(
"/res/languages.json", "/res/languages.json",
@ -127,7 +112,7 @@ fn ProfileTags() -> Html {
} }
{ {
let code_languages = code_languages.clone(); let code_languages = code_languages.setter();
use_effect_with_deps( use_effect_with_deps(
move |_| get_json_resource( move |_| get_json_resource(
"/res/code.json", "/res/code.json",
@ -138,7 +123,7 @@ fn ProfileTags() -> Html {
} }
{ {
let interests = interests.clone(); let interests = interests.setter();
use_effect_with_deps( use_effect_with_deps(
move |_| get_json_resource( move |_| get_json_resource(
"/res/interests.json", "/res/interests.json",
@ -155,18 +140,14 @@ fn ProfileTags() -> Html {
].into_iter().flatten().map(|(tag, tag_type)| { ].into_iter().flatten().map(|(tag, tag_type)| {
html! { html! {
<Tag <Tag
interactive=true text={tag.clone()}
minimal=true color={
round=true
intent={
match tag_type { match tag_type {
TagType::NaturalLanguage => Intent::Primary, TagType::NaturalLanguage => TagColor::Blue,
TagType::CodeLanguage => Intent::Warning, TagType::CodeLanguage => TagColor::Orange,
TagType::Interest => Intent::Success TagType::Interest => TagColor::Green
} }
}> }/>
{tag}
</Tag>
} }
}).collect::<Html>(); }).collect::<Html>();
@ -177,9 +158,9 @@ fn ProfileTags() -> Html {
#[function_component] #[function_component]
fn Profile() -> Html { fn Profile() -> Html {
let profile_text = use_state(|| "".to_owned()); let profile_text = use_state_eq(|| "".to_owned());
{ {
let profile_text = profile_text.clone(); let profile_text = profile_text.setter();
use_effect_with_deps(move |_| { use_effect_with_deps(move |_| {
get_text_resource("/res/profile.md".to_owned(), move |text| profile_text.set(markdown_to_html(&text, &ComrakOptions::default()))); get_text_resource("/res/profile.md".to_owned(), move |text| profile_text.set(markdown_to_html(&text, &ComrakOptions::default())));
}, ()); }, ());
@ -195,38 +176,19 @@ fn Profile() -> Html {
#[function_component] #[function_component]
fn HomeCard(props: &HomeCardProps) -> Html { fn HomeCard(props: &HomeCardProps) -> Html {
let overlay_state = use_state(|| false);
let open_overlay_state = overlay_state.clone();
html! { html! {
<Card elevation={Elevation::Level3} interactive=true class="home-card" onclick={Callback::from(move |_| open_overlay_state.set(true))}> <Card class="home-card">
{ {
if let Some(image) = &props.image { if let Some(image) = &props.image {
html! { html! {
<div class="home-tag"> <div class="home-tag">
{ <img
match &image.image.source { class="home-image circle"
ImageSource::Link(link) => html! { src={if image.image.source.starts_with("https?://") {
<img class="home-image circle" src={if link.starts_with("https?://") { link.to_string() } else { format!("/img/{}", link) }} /> image.image.source.to_string()
},
ImageSource::Icon(icon, intent) => html! { <Icon {icon} {intent} size={20}/> }
}
}
{
if image.image.clickable {
if let ImageSource::Link(link) = &image.image.source {
html! {
<DefaultImageViewer
images={vec![ ImageDescription::new_blank(link) ]}
open={*overlay_state}
onclose={Callback::from(move |_| overlay_state.set(false))} />
}
} else { html! {} }
} else { } else {
html! {} format!("/img/{}", image.image.source)
} }} />
}
<b>{image.description}</b> <b>{image.description}</b>
</div> </div>
} }
@ -241,7 +203,7 @@ fn HomeCard(props: &HomeCardProps) -> Html {
#[function_component] #[function_component]
fn Github() -> Html { fn Github() -> Html {
let github_entries: UseStateHandle<Vec<GithubEntry>> = use_state(|| vec![]); let github_entries: UseStateHandle<Vec<GithubEntry>> = use_state_eq(|| vec![]);
{ {
let github_entries = github_entries.clone(); let github_entries = github_entries.clone();
use_effect_with_deps(move |_| get_json_resource("/res/github.json", move |it| github_entries.set(it)), ()); use_effect_with_deps(move |_| get_json_resource("/res/github.json", move |it| github_entries.set(it)), ());
@ -257,7 +219,7 @@ fn Github() -> Html {
github_entries.iter().map(|it| { github_entries.iter().map(|it| {
let link = it.link.clone(); let link = it.link.clone();
html! { html! {
<Card elevation={Elevation::Level3} interactive=true onclick={Callback::from(move |_| { <Card onclick={Callback::from(move |_| {
if let Err(_) = document().location().unwrap().set_href(link.as_str()) { if let Err(_) = document().location().unwrap().set_href(link.as_str()) {
log("Couldn't change href"); log("Couldn't change href");
} }

View File

@ -7,28 +7,15 @@ use crate::util::log;
fn set_global_dark(is_dark: bool) { fn set_global_dark(is_dark: bool) {
let root = document().get_elements_by_tag_name("html").get_with_index(0).expect("Root html tag"); let root = document().get_elements_by_tag_name("html").get_with_index(0).expect("Root html tag");
let class_list = root.get_elements_by_tag_name("body").get_with_index(0).expect("Body tag").class_list();
if is_dark { if is_dark {
if let Err(_) = root.set_attribute("data-theme", "dark") { if let Err(_) = root.set_attribute("data-theme", "dark") {
log("Couldn't set attribute 'data-theme' on root"); log("Couldn't set attribute 'data-theme' on root");
} }
if !class_list.contains("bp3-dark") {
if let Err(_) = class_list.add_1("bp3-dark") {
log("Couldn't set class 'bp3-dark' on root")
}
}
} else { } else {
if let Err(_) = root.remove_attribute("data-theme") { if let Err(_) = root.remove_attribute("data-theme") {
log("Couldn't remove attribute 'data-theme' from root"); log("Couldn't remove attribute 'data-theme' from root");
} }
if class_list.contains("bp3-dark") {
if let Err(_) = class_list.remove_1("bp3-dark") {
log("Couldn't remove class 'bp3-dark' from root")
}
}
} }
} }

View File

@ -1,39 +1,49 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Gabriel Tofvesson</title> <meta charset="utf-8"/>
<link rel="preconnect" href="https://fonts.gstatic.com"> <title>Gabriel Tofvesson</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap" rel="stylesheet"> <link rel="preconnect" href="https://fonts.gstatic.com">
<script type="text/javascript"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap" rel="stylesheet">
// Single Page Apps for GitHub Pages <link rel="stylesheet" href="/style/app.css">
// MIT License <link rel="stylesheet" href="/style/theme.css">
// https://github.com/rafgraph/spa-github-pages <link rel="stylesheet" href="/style/tag.css">
// This script checks to see if a redirect is present in the query string, <link rel="stylesheet" href="/style/navbar.css">
// converts it back into the correct url and adds it to the <link rel="stylesheet" href="/style/card.css">
// browser's history using window.history.replaceState(...), <link rel="stylesheet" href="/style/gallery.css">
// which won't cause the browser to attempt to load the new url. <link rel="stylesheet" href="/style/overlay.css">
// When the single page app is loaded further down in this file, <link rel="stylesheet" href="/style/overlay-gallery.css">
// the correct url will be waiting in the browser's history for <link rel="stylesheet" href="/style/image-gallery.css">
// the single page app to route accordingly. <script src="https://kit.fontawesome.com/d5cf53d9fb.js" crossorigin="anonymous"></script>
(function(l) { <script type="text/javascript">
if (l.search[1] === '/' ) { // Single Page Apps for GitHub Pages
var decoded = l.search.slice(1).split('&').map(function(s) { // MIT License
return s.replace(/~and~/g, '&') // https://github.com/rafgraph/spa-github-pages
}).join('?'); // This script checks to see if a redirect is present in the query string,
window.history.replaceState(null, null, // converts it back into the correct url and adds it to the
l.pathname.slice(0, -1) + decoded + l.hash // browser's history using window.history.replaceState(...),
); // which won't cause the browser to attempt to load the new url.
} // When the single page app is loaded further down in this file,
}(window.location)) // the correct url will be waiting in the browser's history for
</script> // the single page app to route accordingly.
<meta charset="utf-8"/> (function(l) {
<script type="module"> if (l.search[1] === '/' ) {
import init from "/app.js"; var decoded = l.search.slice(1).split('&').map(function(s) {
init(new URL('app.wasm', import.meta.url)); return s.replace(/~and~/g, '&')
</script> }).join('?');
<link rel="stylesheet" href="/styles.css"> window.history.replaceState(
<link rel="stylesheet" href="/blueprint.css"> null,
</head> null,
<body> l.pathname.slice(0, -1) + decoded + l.hash
</body> );
}
}(window.location))
</script>
<script type="module">
import init from "/app.js";
init(new URL('app.wasm', import.meta.url));
</script>
</head>
<body>
</body>
</html> </html>

90
static/style/app.css Normal file
View File

@ -0,0 +1,90 @@
.main {
margin-top: 6vh;
padding: 20px;
}
:root {
font-size: 100%;
}
@media only screen and (min-width: 320px) and (orientation: portrait) {
:root {
font-size: 125%;
}
}
@media only screen and (min-width: 640px) and (orientation: portrait) {
:root {
font-size: 175%;
}
}
.home-title {
text-align: center;
width: 100%;
}
.home-title > h2 {
display: inline-block;
}
.home-content {
display: grid;
grid-template-columns: 100%;
grid-template-rows: auto auto auto auto;
}
.home-card {
display: flex;
justify-content: flex-start;
align-items: flex-start;
gap: 10px;
flex-direction: row;
margin: 10px;
}
.home-tag {
display: flex;
gap: 10px;
flex-direction: column;
justify-content: flex-start;
align-items: center;
width: min-content;
}
.home-image {
width: 10%;
min-width: 100px;
height: auto;
border-radius: 50%;
}
.home-info {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 10px;
width: 100%;
}
.profiletags {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
@media only screen and (orientation: portrait) {
.home-content {
display: flex;
flex-direction: column;
}
.home-card {
flex-direction: column;
align-items: center;
}
.home-tag {
width: fit-content;
margin-bottom: 20px;
}
}

17
static/style/card.css Normal file
View File

@ -0,0 +1,17 @@
:root {
--card-shadow: var(--drop-shadow-dark);
--card-color: var(--color-primary);
--card-elevation: 5px;
--card-elevation-hover: 15px;
}
.card {
border-radius: 10px;
box-shadow: 0 0 var(--card-elevation) var(--card-shadow);
background-color: var(--card-color);
padding: 10px;
}
.card:hover {
box-shadow: 0 0 var(--card-elevation-hover) var(--card-shadow);
}

0
static/style/gallery.css Normal file
View File

View File

@ -0,0 +1,50 @@
.image-gallery {
line-height: 0;
column-count: 5;
column-gap: 5px;
transition-timing-function: cubic-bezier(0.1, 0.7, 1.0);
}
.image-gallery div {
width: 100% !important;
height: auto !important;
margin-bottom: 10px;
}
.image-gallery div img {
width: 98% !important;
height: auto !important;
filter: grayscale(var(--grayscale,100%)) /*blur(var(--blur,1px))*/;
transition: 250ms all;
border-radius: 10px;
}
.image-gallery div:hover {
--grayscale: 0%;
--blur: 0px;
}
@media (max-width: 1200px) {
.image-gallery {
column-count: 4;
}
}
@media (max-width: 1000px) {
.image-gallery {
column-count: 3;
}
}
@media (max-width: 800px) {
.image-gallery {
column-count: 2;
}
}
@media (max-width: 400px) {
.image-gallery {
column-count: 1;
}
}

124
static/style/navbar.css Normal file
View File

@ -0,0 +1,124 @@
:root {
--navbar-color-dropshadow: var(--drop-shadow);
--navbar-color: var(--color-primary);
--navbar-color-selected: var(--color-accent);
--navbar-padding: 2vh;
}
/* Navigation bar */
.navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: auto;
display: flex;
flex-direction: column;
overflow: hidden;
}
.navbar,
.navbar * {
user-select: none;
}
.navbar > div:nth-child(1) {
display: none;
padding-top: var(--navbar-padding);
padding-bottom: var(--navbar-padding);
}
.navbar > div:nth-child(2) {
display: none;
}
.navbar > div:nth-child(3) {
width: 100%;
height: 100%;
display: flex;
overflow: hidden;
}
.navbar > div:nth-child(3) > div {
flex: 1 1 auto;
overflow: hidden;
transition: box-shadow 250ms, z-index 250ms;
background-color: var(--navbar-color);
padding-top: var(--navbar-padding);
padding-bottom: var(--navbar-padding);
}
.navbar > div:nth-child(3) > div > div {
gap: 5px;
display: flex;
align-items: center;
justify-content: center;
}
.navbar > div:nth-child(3) > div:hover {
box-shadow: 0 0 11px var(--navbar-color-dropshadow);
z-index: 4;
cursor: pointer;
}
.navbar > div:nth-child(3) > div.current {
background-color: var(--navbar-color-selected);
}
@media only screen and (orientation: landscape) {
.navbar > div:nth-child(3) > div:nth-child(1) {
border-bottom-left-radius: 10px;
}
.navbar > div:nth-child(3) > div:nth-last-child(1) {
border-bottom-right-radius: 10px;
}
.navbar > div:nth-child(3) > div.small {
flex-grow: 0;
width: 5%;
}
.navbar > div:nth-child(3) > div.small > div {
gap: 0;
}
}
@media only screen and (orientation: portrait) {
.navbar {
display: grid;
pointer-events: none;
}
.navbar > div:nth-child(1) {
display: flex;
width: 100%;
height: auto;
justify-content: center;
align-items: center;
overflow: hidden;
transition: box-shadow 250ms, z-index 250ms;
background-color: var(--navbar-color);
box-shadow: 0 0 11px var(--navbar-color-dropshadow);
z-index: 5;
cursor: pointer;
pointer-events: auto;
}
.navbar > div:nth-child(3) {
position: relative;
top: -100%;
flex-direction: column;
transition: top 0.5s;
overflow: hidden;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
pointer-events: none;
}
.navbar > div:nth-child(2).open + div:nth-child(3) {
top: 0;
pointer-events: auto;
}
}

View File

@ -0,0 +1,98 @@
:root {
--overlay-gallery-scale: 95vmin;
}
/* Overlay image viewer */
.overlay-gallery {
width: var(--overlay-gallery-scale);
height: var(--overlay-gallery-scale);
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
}
.overlay-gallery > div:nth-of-type(1) {
position: absolute;
width: 100%;
height: 100%;
}
.overlay-gallery > div:nth-of-type(2) {
transform: translate(calc(calc(var(--overlay-gallery-scale) / 2) - 50%), 0);
border-radius: 20px;
overflow: hidden;
position: relative;
}
.overlay-gallery > div:nth-of-type(2) > img {
float: left;
max-width: var(--overlay-gallery-scale);
max-height: var(--overlay-gallery-scale);
position: relative;
}
.overlay-gallery > div:nth-of-type(2) > div {
bottom: 0;
padding: 60px 10px 10px 10px;
width: 100%;
overflow-wrap: break-word;
background: linear-gradient(to top, rgba(0, 75, 196, 0.699), rgba(133, 180, 255, 0));
}
/* Gallery overlay transition properties */
.overlay-gallery > div:nth-of-type(2) > div,
.overlay-gallery-next,
.overlay-gallery-prev {
position: absolute;
transition: opacity 250ms;
transition-timing-function: cubic-bezier(0.1, 0.7, 1.0);
opacity: 0;
}
/* Image viewer overlay button positioning */
.overlay-gallery-next,
.overlay-gallery-prev {
top: 50%;
height: max(100%, 10px);
width: 12.5%;
display: flex !important;
align-items: center;
}
.overlay-gallery-next {
left: 100%;
transform: translate(-100%, -50%);
padding-right: 2.5%;
padding-left: 5%;
justify-content: flex-end;
}
.overlay-gallery-prev {
left: 0%;
transform: translate(-0%, -50%);
padding-left: 2.5%;
padding-right: 5%;;
justify-content: flex-start;
}
.overlay-gallery > div:nth-of-type(2):hover > div,
.overlay-gallery:hover .overlay-gallery-next,
.overlay-gallery:hover .overlay-gallery-prev {
opacity: 1;
}
@media only screen and (orientation: portrait) {
.overlay-gallery > div:nth-of-type(2) > div,
.overlay-gallery .overlay-gallery-next,
.overlay-gallery .overlay-gallery-prev {
opacity: 1;
}
}
.overlay-gallery-next:hover,
.overlay-gallery-prev:hover {
cursor: pointer;
}

29
static/style/overlay.css Normal file
View File

@ -0,0 +1,29 @@
/* Pop-up overlay */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
margin: 0;
display: flex;
z-index: 25;
}
.overlay.hidden {
display: none;
}
/* Background */
.overlay.shade {
background-color: rgba(10, 10, 10, 0.75);
}
/* Content */
.overlay > div.center {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -1,31 +1,4 @@
:root {
--bg-color: #f5f8fa;
--gallery-scale: 95vmin;
}
:root[data-theme='dark'] {
--bg-color: #00251a;
--color-primary: #004d40;
--color-accent: #39796b;
}
:root[data-theme='mauve'] {
--bg-color: #560027;
--color-primary: #880e4f;
--color-accent: #bc477b;
}
:root[data-theme='gray'] {
--bg-color: #455A64;
--color-primary: #607D8B;
--color-accent: #212121;
}
:root[data-theme='purple'] {
--bg-color: #512DA8;
--color-primary: #673AB7;
--color-accent: #009688;
}
.bp3-card.bp3-dark, .bp3-dark .bp3-card { .bp3-card.bp3-dark, .bp3-dark .bp3-card {
background-color: var(--color-primary) !important; background-color: var(--color-primary) !important;
@ -53,11 +26,6 @@ body {
font-family: 'Roboto', sans-serif !important; font-family: 'Roboto', sans-serif !important;
} }
.main {
margin-top: 41px;
padding: 20px;
}
.actionbar { .actionbar {
position: fixed; position: fixed;
top: 0; top: 0;
@ -118,6 +86,7 @@ body {
align-items: center; align-items: center;
width: min-content; width: min-content;
} }
/*
@media only screen and (max-width: 1000px) { @media only screen and (max-width: 1000px) {
.home-content { .home-content {
@ -132,6 +101,7 @@ body {
align-items: center; align-items: center;
} }
} }
*/
.home-image { .home-image {
width: 10%; width: 10%;

61
static/style/tag.css Normal file
View File

@ -0,0 +1,61 @@
:root {
--tag-color-blue: rgba(19, 124, 189, 0.3);
--tag-color-red: rgba(219, 55, 55, 0.25);
--tag-color-orange: rgba(217, 130, 43, 0.25);
--tag-color-green: rgba(15, 153, 96, 0.25);
--tag-shadow: var(--drop-shadow);
--tag-color-text-blue: #48aff0;
--tag-color-text-red: #ff7373;
--tag-color-text-orange: #ffb366;
--tag-color-text-green: #3dcc91;
}
:root[data-theme="dark"] {
--tag-color-blue: #303f9f4d;
--tag-color-red: #7f00004d;
--tag-color-orange: #e651004d;
--tag-color-green: #33691e4d;
--tag-color-text-blue: #448aff;
--tag-color-text-red: #EF9A9A;
--tag-color-text-orange: #FFB74D;
--tag-color-text-green: #AED581;
}
span.tag {
border-radius: 20px;
padding: 2.5px;
user-select: none;
}
span.tag:hover {
box-shadow: 0 0 5px var(--tag-shadow);
cursor: pointer;
}
span.tag > span {
padding: 2.5px;
user-select: none;
}
span.tag.blue {
background-color: var(--tag-color-blue);
color: var(--tag-color-text-blue);
}
span.tag.red {
background-color: var(--tag-color-red);
color: var(--tag-color-text-red);
}
span.tag.orange {
background-color: var(--tag-color-orange);
color: var(--tag-color-text-orange);
}
span.tag.green {
background-color: var(--tag-color-green);
color: var(--tag-color-text-green);
}

54
static/style/theme.css Normal file
View File

@ -0,0 +1,54 @@
:root {
--drop-shadow: rgba(0, 0, 0, 0.61);
--drop-shadow-dark: rgba(255, 255, 255, 0.61);
--bg-color: #f5f8fa;
--color-primary: rgb(210, 210, 210);
--color-accent: rgb(160, 200, 160);
--color-text: rgb(0, 0, 0);
}
:root[data-theme='dark'] {
--drop-shadow: rgba(255, 255, 255, 0.61);
--drop-shadow-dark: rgba(0, 0, 0, 0.61);
--bg-color: #00251a;
--color-primary: #004d40;
--color-accent: #39796b;
--color-text: #f5f8fa;
}
:root[data-theme='mauve'] {
--drop-shadow: rgba(255, 255, 255, 0.61);
--drop-shadow-dark: rgba(0, 0, 0, 0.61);
--bg-color: #560027;
--color-primary: #880e4f;
--color-accent: #bc477b;
--color-text: #f5f8fa;
}
:root[data-theme='gray'] {
--drop-shadow: rgba(0, 0, 0, 0.61);
--drop-shadow-dark: rgba(0, 0, 0, 0.61);
--bg-color: #455A64;
--color-primary: #607D8B;
--color-accent: #212121;
--color-text: #f5f8fa;
}
:root[data-theme='purple'] {
--drop-shadow: rgba(255, 255, 255, 0.61);
--drop-shadow-dark: rgba(0, 0, 0, 0.61);
--bg-color: #512DA8;
--color-primary: #673AB7;
--color-accent: #009688;
--color-text: rgb(0, 0, 0);
}
html {
color: var(--color-text);
transition: color 100ms;
}
html[data-theme="dark"] > body {
background-color: var(--bg-color);
font-family: 'Roboto', sans-serif !important;
}