mirror of
https://github.com/Nutomic/ibis.git
synced 2024-12-04 18:51:08 +00:00
Merge branch 'leptos-0.7'
This commit is contained in:
commit
34cbfe8cc9
33 changed files with 1223 additions and 812 deletions
1188
Cargo.lock
generated
1188
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
49
Cargo.toml
49
Cargo.toml
|
@ -29,14 +29,9 @@ ssr = [
|
|||
"config",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"reqwest","diesel-derive-newtype"
|
||||
]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"leptos_meta/hydrate",
|
||||
"leptos_router/hydrate",
|
||||
"katex/wasm-js",
|
||||
]
|
||||
diesel-derive-newtype = ["dep:diesel-derive-newtype"]
|
||||
hydrate = ["leptos/hydrate", "katex/wasm-js", "gloo-net"]
|
||||
|
||||
# This profile significantly speeds up build time. If debug info is needed you can comment the line
|
||||
# out temporarily, but make sure to leave this in the main branch.
|
||||
|
@ -60,24 +55,28 @@ dbg_macro = "deny"
|
|||
unwrap_used = "deny"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.89"
|
||||
leptos = "0.6.15"
|
||||
leptos_meta = "0.6.15"
|
||||
leptos_router = "0.6.15"
|
||||
anyhow = "1.0.93"
|
||||
leptos = "0.7.0-rc2"
|
||||
leptos_meta = "0.7.0-rc2"
|
||||
leptos_router = "0.7.0-rc2"
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
env_logger = { version = "0.11.5", default-features = false }
|
||||
futures = "0.3.30"
|
||||
futures = "0.3.31"
|
||||
hex = "0.4.3"
|
||||
rand = "0.8.5"
|
||||
serde_json = "1.0.128"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
serde_json = "1.0.132"
|
||||
sha2 = "0.10.8"
|
||||
uuid = { version = "1.10.0", features = ["serde"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
url = { version = "2.5.2", features = ["serde"] }
|
||||
reqwest = { version = "0.12.8", features = ["json", "cookies"] }
|
||||
uuid = { version = "1.11.0", features = ["serde"] }
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
url = { version = "2.5.3", features = ["serde"] }
|
||||
reqwest = { version = "0.12.9", features = [
|
||||
"json",
|
||||
"cookies",
|
||||
], optional = true }
|
||||
log = "0.4"
|
||||
tracing = "0.1.40"
|
||||
once_cell = "1.20.1"
|
||||
once_cell = "1.20.2"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
time = "0.3.36"
|
||||
markdown-it = "0.6.1"
|
||||
|
@ -88,14 +87,15 @@ markdown-it-heading-anchors = "0.3.0"
|
|||
markdown-it-footnote = "0.2.0"
|
||||
markdown-it-sub = "1.0.0"
|
||||
markdown-it-sup = "1.0.0"
|
||||
leptos-use = "0.13.6"
|
||||
leptos-use = "0.14.0-rc3"
|
||||
codee = "0.2.0"
|
||||
wasm-bindgen = "=0.2.95"
|
||||
|
||||
# backend-only features
|
||||
axum = { version = "0.7.7", optional = true }
|
||||
axum-macros = { version = "0.4.2", optional = true }
|
||||
axum-extra = { version = "0.9.4", features = ["cookie"], optional = true }
|
||||
tokio = { version = "1.40.0", features = ["full"], optional = true }
|
||||
tokio = { version = "1.41.1", features = ["full"], optional = true }
|
||||
tower-http = { version = "0.6.1", features = ["cors", "fs"], optional = true }
|
||||
activitypub_federation = { version = "0.6.0", features = [
|
||||
"axum",
|
||||
|
@ -111,15 +111,20 @@ diesel-derive-newtype = { version = "2.1.2", optional = true }
|
|||
diesel_migrations = { version = "2.2.0", optional = true }
|
||||
doku = { version = "0.21.1", optional = true }
|
||||
jsonwebtoken = { version = "9.3.0", optional = true }
|
||||
leptos_axum = { version = "0.6.15", optional = true }
|
||||
leptos_axum = { version = "0.7.0-rc2", optional = true }
|
||||
bcrypt = { version = "0.15.1", optional = true }
|
||||
diffy = { version = "0.4.0", optional = true }
|
||||
enum_delegate = { version = "0.2.0", optional = true }
|
||||
async-trait = { version = "0.1.83", optional = true }
|
||||
config = { version = "0.14.0", features = ["toml"], optional = true }
|
||||
config = { version = "0.14.1", features = ["toml"], optional = true }
|
||||
tower = { version = "0.5.1", optional = true }
|
||||
tower-layer = { version = "0.3.3", optional = true }
|
||||
console_log = "1.0.0"
|
||||
send_wrapper = "0.6.0"
|
||||
gloo-net = { version = "0.6.0", optional = true }
|
||||
web-sys = "0.3.72"
|
||||
http = "1.1.0"
|
||||
serde_urlencoded = "0.7.1"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
use crate::{
|
||||
backend::{database::IbisData, error::MyResult, federation::activities::follow::Follow},
|
||||
common::{DbInstance, FollowInstance, GetInstance, InstanceView, LocalUserView, ResolveObject},
|
||||
common::{
|
||||
DbInstance,
|
||||
FollowInstance,
|
||||
FollowInstanceResponse,
|
||||
GetInstance,
|
||||
InstanceView,
|
||||
LocalUserView,
|
||||
ResolveObject,
|
||||
},
|
||||
};
|
||||
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
|
||||
use axum::{extract::Query, Extension, Form, Json};
|
||||
|
@ -23,13 +31,13 @@ pub(in crate::backend::api) async fn follow_instance(
|
|||
Extension(user): Extension<LocalUserView>,
|
||||
data: Data<IbisData>,
|
||||
Form(query): Form<FollowInstance>,
|
||||
) -> MyResult<()> {
|
||||
) -> MyResult<Json<FollowInstanceResponse>> {
|
||||
let target = DbInstance::read(query.id, &data)?;
|
||||
let pending = !target.local;
|
||||
DbInstance::follow(&user.person, &target, pending, &data)?;
|
||||
let instance = DbInstance::read(query.id, &data)?;
|
||||
Follow::send(user.person, &instance, &data).await?;
|
||||
Ok(())
|
||||
Ok(Json(FollowInstanceResponse { success: true }))
|
||||
}
|
||||
|
||||
/// Fetch a remote instance actor. This automatically synchronizes the remote articles collection to
|
||||
|
|
|
@ -5,7 +5,7 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum_macros::debug_handler;
|
||||
use leptos::LeptosOptions;
|
||||
use leptos::prelude::*;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ use crate::{
|
|||
AUTH_COOKIE,
|
||||
MAIN_PAGE_NAME,
|
||||
},
|
||||
frontend::app::App,
|
||||
frontend::app::{shell, App},
|
||||
};
|
||||
use activitypub_federation::{
|
||||
config::{Data, FederationConfig, FederationMiddleware},
|
||||
|
@ -47,7 +47,7 @@ use federation::objects::{
|
|||
articles_collection::local_articles_url,
|
||||
instance_collection::linked_instances_url,
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use log::info;
|
||||
use std::net::SocketAddr;
|
||||
|
@ -93,7 +93,7 @@ pub async fn start(config: IbisConfig, override_hostname: Option<SocketAddr>) ->
|
|||
setup(&data.to_request_data()).await?;
|
||||
}
|
||||
|
||||
let leptos_options = get_configuration(Some("Cargo.toml")).await?.leptos_options;
|
||||
let leptos_options = get_configuration(Some("Cargo.toml"))?.leptos_options;
|
||||
let mut addr = leptos_options.site_addr;
|
||||
if let Some(override_hostname) = override_hostname {
|
||||
addr = override_hostname;
|
||||
|
@ -102,7 +102,6 @@ pub async fn start(config: IbisConfig, override_hostname: Option<SocketAddr>) ->
|
|||
|
||||
let config = data.clone();
|
||||
let app = Router::new()
|
||||
//.leptos_routes(&leptos_options, routes, App)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options)
|
||||
|
@ -127,16 +126,15 @@ pub async fn start(config: IbisConfig, override_hostname: Option<SocketAddr>) ->
|
|||
/// Make auth token available in hydrate mode
|
||||
async fn leptos_routes_handler(
|
||||
jar: CookieJar,
|
||||
State(option): State<LeptosOptions>,
|
||||
State(leptos_options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_async_with_context(
|
||||
option.clone(),
|
||||
move || {
|
||||
let cookie = jar.get(AUTH_COOKIE).map(|c| c.value().to_string());
|
||||
provide_context(Auth(cookie));
|
||||
},
|
||||
move || view! { <App /> },
|
||||
move || shell(leptos_options.clone()),
|
||||
);
|
||||
|
||||
handler(req).await.into_response()
|
||||
|
|
|
@ -36,7 +36,7 @@ pub struct GetArticleForm {
|
|||
pub id: Option<ArticleId>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default)]
|
||||
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
|
||||
pub struct ListArticlesForm {
|
||||
pub only_local: Option<bool>,
|
||||
pub instance_id: Option<InstanceId>,
|
||||
|
@ -128,13 +128,13 @@ impl Default for EditVersion {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct RegisterUserForm {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct LoginUserForm {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
|
@ -188,7 +188,7 @@ impl DbPerson {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct CreateArticleForm {
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
|
@ -217,19 +217,19 @@ pub struct ProtectArticleForm {
|
|||
pub protected: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct ForkArticleForm {
|
||||
pub article_id: ArticleId,
|
||||
pub new_title: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct ApproveArticleForm {
|
||||
pub article_id: ArticleId,
|
||||
pub approve: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct DeleteConflictForm {
|
||||
pub conflict_id: ConflictId,
|
||||
}
|
||||
|
@ -244,12 +244,17 @@ pub struct FollowInstance {
|
|||
pub id: InstanceId,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct FollowInstanceResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct SearchArticleForm {
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct ResolveObject {
|
||||
pub id: Url,
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
|||
DeleteConflictForm,
|
||||
EditArticleForm,
|
||||
FollowInstance,
|
||||
FollowInstanceResponse,
|
||||
ForkArticleForm,
|
||||
GetArticleForm,
|
||||
GetInstance,
|
||||
|
@ -30,37 +31,36 @@ use crate::{
|
|||
frontend::error::MyResult,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use reqwest::{Client, RequestBuilder, StatusCode};
|
||||
use http::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::LazyLock;
|
||||
use std::{fmt::Debug, sync::LazyLock};
|
||||
use url::Url;
|
||||
|
||||
pub static CLIENT: LazyLock<ApiClient> = LazyLock::new(|| ApiClient::new(Client::new(), None));
|
||||
pub static CLIENT: LazyLock<ApiClient> = LazyLock::new(|| {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
ApiClient::new(reqwest::Client::new(), None)
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
ApiClient::new()
|
||||
}
|
||||
});
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApiClient {
|
||||
client: Client,
|
||||
#[cfg(feature = "ssr")]
|
||||
client: reqwest::Client,
|
||||
pub hostname: String,
|
||||
ssl: bool,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
pub fn new(client: Client, hostname_: Option<String>) -> Self {
|
||||
let mut hostname;
|
||||
let ssl;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
use leptos_use::use_document;
|
||||
hostname = use_document().location().unwrap().host().unwrap();
|
||||
ssl = !cfg!(debug_assertions);
|
||||
}
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use leptos::leptos_config::get_config_from_str;
|
||||
pub fn new(client: reqwest::Client, hostname_: Option<String>) -> Self {
|
||||
use leptos::config::get_config_from_str;
|
||||
let leptos_options = get_config_from_str(include_str!("../../Cargo.toml")).unwrap();
|
||||
hostname = leptos_options.site_addr.to_string();
|
||||
ssl = false;
|
||||
}
|
||||
let mut hostname = leptos_options.site_addr.to_string();
|
||||
// required for tests
|
||||
if let Some(hostname_) = hostname_ {
|
||||
hostname = hostname_;
|
||||
|
@ -68,63 +68,44 @@ impl ApiClient {
|
|||
Self {
|
||||
client,
|
||||
hostname,
|
||||
ssl,
|
||||
ssl: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_query<T, R>(&self, endpoint: &str, query: Option<R>) -> MyResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize,
|
||||
{
|
||||
let mut req = self.client.get(self.request_endpoint(endpoint));
|
||||
if let Some(query) = query {
|
||||
req = req.query(&query);
|
||||
}
|
||||
handle_json_res::<T>(req).await
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn new() -> Self {
|
||||
use leptos_use::use_document;
|
||||
let hostname = use_document().location().unwrap().host().unwrap();
|
||||
let ssl = !cfg!(debug_assertions);
|
||||
Self { hostname, ssl }
|
||||
}
|
||||
|
||||
pub async fn get_article(&self, data: GetArticleForm) -> MyResult<ArticleView> {
|
||||
self.get_query("/api/v1/article", Some(data)).await
|
||||
self.get("/api/v1/article", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn list_articles(&self, data: ListArticlesForm) -> MyResult<Vec<DbArticle>> {
|
||||
self.get_query("/api/v1/article/list", Some(data)).await
|
||||
self.get("/api/v1/article/list", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn register(&self, register_form: RegisterUserForm) -> MyResult<LocalUserView> {
|
||||
let req = self
|
||||
.client
|
||||
.post(self.request_endpoint("/api/v1/account/register"))
|
||||
.form(®ister_form);
|
||||
handle_json_res::<LocalUserView>(req).await
|
||||
self.post("/api/v1/account/register", Some(®ister_form))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn login(&self, login_form: LoginUserForm) -> MyResult<LocalUserView> {
|
||||
let req = self
|
||||
.client
|
||||
.post(self.request_endpoint("/api/v1/account/login"))
|
||||
.form(&login_form);
|
||||
handle_json_res::<LocalUserView>(req).await
|
||||
self.post("/api/v1/account/login", Some(&login_form)).await
|
||||
}
|
||||
|
||||
pub async fn create_article(&self, data: &CreateArticleForm) -> MyResult<ArticleView> {
|
||||
let req = self
|
||||
.client
|
||||
.post(self.request_endpoint("/api/v1/article"))
|
||||
.form(data);
|
||||
handle_json_res(req).await
|
||||
self.send(Method::POST, "/api/v1/article", Some(&data))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn edit_article_with_conflict(
|
||||
&self,
|
||||
edit_form: &EditArticleForm,
|
||||
) -> MyResult<Option<ApiConflict>> {
|
||||
let req = self
|
||||
.client
|
||||
.patch(self.request_endpoint("/api/v1/article"))
|
||||
.form(edit_form);
|
||||
handle_json_res(req).await
|
||||
self.get("/api/v1/article", Some(&edit_form)).await
|
||||
}
|
||||
|
||||
pub async fn edit_article(&self, edit_form: &EditArticleForm) -> MyResult<ArticleView> {
|
||||
|
@ -140,17 +121,13 @@ impl ApiClient {
|
|||
}
|
||||
|
||||
pub async fn notifications_list(&self) -> MyResult<Vec<Notification>> {
|
||||
let req = self
|
||||
.client
|
||||
.get(self.request_endpoint("/api/v1/user/notifications/list"));
|
||||
handle_json_res(req).await
|
||||
self.get("/api/v1/user/notifications/list", None::<()>)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn notifications_count(&self) -> MyResult<usize> {
|
||||
let req = self
|
||||
.client
|
||||
.get(self.request_endpoint("/api/v1/user/notifications/count"));
|
||||
handle_json_res(req).await
|
||||
self.get("/api/v1/user/notifications/count", None::<()>)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn approve_article(&self, article_id: ArticleId, approve: bool) -> MyResult<()> {
|
||||
|
@ -158,36 +135,29 @@ impl ApiClient {
|
|||
article_id,
|
||||
approve,
|
||||
};
|
||||
let req = self
|
||||
.client
|
||||
.post(self.request_endpoint("/api/v1/article/approve"))
|
||||
.form(&form);
|
||||
handle_json_res(req).await
|
||||
self.post("/api/v1/article/approve", Some(&form)).await
|
||||
}
|
||||
|
||||
pub async fn delete_conflict(&self, conflict_id: ConflictId) -> MyResult<()> {
|
||||
let form = DeleteConflictForm { conflict_id };
|
||||
let req = self
|
||||
.client
|
||||
.delete(self.request_endpoint("/api/v1/conflict"))
|
||||
.form(&form);
|
||||
handle_json_res(req).await
|
||||
self.send(Method::DELETE, "/api/v1/conflict", Some(form))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn search(&self, search_form: &SearchArticleForm) -> MyResult<Vec<DbArticle>> {
|
||||
self.get_query("/api/v1/search", Some(search_form)).await
|
||||
self.get("/api/v1/search", Some(search_form)).await
|
||||
}
|
||||
|
||||
pub async fn get_local_instance(&self) -> MyResult<InstanceView> {
|
||||
self.get_query("/api/v1/instance", None::<i32>).await
|
||||
self.get("/api/v1/instance", None::<i32>).await
|
||||
}
|
||||
|
||||
pub async fn get_instance(&self, get_form: &GetInstance) -> MyResult<InstanceView> {
|
||||
self.get_query("/api/v1/instance", Some(get_form)).await
|
||||
self.get("/api/v1/instance", Some(&get_form)).await
|
||||
}
|
||||
|
||||
pub async fn list_instances(&self) -> MyResult<Vec<DbInstance>> {
|
||||
self.get_query("/api/v1/instance/list", None::<i32>).await
|
||||
self.get("/api/v1/instance/list", None::<i32>).await
|
||||
}
|
||||
|
||||
pub async fn follow_instance_with_resolve(
|
||||
|
@ -199,7 +169,7 @@ impl ApiClient {
|
|||
id: Url::parse(&format!("{}://{}", http_protocol_str(), follow_instance))?,
|
||||
};
|
||||
let instance_resolved: DbInstance = self
|
||||
.get_query("/api/v1/instance/resolve", Some(resolve_form))
|
||||
.get("/api/v1/instance/resolve", Some(resolve_form))
|
||||
.await?;
|
||||
|
||||
// send follow
|
||||
|
@ -210,63 +180,148 @@ impl ApiClient {
|
|||
Ok(instance_resolved)
|
||||
}
|
||||
|
||||
pub async fn follow_instance(&self, follow_form: FollowInstance) -> MyResult<()> {
|
||||
// cant use post helper because follow doesnt return json
|
||||
let res = self
|
||||
.client
|
||||
.post(self.request_endpoint("/api/v1/instance/follow"))
|
||||
.form(&follow_form)
|
||||
.send()
|
||||
.await?;
|
||||
if res.status() == StatusCode::OK {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("API error: {}", res.text().await?).into())
|
||||
}
|
||||
pub async fn follow_instance(
|
||||
&self,
|
||||
follow_form: FollowInstance,
|
||||
) -> MyResult<FollowInstanceResponse> {
|
||||
self.post("/api/v1/instance/follow", Some(follow_form))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn site(&self) -> MyResult<SiteView> {
|
||||
let req = self.client.get(self.request_endpoint("/api/v1/site"));
|
||||
handle_json_res(req).await
|
||||
self.get("/api/v1/site", None::<()>).await
|
||||
}
|
||||
|
||||
pub async fn logout(&self) -> MyResult<()> {
|
||||
self.client
|
||||
.get(self.request_endpoint("/api/v1/account/logout"))
|
||||
.send()
|
||||
.await?;
|
||||
Ok(())
|
||||
self.get("/api/v1/account/logout", None::<()>).await
|
||||
}
|
||||
|
||||
pub async fn fork_article(&self, form: &ForkArticleForm) -> MyResult<ArticleView> {
|
||||
let req = self
|
||||
.client
|
||||
.post(self.request_endpoint("/api/v1/article/fork"))
|
||||
.form(form);
|
||||
Ok(handle_json_res(req).await.unwrap())
|
||||
Ok(self.post("/api/v1/article/fork", Some(form)).await.unwrap())
|
||||
}
|
||||
|
||||
pub async fn protect_article(&self, params: &ProtectArticleForm) -> MyResult<DbArticle> {
|
||||
let req = self
|
||||
.client
|
||||
.post(self.request_endpoint("/api/v1/article/protect"))
|
||||
.form(params);
|
||||
handle_json_res(req).await
|
||||
self.post("/api/v1/article/protect", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn resolve_article(&self, id: Url) -> MyResult<ArticleView> {
|
||||
let resolve_object = ResolveObject { id };
|
||||
self.get_query("/api/v1/article/resolve", Some(resolve_object))
|
||||
self.get("/api/v1/article/resolve", Some(resolve_object))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn resolve_instance(&self, id: Url) -> MyResult<DbInstance> {
|
||||
let resolve_object = ResolveObject { id };
|
||||
self.get_query("/api/v1/instance/resolve", Some(resolve_object))
|
||||
self.get("/api/v1/instance/resolve", Some(resolve_object))
|
||||
.await
|
||||
}
|
||||
pub async fn get_user(&self, data: GetUserForm) -> MyResult<DbPerson> {
|
||||
self.get_query("/api/v1/user", Some(data)).await
|
||||
self.get("/api/v1/user", Some(data)).await
|
||||
}
|
||||
|
||||
async fn get<T, R>(&self, endpoint: &str, query: Option<R>) -> MyResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
{
|
||||
self.send(Method::GET, endpoint, query).await
|
||||
}
|
||||
|
||||
async fn post<T, R>(&self, endpoint: &str, query: Option<R>) -> MyResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
{
|
||||
self.send(Method::POST, endpoint, query).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn send<P, T>(&self, method: Method, path: &str, params: Option<P>) -> MyResult<T>
|
||||
where
|
||||
P: Serialize + Debug,
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
use crate::common::{Auth, AUTH_COOKIE};
|
||||
use leptos::prelude::use_context;
|
||||
use reqwest::header::HeaderName;
|
||||
let mut req = self
|
||||
.client
|
||||
.request(method, self.request_endpoint(path))
|
||||
.query(¶ms);
|
||||
let auth = use_context::<Auth>();
|
||||
if let Some(Auth(Some(auth))) = auth {
|
||||
req = req.header(HeaderName::from_static(AUTH_COOKIE), auth);
|
||||
}
|
||||
let res = req.send().await?;
|
||||
let status = res.status();
|
||||
let text = res.text().await?.to_string();
|
||||
Self::response(status.into(), text)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn send<'a, P, T>(
|
||||
&'a self,
|
||||
method: Method,
|
||||
path: &'a str,
|
||||
params: Option<P>,
|
||||
) -> impl std::future::Future<Output = MyResult<T>> + Send + 'a
|
||||
where
|
||||
P: Serialize + Debug + 'a,
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
use gloo_net::http::*;
|
||||
use leptos::prelude::on_cleanup;
|
||||
use send_wrapper::SendWrapper;
|
||||
use web_sys::RequestCredentials;
|
||||
|
||||
SendWrapper::new(async move {
|
||||
let abort_controller = SendWrapper::new(web_sys::AbortController::new().ok());
|
||||
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
|
||||
|
||||
// abort in-flight requests if, e.g., we've navigated away from this page
|
||||
on_cleanup(move || {
|
||||
if let Some(abort_controller) = abort_controller.take() {
|
||||
abort_controller.abort()
|
||||
}
|
||||
});
|
||||
|
||||
let path_with_endpoint = self.request_endpoint(path);
|
||||
let params_encoded = serde_urlencoded::to_string(¶ms).unwrap();
|
||||
let path = if method == Method::GET {
|
||||
// Cannot pass the struct directly but need to convert it manually
|
||||
// https://github.com/rustwasm/gloo/issues/378
|
||||
format!("{path_with_endpoint}?{params_encoded}")
|
||||
} else {
|
||||
path_with_endpoint
|
||||
};
|
||||
let builder = RequestBuilder::new(&path)
|
||||
.method(method.clone())
|
||||
.abort_signal(abort_signal.as_ref())
|
||||
.credentials(RequestCredentials::Include);
|
||||
let req = if method == Method::POST {
|
||||
builder
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(params_encoded)
|
||||
} else {
|
||||
builder.build()
|
||||
}
|
||||
.unwrap();
|
||||
let res = req.send().await.unwrap();
|
||||
let status = res.status();
|
||||
let text = res.text().await.unwrap();
|
||||
Self::response(status, text)
|
||||
})
|
||||
}
|
||||
|
||||
fn response<T>(status: u16, text: String) -> MyResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
if status == StatusCode::OK {
|
||||
Ok(serde_json::from_str(&text).map_err(|e| anyhow!("Json error on {text}: {e}"))?)
|
||||
} else {
|
||||
Err(anyhow!("API error: {text}").into())
|
||||
}
|
||||
}
|
||||
|
||||
fn request_endpoint(&self, path: &str) -> String {
|
||||
|
@ -274,33 +329,3 @@ impl ApiClient {
|
|||
format!("{protocol}://{}{path}", &self.hostname)
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_json_res<T>(#[allow(unused_mut)] mut req: RequestBuilder) -> MyResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
req = req.fetch_credentials_include();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use crate::common::{Auth, AUTH_COOKIE};
|
||||
use leptos::use_context;
|
||||
use reqwest::header::HeaderName;
|
||||
|
||||
let auth = use_context::<Auth>();
|
||||
if let Some(Auth(Some(auth))) = auth {
|
||||
req = req.header(HeaderName::from_static(AUTH_COOKIE), auth);
|
||||
}
|
||||
}
|
||||
let res = req.send().await?;
|
||||
let status = res.status();
|
||||
let text = res.text().await?;
|
||||
if status == StatusCode::OK {
|
||||
Ok(serde_json::from_str(&text).map_err(|e| anyhow!("Json error on {text}: {e}"))?)
|
||||
} else {
|
||||
Err(anyhow!("API error: {text}").into())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
|||
common::SiteView,
|
||||
frontend::{
|
||||
api::CLIENT,
|
||||
components::nav::Nav,
|
||||
components::{nav::Nav, protected_route::IbisProtectedRoute},
|
||||
dark_mode::DarkMode,
|
||||
pages::{
|
||||
article::{
|
||||
|
@ -23,12 +23,15 @@ use crate::{
|
|||
},
|
||||
},
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{provide_meta_context, *};
|
||||
use leptos_router::{Route, Router, Routes};
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
path,
|
||||
};
|
||||
|
||||
pub fn site() -> Resource<(), SiteView> {
|
||||
use_context::<Resource<(), SiteView>>().unwrap()
|
||||
pub fn site() -> Resource<SiteView> {
|
||||
use_context::<Resource<SiteView>>().unwrap()
|
||||
}
|
||||
|
||||
pub fn is_logged_in() -> bool {
|
||||
|
@ -43,11 +46,12 @@ pub fn is_admin() -> bool {
|
|||
})
|
||||
}
|
||||
|
||||
// TODO: can probably get rid of this
|
||||
pub trait DefaultResource<T> {
|
||||
fn with_default<O>(&self, f: impl FnOnce(&T) -> O) -> O;
|
||||
}
|
||||
|
||||
impl<T: Default> DefaultResource<T> for Resource<(), T> {
|
||||
impl<T: Default + Send + Sync> DefaultResource<T> for Resource<T> {
|
||||
fn with_default<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
self.with(|x| match x {
|
||||
Some(x) => f(x),
|
||||
|
@ -55,46 +59,66 @@ impl<T: Default> DefaultResource<T> for Resource<(), T> {
|
|||
})
|
||||
}
|
||||
}
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<AutoReload options=options.clone() />
|
||||
<HydrationScripts options />
|
||||
<MetaTags />
|
||||
</head>
|
||||
<body>
|
||||
<App />
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// TODO: should create_resource() but then things break
|
||||
let site_resource =
|
||||
create_local_resource(move || (), |_| async move { CLIENT.site().await.unwrap() });
|
||||
provide_context(site_resource);
|
||||
provide_meta_context();
|
||||
|
||||
// TODO: should Resource::new() but then things break
|
||||
let site_resource = Resource::new(|| (), |_| async move { CLIENT.site().await.unwrap() });
|
||||
provide_context(site_resource);
|
||||
|
||||
let darkmode = DarkMode::init();
|
||||
provide_context(darkmode.clone());
|
||||
|
||||
view! {
|
||||
<Html attr:data-theme=darkmode.theme class="h-full" />
|
||||
<Body class="h-full max-sm:flex max-sm:flex-col" />
|
||||
<Html attr:data-theme=darkmode.theme {..} class="h-full" />
|
||||
<Body {..} class="h-full max-sm:flex max-sm:flex-col" />
|
||||
<>
|
||||
<Stylesheet id="ibis" href="/pkg/ibis.css" />
|
||||
<Stylesheet id="katex" href="/katex.min.css" />
|
||||
<Router>
|
||||
<Nav />
|
||||
<main class="p-4 md:ml-64">
|
||||
<Routes>
|
||||
<Route path="/" view=ReadArticle />
|
||||
<Route path="/article/:title" view=ReadArticle />
|
||||
<Route path="/article/:title/history" view=ArticleHistory />
|
||||
<Route path="/article/:title/edit/:conflict_id?" view=EditArticle />
|
||||
<Route path="/article/:title/actions" view=ArticleActions />
|
||||
<Route path="/article/:title/diff/:hash" view=EditDiff />
|
||||
// TODO: use protected route, otherwise user can view
|
||||
// /article/create without login
|
||||
// https://github.com/leptos-rs/leptos/blob/leptos_0.7/examples/router/src/lib.rs#L51
|
||||
<Route path="/article/create" view=CreateArticle />
|
||||
<Route path="/article/list" view=ListArticles />
|
||||
<Route path="/instance/:hostname" view=InstanceDetails />
|
||||
<Route path="/instance/list" view=ListInstances />
|
||||
<Route path="/user/:name" view=UserProfile />
|
||||
<Route path="/login" view=Login />
|
||||
<Route path="/register" view=Register />
|
||||
<Route path="/search" view=Search />
|
||||
<Route path="/notifications" view=Notifications />
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
<Route path=path!("/") view=ReadArticle />
|
||||
<Route path=path!("/article/:title") view=ReadArticle />
|
||||
<Route path=path!("/article/:title/history") view=ArticleHistory />
|
||||
<IbisProtectedRoute
|
||||
path=path!("/article/:title/edit/:conflict_id?")
|
||||
view=EditArticle
|
||||
/>
|
||||
<IbisProtectedRoute
|
||||
path=path!("/article/:title/actions")
|
||||
view=ArticleActions
|
||||
/>
|
||||
<Route path=path!("/article/:title/diff/:hash") view=EditDiff />
|
||||
<IbisProtectedRoute path=path!("/create-article") view=CreateArticle />
|
||||
<Route path=path!("/articles") view=ListArticles />
|
||||
<Route path=path!("/instances") view=ListInstances />
|
||||
<Route path=path!("/instance/:hostname") view=InstanceDetails />
|
||||
<Route path=path!("/user/:name") view=UserProfile />
|
||||
<Route path=path!("/login") view=Login />
|
||||
<Route path=path!("/register") view=Register />
|
||||
<Route path=path!("/search") view=Search />
|
||||
<IbisProtectedRoute path=path!("/notifications") view=Notifications />
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
|
|
@ -8,8 +8,8 @@ use crate::{
|
|||
components::instance_follow_button::InstanceFollowButton,
|
||||
},
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::A;
|
||||
|
||||
pub enum ActiveTab {
|
||||
Read,
|
||||
|
@ -19,10 +19,7 @@ pub enum ActiveTab {
|
|||
}
|
||||
|
||||
#[component]
|
||||
pub fn ArticleNav(
|
||||
article: Resource<Option<String>, ArticleView>,
|
||||
active_tab: ActiveTab,
|
||||
) -> impl IntoView {
|
||||
pub fn ArticleNav(article: Resource<ArticleView>, active_tab: ActiveTab) -> impl IntoView {
|
||||
let tab_classes = tab_classes(&active_tab);
|
||||
|
||||
view! {
|
||||
|
@ -32,7 +29,7 @@ pub fn ArticleNav(
|
|||
.get()
|
||||
.map(|article_| {
|
||||
let title = article_title(&article_.article);
|
||||
let instance = create_resource(
|
||||
let instance = Resource::new(
|
||||
move || article_.article.instance_id,
|
||||
move |instance_id| async move {
|
||||
let form = GetInstance {
|
||||
|
@ -46,24 +43,33 @@ pub fn ArticleNav(
|
|||
let protected = article_.article.protected;
|
||||
view! {
|
||||
<div role="tablist" class="tabs tabs-lifted">
|
||||
<A class=tab_classes.read href=article_link.clone()>
|
||||
<A href=article_link.clone() {..} class=tab_classes.read>
|
||||
"Read"
|
||||
</A>
|
||||
<A class=tab_classes.history href=format!("{article_link}/history")>
|
||||
<A
|
||||
href=format!("{article_link}/history")
|
||||
{..}
|
||||
class=tab_classes.history
|
||||
>
|
||||
"History"
|
||||
</A>
|
||||
<Show when=move || {
|
||||
is_logged_in()
|
||||
&& can_edit_article(&article_.article, is_admin()).is_ok()
|
||||
}>
|
||||
<A class=tab_classes.edit href=format!("{article_link}/edit")>
|
||||
<A
|
||||
href=format!("{article_link}/edit")
|
||||
{..}
|
||||
class=tab_classes.edit
|
||||
>
|
||||
"Edit"
|
||||
</A>
|
||||
</Show>
|
||||
<Show when=is_logged_in>
|
||||
<A
|
||||
class=tab_classes.actions
|
||||
href=format!("{article_link_}/actions")
|
||||
{..}
|
||||
class=tab_classes.actions
|
||||
>
|
||||
"Actions"
|
||||
</A>
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
use crate::frontend::api::CLIENT;
|
||||
use leptos::{component, *};
|
||||
use codee::{Decoder, Encoder};
|
||||
use leptos::prelude::*;
|
||||
use std::fmt::Debug;
|
||||
use url::Url;
|
||||
|
||||
#[component]
|
||||
pub fn ConnectView<T: Clone + 'static, R: 'static>(res: Resource<T, R>) -> impl IntoView {
|
||||
let connect_ibis_wiki = create_action(move |_: &()| async move {
|
||||
pub fn ConnectView<T, R>(res: Resource<T, R>) -> impl IntoView
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
R: Encoder<T> + Decoder<T> + Send + Sync + 'static,
|
||||
<R as Encoder<T>>::Error: Debug,
|
||||
<R as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<R as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
<R as Decoder<T>>::Error: Debug,
|
||||
<<R as Decoder<T>>::Encoded as leptos::prelude::FromEncodedStr>::DecodingError: Debug,
|
||||
{
|
||||
let connect_ibis_wiki = Action::new(move |_: &()| async move {
|
||||
CLIENT
|
||||
.resolve_instance(Url::parse("https://ibis.wiki").unwrap())
|
||||
.await
|
||||
|
@ -16,7 +27,9 @@ pub fn ConnectView<T: Clone + 'static, R: 'static>(res: Resource<T, R>) -> impl
|
|||
<div class="flex justify-center h-screen">
|
||||
<button
|
||||
class="btn btn-primary place-self-center"
|
||||
on:click=move |_| connect_ibis_wiki.dispatch(())
|
||||
on:click=move |_| {
|
||||
connect_ibis_wiki.dispatch(());
|
||||
}
|
||||
>
|
||||
Connect with ibis.wiki
|
||||
</button>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use leptos::{ev, *};
|
||||
use leptos::{ev::KeyboardEvent, prelude::*};
|
||||
|
||||
#[component]
|
||||
pub fn CredentialsForm(
|
||||
|
@ -8,8 +8,8 @@ pub fn CredentialsForm(
|
|||
error: Signal<Option<String>>,
|
||||
disabled: Signal<bool>,
|
||||
) -> impl IntoView {
|
||||
let (password, set_password) = create_signal(String::new());
|
||||
let (username, set_username) = create_signal(String::new());
|
||||
let (password, set_password) = signal(String::new());
|
||||
let (username, set_username) = signal(String::new());
|
||||
|
||||
let dispatch_action = move || action.dispatch((username.get(), password.get()));
|
||||
|
||||
|
@ -34,7 +34,7 @@ pub fn CredentialsForm(
|
|||
required
|
||||
placeholder="Username"
|
||||
prop:disabled=move || disabled.get()
|
||||
on:keyup=move |ev: ev::KeyboardEvent| {
|
||||
on:keyup=move |ev: KeyboardEvent| {
|
||||
let val = event_target_value(&ev);
|
||||
set_username.update(|v| *v = val);
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ pub fn CredentialsForm(
|
|||
required
|
||||
placeholder="Password"
|
||||
prop:disabled=move || disabled.get()
|
||||
on:keyup=move |ev: ev::KeyboardEvent| {
|
||||
on:keyup=move |ev: KeyboardEvent| {
|
||||
match &*ev.key() {
|
||||
"Enter" => {
|
||||
dispatch_action();
|
||||
|
@ -73,7 +73,9 @@ pub fn CredentialsForm(
|
|||
<button
|
||||
class="btn btn-primary my-2"
|
||||
prop:disabled=move || button_is_disabled.get()
|
||||
on:click=move |_| dispatch_action()
|
||||
on:click=move |_| {
|
||||
dispatch_action();
|
||||
}
|
||||
>
|
||||
{action_label}
|
||||
</button>
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
use crate::frontend::markdown::render_markdown;
|
||||
use html::Textarea;
|
||||
use leptos::*;
|
||||
use leptos::{html::Textarea, prelude::*};
|
||||
|
||||
#[component]
|
||||
pub fn EditorView(
|
||||
// this param gives a false warning about being unused, ignore that
|
||||
#[allow(unused)] textarea_ref: NodeRef<Textarea>,
|
||||
textarea_ref: NodeRef<Textarea>,
|
||||
content: Signal<String>,
|
||||
set_content: WriteSignal<String>,
|
||||
) -> impl IntoView {
|
||||
let (preview, set_preview) = create_signal(render_markdown(&content.get_untracked()));
|
||||
let (show_preview, set_show_preview) = create_signal(false);
|
||||
let (preview, set_preview) = signal(render_markdown(&content.get_untracked()));
|
||||
let (show_preview, set_show_preview) = signal(false);
|
||||
|
||||
view! {
|
||||
<div>
|
||||
|
|
|
@ -5,11 +5,11 @@ use crate::{
|
|||
app::{site, DefaultResource},
|
||||
},
|
||||
};
|
||||
use leptos::{component, *};
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn InstanceFollowButton(instance: DbInstance) -> impl IntoView {
|
||||
let follow_action = create_action(move |instance_id: &InstanceId| {
|
||||
let follow_action = Action::new(move |instance_id: &InstanceId| {
|
||||
let instance_id = *instance_id;
|
||||
async move {
|
||||
let form = FollowInstance { id: instance_id };
|
||||
|
@ -38,7 +38,9 @@ pub fn InstanceFollowButton(instance: DbInstance) -> impl IntoView {
|
|||
view! {
|
||||
<button
|
||||
class=class_
|
||||
on:click=move |_| follow_action.dispatch(instance.id)
|
||||
on:click=move |_| {
|
||||
follow_action.dispatch(instance.id);
|
||||
}
|
||||
prop:disabled=move || is_following
|
||||
title="Follow the instance so that new edits are synchronized to your instance."
|
||||
>
|
||||
|
|
|
@ -4,3 +4,4 @@ pub mod credentials;
|
|||
pub mod editor;
|
||||
pub mod instance_follow_button;
|
||||
pub mod nav;
|
||||
pub mod protected_route;
|
||||
|
|
|
@ -3,21 +3,21 @@ use crate::frontend::{
|
|||
app::{is_logged_in, site, DefaultResource},
|
||||
dark_mode::DarkMode,
|
||||
};
|
||||
use leptos::{component, view, IntoView, *};
|
||||
use leptos_router::*;
|
||||
use leptos::{component, prelude::*, view, IntoView, *};
|
||||
use leptos_router::{components::A, hooks::use_navigate};
|
||||
|
||||
#[component]
|
||||
pub fn Nav() -> impl IntoView {
|
||||
let logout_action = create_action(move |_| async move {
|
||||
let logout_action = Action::new(move |_| async move {
|
||||
CLIENT.logout().await.unwrap();
|
||||
site().refetch();
|
||||
});
|
||||
let notification_count = create_resource(
|
||||
let notification_count = Resource::new(
|
||||
|| (),
|
||||
move |_| async move { CLIENT.notifications_count().await.unwrap_or_default() },
|
||||
);
|
||||
|
||||
let (search_query, set_search_query) = create_signal(String::new());
|
||||
let (search_query, set_search_query) = signal(String::new());
|
||||
let mut dark_mode = expect_context::<DarkMode>();
|
||||
view! {
|
||||
<nav class="max-sm:navbar p-2.5 h-full md:fixed md:w-64 max-sm: border-b md:border-e border-slate-400 border-solid">
|
||||
|
@ -45,32 +45,32 @@ pub fn Nav() -> impl IntoView {
|
|||
<A href="/">"Main Page"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="/instance/list">"Instances"</A>
|
||||
<A href="/instances">"Instances"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="/article/list">"Articles"</A>
|
||||
<A href="/articles">"Articles"</A>
|
||||
</li>
|
||||
<Transition>
|
||||
<Suspense>
|
||||
<Show when=is_logged_in>
|
||||
<li>
|
||||
<A href="/article/create">"Create Article"</A>
|
||||
<A href="/create-article">"Create Article"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="/notifications">
|
||||
"Notifications "
|
||||
<span class="indicator-item indicator-end badge badge-neutral">
|
||||
{move || notification_count.get()}
|
||||
<Suspense>{move || notification_count.get()}</Suspense>
|
||||
</span>
|
||||
</A>
|
||||
</li>
|
||||
</Show>
|
||||
</Transition>
|
||||
</Suspense>
|
||||
<li>
|
||||
<form
|
||||
class="form-control m-0 p-1"
|
||||
on:submit=move |ev| {
|
||||
ev.prevent_default();
|
||||
let navigate = leptos_router::use_navigate();
|
||||
let navigate = use_navigate();
|
||||
let query = search_query.get();
|
||||
if !query.is_empty() {
|
||||
navigate(
|
||||
|
@ -96,7 +96,7 @@ pub fn Nav() -> impl IntoView {
|
|||
</li>
|
||||
</ul>
|
||||
<div class="divider"></div>
|
||||
<Transition>
|
||||
<Suspense>
|
||||
<Show
|
||||
when=is_logged_in
|
||||
fallback=move || {
|
||||
|
@ -127,7 +127,9 @@ pub fn Nav() -> impl IntoView {
|
|||
</p>
|
||||
<button
|
||||
class="btn btn-outline btn-xs w-min self-center"
|
||||
on:click=move |_| logout_action.dispatch(())
|
||||
on:click=move |_| {
|
||||
logout_action.dispatch(());
|
||||
}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
|
@ -135,7 +137,7 @@ pub fn Nav() -> impl IntoView {
|
|||
}
|
||||
|
||||
</Show>
|
||||
</Transition>
|
||||
</Suspense>
|
||||
<div class="grow min-h-2"></div>
|
||||
<div class="m-1 grid gap-2">
|
||||
<label class="flex cursor-pointer gap-2">
|
||||
|
|
30
src/frontend/components/protected_route.rs
Normal file
30
src/frontend/components/protected_route.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use crate::frontend::app::is_logged_in;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{ProtectedRoute, ProtectedRouteProps},
|
||||
NestedRoute,
|
||||
SsrMode,
|
||||
};
|
||||
|
||||
#[component(transparent)]
|
||||
pub fn IbisProtectedRoute<Segments, ViewFn, View>(
|
||||
path: Segments,
|
||||
view: ViewFn,
|
||||
#[prop(optional)] ssr: SsrMode,
|
||||
) -> NestedRoute<Segments, (), (), impl Fn() -> AnyView + Send + Clone>
|
||||
where
|
||||
ViewFn: Fn() -> View + Send + Clone + 'static,
|
||||
View: IntoView + 'static,
|
||||
{
|
||||
let condition = move || Some(is_logged_in());
|
||||
let redirect_path = || "/";
|
||||
let props = ProtectedRouteProps {
|
||||
path,
|
||||
view,
|
||||
condition,
|
||||
redirect_path,
|
||||
ssr,
|
||||
fallback: Default::default(),
|
||||
};
|
||||
ProtectedRoute(props)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use chrono::{Duration, Local};
|
||||
use codee::string::FromToStringCodec;
|
||||
use leptos::{Signal, SignalGet, SignalGetUntracked, SignalSet, WriteSignal};
|
||||
use leptos::prelude::*;
|
||||
use leptos_use::{use_cookie_with_options, use_preferred_dark, SameSite, UseCookieOptions};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::common::{utils::extract_domain, DbArticle, DbPerson};
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
pub mod api;
|
||||
pub mod app;
|
||||
|
@ -16,7 +16,7 @@ pub fn hydrate() {
|
|||
use crate::frontend::app::App;
|
||||
console_log::init_with_level(log::Level::Debug).expect("error initializing logger");
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App);
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
|
||||
fn article_link(article: &DbArticle) -> String {
|
||||
|
|
|
@ -9,16 +9,16 @@ use crate::{
|
|||
DbArticle,
|
||||
},
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_router::Redirect;
|
||||
use leptos::{ev::KeyboardEvent, prelude::*};
|
||||
use leptos_router::components::Redirect;
|
||||
|
||||
#[component]
|
||||
pub fn ArticleActions() -> impl IntoView {
|
||||
let article = article_resource();
|
||||
let (new_title, set_new_title) = create_signal(String::new());
|
||||
let (fork_response, set_fork_response) = create_signal(Option::<DbArticle>::None);
|
||||
let (error, set_error) = create_signal(None::<String>);
|
||||
let fork_action = create_action(move |(article_id, new_title): &(ArticleId, String)| {
|
||||
let (new_title, set_new_title) = signal(String::new());
|
||||
let (fork_response, set_fork_response) = signal(Option::<DbArticle>::None);
|
||||
let (error, set_error) = signal(None::<String>);
|
||||
let fork_action = Action::new(move |(article_id, new_title): &(ArticleId, String)| {
|
||||
let params = ForkArticleForm {
|
||||
article_id: *article_id,
|
||||
new_title: new_title.to_string(),
|
||||
|
@ -34,7 +34,7 @@ pub fn ArticleActions() -> impl IntoView {
|
|||
}
|
||||
}
|
||||
});
|
||||
let protect_action = create_action(move |(id, protected): &(ArticleId, bool)| {
|
||||
let protect_action = Action::new(move |(id, protected): &(ArticleId, bool)| {
|
||||
let params = ProtectArticleForm {
|
||||
article_id: *id,
|
||||
protected: !protected,
|
||||
|
@ -72,7 +72,7 @@ pub fn ArticleActions() -> impl IntoView {
|
|||
class="btn btn-secondary"
|
||||
on:click=move |_| {
|
||||
protect_action
|
||||
.dispatch((article.article.id, article.article.protected))
|
||||
.dispatch((article.article.id, article.article.protected));
|
||||
}
|
||||
>
|
||||
Toggle Article Protection
|
||||
|
@ -82,7 +82,7 @@ pub fn ArticleActions() -> impl IntoView {
|
|||
<input
|
||||
class="input"
|
||||
placeholder="New Title"
|
||||
on:keyup=move |ev: ev::KeyboardEvent| {
|
||||
on:keyup=move |ev: KeyboardEvent| {
|
||||
let val = event_target_value(&ev);
|
||||
set_new_title.update(|v| *v = val);
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ pub fn ArticleActions() -> impl IntoView {
|
|||
class="btn"
|
||||
disabled=move || new_title.get().is_empty()
|
||||
on:click=move |_| {
|
||||
fork_action.dispatch((article.article.id, new_title.get()))
|
||||
fork_action.dispatch((article.article.id, new_title.get()));
|
||||
}
|
||||
>
|
||||
|
||||
|
|
|
@ -2,27 +2,26 @@ use crate::{
|
|||
common::CreateArticleForm,
|
||||
frontend::{api::CLIENT, components::editor::EditorView},
|
||||
};
|
||||
use html::Textarea;
|
||||
use leptos::*;
|
||||
use leptos_router::Redirect;
|
||||
use leptos::{html::Textarea, prelude::*};
|
||||
use leptos_router::components::Redirect;
|
||||
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
|
||||
|
||||
#[component]
|
||||
pub fn CreateArticle() -> impl IntoView {
|
||||
let (title, set_title) = create_signal(String::new());
|
||||
let textarea_ref = create_node_ref::<Textarea>();
|
||||
let (title, set_title) = signal(String::new());
|
||||
let textarea_ref = NodeRef::<Textarea>::new();
|
||||
let UseTextareaAutosizeReturn {
|
||||
content,
|
||||
set_content,
|
||||
trigger_resize: _,
|
||||
} = use_textarea_autosize(textarea_ref);
|
||||
let (summary, set_summary) = create_signal(String::new());
|
||||
let (create_response, set_create_response) = create_signal(None::<()>);
|
||||
let (create_error, set_create_error) = create_signal(None::<String>);
|
||||
let (wait_for_response, set_wait_for_response) = create_signal(false);
|
||||
let (summary, set_summary) = signal(String::new());
|
||||
let (create_response, set_create_response) = signal(None::<()>);
|
||||
let (create_error, set_create_error) = signal(None::<String>);
|
||||
let (wait_for_response, set_wait_for_response) = signal(false);
|
||||
let button_is_disabled =
|
||||
Signal::derive(move || wait_for_response.get() || summary.get().is_empty());
|
||||
let submit_action = create_action(move |(title, text, summary): &(String, String, String)| {
|
||||
let submit_action = Action::new(move |(title, text, summary): &(String, String, String)| {
|
||||
let title = title.clone();
|
||||
let text = text.clone();
|
||||
let summary = summary.clone();
|
||||
|
@ -94,7 +93,7 @@ pub fn CreateArticle() -> impl IntoView {
|
|||
prop:disabled=move || button_is_disabled.get()
|
||||
on:click=move |_| {
|
||||
submit_action
|
||||
.dispatch((title.get(), content.get(), summary.get()))
|
||||
.dispatch((title.get(), content.get(), summary.get()));
|
||||
}
|
||||
>
|
||||
Submit
|
||||
|
|
|
@ -9,9 +9,8 @@ use crate::{
|
|||
pages::article_resource,
|
||||
},
|
||||
};
|
||||
use html::Textarea;
|
||||
use leptos::*;
|
||||
use leptos_router::use_params_map;
|
||||
use leptos::{html::Textarea, prelude::*};
|
||||
use leptos_router::hooks::use_params_map;
|
||||
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
|
@ -26,12 +25,12 @@ const CONFLICT_MESSAGE: &str = "There was an edit conflict. Resolve it manually
|
|||
#[component]
|
||||
pub fn EditArticle() -> impl IntoView {
|
||||
let article = article_resource();
|
||||
let (edit_response, set_edit_response) = create_signal(EditResponse::None);
|
||||
let (edit_error, set_edit_error) = create_signal(None::<String>);
|
||||
let (edit_response, set_edit_response) = signal(EditResponse::None);
|
||||
let (edit_error, set_edit_error) = signal(None::<String>);
|
||||
|
||||
let conflict_id = move || use_params_map().get_untracked().get("conflict_id").cloned();
|
||||
let conflict_id = move || use_params_map().get_untracked().get("conflict_id").clone();
|
||||
if let Some(conflict_id) = conflict_id() {
|
||||
create_action(move |conflict_id: &String| {
|
||||
Action::new(move |conflict_id: &String| {
|
||||
let conflict_id = ConflictId(conflict_id.parse().unwrap());
|
||||
async move {
|
||||
let conflict = CLIENT
|
||||
|
@ -52,17 +51,17 @@ pub fn EditArticle() -> impl IntoView {
|
|||
.dispatch(conflict_id);
|
||||
}
|
||||
|
||||
let textarea_ref = create_node_ref::<Textarea>();
|
||||
let textarea_ref = NodeRef::<Textarea>::new();
|
||||
let UseTextareaAutosizeReturn {
|
||||
content,
|
||||
set_content,
|
||||
trigger_resize: _,
|
||||
} = use_textarea_autosize(textarea_ref);
|
||||
let (summary, set_summary) = create_signal(String::new());
|
||||
let (wait_for_response, set_wait_for_response) = create_signal(false);
|
||||
let (summary, set_summary) = signal(String::new());
|
||||
let (wait_for_response, set_wait_for_response) = signal(false);
|
||||
let button_is_disabled =
|
||||
Signal::derive(move || wait_for_response.get() || summary.get().is_empty());
|
||||
let submit_action = create_action(
|
||||
let submit_action = Action::new(
|
||||
move |(new_text, summary, article, edit_response): &(
|
||||
String,
|
||||
String,
|
||||
|
@ -161,7 +160,7 @@ pub fn EditArticle() -> impl IntoView {
|
|||
summary.get(),
|
||||
article_.clone(),
|
||||
edit_response.get(),
|
||||
))
|
||||
));
|
||||
}
|
||||
>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::frontend::{
|
|||
render_date_time,
|
||||
user_link,
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn ArticleHistory() -> impl IntoView {
|
||||
|
|
|
@ -2,15 +2,12 @@ use crate::{
|
|||
common::ListArticlesForm,
|
||||
frontend::{api::CLIENT, article_link, article_title, components::connect::ConnectView},
|
||||
};
|
||||
use html::Input;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn ListArticles() -> impl IntoView {
|
||||
let (only_local, set_only_local) = create_signal(false);
|
||||
let button_only_local = create_node_ref::<Input>();
|
||||
let button_all = create_node_ref::<Input>();
|
||||
let articles = create_resource(
|
||||
let (only_local, set_only_local) = signal(false);
|
||||
let articles = Resource::new(
|
||||
move || only_local.get(),
|
||||
|only_local| async move {
|
||||
CLIENT
|
||||
|
@ -22,6 +19,28 @@ pub fn ListArticles() -> impl IntoView {
|
|||
.unwrap()
|
||||
},
|
||||
);
|
||||
let only_local_class = Resource::new(
|
||||
move || only_local.get(),
|
||||
|only_local| async move {
|
||||
if only_local {
|
||||
"btn rounded-r-none btn-primary"
|
||||
} else {
|
||||
"btn rounded-r-none"
|
||||
}
|
||||
.to_string()
|
||||
},
|
||||
);
|
||||
let all_class = Resource::new(
|
||||
move || only_local.get(),
|
||||
|only_local| async move {
|
||||
if !only_local {
|
||||
"btn rounded-l-none btn-primary"
|
||||
} else {
|
||||
"btn rounded-l-none"
|
||||
}
|
||||
.to_string()
|
||||
},
|
||||
);
|
||||
|
||||
view! {
|
||||
<h1 class="text-4xl font-bold font-serif my-4">Most recently edited Articles</h1>
|
||||
|
@ -30,22 +49,16 @@ pub fn ListArticles() -> impl IntoView {
|
|||
<input
|
||||
type="button"
|
||||
value="Only Local"
|
||||
class="btn rounded-r-none"
|
||||
node_ref=button_only_local
|
||||
class=move || only_local_class.get()
|
||||
on:click=move |_| {
|
||||
button_all.get().map(|c| c.class("btn-primary", false));
|
||||
button_only_local.get().map(|c| c.class("btn-primary", true));
|
||||
set_only_local.set(true);
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="button"
|
||||
value="All"
|
||||
class="btn btn-primary rounded-l-none"
|
||||
node_ref=button_all
|
||||
class=move || all_class.get()
|
||||
on:click=move |_| {
|
||||
button_all.get().map(|c| c.class("btn-primary", true));
|
||||
button_only_local.get().map(|c| c.class("btn-primary", false));
|
||||
set_only_local.set(false);
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::frontend::{
|
|||
markdown::render_markdown,
|
||||
pages::article_resource,
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn ReadArticle() -> impl IntoView {
|
||||
|
|
|
@ -4,8 +4,8 @@ use crate::frontend::{
|
|||
render_date_time,
|
||||
user_link,
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
#[component]
|
||||
pub fn EditDiff() -> impl IntoView {
|
||||
|
@ -21,7 +21,7 @@ pub fn EditDiff() -> impl IntoView {
|
|||
article
|
||||
.get()
|
||||
.map(|article| {
|
||||
let hash = params.get_untracked().get("hash").cloned().unwrap();
|
||||
let hash = params.get_untracked().get("hash").clone().unwrap();
|
||||
let edit = article
|
||||
.edits
|
||||
.iter()
|
||||
|
|
|
@ -7,15 +7,15 @@ use crate::{
|
|||
components::instance_follow_button::InstanceFollowButton,
|
||||
},
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_router::use_params_map;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
use url::Url;
|
||||
|
||||
#[component]
|
||||
pub fn InstanceDetails() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let hostname = move || params.get().get("hostname").cloned().unwrap();
|
||||
let instance_profile = create_resource(hostname, move |hostname| async move {
|
||||
let hostname = move || params.get().get("hostname").clone().unwrap();
|
||||
let instance_profile = Resource::new(hostname, move |hostname| async move {
|
||||
let url = Url::parse(&format!("{}://{hostname}", http_protocol_str())).unwrap();
|
||||
CLIENT.resolve_instance(url).await.unwrap()
|
||||
});
|
||||
|
@ -28,7 +28,7 @@ pub fn InstanceDetails() -> impl IntoView {
|
|||
instance_profile
|
||||
.get()
|
||||
.map(|instance: DbInstance| {
|
||||
let articles = create_resource(
|
||||
let articles = Resource::new(
|
||||
move || instance.id,
|
||||
|instance_id| async move {
|
||||
CLIENT
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use crate::frontend::{api::CLIENT, components::connect::ConnectView};
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn ListInstances() -> impl IntoView {
|
||||
let instances = create_resource(
|
||||
let instances = Resource::new(
|
||||
move || (),
|
||||
|_| async move { CLIENT.list_instances().await.unwrap() },
|
||||
);
|
||||
|
@ -21,14 +21,14 @@ pub fn ListInstances() -> impl IntoView {
|
|||
.get()
|
||||
.map(|a| {
|
||||
a.into_iter()
|
||||
.map(|i| {
|
||||
.map(|ref i| {
|
||||
view! {
|
||||
<li>
|
||||
<a
|
||||
class="link text-lg"
|
||||
href=format!("/instance/{}", i.domain)
|
||||
>
|
||||
{i.domain}
|
||||
{i.domain.to_string()}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
|
|
@ -2,16 +2,16 @@ use crate::{
|
|||
common::LoginUserForm,
|
||||
frontend::{api::CLIENT, app::site, components::credentials::*},
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_router::Redirect;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::Redirect;
|
||||
|
||||
#[component]
|
||||
pub fn Login() -> impl IntoView {
|
||||
let (login_response, set_login_response) = create_signal(None::<()>);
|
||||
let (login_error, set_login_error) = create_signal(None::<String>);
|
||||
let (wait_for_response, set_wait_for_response) = create_signal(false);
|
||||
let (login_response, set_login_response) = signal(None::<()>);
|
||||
let (login_error, set_login_error) = signal(None::<String>);
|
||||
let (wait_for_response, set_wait_for_response) = signal(false);
|
||||
|
||||
let login_action = create_action(move |(email, password): &(String, String)| {
|
||||
let login_action = Action::new(move |(email, password): &(String, String)| {
|
||||
let username = email.to_string();
|
||||
let password = password.to_string();
|
||||
let credentials = LoginUserForm { username, password };
|
||||
|
|
|
@ -2,8 +2,8 @@ use crate::{
|
|||
common::{ArticleView, GetArticleForm, MAIN_PAGE_NAME},
|
||||
frontend::api::CLIENT,
|
||||
};
|
||||
use leptos::{create_resource, Resource, SignalGet};
|
||||
use leptos_router::use_params_map;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
pub(crate) mod article;
|
||||
pub(crate) mod diff;
|
||||
|
@ -14,10 +14,10 @@ pub(crate) mod register;
|
|||
pub(crate) mod search;
|
||||
pub(crate) mod user_profile;
|
||||
|
||||
fn article_resource() -> Resource<Option<String>, ArticleView> {
|
||||
fn article_resource() -> Resource<ArticleView> {
|
||||
let params = use_params_map();
|
||||
let title = move || params.get().get("title").cloned();
|
||||
create_resource(title, move |title| async move {
|
||||
let title = move || params.get().get("title").clone();
|
||||
Resource::new(title, move |title| async move {
|
||||
let mut title = title.unwrap_or(MAIN_PAGE_NAME.to_string());
|
||||
let mut domain = None;
|
||||
if let Some((title_, domain_)) = title.clone().split_once('@') {
|
||||
|
|
|
@ -2,11 +2,11 @@ use crate::{
|
|||
common::Notification,
|
||||
frontend::{api::CLIENT, article_link, article_title},
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn Notifications() -> impl IntoView {
|
||||
let notifications = create_resource(
|
||||
let notifications = Resource::new(
|
||||
move || {},
|
||||
|_| async move { CLIENT.notifications_list().await.unwrap() },
|
||||
);
|
||||
|
@ -43,7 +43,7 @@ pub fn Notifications() -> impl IntoView {
|
|||
}
|
||||
};
|
||||
let notif_ = notif.clone();
|
||||
let click_approve = create_action(move |_: &()| {
|
||||
let click_approve = Action::new(move |_: &()| {
|
||||
let notif_ = notif_.clone();
|
||||
async move {
|
||||
if let ArticleApprovalRequired(a) = notif_ {
|
||||
|
@ -53,7 +53,7 @@ pub fn Notifications() -> impl IntoView {
|
|||
}
|
||||
});
|
||||
let notif_ = notif.clone();
|
||||
let click_reject = create_action(move |_: &()| {
|
||||
let click_reject = Action::new(move |_: &()| {
|
||||
let notif_ = notif_.clone();
|
||||
async move {
|
||||
match notif_ {
|
||||
|
@ -76,13 +76,17 @@ pub fn Notifications() -> impl IntoView {
|
|||
<button
|
||||
class="btn btn-sm btn-outline"
|
||||
style=my_style
|
||||
on:click=move |_| click_approve.dispatch(())
|
||||
on:click=move |_| {
|
||||
click_approve.dispatch(());
|
||||
}
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline"
|
||||
on:click=move |_| click_reject.dispatch(())
|
||||
on:click=move |_| {
|
||||
click_reject.dispatch(());
|
||||
}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
|
|
|
@ -2,19 +2,20 @@ use crate::{
|
|||
common::{LocalUserView, RegisterUserForm},
|
||||
frontend::{api::CLIENT, app::site, components::credentials::*, error::MyResult},
|
||||
};
|
||||
use leptos::{logging::log, *};
|
||||
use leptos::prelude::*;
|
||||
use log::info;
|
||||
|
||||
#[component]
|
||||
pub fn Register() -> impl IntoView {
|
||||
let (register_response, set_register_response) = create_signal(None::<()>);
|
||||
let (register_error, set_register_error) = create_signal(None::<String>);
|
||||
let (wait_for_response, set_wait_for_response) = create_signal(false);
|
||||
let (register_response, set_register_response) = signal(None::<()>);
|
||||
let (register_error, set_register_error) = signal(None::<String>);
|
||||
let (wait_for_response, set_wait_for_response) = signal(false);
|
||||
|
||||
let register_action = create_action(move |(email, password): &(String, String)| {
|
||||
let register_action = Action::new(move |(email, password): &(String, String)| {
|
||||
let username = email.to_string();
|
||||
let password = password.to_string();
|
||||
let credentials = RegisterUserForm { username, password };
|
||||
log!("Try to register new account for {}", credentials.username);
|
||||
info!("Try to register new account for {}", credentials.username);
|
||||
async move {
|
||||
set_wait_for_response.update(|w| *w = true);
|
||||
let result: MyResult<LocalUserView> = CLIENT.register(credentials).await;
|
||||
|
|
|
@ -2,8 +2,8 @@ use crate::{
|
|||
common::{DbArticle, DbInstance, SearchArticleForm},
|
||||
frontend::{api::CLIENT, article_link, article_title},
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_router::use_query_map;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::hooks::use_query_map;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
|
@ -22,9 +22,9 @@ impl SearchResults {
|
|||
#[component]
|
||||
pub fn Search() -> impl IntoView {
|
||||
let params = use_query_map();
|
||||
let query = move || params.get().get("query").cloned().unwrap();
|
||||
let (error, set_error) = create_signal(None::<String>);
|
||||
let search_results = create_resource(query, move |query| async move {
|
||||
let query = move || params.get().get("query").clone().unwrap();
|
||||
let (error, set_error) = signal(None::<String>);
|
||||
let search_results = Resource::new(query, move |query| async move {
|
||||
set_error.set(None);
|
||||
let mut search_results = SearchResults::default();
|
||||
let url = Url::parse(&query);
|
||||
|
@ -89,7 +89,7 @@ pub fn Search() -> impl IntoView {
|
|||
view! {
|
||||
<li>
|
||||
<a class="link text-lg" href=format!("/instance/{domain}")>
|
||||
{domain}
|
||||
{domain.to_string()}
|
||||
</a>
|
||||
</li>
|
||||
},
|
||||
|
|
|
@ -2,15 +2,15 @@ use crate::{
|
|||
common::{DbPerson, GetUserForm},
|
||||
frontend::{api::CLIENT, user_title},
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_router::use_params_map;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
#[component]
|
||||
pub fn UserProfile() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let name = move || params.get().get("name").cloned().unwrap();
|
||||
let (error, set_error) = create_signal(None::<String>);
|
||||
let user_profile = create_resource(name, move |mut name| async move {
|
||||
let name = move || params.get().get("name").clone().unwrap();
|
||||
let (error, set_error) = signal(None::<String>);
|
||||
let user_profile = Resource::new(name, move |mut name| async move {
|
||||
set_error.set(None);
|
||||
let mut domain = None;
|
||||
if let Some((title_, domain_)) = name.clone().split_once('@') {
|
||||
|
|
Loading…
Reference in a new issue