wip
This commit is contained in:
parent
5deba2fdb1
commit
4e8f84480d
48 changed files with 6355 additions and 1192 deletions
2
webui/.cargo/config.toml
Normal file
2
webui/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
29
webui/Cargo.toml
Normal file
29
webui/Cargo.toml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
cargo-features = ["per-package-target"]
|
||||
|
||||
[package]
|
||||
name = "webcomment-webui"
|
||||
version = "0.1.0"
|
||||
authors = ["tuxmain <tuxmain@zettascript.org>"]
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://git.txmn.tk/tuxmain/webcomment"
|
||||
description = "Comment web client"
|
||||
edition = "2021"
|
||||
forced-target = "wasm32-unknown-unknown"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
webcomment-common = { path = "../common" }
|
||||
|
||||
getrandom = { version = "0.2.10", features = ["js"] }
|
||||
gloo = "0.8"
|
||||
js-sys = "0.3"
|
||||
lazy_static = "1.4.0"
|
||||
parking_lot = "0.12.1"
|
||||
serde = { version = "1.0.171", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.100"
|
||||
yew = { version = "0.20.0", features = ["csr"] }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4.37"
|
||||
web-sys = { version = "0.3.64", features = ["HtmlFormElement"] }
|
||||
19
webui/index.html
Normal file
19
webui/index.html
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Webcomment</title>
|
||||
<style type="text/css">
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html, input, textarea {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
a, a:visited {
|
||||
color: #f80;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
118
webui/src/api.rs
Normal file
118
webui/src/api.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
use crate::types::*;
|
||||
|
||||
use webcomment_common::{api::*, types::*};
|
||||
|
||||
use gloo::{console, net::http};
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ApiInner {
|
||||
pub admin_psw: Option<String>,
|
||||
pub comments: HashMap<CommentId, StoredComment>,
|
||||
/// Comments that are not yet attributed CommentId by the server
|
||||
pub local_comments: HashMap<CommentId, StoredComment>,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Api {
|
||||
pub inner: Arc<RwLock<ApiInner>>,
|
||||
}
|
||||
|
||||
impl Api {
|
||||
pub async fn get_comments_by_topic(&self, topic: String) {
|
||||
match http::Request::post(&format!("{}/api/comments_by_topic", &self.inner.read().url))
|
||||
.body(
|
||||
serde_json::to_string(&queries::CommentsByTopic {
|
||||
mutation_token: None,
|
||||
topic,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(resp) => {
|
||||
let Ok(Ok(resps::Response::CommentsByTopic(resp))) = resp.json::<resps::Result>().await else {
|
||||
// TODO error
|
||||
return;
|
||||
};
|
||||
let mut inner = self.inner.write();
|
||||
for comment in resp.approved_comments {
|
||||
let Ok(comment_id) = CommentId::from_base64(&comment.id) else {
|
||||
continue
|
||||
};
|
||||
inner.comments.insert(
|
||||
comment_id,
|
||||
StoredComment {
|
||||
author: comment.author,
|
||||
email: comment.email,
|
||||
last_edit_time: comment.last_edit_time,
|
||||
post_time: comment.post_time,
|
||||
text: comment.text,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => console::log!("get_comments_by_topic: {}", e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_comment(&self, new_comment: StoredComment, topic: String) {
|
||||
let local_id = CommentId::new();
|
||||
self.inner
|
||||
.write()
|
||||
.local_comments
|
||||
.insert(local_id.clone(), new_comment.clone());
|
||||
match http::Request::post(&format!("{}/api/new_comment", &self.inner.read().url))
|
||||
.body(
|
||||
serde_json::to_string(&queries::NewComment {
|
||||
author: new_comment.author,
|
||||
topic,
|
||||
email: new_comment.email.unwrap_or_default(),
|
||||
text: new_comment.text,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(resp) => {
|
||||
let Ok(Ok(resps::Response::NewComment(resp))) = resp.json::<resps::Result>().await else {
|
||||
// TODO error
|
||||
return;
|
||||
};
|
||||
let mut inner = self.inner.write();
|
||||
let Ok(comment_id) = CommentId::from_base64(&resp.id) else {
|
||||
// TODO error
|
||||
return;
|
||||
};
|
||||
let Some(comment) = inner.local_comments.remove(&local_id) else {
|
||||
// TODO error
|
||||
return;
|
||||
};
|
||||
inner.comments.insert(comment_id, comment);
|
||||
}
|
||||
Err(e) => console::log!("get_comments_by_topic: {}", e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Api> for Api {
|
||||
fn eq(&self, rhs: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/*pub enum Msg {
|
||||
NewComment {
|
||||
new_comment: StoredComment, topic: String
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn msg_handler(yew::platform::pinned::mpsc::) {
|
||||
|
||||
}*/
|
||||
9
webui/src/components.rs
Normal file
9
webui/src/components.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
pub mod admin_login_form;
|
||||
pub mod comment;
|
||||
pub mod comments;
|
||||
pub mod new_comment_form;
|
||||
|
||||
pub use admin_login_form::*;
|
||||
pub use comment::*;
|
||||
pub use comments::*;
|
||||
pub use new_comment_form::*;
|
||||
69
webui/src/components/admin_login_form.rs
Normal file
69
webui/src/components/admin_login_form.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use crate::api::Api;
|
||||
|
||||
use gloo::console;
|
||||
use web_sys::HtmlFormElement;
|
||||
use yew::{html, Component, Context, Html, Properties, SubmitEvent, html::TargetCast};
|
||||
|
||||
pub struct AdminLoginFormComponent {}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct AdminLoginFormProps {
|
||||
pub root_id: String, // TODO maybe opti
|
||||
pub api: Api,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
Login(HtmlFormElement)
|
||||
}
|
||||
|
||||
impl Component for AdminLoginFormComponent {
|
||||
type Message = Msg;
|
||||
type Properties = AdminLoginFormProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::Login(form) => {
|
||||
console::log!("{:?}", &form);
|
||||
let formdata = web_sys::FormData::new_with_form(&form).unwrap();
|
||||
console::log!("{:?}", &formdata);
|
||||
let password = formdata.get("password").as_string().unwrap();
|
||||
let mut api = ctx.props().api.inner.write();
|
||||
api.admin_psw = if password.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(password)
|
||||
};
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let props = ctx.props();
|
||||
let elem_id = format!("{}-admin_login_form", props.root_id);
|
||||
html! {
|
||||
<form
|
||||
id={ elem_id }
|
||||
method="post"
|
||||
action="#"
|
||||
onsubmit={ctx.link().callback(|event: SubmitEvent| {
|
||||
event.prevent_default();
|
||||
Msg::Login(event.target_unchecked_into())
|
||||
})}
|
||||
>
|
||||
<fieldset>
|
||||
<legend>{ "Admin Login" }</legend>
|
||||
<label>
|
||||
{ "Password:" }
|
||||
<input type="password" name="password"/>
|
||||
</label><br/>
|
||||
<input type="submit" value="Login"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
}
|
||||
50
webui/src/components/comment.rs
Normal file
50
webui/src/components/comment.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use crate::types::*;
|
||||
|
||||
use yew::{html, Component, Context, Html, Properties};
|
||||
|
||||
pub struct CommentComponent {}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct CommentProps {
|
||||
pub root_id: String, // TODO maybe opti
|
||||
pub comment: FullComment,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
}
|
||||
|
||||
impl Component for CommentComponent {
|
||||
type Message = Msg;
|
||||
type Properties = CommentProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let props = ctx.props();
|
||||
let comment_id = props.comment.id.to_base64();
|
||||
let elem_id = format!("{}-{}", props.root_id, comment_id);
|
||||
html! {
|
||||
<div class={ format!("comment comment-{}", comment_id) } id={ elem_id.clone() }>
|
||||
<p class="comment-meta">
|
||||
<a class="comment-post_time" aria-label="Permalink" title="Permalink" href={ format!("#{elem_id}") }>{ props.comment.post_time }</a>
|
||||
<span class="comment-author"></span>
|
||||
{
|
||||
if let Some(email) = &props.comment.email {
|
||||
html! { <span class="comment-email">{ email }</span> }
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
<a class="comment-edition comment-edition-remove" href="#">{ "Remove" }</a>
|
||||
</p>
|
||||
<p class="comment-text">{ &props.comment.text }</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
49
webui/src/components/comments.rs
Normal file
49
webui/src/components/comments.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use crate::{types::*, components::comment::*, api::*};
|
||||
|
||||
use webcomment_common::types::*;
|
||||
use yew::{html, Component, Context, Html, Properties};
|
||||
|
||||
pub struct CommentsComponent {}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct CommentsProps {
|
||||
pub root_id: String, // TODO maybe opti
|
||||
pub api: Api,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
}
|
||||
|
||||
impl Component for CommentsComponent {
|
||||
type Message = Msg;
|
||||
type Properties = CommentsProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let props = ctx.props();
|
||||
let comment_props = CommentProps {
|
||||
root_id: String::from("comments"),
|
||||
comment: FullComment {
|
||||
author: String::from("Toto"),
|
||||
email: Some(String::from("toto@fai.tld")),
|
||||
id: CommentId::new(),
|
||||
last_edit_time: Some(123),
|
||||
post_time: 42,
|
||||
text: String::from("Bonjour"),
|
||||
},
|
||||
};
|
||||
let element_id = format!("{}-comments", props.root_id);
|
||||
html! {
|
||||
<div id={ element_id }>
|
||||
<CommentComponent ..comment_props />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
93
webui/src/components/new_comment_form.rs
Normal file
93
webui/src/components/new_comment_form.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use crate::{types::*, api::Api};
|
||||
|
||||
use gloo::console;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlFormElement;
|
||||
use yew::{html, Callback, Component, Context, Html, Properties, SubmitEvent, html::TargetCast};
|
||||
|
||||
pub struct NewCommentFormComponent {}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct NewCommentFormProps {
|
||||
pub root_id: String, // TODO maybe opti
|
||||
pub api: Api,
|
||||
pub topic: String,
|
||||
pub comment: NotSentComment,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
Submit(HtmlFormElement)
|
||||
}
|
||||
|
||||
impl Component for NewCommentFormComponent {
|
||||
type Message = Msg;
|
||||
type Properties = NewCommentFormProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::Submit(form) => {
|
||||
console::log!("{:?}", &form);
|
||||
let formdata = web_sys::FormData::new_with_form(&form).unwrap();
|
||||
console::log!("{:?}", &formdata);
|
||||
let email = formdata.get("email").as_string().unwrap();
|
||||
let api = ctx.props().api.clone();
|
||||
let topic = ctx.props().topic.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
let email_trimmed = email.trim();
|
||||
api.new_comment(
|
||||
StoredComment {
|
||||
author: formdata.get("author").as_string().unwrap(),
|
||||
email: if email_trimmed.is_empty() {None} else {Some(email_trimmed.to_string())},
|
||||
last_edit_time: None,
|
||||
post_time: 0,// TODO
|
||||
text: formdata.get("text").as_string().unwrap()
|
||||
},
|
||||
topic,
|
||||
).await;
|
||||
});
|
||||
// TODO move req to dedicated async part
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let props = ctx.props();
|
||||
let elem_id = format!("{}-new_comment_form", props.root_id);
|
||||
html! {
|
||||
<form
|
||||
id={ elem_id }
|
||||
method="post"
|
||||
action="#"
|
||||
onsubmit={ctx.link().callback(|event: SubmitEvent| {
|
||||
event.prevent_default();
|
||||
Msg::Submit(event.target_unchecked_into())
|
||||
})}
|
||||
>
|
||||
<fieldset>
|
||||
<legend>{ "New comment" }</legend>
|
||||
<label>
|
||||
{ "Your name:" }
|
||||
<input type="text" name="author" class="comment-form-author"/>
|
||||
</label><br/>
|
||||
<label>
|
||||
{ "Your email:" }
|
||||
<input type="email" name="email" class="comment-form-email"/>
|
||||
</label><br/>
|
||||
<textarea class="comment-form-text" name="text"></textarea><br/>
|
||||
<input type="submit" value="Post"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*impl NewCommentFormComponent {
|
||||
fn submit(&mut self, element: HtmlElement) {
|
||||
|
||||
}
|
||||
}*/
|
||||
109
webui/src/lib.rs
Normal file
109
webui/src/lib.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
mod api;
|
||||
mod components;
|
||||
mod types;
|
||||
|
||||
use gloo::console;
|
||||
use js_sys::Date;
|
||||
use parking_lot::RwLock;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use webcomment_common::types::*;
|
||||
use yew::{html, Component, Context, Html, Properties};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{components::{*, admin_login_form::AdminLoginFormProps}, types::*};
|
||||
|
||||
pub enum Msg {
|
||||
Increment,
|
||||
Decrement,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct AppProps {
|
||||
root_id: String,
|
||||
api: api::Api,
|
||||
}
|
||||
|
||||
/*impl Default for AppProps {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
root_id: String::from("comments"),
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
pub struct App {
|
||||
value: i64,
|
||||
}
|
||||
|
||||
impl Component for App {
|
||||
type Message = Msg;
|
||||
type Properties = AppProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self { value: 0 }
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::Increment => {
|
||||
self.value += 1;
|
||||
console::log!("plus one"); // Will output a string to the browser console
|
||||
true // Return true to cause the displayed change to update
|
||||
}
|
||||
Msg::Decrement => {
|
||||
self.value -= 1;
|
||||
console::log!("minus one");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let props = ctx.props();
|
||||
let comments_props = CommentsProps {
|
||||
root_id: String::from("comments"),
|
||||
api: props.api.clone(),
|
||||
};
|
||||
let new_comment_form_props = NewCommentFormProps {
|
||||
root_id: String::from("comments"),
|
||||
api: props.api.clone(),
|
||||
topic: String::from("test"),
|
||||
comment: Default::default(),
|
||||
};
|
||||
let admin_login_form_props = AdminLoginFormProps {
|
||||
root_id: String::from("comments"),
|
||||
api: props.api.clone(),
|
||||
};
|
||||
html! {
|
||||
<div id={ props.root_id.clone() }>
|
||||
<CommentsComponent ..comments_props />
|
||||
<NewCommentFormComponent ..new_comment_form_props />
|
||||
<AdminLoginFormComponent ..admin_login_form_props />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
async fn main_js() {
|
||||
let api = api::Api {
|
||||
inner: Arc::new(RwLock::new(api::ApiInner { admin_psw: None, comments: Default::default(), url: "http://127.0.0.1:31720".into(), local_comments: Default::default() }))
|
||||
};
|
||||
|
||||
/*let (tx, mut rx) = yew::platform::pinned::mpsc::unbounded::<AttrValue>();
|
||||
|
||||
// A thread is needed because of async
|
||||
spawn_local(async move {
|
||||
while let Some(msg) = rx.next().await {
|
||||
sleep(ONE_SEC).await;
|
||||
let score = joke.len() as i16;
|
||||
fun_score_cb.emit(score);
|
||||
}
|
||||
});*/
|
||||
|
||||
/*api.get_comments_by_topic("test".into()).await;*/
|
||||
yew::Renderer::<App>::with_props(AppProps {
|
||||
root_id: String::from("comments"),
|
||||
api,
|
||||
}).render();
|
||||
}
|
||||
29
webui/src/types.rs
Normal file
29
webui/src/types.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use webcomment_common::types::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct NotSentComment {
|
||||
pub author: String,
|
||||
pub email: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct StoredComment {
|
||||
pub author: String,
|
||||
pub email: Option<String>,
|
||||
pub last_edit_time: Option<u64>,
|
||||
pub post_time: u64,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct FullComment {
|
||||
pub author: String,
|
||||
pub email: Option<String>,
|
||||
pub id: CommentId,
|
||||
pub last_edit_time: Option<u64>,
|
||||
pub post_time: u64,
|
||||
pub text: String,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue