Init
This commit is contained in:
commit
e82f35da2a
78 changed files with 10821 additions and 0 deletions
429
frontend/src/routes/index.rs
Normal file
429
frontend/src/routes/index.rs
Normal file
|
@ -0,0 +1,429 @@
|
|||
use bounce::helmet::Helmet;
|
||||
use cynic::{MutationBuilder, QueryBuilder};
|
||||
use std::rc::Rc;
|
||||
use web_sys::HtmlInputElement;
|
||||
use yew::prelude::*;
|
||||
use yewdux::prelude::*;
|
||||
|
||||
use crate::{
|
||||
components,
|
||||
graphql::{self, ReqwestExt},
|
||||
stores,
|
||||
};
|
||||
|
||||
pub enum Msg {
|
||||
Void,
|
||||
|
||||
UpdateUser(Rc<stores::User>),
|
||||
LoadUsers,
|
||||
UsersDone(graphql::GraphQLResult<graphql::queries::Users>),
|
||||
|
||||
OpenNewModal,
|
||||
CloseNewModal,
|
||||
NewModalSubmit,
|
||||
NewModalSubmitDone(graphql::GraphQLResult<graphql::queries::CreateRepository>),
|
||||
|
||||
OpenEditModal(usize, usize),
|
||||
CloseEditModal,
|
||||
Delete,
|
||||
DeleteDone(graphql::GraphQLResult<graphql::queries::DeleteRepository>),
|
||||
}
|
||||
|
||||
pub struct Index {
|
||||
notifications_dispatch: Dispatch<stores::Notifications>,
|
||||
user: Rc<stores::User>,
|
||||
_user_dispatch: Dispatch<stores::User>,
|
||||
loading: bool,
|
||||
users: Vec<graphql::queries::User>,
|
||||
|
||||
new_modal: bool,
|
||||
new_modal_save_loading: bool,
|
||||
new_modal_user: NodeRef,
|
||||
new_modal_repo: NodeRef,
|
||||
|
||||
delete_modal: Option<(usize, usize)>,
|
||||
delete_modal_delete_loading: bool,
|
||||
}
|
||||
|
||||
impl Component for Index {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let notifications_dispatch = Dispatch::subscribe(ctx.link().callback(|_| Msg::Void));
|
||||
let user_dispatch = Dispatch::subscribe(ctx.link().callback(Msg::UpdateUser));
|
||||
|
||||
ctx.link().send_message(Msg::LoadUsers);
|
||||
|
||||
Self {
|
||||
notifications_dispatch,
|
||||
user: user_dispatch.get(),
|
||||
_user_dispatch: user_dispatch,
|
||||
loading: false,
|
||||
users: vec![],
|
||||
|
||||
new_modal: false,
|
||||
new_modal_save_loading: false,
|
||||
new_modal_user: NodeRef::default(),
|
||||
new_modal_repo: NodeRef::default(),
|
||||
|
||||
delete_modal: None,
|
||||
delete_modal_delete_loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::Void => false,
|
||||
|
||||
Msg::UpdateUser(x) => {
|
||||
self.user = x;
|
||||
|
||||
true
|
||||
}
|
||||
Msg::LoadUsers => {
|
||||
self.loading = true;
|
||||
|
||||
let client = graphql::client(None);
|
||||
let operation = graphql::queries::Users::build(());
|
||||
ctx.link().send_future(async move {
|
||||
Msg::UsersDone(client.run_graphql(operation).await)
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
Msg::UsersDone(x) => {
|
||||
self.loading = false;
|
||||
|
||||
match x {
|
||||
Ok(resp) => {
|
||||
if let Some(errors) = resp.errors {
|
||||
self.notifications_dispatch.reduce_mut(|notifs| {
|
||||
for e in errors {
|
||||
notifs.push(stores::Notification::danger(Some(
|
||||
components::notification::NotificationMessage::GraphQLError(
|
||||
e,
|
||||
),
|
||||
)));
|
||||
}
|
||||
});
|
||||
|
||||
false
|
||||
} else {
|
||||
let data = resp.data.unwrap();
|
||||
self.users = data.users;
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.notifications_dispatch.reduce_mut(|notifs| {
|
||||
notifs.push(stores::Notification::danger(Some(
|
||||
components::notification::NotificationMessage::Text(vec![
|
||||
e.to_string()
|
||||
]),
|
||||
)));
|
||||
});
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Msg::OpenNewModal => {
|
||||
self.new_modal = true;
|
||||
|
||||
true
|
||||
}
|
||||
Msg::CloseNewModal => {
|
||||
self.new_modal = false;
|
||||
self.new_modal_save_loading = false;
|
||||
ctx.link().send_message(Msg::LoadUsers);
|
||||
|
||||
true
|
||||
}
|
||||
Msg::NewModalSubmit => {
|
||||
let user = match self.new_modal_user.cast::<HtmlInputElement>() {
|
||||
Some(x) => x.value(),
|
||||
None => return false,
|
||||
};
|
||||
let name = match self.new_modal_repo.cast::<HtmlInputElement>() {
|
||||
Some(x) => x.value(),
|
||||
None => return false,
|
||||
};
|
||||
|
||||
self.new_modal_save_loading = true;
|
||||
let operation = graphql::queries::CreateRepository::build(
|
||||
graphql::queries::CreateRepositoryVariables { user, name },
|
||||
);
|
||||
let client = graphql::client(self.user.0.as_ref());
|
||||
ctx.link().send_future(async move {
|
||||
Msg::NewModalSubmitDone(client.run_graphql(operation).await)
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
Msg::NewModalSubmitDone(x) => {
|
||||
self.new_modal_save_loading = false;
|
||||
|
||||
match x {
|
||||
Ok(resp) => {
|
||||
if let Some(errors) = resp.errors {
|
||||
self.notifications_dispatch.reduce_mut(|notifs| {
|
||||
for e in errors {
|
||||
notifs.push(stores::Notification {
|
||||
notification_type: components::notification::NotificationType::Danger,
|
||||
message: Some(components::notification::NotificationMessage::GraphQLError(e)),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
true
|
||||
} else {
|
||||
ctx.link().send_message(Msg::CloseNewModal);
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.notifications_dispatch.reduce_mut(|notifs| {
|
||||
notifs.push(stores::Notification::danger(Some(
|
||||
components::notification::NotificationMessage::Text(vec![
|
||||
e.to_string()
|
||||
]),
|
||||
)));
|
||||
});
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Msg::OpenEditModal(i, j) => {
|
||||
self.delete_modal = Some((i, j));
|
||||
|
||||
true
|
||||
}
|
||||
Msg::CloseEditModal => {
|
||||
self.delete_modal = None;
|
||||
self.delete_modal_delete_loading = false;
|
||||
ctx.link().send_message(Msg::LoadUsers);
|
||||
|
||||
true
|
||||
}
|
||||
Msg::Delete => {
|
||||
let modal = self.delete_modal.unwrap();
|
||||
|
||||
self.delete_modal_delete_loading = true;
|
||||
let operation = graphql::queries::DeleteRepository::build(
|
||||
graphql::queries::DeleteRepositoryVariables {
|
||||
id: self.users[modal.0].repositories[modal.1].id.to_owned(),
|
||||
},
|
||||
);
|
||||
let client = graphql::client(self.user.0.as_ref());
|
||||
ctx.link().send_future(async move {
|
||||
Msg::DeleteDone(client.run_graphql(operation).await)
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
Msg::DeleteDone(x) => match x {
|
||||
Ok(_) => {
|
||||
self.delete_modal = None;
|
||||
self.delete_modal_delete_loading = false;
|
||||
ctx.link().send_message(Msg::LoadUsers);
|
||||
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
self.notifications_dispatch.reduce_mut(|notifs| {
|
||||
notifs.push(stores::Notification {
|
||||
notification_type: components::notification::NotificationType::Danger,
|
||||
message: Some(components::notification::NotificationMessage::Text(
|
||||
vec![e.to_string()],
|
||||
)),
|
||||
});
|
||||
});
|
||||
|
||||
false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{ "Home" }</title>
|
||||
</Helmet>
|
||||
|
||||
<div class={classes!("columns", "is-centered")}>
|
||||
<div class={classes!("column", "is-half")}>
|
||||
<div class={classes!("columns", "is-multiline")}>
|
||||
<div class={classes!("column", "is-full")}>
|
||||
<button
|
||||
class={classes!("button", "is-primary", "is-fullwidth")}
|
||||
onclick={ctx.link().callback(|_| Msg::OpenNewModal)}
|
||||
>
|
||||
<span class={classes!("icon", "is-small")}>
|
||||
<i class={classes!("fa-solid", "fa-plus")} />
|
||||
</span>
|
||||
<span>{ "New" }</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
if self.loading {
|
||||
<div class={classes!("column", "is-full")}>
|
||||
<components::Loading />
|
||||
</div>
|
||||
} else {
|
||||
{
|
||||
for self.users.iter().enumerate().map(|(i, user)| html! {
|
||||
<div class={classes!("column", "is-full")}>
|
||||
<components::UserPane
|
||||
name={user.name.to_owned()}
|
||||
repositories={user.repositories
|
||||
.iter()
|
||||
.map(|repo| components::user_pane::Repository {
|
||||
name: repo.name.to_owned(),
|
||||
url: None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
on_click={ctx.link().callback(move |j| Msg::OpenEditModal(i, j))}
|
||||
/>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
if self.new_modal {
|
||||
<div class={classes!("modal", "is-active")}>
|
||||
<div class={classes!("modal-background")} onclick={ctx.link().callback(|_| Msg::CloseNewModal)} />
|
||||
|
||||
<div class={classes!("modal-card")}>
|
||||
<header class={classes!("modal-card-head")}>
|
||||
<p class={classes!("modal-card-title")}>
|
||||
{ "Edit" }
|
||||
</p>
|
||||
<button
|
||||
class={classes!("delete")}
|
||||
aria-label="close"
|
||||
onclick={ctx.link().callback(|_| Msg::CloseNewModal)}
|
||||
/>
|
||||
</header>
|
||||
<section class={classes!("modal-card-body")}>
|
||||
<table class={classes!("table", "is-bordered", "is-striped", "is-hoverable", "is-fullwidth")}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{ "User" }</th>
|
||||
<td>
|
||||
<input
|
||||
class={classes!("input")}
|
||||
type="text"
|
||||
required=true
|
||||
placeholder="User"
|
||||
ref={self.new_modal_user.clone()}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{ "Repository" }</th>
|
||||
<td>
|
||||
<input
|
||||
class={classes!("input")}
|
||||
type="text"
|
||||
required=true
|
||||
placeholder="Repository"
|
||||
ref={self.new_modal_repo.clone()}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<footer class={classes!("modal-card-foot")}>
|
||||
<div class={classes!("field", "is-grouped")}>
|
||||
<p class={classes!("control")}>
|
||||
<button
|
||||
class={classes!(
|
||||
"button",
|
||||
"is-success",
|
||||
if self.new_modal_save_loading { Some("is-loading") } else { None }
|
||||
)}
|
||||
onclick={ctx.link().callback(|_| Msg::NewModalSubmit)}
|
||||
>
|
||||
<span class={classes!("icon", "is-small")}>
|
||||
<i class={classes!("fa-solid", "fa-check")} />
|
||||
</span>
|
||||
<span>{ "Save" }</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class={classes!("control")}>
|
||||
<button class={classes!("button")} onclick={ctx.link().callback(|_| Msg::CloseNewModal)}>
|
||||
{ "Cancel" }
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
if let Some(modal) = &self.delete_modal {
|
||||
<div class={classes!("modal", "is-active")}>
|
||||
<div class={classes!("modal-background")} onclick={ctx.link().callback(|_| Msg::CloseEditModal)} />
|
||||
|
||||
<div class={classes!("modal-card")}>
|
||||
<header class={classes!("modal-card-head")}>
|
||||
<p class={classes!("modal-card-title")}>
|
||||
{ "Edit" }
|
||||
</p>
|
||||
<button
|
||||
class={classes!("delete")}
|
||||
aria-label="close"
|
||||
onclick={ctx.link().callback(|_| Msg::CloseEditModal)}
|
||||
/>
|
||||
</header>
|
||||
<section class={classes!("modal-card-body")}>
|
||||
<p>
|
||||
{ &self.users[modal.0].name }
|
||||
{ "/" }
|
||||
{ &self.users[modal.0].repositories[modal.1].name }
|
||||
</p>
|
||||
</section>
|
||||
<footer class={classes!("modal-card-foot")}>
|
||||
<div class={classes!("field", "is-grouped")}>
|
||||
<p class={classes!("control")}>
|
||||
<button
|
||||
class={classes!(
|
||||
"button",
|
||||
"is-danger",
|
||||
if self.delete_modal_delete_loading { Some("is-loading") } else { None }
|
||||
)}
|
||||
onclick={ctx.link().callback(|_| Msg::Delete)}
|
||||
>
|
||||
<span class={classes!("icon", "is-small")}>
|
||||
<i class={classes!("fa-solid", "fa-trash")} />
|
||||
</span>
|
||||
<span>{ "Delete" }</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class={classes!("control")}>
|
||||
<button class={classes!("button")} onclick={ctx.link().callback(|_| Msg::CloseEditModal)}>
|
||||
{ "Cancel" }
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
237
frontend/src/routes/login.rs
Normal file
237
frontend/src/routes/login.rs
Normal file
|
@ -0,0 +1,237 @@
|
|||
use bounce::helmet::Helmet;
|
||||
use cynic::QueryBuilder;
|
||||
use std::rc::Rc;
|
||||
use web_sys::HtmlInputElement;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use yewdux::prelude::*;
|
||||
|
||||
use crate::{
|
||||
components,
|
||||
graphql::{self, ReqwestExt},
|
||||
routes, stores,
|
||||
};
|
||||
|
||||
pub enum Msg {
|
||||
UpdateUser(Rc<stores::User>),
|
||||
UpdateNotifications(Rc<stores::Notifications>),
|
||||
Login,
|
||||
LoginDone {
|
||||
user_data: stores::UserData,
|
||||
result: graphql::GraphQLResult<graphql::queries::VerifyLogin>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct Login {
|
||||
user: Rc<stores::User>,
|
||||
user_dispatch: Dispatch<stores::User>,
|
||||
notifications: Rc<stores::Notifications>,
|
||||
notifications_dispatch: Dispatch<stores::Notifications>,
|
||||
username: NodeRef,
|
||||
password: NodeRef,
|
||||
loading: bool,
|
||||
error: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl Component for Login {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let user_dispatch = Dispatch::subscribe(ctx.link().callback(Msg::UpdateUser));
|
||||
let notifications_dispatch =
|
||||
Dispatch::subscribe(ctx.link().callback(Msg::UpdateNotifications));
|
||||
|
||||
Self {
|
||||
user: user_dispatch.get(),
|
||||
user_dispatch,
|
||||
notifications: notifications_dispatch.get(),
|
||||
notifications_dispatch,
|
||||
username: NodeRef::default(),
|
||||
password: NodeRef::default(),
|
||||
loading: false,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::UpdateUser(x) => {
|
||||
self.error = None;
|
||||
|
||||
let prev = self.user.logged_in();
|
||||
self.user = x;
|
||||
|
||||
prev != self.user.logged_in()
|
||||
}
|
||||
Msg::UpdateNotifications(x) => {
|
||||
self.notifications = x;
|
||||
|
||||
false
|
||||
}
|
||||
Msg::Login => {
|
||||
self.error = None;
|
||||
|
||||
let username = match self.username.cast::<HtmlInputElement>().map(|x| x.value()) {
|
||||
Some(x) => {
|
||||
if x.is_empty() {
|
||||
return false;
|
||||
} else {
|
||||
x
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let password = match self.password.cast::<HtmlInputElement>().map(|x| x.value()) {
|
||||
Some(x) => {
|
||||
if x.is_empty() {
|
||||
return false;
|
||||
} else {
|
||||
x
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
self.loading = true;
|
||||
let operation =
|
||||
graphql::queries::VerifyLogin::build(graphql::queries::VerifyLoginVariables {
|
||||
username: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
});
|
||||
ctx.link().send_future(async move {
|
||||
Msg::LoginDone {
|
||||
user_data: stores::UserData { username, password },
|
||||
result: graphql::client(None).run_graphql(operation).await,
|
||||
}
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
Msg::LoginDone { user_data, result } => {
|
||||
self.loading = false;
|
||||
|
||||
match result {
|
||||
Ok(resp) => {
|
||||
if let Some(errors) = resp.errors {
|
||||
self.notifications_dispatch.reduce_mut(|notifs| {
|
||||
for e in errors {
|
||||
notifs.push(stores::Notification::danger(Some(
|
||||
components::notification::NotificationMessage::GraphQLError(
|
||||
e,
|
||||
),
|
||||
)));
|
||||
}
|
||||
});
|
||||
|
||||
true
|
||||
} else if resp.data.unwrap().verify_login {
|
||||
self.error = None;
|
||||
self.user_dispatch.set(stores::User(Some(user_data)));
|
||||
|
||||
false
|
||||
} else {
|
||||
const MESSAGE: &str = "Username or password not correct";
|
||||
|
||||
self.error = Some(MESSAGE);
|
||||
self.notifications_dispatch.reduce_mut(|notifs| {
|
||||
notifs.push(stores::Notification {
|
||||
notification_type:
|
||||
components::notification::NotificationType::Danger,
|
||||
message: Some(
|
||||
components::notification::NotificationMessage::Text(vec![
|
||||
MESSAGE.to_string(),
|
||||
]),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.notifications_dispatch.reduce_mut(|notifs| {
|
||||
notifs.push(stores::Notification {
|
||||
notification_type:
|
||||
components::notification::NotificationType::Danger,
|
||||
message: Some(components::notification::NotificationMessage::Text(
|
||||
vec![e.to_string()],
|
||||
)),
|
||||
});
|
||||
});
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
if self.user.logged_in() {
|
||||
ctx.link().navigator().unwrap().push(&routes::Route::Index);
|
||||
}
|
||||
|
||||
html! {
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{ "Login" }</title>
|
||||
</Helmet>
|
||||
|
||||
<div class={classes!("columns", "is-centered", "my-auto")}>
|
||||
<div class={classes!("column", "is-half", "box")}>
|
||||
<div class={classes!("p-4")}>
|
||||
<form onsubmit={ctx.link().callback(|e: SubmitEvent| { e.prevent_default(); Msg::Login })}>
|
||||
<div class={classes!("field")}>
|
||||
<p class={classes!("control", "has-icons-left")}>
|
||||
<input
|
||||
class={classes!("input")}
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
ref={self.username.clone()}
|
||||
/>
|
||||
<span class={classes!("icon", "is-small", "is-left")}>
|
||||
<i class={classes!("fa-solid", "fa-envelope")} />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class={classes!("field")}>
|
||||
<p class={classes!("control", "has-icons-left")}>
|
||||
<input
|
||||
class={classes!("input")}
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
ref={self.password.clone()}
|
||||
/>
|
||||
<span class={classes!("icon", "is-small", "is-left")}>
|
||||
<i class={classes!("fa-solid", "fa-lock")} />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class={classes!("field")}>
|
||||
<p class={classes!("control")}>
|
||||
<button
|
||||
type="submit"
|
||||
class={classes!("button", "is-success", "is-fullwidth", if self.loading { Some("is-loading") } else { None })}
|
||||
>
|
||||
{ "Login" }
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
if let Some(message) = self.error {
|
||||
<p class={classes!("help", "is-danger")}>
|
||||
{ message }
|
||||
</p>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
48
frontend/src/routes/mod.rs
Normal file
48
frontend/src/routes/mod.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use crate::layouts;
|
||||
|
||||
pub mod index;
|
||||
pub mod login;
|
||||
pub mod not_found;
|
||||
pub mod user;
|
||||
|
||||
pub use index::Index;
|
||||
pub use login::Login;
|
||||
pub use not_found::NotFound;
|
||||
pub use user::User;
|
||||
|
||||
#[derive(Clone, Routable, PartialEq, Eq, Debug)]
|
||||
pub enum Route {
|
||||
#[at("/")]
|
||||
Index,
|
||||
|
||||
// #[at("/user/:name")]
|
||||
// User { name: String },
|
||||
#[at("/login")]
|
||||
Login,
|
||||
|
||||
#[not_found]
|
||||
#[at("/404")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
pub fn switch(route: Route) -> Html {
|
||||
match route {
|
||||
Route::Index => html! {
|
||||
<layouts::LoggedIn>
|
||||
<Index />
|
||||
</layouts::LoggedIn>
|
||||
},
|
||||
// Route::User { name } => html! {
|
||||
// <User {name} />
|
||||
// },
|
||||
Route::Login => html! {
|
||||
<Login />
|
||||
},
|
||||
Route::NotFound => html! {
|
||||
<NotFound />
|
||||
},
|
||||
}
|
||||
}
|
20
frontend/src/routes/not_found.rs
Normal file
20
frontend/src/routes/not_found.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
use crate::layouts;
|
||||
|
||||
pub struct NotFound;
|
||||
|
||||
impl Component for NotFound {
|
||||
type Message = ();
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<layouts::NotFound message="Page not found" />
|
||||
}
|
||||
}
|
||||
}
|
143
frontend/src/routes/user.rs
Normal file
143
frontend/src/routes/user.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
use bounce::helmet::Helmet;
|
||||
use cynic::QueryBuilder;
|
||||
use yew::prelude::*;
|
||||
use yewdux::prelude::*;
|
||||
|
||||
use crate::{
|
||||
components,
|
||||
graphql::{self, ReqwestExt},
|
||||
stores,
|
||||
};
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct Props {
|
||||
pub name: AttrValue,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
Void,
|
||||
Done(graphql::GraphQLResult<graphql::queries::UserByName>),
|
||||
}
|
||||
|
||||
struct Repository {
|
||||
name: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
pub struct User {
|
||||
notifications_dispatch: Dispatch<stores::Notifications>,
|
||||
loading: bool,
|
||||
repositories: Vec<Repository>,
|
||||
}
|
||||
|
||||
impl Component for User {
|
||||
type Message = Msg;
|
||||
type Properties = Props;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let operation =
|
||||
graphql::queries::UserByName::build(graphql::queries::UserByNameVariables {
|
||||
name: ctx.props().name.to_string(),
|
||||
});
|
||||
let client = graphql::client(None);
|
||||
ctx.link()
|
||||
.send_future(async move { Msg::Done(client.run_graphql(operation).await) });
|
||||
|
||||
Self {
|
||||
notifications_dispatch: Dispatch::subscribe(ctx.link().callback(|_| Msg::Void)),
|
||||
loading: true,
|
||||
repositories: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::Void => false,
|
||||
Msg::Done(x) => {
|
||||
self.loading = false;
|
||||
|
||||
match x {
|
||||
Ok(resp) => {
|
||||
if let Some(errors) = resp.errors {
|
||||
self.notifications_dispatch.reduce_mut(|notifs| {
|
||||
for e in errors {
|
||||
notifs.push(stores::Notification::danger(Some(
|
||||
components::notification::NotificationMessage::GraphQLError(
|
||||
e,
|
||||
),
|
||||
)));
|
||||
}
|
||||
});
|
||||
|
||||
false
|
||||
} else {
|
||||
self.repositories = resp
|
||||
.data
|
||||
.unwrap()
|
||||
.user_by_name
|
||||
.repositories
|
||||
.into_iter()
|
||||
.map(|repo| Repository {
|
||||
name: repo.name,
|
||||
url: repo.url,
|
||||
})
|
||||
.collect();
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.notifications_dispatch.reduce_mut(|notifs| {
|
||||
notifs.push(stores::Notification::danger(Some(
|
||||
components::notification::NotificationMessage::Text(vec![
|
||||
e.to_string()
|
||||
]),
|
||||
)));
|
||||
});
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let name = &ctx.props().name;
|
||||
|
||||
html! {
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{ name }</title>
|
||||
</Helmet>
|
||||
|
||||
<div class={classes!("columns", "is-centered")}>
|
||||
<div class={classes!("column", "is-half")}>
|
||||
<div class={classes!("columns", "is-multiline")}>
|
||||
if self.loading {
|
||||
<div class={classes!("column", "is-full")}>
|
||||
<components::Loading />
|
||||
</div>
|
||||
} else {
|
||||
<div class={classes!("column", "is-full")}>
|
||||
<components::UserPane
|
||||
name={&ctx.props().name}
|
||||
repositories={
|
||||
self.repositories
|
||||
.iter()
|
||||
.map(|repo| components::user_pane::Repository {
|
||||
name: repo.name.to_owned(),
|
||||
url: Some(repo.url.to_owned())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue