Implement single-page app
This commit is contained in:
parent
b4f4db1f81
commit
0d40dd7e1f
39
Cargo.lock
generated
39
Cargo.lock
generated
@ -599,6 +599,7 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
|
checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"gloo-events",
|
"gloo-events",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@ -672,6 +673,8 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "98c4a8d6391675c6b2ee1a6c8d06e8e2d03605c44cec1270675985a4c2a5500b"
|
checksum = "98c4a8d6391675c6b2ee1a6c8d06e8e2d03605c44cec1270675985a4c2a5500b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@ -1286,6 +1289,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "route-recognizer"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.21"
|
version = "0.1.21"
|
||||||
@ -2268,6 +2277,35 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yew-router"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "426ee0486d2572a6c5e39fbdbc48b58d59bb555f3326f54631025266cf04146e"
|
||||||
|
dependencies = [
|
||||||
|
"gloo",
|
||||||
|
"js-sys",
|
||||||
|
"route-recognizer",
|
||||||
|
"serde",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"tracing",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
"yew",
|
||||||
|
"yew-router-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yew-router-macro"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89b249cdb39e0cddaf0644dedc781854524374664793479fdc01e6a65d6e6ae3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yewprint"
|
name = "yewprint"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -2299,6 +2337,7 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
"wee_alloc",
|
"wee_alloc",
|
||||||
"yew",
|
"yew",
|
||||||
|
"yew-router",
|
||||||
"yewprint",
|
"yewprint",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ js-sys = "0.3"
|
|||||||
wee_alloc = { version = "0.4.5", optional = false }
|
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" }
|
||||||
yewprint = "0.4.0"
|
yewprint = "0.4.0"
|
||||||
gloo = "0.8.0"
|
gloo = "0.8.0"
|
||||||
gloo-events = "0.1"
|
gloo-events = "0.1"
|
||||||
|
30
src/app.rs
30
src/app.rs
@ -1,6 +1,10 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
use yew_router::prelude::Navigator;
|
||||||
|
use yew_router::prelude::use_navigator;
|
||||||
use yewprint::Icon;
|
use yewprint::Icon;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
|
use crate::page::PageRouter;
|
||||||
|
use crate::page::Pages;
|
||||||
use crate::theme::ThemeContext;
|
use crate::theme::ThemeContext;
|
||||||
use crate::theme::ThemeMsg;
|
use crate::theme::ThemeMsg;
|
||||||
use crate::theme::ThemeProvider;
|
use crate::theme::ThemeProvider;
|
||||||
@ -11,15 +15,20 @@ use crate::component::actionbar::{Actionbar, ActionbarOption};
|
|||||||
pub fn AppRoot() -> Html {
|
pub fn AppRoot() -> Html {
|
||||||
html! {
|
html! {
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ThemedApp />
|
<PageRouter>
|
||||||
|
<ThemedApp />
|
||||||
|
</PageRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn ThemedApp() -> Html {
|
fn ThemedApp() -> Html {
|
||||||
|
let navigator = use_navigator().unwrap();
|
||||||
|
|
||||||
// Helper function for generating tabs
|
// Helper function for generating tabs
|
||||||
fn page_option(current: &Page, page: Page, state: UseStateHandle<Page>) -> ActionbarOption {
|
fn page_option(current: &Page, page: Page, navigator: Navigator, state: UseStateHandle<Page>) -> ActionbarOption {
|
||||||
|
let is_current = *current == page;
|
||||||
ActionbarOption::new(
|
ActionbarOption::new(
|
||||||
page.name(),
|
page.name(),
|
||||||
match page {
|
match page {
|
||||||
@ -28,8 +37,11 @@ fn ThemedApp() -> Html {
|
|||||||
Page::Socials => Icon::SocialMedia,
|
Page::Socials => Icon::SocialMedia,
|
||||||
Page::Contact => Icon::Envelope,
|
Page::Contact => Icon::Envelope,
|
||||||
},
|
},
|
||||||
Callback::from(move |_| state.set(page)),
|
Callback::from(move |_| {
|
||||||
*current == page
|
navigator.push(&page);
|
||||||
|
state.set(page);
|
||||||
|
}),
|
||||||
|
is_current
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,10 +53,10 @@ fn ThemedApp() -> Html {
|
|||||||
<>
|
<>
|
||||||
<Actionbar>
|
<Actionbar>
|
||||||
{vec![
|
{vec![
|
||||||
page_option(¤t_page, Page::Home, page_state.clone()),
|
page_option(¤t_page, Page::Home, navigator.clone(), page_state.clone()),
|
||||||
page_option(¤t_page, Page::Projects, page_state.clone()),
|
page_option(¤t_page, Page::Projects, navigator.clone(), page_state.clone()),
|
||||||
page_option(¤t_page, Page::Socials, page_state.clone()),
|
page_option(¤t_page, Page::Socials, navigator.clone(), page_state.clone()),
|
||||||
page_option(¤t_page, Page::Contact, page_state),
|
page_option(¤t_page, Page::Contact, navigator, page_state),
|
||||||
ActionbarOption::new_opt(
|
ActionbarOption::new_opt(
|
||||||
None,
|
None,
|
||||||
Some(Icon::Flash),
|
Some(Icon::Flash),
|
||||||
@ -56,7 +68,7 @@ fn ThemedApp() -> Html {
|
|||||||
]}
|
]}
|
||||||
</Actionbar>
|
</Actionbar>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
{current_page.content()}
|
<Pages />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use yew::{function_component, Html, html, Callback, Properties, use_state};
|
use yew::{function_component, Html, html, Callback, Properties, use_state};
|
||||||
use yewprint::{Overlay, Icon, IconSize};
|
use yewprint::{Overlay, Icon, IconSize, Intent};
|
||||||
|
|
||||||
const CHEVRON_SIZE: f64 = 40.0;
|
const CHEVRON_SIZE: f64 = 40.0;
|
||||||
|
const CHEVRON_TYPE: Intent = Intent::Danger;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub struct ImageDescription {
|
pub struct ImageDescription {
|
||||||
@ -60,7 +61,7 @@ pub fn ImageViewer(props: &ImageViewerProps) -> Html {
|
|||||||
Some(html! {
|
Some(html! {
|
||||||
<Icon
|
<Icon
|
||||||
icon={yewprint::Icon::ChevronRight}
|
icon={yewprint::Icon::ChevronRight}
|
||||||
intent={yewprint::Intent::Warning}
|
intent={CHEVRON_TYPE}
|
||||||
size={IconSize(CHEVRON_SIZE)}
|
size={IconSize(CHEVRON_SIZE)}
|
||||||
onclick={Callback::from(move |_| select_next.set(*select_next + 1))}
|
onclick={Callback::from(move |_| select_next.set(*select_next + 1))}
|
||||||
class="gallery-next"
|
class="gallery-next"
|
||||||
@ -75,7 +76,7 @@ pub fn ImageViewer(props: &ImageViewerProps) -> Html {
|
|||||||
Some(html! {
|
Some(html! {
|
||||||
<Icon
|
<Icon
|
||||||
icon={yewprint::Icon::ChevronLeft}
|
icon={yewprint::Icon::ChevronLeft}
|
||||||
intent={yewprint::Intent::Warning}
|
intent={CHEVRON_TYPE}
|
||||||
size={IconSize(CHEVRON_SIZE)}
|
size={IconSize(CHEVRON_SIZE)}
|
||||||
onclick={Callback::from(move |_| select_prev.set(*select_prev - 1))}
|
onclick={Callback::from(move |_| select_prev.set(*select_prev - 1))}
|
||||||
class="gallery-prev"
|
class="gallery-prev"
|
||||||
|
@ -3,7 +3,7 @@ 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_state, use_effect_with_deps, Properties, Children, use_context, Callback};
|
||||||
use yewprint::{Divider, Elevation, Card, Tag, Intent, Icon, Overlay};
|
use yewprint::{Divider, Elevation, Card, Tag, Intent, Icon};
|
||||||
|
|
||||||
use crate::{util::log, theme::{ThemeContext, ThemeState}, component::image_viewer::{ImageDescription, ImageViewer}};
|
use crate::{util::log, theme::{ThemeContext, ThemeState}, component::image_viewer::{ImageDescription, ImageViewer}};
|
||||||
|
|
||||||
|
@ -1,18 +1,56 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
use yew_router::prelude::*;
|
||||||
|
|
||||||
mod home;
|
mod home;
|
||||||
mod projects;
|
mod projects;
|
||||||
mod contact;
|
mod contact;
|
||||||
mod socials;
|
mod socials;
|
||||||
|
|
||||||
#[derive(PartialEq, Copy, Clone)]
|
#[derive(PartialEq, Copy, Clone, Routable)]
|
||||||
pub enum Page {
|
pub enum Page {
|
||||||
|
#[at("/")]
|
||||||
Home,
|
Home,
|
||||||
|
|
||||||
|
#[at("/projects")]
|
||||||
Projects,
|
Projects,
|
||||||
|
|
||||||
|
#[at("/contacts")]
|
||||||
Contact,
|
Contact,
|
||||||
|
|
||||||
|
#[at("/socials")]
|
||||||
Socials
|
Socials
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn switch(page: Page) -> Html {
|
||||||
|
match page {
|
||||||
|
Page::Home => html!{<home::Home />},
|
||||||
|
Page::Contact => html!{<contact::Contact />},
|
||||||
|
Page::Socials => html!{<socials::Socials />},
|
||||||
|
Page::Projects => html!{<projects::Projects />},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct PageRouterProps {
|
||||||
|
pub children: Children
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn PageRouter(props: &PageRouterProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<BrowserRouter>
|
||||||
|
{props.children.clone()}
|
||||||
|
</BrowserRouter>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn Pages() -> Html {
|
||||||
|
html! {
|
||||||
|
<Switch<Page> render={switch} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
pub fn name(&self) -> &'static str{
|
pub fn name(&self) -> &'static str{
|
||||||
match self {
|
match self {
|
||||||
@ -22,13 +60,4 @@ impl Page {
|
|||||||
Page::Contact => "Contact"
|
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 />},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
40
static/404.html
Normal file
40
static/404.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Single Page Apps for GitHub Pages</title>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Single Page Apps for GitHub Pages
|
||||||
|
// MIT License
|
||||||
|
// https://github.com/rafgraph/spa-github-pages
|
||||||
|
// This script takes the current url and converts the path and query
|
||||||
|
// string into just a query string, and then redirects the browser
|
||||||
|
// to the new url with only a query string and hash fragment,
|
||||||
|
// e.g. https://www.foo.tld/one/two?a=b&c=d#qwe, becomes
|
||||||
|
// https://www.foo.tld/?/one/two&a=b~and~c=d#qwe
|
||||||
|
// Note: this 404.html file must be at least 512 bytes for it to work
|
||||||
|
// with Internet Explorer (it is currently > 512 bytes)
|
||||||
|
y
|
||||||
|
// If you're creating a Project Pages site and NOT using a custom domain,
|
||||||
|
// then set pathSegmentsToKeep to 1 (enterprise users ma need to set it to > 1).
|
||||||
|
// This way the code will only replace the route part of the path, and not
|
||||||
|
// the real directory in which the app resides, for example:
|
||||||
|
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
|
||||||
|
// https://username.github.io/repo-name/?/one/two&a=b~and~c=d#qwe
|
||||||
|
// Otherwise, leave pathSegmentsToKeep as 0.
|
||||||
|
var pathSegmentsToKeep = window.location.href.indexOf("github.io") >= 0 ? 1 : 0;
|
||||||
|
|
||||||
|
var l = window.location;
|
||||||
|
l.replace(
|
||||||
|
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
|
||||||
|
l.pathname.split('/').slice(0, 1 + pathSegmentsToKeep).join('/') + '/?/' +
|
||||||
|
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
|
||||||
|
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
|
||||||
|
l.hash
|
||||||
|
);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -3,6 +3,28 @@
|
|||||||
<head>
|
<head>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap" rel="stylesheet">
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Single Page Apps for GitHub Pages
|
||||||
|
// MIT License
|
||||||
|
// https://github.com/rafgraph/spa-github-pages
|
||||||
|
// This script checks to see if a redirect is present in the query string,
|
||||||
|
// converts it back into the correct url and adds it to the
|
||||||
|
// 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,
|
||||||
|
// the correct url will be waiting in the browser's history for
|
||||||
|
// the single page app to route accordingly.
|
||||||
|
(function(l) {
|
||||||
|
if (l.search[1] === '/' ) {
|
||||||
|
var decoded = l.search.slice(1).split('&').map(function(s) {
|
||||||
|
return s.replace(/~and~/g, '&')
|
||||||
|
}).join('?');
|
||||||
|
window.history.replaceState(null, null,
|
||||||
|
l.pathname.slice(0, -1) + decoded + l.hash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}(window.location))
|
||||||
|
</script>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import init from "/app.js";
|
import init from "/app.js";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user