impl toggle
This commit is contained in:
@@ -114,7 +114,7 @@ pub mod view {
|
|||||||
pub mod route {
|
pub mod route {
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::{models::user::User, AppState};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::{BoxBody, HttpBody},
|
body::{BoxBody, HttpBody},
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
@@ -137,7 +137,7 @@ pub mod route {
|
|||||||
const URL: &'static str;
|
const URL: &'static str;
|
||||||
|
|
||||||
async fn create(
|
async fn create(
|
||||||
user: Extension<crate::models::user::User>,
|
user: Extension<User>,
|
||||||
state: State<AppState>,
|
state: State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
path: Path<Self::UrlParams>,
|
path: Path<Self::UrlParams>,
|
||||||
@@ -153,7 +153,7 @@ pub mod route {
|
|||||||
const URL: &'static str;
|
const URL: &'static str;
|
||||||
|
|
||||||
async fn read(
|
async fn read(
|
||||||
user: Extension<crate::models::user::User>,
|
user: Extension<User>,
|
||||||
state: State<AppState>,
|
state: State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
query: Query<Self::QueryParams>,
|
query: Query<Self::QueryParams>,
|
||||||
@@ -169,14 +169,14 @@ pub mod route {
|
|||||||
const URL: &'static str;
|
const URL: &'static str;
|
||||||
|
|
||||||
async fn start(
|
async fn start(
|
||||||
user: Extension<crate::models::user::User>,
|
user: Extension<User>,
|
||||||
state: State<AppState>,
|
state: State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
path: Path<Self::UrlParams>,
|
path: Path<Self::UrlParams>,
|
||||||
) -> Result<Response<BoxBody>, crate::Error>;
|
) -> Result<Response<BoxBody>, crate::Error>;
|
||||||
|
|
||||||
async fn save(
|
async fn save(
|
||||||
user: Extension<crate::models::user::User>,
|
user: Extension<User>,
|
||||||
state: State<AppState>,
|
state: State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
path: Path<Self::UrlParams>,
|
path: Path<Self::UrlParams>,
|
||||||
@@ -184,13 +184,103 @@ pub mod route {
|
|||||||
) -> Result<Response<BoxBody>, crate::Error>;
|
) -> Result<Response<BoxBody>, crate::Error>;
|
||||||
|
|
||||||
async fn cancel(
|
async fn cancel(
|
||||||
user: Extension<crate::models::user::User>,
|
user: Extension<User>,
|
||||||
state: State<AppState>,
|
state: State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
path: Path<Self::UrlParams>,
|
path: Path<Self::UrlParams>,
|
||||||
) -> Result<Response<BoxBody>, crate::Error>;
|
) -> Result<Response<BoxBody>, crate::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait ToggleFallback: Send + Sync + Sized + 'static {
|
||||||
|
type UrlParams: Clone + Copy + Send + Sync + Sized + 'static;
|
||||||
|
|
||||||
|
const URL_TRUE: &'static str;
|
||||||
|
const URL_FALSE: &'static str;
|
||||||
|
|
||||||
|
async fn set(
|
||||||
|
current_user: User,
|
||||||
|
state: AppState,
|
||||||
|
headers: HeaderMap,
|
||||||
|
params: Self::UrlParams,
|
||||||
|
value: bool,
|
||||||
|
) -> Result<Response<BoxBody>, crate::Error>;
|
||||||
|
|
||||||
|
async fn set_true(
|
||||||
|
Extension(user): Extension<User>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(path): Path<Self::UrlParams>,
|
||||||
|
) -> Result<Response<BoxBody>, crate::Error> {
|
||||||
|
Self::set(user, state, headers, path, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_false(
|
||||||
|
Extension(user): Extension<User>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(path): Path<Self::UrlParams>,
|
||||||
|
) -> Result<Response<BoxBody>, crate::Error> {
|
||||||
|
Self::set(user, state, headers, path, false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn router<B>() -> axum::Router<AppState, B>
|
||||||
|
where
|
||||||
|
B: HttpBody + Send + 'static,
|
||||||
|
<B as HttpBody>::Data: Send,
|
||||||
|
<B as HttpBody>::Error: std::error::Error + Sync + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait ToggleHtmx {
|
||||||
|
type UrlParams: Send + Sync + 'static;
|
||||||
|
|
||||||
|
const URL_TRUE: &'static str;
|
||||||
|
const URL_FALSE: &'static str;
|
||||||
|
|
||||||
|
async fn set(
|
||||||
|
current_user: User,
|
||||||
|
state: AppState,
|
||||||
|
params: Self::UrlParams,
|
||||||
|
value: bool,
|
||||||
|
) -> Result<Response<BoxBody>, crate::Error>;
|
||||||
|
|
||||||
|
async fn set_true(
|
||||||
|
Extension(user): Extension<User>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(path): Path<Self::UrlParams>,
|
||||||
|
) -> Result<Response<BoxBody>, crate::Error> {
|
||||||
|
Self::set(user, state, path, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_false(
|
||||||
|
Extension(user): Extension<User>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(path): Path<Self::UrlParams>,
|
||||||
|
) -> Result<Response<BoxBody>, crate::Error> {
|
||||||
|
Self::set(user, state, path, false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn router<B>() -> axum::Router<AppState, B>
|
||||||
|
where
|
||||||
|
B: HttpBody + Send + 'static,
|
||||||
|
<B as HttpBody>::Data: Send,
|
||||||
|
<B as HttpBody>::Error: std::error::Error + Sync + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Toggle: ToggleHtmx + ToggleFallback {
|
||||||
|
fn router<B>() -> axum::Router<AppState, B>
|
||||||
|
where
|
||||||
|
B: HttpBody + Send + 'static,
|
||||||
|
<B as HttpBody>::Data: Send,
|
||||||
|
<B as HttpBody>::Error: std::error::Error + Sync + Send,
|
||||||
|
{
|
||||||
|
axum::Router::new()
|
||||||
|
.merge(<Self as ToggleHtmx>::router())
|
||||||
|
.merge(<Self as ToggleFallback>::router())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Delete: super::crud::Delete {
|
pub trait Delete: super::crud::Delete {
|
||||||
type UrlParams: Send + Sync + 'static;
|
type UrlParams: Send + Sync + 'static;
|
||||||
@@ -198,7 +288,7 @@ pub mod route {
|
|||||||
const URL: &'static str;
|
const URL: &'static str;
|
||||||
|
|
||||||
async fn delete(
|
async fn delete(
|
||||||
user: Extension<crate::models::user::User>,
|
user: Extension<User>,
|
||||||
state: State<AppState>,
|
state: State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
path: Path<Self::UrlParams>,
|
path: Path<Self::UrlParams>,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(unused_variables)]
|
||||||
|
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub use list::List;
|
pub use list::List;
|
||||||
|
|
||||||
@@ -6,6 +8,7 @@ use axum::{
|
|||||||
extract::{Form, Path, State as StateExtractor},
|
extract::{Form, Path, State as StateExtractor},
|
||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
|
routing::post,
|
||||||
Extension,
|
Extension,
|
||||||
};
|
};
|
||||||
use maud::{html, Markup};
|
use maud::{html, Markup};
|
||||||
@@ -216,10 +219,38 @@ impl crud::Create for Todo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct StateUpdate {
|
||||||
|
new_state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for StateUpdate {
|
||||||
|
fn from(state: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
new_state: state.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<State> for StateUpdate {
|
||||||
|
fn from(new_state: State) -> Self {
|
||||||
|
Self { new_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct DescriptionUpdate(String);
|
||||||
|
|
||||||
|
impl From<String> for DescriptionUpdate {
|
||||||
|
fn from(new_description: String) -> Self {
|
||||||
|
Self(new_description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum UpdateElement {
|
pub enum UpdateElement {
|
||||||
State(State),
|
State(StateUpdate),
|
||||||
Description(String),
|
Description(DescriptionUpdate),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -241,7 +272,7 @@ impl crud::Update for Todo {
|
|||||||
let todo_id_param = id.to_string();
|
let todo_id_param = id.to_string();
|
||||||
match update_element {
|
match update_element {
|
||||||
UpdateElement::State(state) => {
|
UpdateElement::State(state) => {
|
||||||
let done = state == State::Done;
|
let done = state == State::Done.into();
|
||||||
|
|
||||||
let result = crate::query_one!(
|
let result = crate::query_one!(
|
||||||
&sqlite::QueryClassification {
|
&sqlite::QueryClassification {
|
||||||
@@ -297,7 +328,7 @@ impl crud::Update for Todo {
|
|||||||
description,
|
description,
|
||||||
done
|
done
|
||||||
"#,
|
"#,
|
||||||
new_description,
|
new_description.0,
|
||||||
todo_id_param,
|
todo_id_param,
|
||||||
trip_id_param,
|
trip_id_param,
|
||||||
trip_id_param,
|
trip_id_param,
|
||||||
@@ -468,12 +499,12 @@ impl view::View for Todo {
|
|||||||
href={
|
href={
|
||||||
"/trips/" (input.trip_id)
|
"/trips/" (input.trip_id)
|
||||||
"/todo/" (self.id)
|
"/todo/" (self.id)
|
||||||
"/undone"
|
"/done/false"
|
||||||
}
|
}
|
||||||
hx-post={
|
hx-post={
|
||||||
"/trips/" (input.trip_id)
|
"/trips/" (input.trip_id)
|
||||||
"/todo/" (self.id)
|
"/todo/" (self.id)
|
||||||
"/undone"
|
"/done/htmx/false"
|
||||||
}
|
}
|
||||||
hx-target="closest li"
|
hx-target="closest li"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
@@ -494,12 +525,12 @@ impl view::View for Todo {
|
|||||||
href={
|
href={
|
||||||
"/trips/" (input.trip_id)
|
"/trips/" (input.trip_id)
|
||||||
"/todo/" (self.id)
|
"/todo/" (self.id)
|
||||||
"/done"
|
"/done/true"
|
||||||
}
|
}
|
||||||
hx-post={
|
hx-post={
|
||||||
"/trips/" (input.trip_id)
|
"/trips/" (input.trip_id)
|
||||||
"/todo/" (self.id)
|
"/todo/" (self.id)
|
||||||
"/done"
|
"/done/htmx/true"
|
||||||
}
|
}
|
||||||
hx-target="closest li"
|
hx-target="closest li"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
@@ -676,36 +707,6 @@ impl route::Router for Todo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn trip_todo_done_htmx(
|
|
||||||
Extension(current_user): Extension<User>,
|
|
||||||
StateExtractor(state): StateExtractor<AppState>,
|
|
||||||
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
|
|
||||||
) -> Result<impl IntoResponse, crate::Error> {
|
|
||||||
let ctx = Context::build(current_user);
|
|
||||||
Todo::update(
|
|
||||||
&ctx,
|
|
||||||
&state.database_pool,
|
|
||||||
Filter { trip_id },
|
|
||||||
todo_id,
|
|
||||||
UpdateElement::State(State::Done),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, todo_id)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| {
|
|
||||||
crate::Error::Request(RequestError::NotFound {
|
|
||||||
message: format!("todo with id {todo_id} not found"),
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(todo_item.build(BuildInput {
|
|
||||||
trip_id,
|
|
||||||
state: UiState::Default,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn trip_todo_done(
|
pub async fn trip_todo_done(
|
||||||
Extension(current_user): Extension<User>,
|
Extension(current_user): Extension<User>,
|
||||||
@@ -719,7 +720,7 @@ pub async fn trip_todo_done(
|
|||||||
&state.database_pool,
|
&state.database_pool,
|
||||||
Filter { trip_id },
|
Filter { trip_id },
|
||||||
todo_id,
|
todo_id,
|
||||||
UpdateElement::State(State::Done),
|
UpdateElement::State(State::Done.into()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -738,7 +739,7 @@ pub async fn trip_todo_undone_htmx(
|
|||||||
&state.database_pool,
|
&state.database_pool,
|
||||||
Filter { trip_id },
|
Filter { trip_id },
|
||||||
todo_id,
|
todo_id,
|
||||||
UpdateElement::State(State::Todo),
|
UpdateElement::State(State::Todo.into()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -769,7 +770,7 @@ pub async fn trip_todo_undone(
|
|||||||
&state.database_pool,
|
&state.database_pool,
|
||||||
Filter { trip_id },
|
Filter { trip_id },
|
||||||
todo_id,
|
todo_id,
|
||||||
UpdateElement::State(State::Todo),
|
UpdateElement::State(State::Todo.into()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -820,7 +821,7 @@ pub async fn trip_todo_edit_save(
|
|||||||
&state.database_pool,
|
&state.database_pool,
|
||||||
Filter { trip_id },
|
Filter { trip_id },
|
||||||
todo_id,
|
todo_id,
|
||||||
UpdateElement::Description(form.description),
|
UpdateElement::Description(form.description.into()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -865,3 +866,96 @@ pub async fn trip_todo_edit_cancel(
|
|||||||
.into_response()),
|
.into_response()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl route::ToggleFallback for StateUpdate {
|
||||||
|
type UrlParams = (Uuid, Uuid);
|
||||||
|
|
||||||
|
const URL_TRUE: &'static str = "/:id/done/true";
|
||||||
|
const URL_FALSE: &'static str = "/:id/done/false";
|
||||||
|
|
||||||
|
async fn set(
|
||||||
|
current_user: User,
|
||||||
|
state: AppState,
|
||||||
|
headers: HeaderMap,
|
||||||
|
(trip_id, todo_id): (Uuid, Uuid),
|
||||||
|
value: bool,
|
||||||
|
) -> Result<Response<BoxBody>, crate::Error> {
|
||||||
|
let ctx = Context::build(current_user);
|
||||||
|
Todo::update(
|
||||||
|
&ctx,
|
||||||
|
&state.database_pool,
|
||||||
|
Filter { trip_id },
|
||||||
|
todo_id,
|
||||||
|
UpdateElement::State(value.into()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Redirect::to(get_referer(&headers)?).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn router<B>() -> axum::Router<AppState, B>
|
||||||
|
where
|
||||||
|
B: HttpBody + Send + 'static,
|
||||||
|
<B as HttpBody>::Data: Send,
|
||||||
|
<B as HttpBody>::Error: std::error::Error + Sync + Send,
|
||||||
|
{
|
||||||
|
axum::Router::new()
|
||||||
|
.route(Self::URL_TRUE, post(Self::set_true))
|
||||||
|
.route(Self::URL_FALSE, post(Self::set_false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl route::ToggleHtmx for StateUpdate {
|
||||||
|
type UrlParams = (Uuid, Uuid);
|
||||||
|
|
||||||
|
const URL_TRUE: &'static str = "/:id/done/htmx/true";
|
||||||
|
const URL_FALSE: &'static str = "/:id/done/htmx/false";
|
||||||
|
|
||||||
|
async fn set(
|
||||||
|
current_user: User,
|
||||||
|
state: AppState,
|
||||||
|
(trip_id, todo_id): (Uuid, Uuid),
|
||||||
|
value: bool,
|
||||||
|
) -> Result<Response<BoxBody>, crate::Error> {
|
||||||
|
let ctx = Context::build(current_user);
|
||||||
|
Todo::update(
|
||||||
|
&ctx,
|
||||||
|
&state.database_pool,
|
||||||
|
Filter { trip_id },
|
||||||
|
todo_id,
|
||||||
|
UpdateElement::State(value.into()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, todo_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::Error::Request(RequestError::NotFound {
|
||||||
|
message: format!("todo with id {todo_id} not found"),
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(todo_item
|
||||||
|
.build(BuildInput {
|
||||||
|
trip_id,
|
||||||
|
state: UiState::Default,
|
||||||
|
})
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn router<B>() -> axum::Router<AppState, B>
|
||||||
|
where
|
||||||
|
B: HttpBody + Send + 'static,
|
||||||
|
<B as HttpBody>::Data: Send,
|
||||||
|
<B as HttpBody>::Error: std::error::Error + Sync + Send,
|
||||||
|
{
|
||||||
|
axum::Router::new()
|
||||||
|
.route(Self::URL_TRUE, post(Self::set_true))
|
||||||
|
.route(Self::URL_FALSE, post(Self::set_false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl route::Toggle for StateUpdate {}
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ use std::{fmt, time::Duration};
|
|||||||
use tower::{timeout::TimeoutLayer, ServiceBuilder};
|
use tower::{timeout::TimeoutLayer, ServiceBuilder};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{self, route::Router as _},
|
components::{
|
||||||
|
self,
|
||||||
|
route::{Router as _, Toggle},
|
||||||
|
},
|
||||||
AppState, Error, RequestError, TopLevelPage,
|
AppState, Error, RequestError, TopLevelPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,16 +146,16 @@ pub fn router(state: AppState) -> Router {
|
|||||||
"/:id/items/:id/unready",
|
"/:id/items/:id/unready",
|
||||||
get(trip_item_set_unready).post(trip_item_set_unready_htmx),
|
get(trip_item_set_unready).post(trip_item_set_unready_htmx),
|
||||||
)
|
)
|
||||||
.route(
|
// .route(
|
||||||
"/:id/todo/:id/done",
|
// "/:id/todo/:id/done",
|
||||||
get(components::trips::todos::trip_todo_done)
|
// get(components::trips::todos::trip_todo_done)
|
||||||
.post(components::trips::todos::trip_todo_done_htmx),
|
// .post(components::trips::todos::trip_todo_done_htmx),
|
||||||
)
|
// )
|
||||||
.route(
|
// .route(
|
||||||
"/:id/todo/:id/undone",
|
// "/:id/todo/:id/undone",
|
||||||
get(components::trips::todos::trip_todo_undone)
|
// get(components::trips::todos::trip_todo_undone)
|
||||||
.post(components::trips::todos::trip_todo_undone_htmx),
|
// .post(components::trips::todos::trip_todo_undone_htmx),
|
||||||
)
|
// )
|
||||||
.route(
|
.route(
|
||||||
"/:id/todo/:id/edit",
|
"/:id/todo/:id/edit",
|
||||||
post(components::trips::todos::trip_todo_edit),
|
post(components::trips::todos::trip_todo_edit),
|
||||||
@@ -165,7 +168,11 @@ pub fn router(state: AppState) -> Router {
|
|||||||
"/:id/todo/:id/edit/cancel",
|
"/:id/todo/:id/edit/cancel",
|
||||||
post(components::trips::todos::trip_todo_edit_cancel),
|
post(components::trips::todos::trip_todo_edit_cancel),
|
||||||
)
|
)
|
||||||
.nest("/:id/todo/", components::trips::todos::Todo::get()),
|
.nest(
|
||||||
|
"/:id/todo/",
|
||||||
|
components::trips::todos::Todo::get()
|
||||||
|
.merge(<components::trips::todos::StateUpdate as Toggle>::router()),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.nest(
|
.nest(
|
||||||
(&TopLevelPage::Inventory.path()).into(),
|
(&TopLevelPage::Inventory.path()).into(),
|
||||||
|
|||||||
Reference in New Issue
Block a user