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