Initial commit
This commit is contained in:
commit
b4f4db1f81
.cargo
.github/workflows
.gitignoreCargo.lockCargo.tomlLICENSE.Apache-2.0LICENSE.MITREADME.mdnetlify.tomlsrc
static
img
github
github-mark-white.pnggithub-mark-white.svggithub-mark.pnggithub-mark.svggithub-mark.svg:Zone.Identifier
profile.jpgres
styles.cssxtask
2
.cargo/config
Normal file
2
.cargo/config
Normal file
@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
25
.github/workflows/main.yml
vendored
Normal file
25
.github/workflows/main.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: main
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: 0 0 1 * *
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
cargo-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --workspace --all-features
|
35
.github/workflows/pr.yml
vendored
Normal file
35
.github/workflows/pr.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
name: PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
cargo-test-and-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --workspace --all-features
|
||||
|
||||
- name: rustfmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: clippy
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all --all-features --tests -- -D warnings
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
2317
Cargo.lock
generated
Normal file
2317
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
Cargo.toml
Normal file
51
Cargo.toml
Normal file
@ -0,0 +1,51 @@
|
||||
[package]
|
||||
name = "yewprint-app"
|
||||
version = "0.1.0"
|
||||
authors = ["Gabriel Tofvesson <contact@w1zzrd.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||
# code size when deploying.
|
||||
console_error_panic_hook = { version = "0.1.6", optional = true }
|
||||
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["Window", "MediaQueryList"] }
|
||||
js-sys = "0.3"
|
||||
|
||||
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
|
||||
# compared to the default allocator's ~10K. It is slower than the default
|
||||
# allocator, however.
|
||||
#
|
||||
# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now.
|
||||
wee_alloc = { version = "0.4.5", optional = false }
|
||||
|
||||
yew = { version = "0.20.0", features = ["csr"] }
|
||||
yewprint = "0.4.0"
|
||||
gloo = "0.8.0"
|
||||
gloo-events = "0.1"
|
||||
futures = "0.3.25"
|
||||
|
||||
# Resource access
|
||||
gloo-net = "0.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
||||
comrak = "0.15"
|
||||
|
||||
[profile.release]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
# opt-level = "s"
|
||||
lto = true
|
||||
panic = "abort"
|
||||
opt-level = "z"
|
||||
|
||||
[workspace]
|
||||
members = ["xtask"]
|
201
LICENSE.Apache-2.0
Normal file
201
LICENSE.Apache-2.0
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2023 Gabriel Tofvesson <contact@w1zzrd.dev>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
21
LICENSE.MIT
Normal file
21
LICENSE.MIT
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Gabriel Tofvesson <contact@w1zzrd.dev>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
15
README.md
Normal file
15
README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# yewprint-app
|
||||
|
||||
## Development Server
|
||||
|
||||
```
|
||||
cargo xtask start
|
||||
```
|
||||
|
||||
You can now go to http://localhost:8000
|
||||
|
||||
## Production Build
|
||||
|
||||
```
|
||||
cargo xtask dist
|
||||
```
|
3
netlify.toml
Normal file
3
netlify.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[build]
|
||||
publish = "target/release/dist"
|
||||
command = "cargo xtask dist --release"
|
63
src/app.rs
Normal file
63
src/app.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use yew::prelude::*;
|
||||
use yewprint::Icon;
|
||||
use crate::page::Page;
|
||||
use crate::theme::ThemeContext;
|
||||
use crate::theme::ThemeMsg;
|
||||
use crate::theme::ThemeProvider;
|
||||
use crate::component::actionbar::{Actionbar, ActionbarOption};
|
||||
|
||||
|
||||
#[function_component]
|
||||
pub fn AppRoot() -> Html {
|
||||
html! {
|
||||
<ThemeProvider>
|
||||
<ThemedApp />
|
||||
</ThemeProvider>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn ThemedApp() -> Html {
|
||||
// Helper function for generating tabs
|
||||
fn page_option(current: &Page, page: Page, state: UseStateHandle<Page>) -> ActionbarOption {
|
||||
ActionbarOption::new(
|
||||
page.name(),
|
||||
match page {
|
||||
Page::Home => Icon::Home,
|
||||
Page::Projects => Icon::Code,
|
||||
Page::Socials => Icon::SocialMedia,
|
||||
Page::Contact => Icon::Envelope,
|
||||
},
|
||||
Callback::from(move |_| state.set(page)),
|
||||
*current == page
|
||||
)
|
||||
}
|
||||
|
||||
let ctx = use_context::<ThemeContext>().expect("No theme context supplied");
|
||||
let page_state = use_state_eq(|| Page::Home);
|
||||
let current_page = *page_state;
|
||||
|
||||
html! {
|
||||
<>
|
||||
<Actionbar>
|
||||
{vec![
|
||||
page_option(¤t_page, Page::Home, page_state.clone()),
|
||||
page_option(¤t_page, Page::Projects, page_state.clone()),
|
||||
page_option(¤t_page, Page::Socials, page_state.clone()),
|
||||
page_option(¤t_page, Page::Contact, page_state),
|
||||
ActionbarOption::new_opt(
|
||||
None,
|
||||
Some(Icon::Flash),
|
||||
Some(Callback::from(move |_| ctx.dispatch(ThemeMsg::Toggle))),
|
||||
true,
|
||||
false,
|
||||
false
|
||||
)
|
||||
]}
|
||||
</Actionbar>
|
||||
<div class="main">
|
||||
{current_page.content()}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
90
src/component/actionbar.rs
Normal file
90
src/component/actionbar.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use yew::html::ChildrenRenderer;
|
||||
use yew::prelude::*;
|
||||
use yew::virtual_dom::VNode;
|
||||
use yewprint::{Collapse, Icon, ButtonGroup};
|
||||
use yewprint::Button;
|
||||
use yewprint::Icon::{MenuClosed, MenuOpen};
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub struct ActionbarOption {
|
||||
name: Option<String>,
|
||||
icon: Option<Icon>,
|
||||
onclick: Option<Callback<MouseEvent>>,
|
||||
center_content: bool,
|
||||
selected: bool,
|
||||
fill: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
struct ActionbarOptionState {
|
||||
inner: ActionbarOption,
|
||||
callback: UseStateHandle<bool>
|
||||
}
|
||||
|
||||
impl Into<VNode> for ActionbarOption {
|
||||
fn into(self) -> VNode {
|
||||
/*
|
||||
let classes = if self.center_content { classes!() } else { classes!("ab-navbutton") };
|
||||
html! {
|
||||
<Button fill=true large=true class={classes} icon={self.icon} onclick={self.onclick.unwrap_or_default()} active={self.selected}>{self.name}</Button>
|
||||
}
|
||||
*/
|
||||
panic!("Stateless actionbar option")
|
||||
}
|
||||
}
|
||||
|
||||
impl ActionbarOption {
|
||||
pub fn new_opt(name: Option<String>, icon: Option<Icon>, onclick: Option<Callback<MouseEvent>>, center_content: bool, selected: bool, fill: bool) -> ActionbarOption {
|
||||
ActionbarOption { name, icon, onclick, center_content, selected, fill }
|
||||
}
|
||||
|
||||
pub fn new(name: &str, icon: Icon, onclick: Callback<MouseEvent>, selected: bool) -> ActionbarOption {
|
||||
ActionbarOption::new_opt(Some(name.to_owned()), Some(icon), Some(onclick), false, selected, true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<VNode> for ActionbarOptionState {
|
||||
fn into(self) -> VNode {
|
||||
let classes = if self.inner.center_content { classes!() } else { classes!("ab-navbutton") };
|
||||
let onclick = self.inner.onclick.unwrap_or_default();
|
||||
html! {
|
||||
<Button
|
||||
fill={self.inner.fill}
|
||||
large=true
|
||||
class={classes}
|
||||
icon={self.inner.icon}
|
||||
onclick={Callback::from(move |e| { self.callback.set(false); onclick.emit(e) })}
|
||||
active={self.inner.selected}>
|
||||
{self.inner.name}
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct ActionbarProps {
|
||||
pub children: ChildrenRenderer<ActionbarOption>
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Actionbar(props: &ActionbarProps) -> Html {
|
||||
let expanded = use_state_eq(|| false);
|
||||
let is_expanded = *expanded;
|
||||
let children = props.children.iter().map(|opt| ActionbarOptionState { inner: opt, callback: expanded.clone() }).collect::<Vec<ActionbarOptionState>>();
|
||||
|
||||
html! {
|
||||
<div class="actionbar">
|
||||
<div class="ab-portrait">
|
||||
<Button onclick={Callback::from(move |_| expanded.set(!is_expanded))} icon={if is_expanded { MenuOpen } else { MenuClosed }} large=true />
|
||||
<Collapse is_open={is_expanded}>
|
||||
<ButtonGroup class="ab-portrait-content" vertical=true>
|
||||
{children.clone()}
|
||||
</ButtonGroup>
|
||||
</Collapse>
|
||||
</div>
|
||||
<ButtonGroup class="ab-landscape" fill=true>
|
||||
{children}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
}
|
||||
}
|
92
src/component/image_viewer.rs
Normal file
92
src/component/image_viewer.rs
Normal file
@ -0,0 +1,92 @@
|
||||
use yew::{function_component, Html, html, Callback, Properties, use_state};
|
||||
use yewprint::{Overlay, Icon, IconSize};
|
||||
|
||||
const CHEVRON_SIZE: f64 = 40.0;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct ImageDescription {
|
||||
link: String,
|
||||
description: Option<&'static str>
|
||||
}
|
||||
|
||||
impl ImageDescription {
|
||||
pub fn new(link: impl Into<String>, description: &'static str) -> Self {
|
||||
Self {
|
||||
link: link.into(),
|
||||
description: Some(description)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_blank(link: impl Into<String>) -> Self {
|
||||
Self {
|
||||
link: link.into(),
|
||||
description: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct ImageViewerProps {
|
||||
pub images: Vec<ImageDescription>,
|
||||
pub open: bool,
|
||||
pub onclose: Callback<()>
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn ImageViewer(props: &ImageViewerProps) -> Html {
|
||||
let selected_image = use_state(|| 0);
|
||||
let (select_next, select_prev) = (selected_image.clone(), selected_image.clone());
|
||||
html! {
|
||||
<Overlay
|
||||
class="gallery"
|
||||
open={props.open && !props.images.is_empty()}
|
||||
onclose={&props.onclose}>
|
||||
{
|
||||
if let Some(image_desc) = props.images.get(*selected_image) {
|
||||
Some(html! {
|
||||
<>
|
||||
<img class="gallery-image" 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 {
|
||||
Some(html! {
|
||||
<div class="gallery-image-description">{description}</div>
|
||||
})
|
||||
} else { None }
|
||||
}
|
||||
|
||||
{
|
||||
// Controls: Next
|
||||
if *selected_image < props.images.len() - 1 {
|
||||
Some(html! {
|
||||
<Icon
|
||||
icon={yewprint::Icon::ChevronRight}
|
||||
intent={yewprint::Intent::Warning}
|
||||
size={IconSize(CHEVRON_SIZE)}
|
||||
onclick={Callback::from(move |_| select_next.set(*select_next + 1))}
|
||||
class="gallery-next"
|
||||
/>
|
||||
})
|
||||
} else { None }
|
||||
}
|
||||
|
||||
{
|
||||
// Controls: Prev
|
||||
if *selected_image > 0 {
|
||||
Some(html! {
|
||||
<Icon
|
||||
icon={yewprint::Icon::ChevronLeft}
|
||||
intent={yewprint::Intent::Warning}
|
||||
size={IconSize(CHEVRON_SIZE)}
|
||||
onclick={Callback::from(move |_| select_prev.set(*select_prev - 1))}
|
||||
class="gallery-prev"
|
||||
/>
|
||||
})
|
||||
} else { None }
|
||||
}
|
||||
</>
|
||||
})
|
||||
} else { None }
|
||||
}
|
||||
</Overlay>
|
||||
}
|
||||
}
|
2
src/component/mod.rs
Normal file
2
src/component/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod actionbar;
|
||||
pub mod image_viewer;
|
22
src/lib.rs
Normal file
22
src/lib.rs
Normal file
@ -0,0 +1,22 @@
|
||||
mod util;
|
||||
mod app;
|
||||
mod theme;
|
||||
mod component;
|
||||
mod page;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global allocator.
|
||||
// #[cfg(feature = "wee_alloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run_app() -> Result<(), JsValue> {
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
yew::Renderer::<app::AppRoot>::new().render();
|
||||
|
||||
Ok(())
|
||||
}
|
8
src/page/contact.rs
Normal file
8
src/page/contact.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use yew::{function_component, html, Html};
|
||||
|
||||
#[function_component]
|
||||
pub fn Contact() -> Html {
|
||||
html! {
|
||||
<>{"Contact"}</>
|
||||
}
|
||||
}
|
277
src/page/home.rs
Normal file
277
src/page/home.rs
Normal file
@ -0,0 +1,277 @@
|
||||
use comrak::{markdown_to_html, ComrakOptions};
|
||||
use gloo::utils::document;
|
||||
use gloo_net::http::Request;
|
||||
use serde::Deserialize;
|
||||
use yew::{function_component, html, Html, UseStateHandle, use_state, use_effect_with_deps, Properties, Children, use_context, Callback};
|
||||
use yewprint::{Divider, Elevation, Card, Tag, Intent, Icon, Overlay};
|
||||
|
||||
use crate::{util::log, theme::{ThemeContext, ThemeState}, component::image_viewer::{ImageDescription, ImageViewer}};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TagType {
|
||||
NaturalLanguage, CodeLanguage, Interest
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum ImageSource {
|
||||
Link(String),
|
||||
Icon(Icon, Intent)
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct ImageResource {
|
||||
source: ImageSource,
|
||||
clickable: bool
|
||||
}
|
||||
|
||||
impl ImageResource {
|
||||
pub fn new_link(link: String, clickable: bool) -> Self {
|
||||
Self {
|
||||
source: ImageSource::Link(link),
|
||||
clickable
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_icon(icon: Icon, intent: Intent, clickable: bool) -> Self {
|
||||
Self {
|
||||
source: ImageSource::Icon(icon, intent),
|
||||
clickable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct DescribedImage {
|
||||
pub image: ImageResource,
|
||||
pub description: &'static str
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct HomeCardProps {
|
||||
#[prop_or_default]
|
||||
pub image: Option<DescribedImage>,
|
||||
pub children: Children
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize)]
|
||||
struct GithubEntry {
|
||||
link: String,
|
||||
title: String,
|
||||
description: String
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Home() -> Html {
|
||||
html! {
|
||||
<>
|
||||
<HomeTitle />
|
||||
<Divider />
|
||||
<div class="home-content">
|
||||
<Profile />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn HomeTitle() -> Html {
|
||||
html! {
|
||||
<div class="home-title">
|
||||
<h2>{"Gabriel Tofvesson"}</h2>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn get_text_resource(file: String, on_result: impl (FnOnce(String) -> ()) + 'static) {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
log(&format!("Fetching {file}"));
|
||||
let response = Request::get(file.as_str()).send().await;
|
||||
if let Ok(response) = response {
|
||||
if let Ok(text) = response.text().await {
|
||||
on_result(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn get_json_resource<T>(file: &'static str, on_result: impl FnOnce(Vec<T>) -> () + 'static) where T: for<'a> Deserialize<'a> {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
log(&format!("Fetching {file}"));
|
||||
let response = Request::get(file).send().await;
|
||||
|
||||
if let Ok(response) = response {
|
||||
if let Ok(value) = response.json::<Vec<T>>().await {
|
||||
on_result(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn ProfileTags() -> Html {
|
||||
let natural_languages: UseStateHandle<Vec<String>> = use_state(|| vec![]);
|
||||
let code_languages: UseStateHandle<Vec<String>> = use_state(|| vec![]);
|
||||
let interests: UseStateHandle<Vec<String>> = use_state(|| vec![]);
|
||||
|
||||
|
||||
// TODO: Cache results
|
||||
{
|
||||
let natural_languages = natural_languages.clone();
|
||||
use_effect_with_deps(
|
||||
move |_| get_json_resource(
|
||||
"/res/languages.json",
|
||||
move |it| natural_languages.set(it)
|
||||
),
|
||||
()
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let code_languages = code_languages.clone();
|
||||
use_effect_with_deps(
|
||||
move |_| get_json_resource(
|
||||
"/res/code.json",
|
||||
move |it| code_languages.set(it)
|
||||
),
|
||||
()
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let interests = interests.clone();
|
||||
use_effect_with_deps(
|
||||
move |_| get_json_resource(
|
||||
"/res/interests.json",
|
||||
move |it| interests.set(it)
|
||||
),
|
||||
()
|
||||
);
|
||||
}
|
||||
|
||||
let tags = vec![
|
||||
natural_languages.iter().map(|it| (it, TagType::NaturalLanguage)).collect::<Vec<(&String, TagType)>>(),
|
||||
code_languages.iter().map(|it| (it, TagType::CodeLanguage)).collect::<Vec<(&String, TagType)>>(),
|
||||
interests.iter().map(|it| (it, TagType::Interest)).collect::<Vec<(&String, TagType)>>()
|
||||
].into_iter().flatten().map(|(tag, tag_type)| {
|
||||
html! {
|
||||
<Tag
|
||||
interactive=true
|
||||
minimal=true
|
||||
round=true
|
||||
intent={
|
||||
match tag_type {
|
||||
TagType::NaturalLanguage => Intent::Primary,
|
||||
TagType::CodeLanguage => Intent::Warning,
|
||||
TagType::Interest => Intent::Success
|
||||
}
|
||||
}>
|
||||
{tag}
|
||||
</Tag>
|
||||
}
|
||||
}).collect::<Html>();
|
||||
|
||||
html! {
|
||||
<div class="profiletags">{tags}</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Profile() -> Html {
|
||||
let profile_text = use_state(|| "".to_owned());
|
||||
{
|
||||
let profile_text = profile_text.clone();
|
||||
use_effect_with_deps(move |_| {
|
||||
get_text_resource("/res/profile.md".to_owned(), move |text| profile_text.set(markdown_to_html(&text, &ComrakOptions::default())));
|
||||
}, ());
|
||||
}
|
||||
|
||||
html! {
|
||||
<HomeCard image={DescribedImage{ image: ImageResource::new_link("profile.jpg".to_owned(), true), description: "About me" }}>
|
||||
<ProfileTags />
|
||||
{Html::from_html_unchecked(profile_text.to_string().into())}
|
||||
</HomeCard>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn HomeCard(props: &HomeCardProps) -> Html {
|
||||
let overlay_state = use_state(|| false);
|
||||
let open_overlay_state = overlay_state.clone();
|
||||
html! {
|
||||
<Card elevation={Elevation::Level3} interactive=true class="home-card" onclick={Callback::from(move |_| open_overlay_state.set(true))}>
|
||||
{
|
||||
if let Some(image) = &props.image {
|
||||
html! {
|
||||
<div class="home-tag">
|
||||
{
|
||||
match &image.image.source {
|
||||
ImageSource::Link(link) => html! {
|
||||
<img class="home-image circle" src={if link.starts_with("https?://") { link.to_string() } else { format!("/img/{}", link) }} />
|
||||
},
|
||||
ImageSource::Icon(icon, intent) => html! { <Icon {icon} {intent} size={20}/> }
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if image.image.clickable {
|
||||
if let ImageSource::Link(link) = &image.image.source {
|
||||
html! {
|
||||
<ImageViewer
|
||||
images={vec![
|
||||
ImageDescription::new(link, "This is me"),
|
||||
ImageDescription::new(link, "This is also me"),
|
||||
ImageDescription::new(link, "This is not me")
|
||||
]}
|
||||
open={*overlay_state}
|
||||
onclose={Callback::from(move |_| overlay_state.set(false))} />
|
||||
}
|
||||
} else { html! {} }
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
|
||||
<b>{image.description}</b>
|
||||
</div>
|
||||
}
|
||||
} else { html! {} }
|
||||
}
|
||||
<div class="home-info">
|
||||
{props.children.clone()}
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Github() -> Html {
|
||||
let github_entries: UseStateHandle<Vec<GithubEntry>> = use_state(|| vec![]);
|
||||
{
|
||||
let github_entries = github_entries.clone();
|
||||
use_effect_with_deps(move |_| get_json_resource("/res/github.json", move |it| github_entries.set(it)), ());
|
||||
}
|
||||
|
||||
let theme_state = use_context::<ThemeContext>().expect("Theme context");
|
||||
html! {
|
||||
<HomeCard image={ DescribedImage {
|
||||
image: ImageResource::new_link(format!("github/github-mark{}.png", if theme_state.theme == ThemeState::Dark { "-white" } else { "" }), false),
|
||||
description: "GitHub"
|
||||
} }>
|
||||
{
|
||||
github_entries.iter().map(|it| {
|
||||
let link = it.link.clone();
|
||||
html! {
|
||||
<Card elevation={Elevation::Level3} interactive=true onclick={Callback::from(move |_| {
|
||||
if let Err(_) = document().location().unwrap().set_href(link.as_str()) {
|
||||
log("Couldn't change href");
|
||||
}
|
||||
})}>
|
||||
<h3>{it.title.clone()}</h3>
|
||||
<p>{it.description.clone()}</p>
|
||||
</Card>
|
||||
}
|
||||
}).collect::<Html>()
|
||||
}
|
||||
</HomeCard>
|
||||
}
|
||||
}
|
34
src/page/mod.rs
Normal file
34
src/page/mod.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
mod home;
|
||||
mod projects;
|
||||
mod contact;
|
||||
mod socials;
|
||||
|
||||
#[derive(PartialEq, Copy, Clone)]
|
||||
pub enum Page {
|
||||
Home,
|
||||
Projects,
|
||||
Contact,
|
||||
Socials
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn name(&self) -> &'static str{
|
||||
match self {
|
||||
Page::Home => "Home",
|
||||
Page::Projects => "Projects",
|
||||
Page::Socials => "Social Media",
|
||||
Page::Contact => "Contact"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content(&self) -> Html {
|
||||
match self {
|
||||
Page::Home => html!{<home::Home />},
|
||||
Page::Contact => html!{<contact::Contact />},
|
||||
Page::Socials => html!{<socials::Socials />},
|
||||
Page::Projects => html!{<projects::Projects />},
|
||||
}
|
||||
}
|
||||
}
|
8
src/page/projects.rs
Normal file
8
src/page/projects.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use yew::{function_component, html, Html};
|
||||
|
||||
#[function_component]
|
||||
pub fn Projects() -> Html {
|
||||
html! {
|
||||
<>{"Projects"}</>
|
||||
}
|
||||
}
|
8
src/page/socials.rs
Normal file
8
src/page/socials.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use yew::{function_component, html, Html};
|
||||
|
||||
#[function_component]
|
||||
pub fn Socials() -> Html {
|
||||
html! {
|
||||
<>{"Social Media"}</>
|
||||
}
|
||||
}
|
108
src/theme.rs
Normal file
108
src/theme.rs
Normal file
@ -0,0 +1,108 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gloo::utils::document;
|
||||
use yew::{Children, Properties, Html, html};
|
||||
use yew::prelude::*;
|
||||
use crate::util::log;
|
||||
|
||||
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 class_list = root.get_elements_by_tag_name("body").get_with_index(0).expect("Body tag").class_list();
|
||||
|
||||
if is_dark {
|
||||
if let Err(_) = root.set_attribute("data-theme", "dark") {
|
||||
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 {
|
||||
if let Err(_) = root.remove_attribute("data-theme") {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ThemeCtx {
|
||||
pub theme: ThemeState
|
||||
}
|
||||
|
||||
impl Reducible for ThemeCtx {
|
||||
type Action = ThemeMsg;
|
||||
|
||||
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
|
||||
let ctx: Rc<ThemeCtx> = ThemeCtx {
|
||||
theme: match action {
|
||||
//ThemeMsg::Dark => ThemeState::Dark,
|
||||
//ThemeMsg::Light => ThemeState::Light,
|
||||
ThemeMsg::Toggle => match self.theme {
|
||||
ThemeState::Dark => ThemeState::Light,
|
||||
ThemeState::Light => ThemeState::Dark
|
||||
}
|
||||
}
|
||||
}.into();
|
||||
|
||||
set_global_dark(
|
||||
match &ctx.theme {
|
||||
ThemeState::Dark => true,
|
||||
ThemeState::Light => false
|
||||
}
|
||||
);
|
||||
|
||||
ctx
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
|
||||
pub enum ThemeState {
|
||||
Dark, Light
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
|
||||
pub enum ThemeMsg {
|
||||
//Dark,
|
||||
//Light,
|
||||
Toggle
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq, Debug)]
|
||||
pub struct ThemeProps {
|
||||
#[prop_or_default]
|
||||
pub children: Children
|
||||
}
|
||||
|
||||
pub type ThemeContext = UseReducerHandle<ThemeCtx>;
|
||||
|
||||
#[function_component]
|
||||
pub fn ThemeProvider(props: &ThemeProps) -> Html {
|
||||
let theme = use_reducer(|| {
|
||||
let dark_mode = web_sys::window()
|
||||
.and_then(|x| x.match_media("(prefers-color-scheme: dark)").ok().flatten())
|
||||
.map(|x| x.matches())
|
||||
.unwrap_or(true);
|
||||
set_global_dark(dark_mode);
|
||||
ThemeCtx {
|
||||
theme: if dark_mode {
|
||||
ThemeState::Dark
|
||||
} else {
|
||||
ThemeState::Light
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<ContextProvider<ThemeContext> context={theme}>
|
||||
{props.children.clone()}
|
||||
</ContextProvider<ThemeContext>>
|
||||
}
|
||||
}
|
7
src/util.rs
Normal file
7
src/util.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
pub fn log(message: &str);
|
||||
}
|
BIN
static/img/github/github-mark-white.png
Normal file
BIN
static/img/github/github-mark-white.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 4.7 KiB |
1
static/img/github/github-mark-white.svg
Normal file
1
static/img/github/github-mark-white.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
After (image error) Size: 960 B |
BIN
static/img/github/github-mark.png
Normal file
BIN
static/img/github/github-mark.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 6.2 KiB |
1
static/img/github/github-mark.svg
Normal file
1
static/img/github/github-mark.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>
|
After (image error) Size: 963 B |
0
static/img/github/github-mark.svg:Zone.Identifier
Normal file
0
static/img/github/github-mark.svg:Zone.Identifier
Normal file
BIN
static/img/profile.jpg
Normal file
BIN
static/img/profile.jpg
Normal file
Binary file not shown.
After ![]() (image error) Size: 779 KiB |
16
static/index.html
Normal file
16
static/index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap" rel="stylesheet">
|
||||
<meta charset="utf-8"/>
|
||||
<script type="module">
|
||||
import init from "/app.js";
|
||||
init(new URL('app.wasm', import.meta.url));
|
||||
</script>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
<link rel="stylesheet" href="/blueprint.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
15
static/res/code.json
Normal file
15
static/res/code.json
Normal file
@ -0,0 +1,15 @@
|
||||
[
|
||||
"C",
|
||||
"Rust",
|
||||
"C++",
|
||||
"TypeScript",
|
||||
"JavaScript",
|
||||
"HTML",
|
||||
"Java",
|
||||
"Kotlin",
|
||||
"Lua",
|
||||
"System-/Verilog",
|
||||
"C#",
|
||||
"Objective-C",
|
||||
"Python"
|
||||
]
|
22
static/res/github.json
Normal file
22
static/res/github.json
Normal file
@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"link": "https://github.com/GabrielTofvesson/BankProject",
|
||||
"title": "Bank Project",
|
||||
"description": "Client-server project simulating a simple banking system with hand-written cryptographic and graphics implementations"
|
||||
},
|
||||
{
|
||||
"link": "https://github.com/BTPW",
|
||||
"title": "BTPW",
|
||||
"description": "Ongoing project developing a password manager with a hardware component for logging in to insecure devices"
|
||||
},
|
||||
{
|
||||
"link": "https://github.com/GabrielTofvesson/ASMThread",
|
||||
"title": "ASMThread",
|
||||
"description": "A simple project in ARM assembly implementing virtual threads and *malloc*"
|
||||
},
|
||||
{
|
||||
"link": "https://github.com/GabrielTofvesson/framebuffer_graphics",
|
||||
"title": "framebuffer graphics",
|
||||
"description": "A little project I created one sleepless night for drawing items to a Linux framebuffer device. It implements double-buffering, simple shapes and a custom file format for defining blit masks/shapes. Additionally, it adds a simple character mapping system on top of the blit system for rudimentary character output"
|
||||
}
|
||||
]
|
9
static/res/interests.json
Normal file
9
static/res/interests.json
Normal file
@ -0,0 +1,9 @@
|
||||
[
|
||||
"Data structures",
|
||||
"Cryptography",
|
||||
"Hardware",
|
||||
"Fullstack",
|
||||
"Embedded",
|
||||
"Web",
|
||||
"Android"
|
||||
]
|
6
static/res/languages.json
Normal file
6
static/res/languages.json
Normal file
@ -0,0 +1,6 @@
|
||||
[
|
||||
"English",
|
||||
"Swedish",
|
||||
"French",
|
||||
"Dutch"
|
||||
]
|
1
static/res/profile.md
Normal file
1
static/res/profile.md
Normal file
@ -0,0 +1 @@
|
||||
A programmer with a burning passion for data structures, security and efficiency. This personal site was designed entirely using [Yew](https://yew.rs/). I love most kinds of music, but I prefer rock, bossa nova or EDM. I strive to leave people happier than when I met them.
|
185
static/styles.css
Normal file
185
static/styles.css
Normal file
@ -0,0 +1,185 @@
|
||||
:root {
|
||||
--bg-color: #f5f8fa;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--bg-color: #182026;
|
||||
}
|
||||
|
||||
button:focus, input[type=button]:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--bg-color);
|
||||
transition-duration: 250ms;
|
||||
|
||||
font-family: 'Roboto', sans-serif !important;
|
||||
}
|
||||
|
||||
.main {
|
||||
margin-top: 41px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.actionbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@media only screen and (orientation: landscape) {
|
||||
.ab-portrait {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ab-landscape {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (orientation: portrait) {
|
||||
.ab-landscape {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ab-navbutton {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1000px) {
|
||||
.home-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.home-card {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
overflow: hidden;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.gallery-image {
|
||||
width: 33vw;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Gallery overlay transition properties */
|
||||
.gallery-image-description,
|
||||
.gallery-next,
|
||||
.gallery-prev {
|
||||
position: absolute;
|
||||
transition: opacity 250ms;
|
||||
transition-timing-function: cubic-bezier(0.1, 0.7, 1.0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.gallery-image-description {
|
||||
width: 33vw;
|
||||
transform: translate(0px, calc(-100% - 4px));
|
||||
background: linear-gradient(to top, rgba(0, 75, 196, 0.699), rgba(133, 180, 255, 0));
|
||||
padding: 60px 10px 10px 10px;
|
||||
}
|
||||
|
||||
/* Gallery overlay button positioning */
|
||||
.gallery-next,
|
||||
.gallery-prev {
|
||||
top: 50%;
|
||||
height: max(calc(100% - 4px), 10px);
|
||||
width: 7.5%;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gallery-next {
|
||||
left: 100%;
|
||||
transform: translate(-100%, -50%);
|
||||
padding-right: 2.5%;
|
||||
}
|
||||
|
||||
.gallery-prev {
|
||||
left: 0%;
|
||||
transform: translate(-0%, -50%);
|
||||
padding-left: 2.5%;
|
||||
}
|
||||
|
||||
.gallery:hover > .gallery-image-description,
|
||||
.gallery:hover > .gallery-next,
|
||||
.gallery:hover > .gallery-prev {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gallery-next:hover,
|
||||
.gallery-prev:hover {
|
||||
cursor: pointer;
|
||||
}
|
10
xtask/Cargo.toml
Normal file
10
xtask/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
xtask-wasm = "0.1"
|
||||
yewprint-css = "0.4.0"
|
44
xtask/src/main.rs
Normal file
44
xtask/src/main.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use std::{path::Path, process};
|
||||
use xtask_wasm::{anyhow::Result, clap, DistResult};
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
enum Cli {
|
||||
Dist(xtask_wasm::Dist),
|
||||
Watch(xtask_wasm::Watch),
|
||||
Start(xtask_wasm::DevServer),
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli: Cli = clap::Parser::parse();
|
||||
|
||||
match cli {
|
||||
Cli::Dist(args) => {
|
||||
let DistResult { dist_dir, .. } =
|
||||
args.static_dir_path("static").run("yewprint-app")?;
|
||||
|
||||
download_css(&dist_dir)?;
|
||||
}
|
||||
Cli::Watch(args) => {
|
||||
let mut command = process::Command::new("cargo");
|
||||
command.arg("check");
|
||||
|
||||
args.run(command)?;
|
||||
}
|
||||
Cli::Start(args) => {
|
||||
args.arg("dist")
|
||||
.start(xtask_wasm::default_dist_dir(false))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn download_css(path: &Path) -> Result<()> {
|
||||
let css_path = path.join("blueprint.css");
|
||||
|
||||
if !css_path.exists() {
|
||||
yewprint_css::download_css(&css_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user