create frontend template
This commit is contained in:
69
src/frontend/app.rs
Normal file
69
src/frontend/app.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use yew::{function_component, html, Html};
|
||||
use yew_router::history::History;
|
||||
|
||||
|
||||
#[derive(yew_router::Routable, PartialEq, Eq, Clone, Debug)]
|
||||
pub enum Route {
|
||||
#[at("/")]
|
||||
Home,
|
||||
#[at("/counter")]
|
||||
Counter,
|
||||
#[not_found]
|
||||
#[at("/404")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
fn switch(routes: Route) -> Html {
|
||||
use super::pages::*;
|
||||
|
||||
match routes {
|
||||
Route::Home => html! { <IndexPage /> },
|
||||
Route::Counter => html! { <CounterPage /> },
|
||||
Route::NotFound => html! { <NotFound /> },
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Layout() -> Html {
|
||||
use yew_router::Switch;
|
||||
use super::components::Nav;
|
||||
html! {
|
||||
<>
|
||||
<Nav />
|
||||
<main class="p-6">
|
||||
<Switch<Route> render={switch} />
|
||||
</main>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(yew::Properties, PartialEq, Eq, Debug, Default)]
|
||||
pub struct ServerAppProps {
|
||||
pub url: yew::AttrValue,
|
||||
pub queries: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn ServerApp(props: &ServerAppProps) -> Html {
|
||||
let history = yew_router::history::AnyHistory::from(yew_router::history::MemoryHistory::new());
|
||||
history
|
||||
.push_with_query(&*props.url, &props.queries)
|
||||
.unwrap();
|
||||
|
||||
html! {
|
||||
<yew_router::Router history={history}>
|
||||
<Layout />
|
||||
</yew_router::Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn App() -> Html {
|
||||
html! {
|
||||
<yew_router::BrowserRouter>
|
||||
<Layout />
|
||||
</yew_router::BrowserRouter>
|
||||
}
|
||||
}
|
3
src/frontend/components.rs
Normal file
3
src/frontend/components.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod nav;
|
||||
|
||||
pub use nav::*;
|
158
src/frontend/components/nav.rs
Normal file
158
src/frontend/components/nav.rs
Normal file
@ -0,0 +1,158 @@
|
||||
use yew::{classes, function_component, html, use_state_eq, Callback, Html, MouseEvent};
|
||||
use yew_router::{components::Link, hooks::use_location, Routable};
|
||||
use crate::app::Route;
|
||||
|
||||
#[function_component(Nav)]
|
||||
pub fn nav_bar() -> Html {
|
||||
let show_dropdown = use_state_eq(|| false);
|
||||
let show_dropdown_handle = show_dropdown.clone();
|
||||
|
||||
let mut dropdown_classes = vec![
|
||||
"absolute", "right-0", "z-10", "mt-2", "w-48", "origin-top-right", "rounded-md", "bg-white", "py-1", "shadow-lg", "ring-1", "ring-black", "ring-opacity-5", "focus:outline-none"
|
||||
];
|
||||
if !*show_dropdown {
|
||||
dropdown_classes.push("hidden");
|
||||
}
|
||||
let dropdown_toggler = Callback::from(move |_: MouseEvent| {
|
||||
gloo::console::log!("dropdown: ", *show_dropdown_handle);
|
||||
show_dropdown_handle.set(!*show_dropdown_handle);
|
||||
});
|
||||
|
||||
let show_dropdown_handle = show_dropdown.clone();
|
||||
let dropdown_leave = Callback::from(move |_: MouseEvent| {
|
||||
show_dropdown_handle.set(false);
|
||||
});
|
||||
|
||||
let show_mobile_menu = use_state_eq(||false);
|
||||
let (mut mobile_icon, mut mobile_icon_expanded) = (vec!["h-6", "w-6", "block"], vec!["h-6", "w-6", "hidden"]);
|
||||
let mut mobile_menu_classes = vec!["sm:hidden"];
|
||||
if *show_mobile_menu {
|
||||
(mobile_icon, mobile_icon_expanded) = (mobile_icon_expanded, mobile_icon);
|
||||
} else {
|
||||
mobile_menu_classes.push("hidden");
|
||||
}
|
||||
let show_mobile_menu_handler = show_mobile_menu.clone();
|
||||
let mobile_menu_toggler = Callback::from(move |_:MouseEvent| {
|
||||
show_mobile_menu_handler.set(!*show_mobile_menu_handler);
|
||||
});
|
||||
|
||||
let mut menu = Vec::new();
|
||||
let mut mobile_menu = Vec::new();
|
||||
let maybe_location = use_location();
|
||||
for (route, title) in [
|
||||
(Route::Home, "Home"),
|
||||
(Route::Counter, "Counter"),
|
||||
] {
|
||||
let active_item = if let Some(ref location) = maybe_location {
|
||||
let path = location.path();
|
||||
if let Some(recognized_route) = Route::recognize(path) {
|
||||
recognized_route == route
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let item_classes = if active_item {
|
||||
"rounded-md bg-gray-900 px-3 py-2 text-sm font-medium text-white"
|
||||
} else {
|
||||
"rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white"
|
||||
};
|
||||
let mobile_classes = format!("block {item_classes}");
|
||||
menu.push(html! {
|
||||
<Link<Route> classes={item_classes} to={route.clone()}>{ title }</Link<Route>>
|
||||
});
|
||||
mobile_menu.push(html! {
|
||||
<Link<Route> classes={classes!(mobile_classes)} to={route.clone()}>{ title }</Link<Route>>
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
html! {
|
||||
<nav class="bg-gray-800">
|
||||
<div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
|
||||
<div class="relative flex h-16 items-center justify-between">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||
// <!-- Mobile menu button-->
|
||||
<button onclick={mobile_menu_toggler} type="button" class="relative inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="absolute -inset-0.5"></span>
|
||||
<span class="sr-only">{"Open main menu"}</span>
|
||||
/*
|
||||
Icon when menu is closed.
|
||||
|
||||
Menu open: "hidden", Menu closed: "block"
|
||||
*/
|
||||
<svg class={classes!(mobile_icon)} fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
/*
|
||||
Icon when menu is open.
|
||||
|
||||
Menu open: "block", Menu closed: "hidden"
|
||||
*/
|
||||
<svg class={classes!(mobile_icon_expanded)} fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<div class="flex flex-shrink-0 items-center">
|
||||
<img class="h-8 w-auto" src="https://tailwindui.com/plus/img/logos/mark.svg?color=indigo&shade=500" alt="Your Company" />
|
||||
</div>
|
||||
<div class="hidden sm:ml-6 sm:block">
|
||||
<div class="flex space-x-4">
|
||||
/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */
|
||||
{ menu.clone() }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||
<button type="button" class="relative rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
|
||||
<span class="absolute -inset-1.5"></span>
|
||||
<span class="sr-only">{"View notifications"}</span>
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
/* Profile dropdown */
|
||||
<div class={classes!(vec!["relative", "ml-3"])} onclick={dropdown_toggler} >
|
||||
<div>
|
||||
<button type="button" class="relative flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
|
||||
<span class="absolute -inset-1.5"></span>
|
||||
<span class="sr-only">{"Open user menu"}</span>
|
||||
<img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
/*
|
||||
Dropdown menu, show/hide based on menu state.
|
||||
|
||||
Entering: "transition ease-out duration-100"
|
||||
From: "transform opacity-0 scale-95"
|
||||
To: "transform opacity-100 scale-100"
|
||||
Leaving: "transition ease-in duration-75"
|
||||
From: "transform opacity-100 scale-100"
|
||||
To: "transform opacity-0 scale-95"
|
||||
*/
|
||||
<div class={classes!(dropdown_classes)} onmouseleave={dropdown_leave} role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
|
||||
/* Active: "bg-gray-100", Not Active: "" */
|
||||
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-0">{"Your Profile"}</a>
|
||||
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-1">{"Settings"}</a>
|
||||
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-2">{"Sign out"}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
/* Mobile menu, show/hide based on menu state. */
|
||||
<div class={classes!(mobile_menu_classes)} id="mobile-menu">
|
||||
<div class="space-y-1 px-2 pb-3 pt-2">
|
||||
/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */
|
||||
{ mobile_menu.clone() }
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
}
|
7
src/frontend/pages.rs
Normal file
7
src/frontend/pages.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub mod index;
|
||||
pub mod counter;
|
||||
pub mod _404;
|
||||
|
||||
pub use counter::CounterPage;
|
||||
pub use index::IndexPage;
|
||||
pub use _404::NotFound;
|
8
src/frontend/pages/_404.rs
Normal file
8
src/frontend/pages/_404.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use yew::{function_component, html, Html};
|
||||
|
||||
#[function_component(NotFound)]
|
||||
pub fn not_found() -> Html {
|
||||
html! {
|
||||
<h1>{"Help Kid!"}</h1>
|
||||
}
|
||||
}
|
16
src/frontend/pages/counter.rs
Normal file
16
src/frontend/pages/counter.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use yew::{function_component, html, use_state_eq, Callback, Html};
|
||||
|
||||
|
||||
#[function_component]
|
||||
pub fn CounterPage() -> Html {
|
||||
let counter = use_state_eq(|| 1);
|
||||
let counter_handle = counter.clone();
|
||||
let callback = Callback::from(move |_: yew::MouseEvent| {
|
||||
counter_handle.set(*counter_handle + 1);
|
||||
});
|
||||
html! {
|
||||
<div class="w-full h-full flex">{"Counter: "}
|
||||
<h1 class="mx-auto my-auto" onclick={callback}>{format!("{}",*counter)}</h1>
|
||||
</div>
|
||||
}
|
||||
}
|
16
src/frontend/pages/index.rs
Normal file
16
src/frontend/pages/index.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use yew::{function_component, html, use_state_eq, Callback, Html};
|
||||
|
||||
|
||||
#[function_component]
|
||||
pub fn IndexPage() -> Html {
|
||||
let counter = use_state_eq(|| 1);
|
||||
let counter_handle = counter.clone();
|
||||
let callback = Callback::from(move |_: yew::MouseEvent| {
|
||||
counter_handle.set(*counter_handle + 1);
|
||||
});
|
||||
html! {
|
||||
<div class="w-full h-full flex">
|
||||
<h1 class="mx-auto my-auto" onclick={callback}>{format!("{}",*counter)}</h1>
|
||||
</div>
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user