Compare commits
7 Commits
less-lifet
...
master
Author | SHA1 | Date |
---|---|---|
Daan Vanoverloop | 0b9318b5c4 | |
Daan Vanoverloop | c1a61d28a4 | |
Daan Vanoverloop | f2d6d3a20f | |
Daan Vanoverloop | 3ef2c282b0 | |
Daan Vanoverloop | 59eeabc888 | |
Daan Vanoverloop | 44745b252a | |
Daan Vanoverloop | 667f2c5bbf |
|
@ -376,8 +376,18 @@ version = "0.13.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.13.4",
|
||||||
"darling_macro",
|
"darling_macro 0.13.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.14.1",
|
||||||
|
"darling_macro 0.14.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -394,13 +404,38 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.13.4"
|
version = "0.13.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.13.4",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.14.1",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
@ -1173,6 +1208,7 @@ name = "lan_party_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case",
|
||||||
|
"darling 0.14.1",
|
||||||
"paste",
|
"paste",
|
||||||
"quote",
|
"quote",
|
||||||
"sycamore",
|
"sycamore",
|
||||||
|
@ -1184,7 +1220,10 @@ name = "lan_party_web"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
"console_log",
|
"console_log",
|
||||||
|
"futures",
|
||||||
|
"gloo-timers",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"lan_party_core",
|
"lan_party_core",
|
||||||
"log",
|
"log",
|
||||||
|
@ -1964,7 +2003,7 @@ version = "0.8.0-rc.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54f94d1ffe41472e08463d7a2674f1db04dc4df745285e8369b33d3cfd6b0308"
|
checksum = "54f94d1ffe41472e08463d7a2674f1db04dc4df745285e8369b33d3cfd6b0308"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.13.4",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rocket_http",
|
"rocket_http",
|
||||||
|
@ -2212,7 +2251,7 @@ version = "1.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
|
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.13.4",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
|
|
|
@ -14,13 +14,14 @@ api_routes!(
|
||||||
update_event,
|
update_event,
|
||||||
get_all_events,
|
get_all_events,
|
||||||
event_outcome,
|
event_outcome,
|
||||||
|
delete_event,
|
||||||
);
|
);
|
||||||
|
|
||||||
pub async fn apply_outcome(outcome: &EventOutcome, db: &Connection<Db>) -> Result<(), PartyError> {
|
pub async fn apply_outcome(outcome: &EventOutcome, db: &Connection<Db>) -> Result<(), PartyError> {
|
||||||
for (player, reward) in outcome.points.iter() {
|
for (player, reward) in outcome.points.iter() {
|
||||||
db.users()
|
db.users()
|
||||||
.update_one(
|
.update_one(
|
||||||
doc! { "id": player },
|
doc! { "name": player.to_string() },
|
||||||
doc! { "$inc": { "score": reward } },
|
doc! { "$inc": { "score": reward } },
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
@ -164,3 +165,16 @@ pub async fn stop_event(
|
||||||
|
|
||||||
Ok(Json(outcome))
|
Ok(Json(outcome))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Delete event by name
|
||||||
|
#[openapi(tag = "Event")]
|
||||||
|
#[delete("/<name>")]
|
||||||
|
pub async fn delete_event(
|
||||||
|
_api_key: ApiKey,
|
||||||
|
db: Connection<Db>,
|
||||||
|
name: String,
|
||||||
|
) -> Result<Status, PartyError> {
|
||||||
|
db.events().delete_one(doc! { "name": name }, None).await?;
|
||||||
|
|
||||||
|
Ok(Status::Ok)
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
use rocket::{http::Status, response, response::Responder, Request};
|
use std::borrow::Borrow;
|
||||||
|
|
||||||
|
use rocket::{
|
||||||
|
http::Status,
|
||||||
|
response::{self, status, Responder},
|
||||||
|
Request,
|
||||||
|
};
|
||||||
use rocket_db_pools::mongodb;
|
use rocket_db_pools::mongodb;
|
||||||
use rocket_okapi::response::OpenApiResponderInner;
|
use rocket_okapi::response::OpenApiResponderInner;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
|
@ -30,7 +36,7 @@ impl ToString for Ordering {
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum PartyError {
|
pub enum PartyError {
|
||||||
#[error("internal error {source:?}")]
|
#[error("{source}")]
|
||||||
CoreError {
|
CoreError {
|
||||||
#[from]
|
#[from]
|
||||||
source: CoreError,
|
source: CoreError,
|
||||||
|
@ -68,13 +74,16 @@ impl OpenApiResponderInner for PartyError {
|
||||||
|
|
||||||
impl<'r, 'o: 'r> Responder<'r, 'o> for PartyError {
|
impl<'r, 'o: 'r> Responder<'r, 'o> for PartyError {
|
||||||
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> {
|
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> {
|
||||||
println!("{:#?}", &self);
|
println!("{}", self.to_string());
|
||||||
|
let message = self.to_string();
|
||||||
match self {
|
match self {
|
||||||
Self::CoreError { source } => match source {
|
Self::CoreError { source } => match source {
|
||||||
CoreError::UserNotFound(_) | CoreError::EventNotFound(_) => Status::NotFound,
|
CoreError::UserNotFound(_) | CoreError::EventNotFound(_) => {
|
||||||
_ => Status::InternalServerError,
|
(Status::NotFound, "not found".to_string())
|
||||||
|
}
|
||||||
|
_ => (Status::BadRequest, message),
|
||||||
},
|
},
|
||||||
_ => Status::InternalServerError,
|
_ => (Status::InternalServerError, "unknown error".to_string()),
|
||||||
}
|
}
|
||||||
.respond_to(req)
|
.respond_to(req)
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View<G> {
|
||||||
let children = props.children.call(cx);
|
let children = props.children.call(cx);
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
details {
|
details(open=true) {
|
||||||
summary { (props.title) }
|
summary { (props.title) }
|
||||||
p { (children) }
|
p { (children) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,11 @@ use std::{
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::components::Block;
|
use crate::{
|
||||||
|
components::Block,
|
||||||
|
util::{ContextOptions, WithContext},
|
||||||
|
view::Viewable,
|
||||||
|
};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use paste::paste;
|
use paste::paste;
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
|
@ -284,7 +288,6 @@ where
|
||||||
vec.get()
|
vec.get()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
|
||||||
.map(|x| x.get().as_ref().clone())
|
.map(|x| x.get().as_ref().clone())
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
|
@ -459,6 +462,58 @@ impl From<User> for WithLabel<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ContextEdit;
|
||||||
|
|
||||||
|
impl<'a, G, Ctx, T> Editor<'a, G, WithContext<Ctx, T>> for ContextEdit
|
||||||
|
where
|
||||||
|
G: Html,
|
||||||
|
Ctx: ContextOptions<T> + 'static,
|
||||||
|
T: FromStr + ToString + Default + Clone + PartialEq,
|
||||||
|
{
|
||||||
|
fn edit(cx: Scope<'a>, props: EditProps<'a, WithContext<Ctx, T>>) -> View<G> {
|
||||||
|
let ctx = use_context::<Ctx>(cx);
|
||||||
|
let value = create_signal(cx, props.state.get_untracked().to_string());
|
||||||
|
|
||||||
|
create_effect(cx, move || {
|
||||||
|
if value.get_untracked().is_empty() {
|
||||||
|
if let Some(first) = ctx.options(cx).get().get(0) {
|
||||||
|
value.set(first.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
create_effect(cx, move || {
|
||||||
|
props
|
||||||
|
.state
|
||||||
|
.set((*value.get()).parse::<T>().unwrap_or(T::default()).into())
|
||||||
|
});
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
select(bind:value=value) {
|
||||||
|
Indexed(
|
||||||
|
iterable=ctx.options(cx),
|
||||||
|
view=move |cx: BoundedScope<'_, 'a>, x| {
|
||||||
|
let x = create_ref(cx, x);
|
||||||
|
view! { cx,
|
||||||
|
option(value=x.to_string(), selected=x.to_string()==*value.get()) { (x.to_string()) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
" "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, G, Ctx, T> Editable<'a, G> for WithContext<Ctx, T>
|
||||||
|
where
|
||||||
|
G: Html,
|
||||||
|
Ctx: ContextOptions<T> + 'static,
|
||||||
|
T: FromStr + ToString + Default + Clone + PartialEq,
|
||||||
|
{
|
||||||
|
type Editor = ContextEdit;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct Test {
|
pub struct Test {
|
||||||
inner: TestInner,
|
inner: TestInner,
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
#[cfg(feature = "sycamore")]
|
#[cfg(feature = "sycamore")]
|
||||||
use crate::edit::prelude::*;
|
use crate::edit::prelude::*;
|
||||||
use crate::util::PartyError;
|
|
||||||
#[cfg(feature = "sycamore")]
|
#[cfg(feature = "sycamore")]
|
||||||
use crate::view::prelude::*;
|
use crate::view::prelude::*;
|
||||||
|
use crate::{
|
||||||
|
state::Users,
|
||||||
|
util::{PartyError, WithContext},
|
||||||
|
};
|
||||||
#[cfg(feature = "sycamore")]
|
#[cfg(feature = "sycamore")]
|
||||||
use lan_party_macros::{WebEdit, WebView};
|
use lan_party_macros::{WebEdit, WebView};
|
||||||
use paste::paste;
|
use paste::paste;
|
||||||
|
@ -10,13 +13,54 @@ use paste::paste;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
hash::Hash,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
type User = WithContext<Users, String>;
|
||||||
|
type Team = String;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
|
pub enum Ranking<T: Hash + PartialEq + Eq + Clone + Default + std::fmt::Debug> {
|
||||||
|
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
||||||
|
/// place, etc.)
|
||||||
|
Ranking(Vec<T>),
|
||||||
|
/// Score based ranking of participants/teams
|
||||||
|
Scores(HashMap<T, i64>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Hash + PartialEq + Eq + Clone + Default + std::fmt::Debug> Default for Ranking<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Ranking(Vec::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Hash + PartialEq + Eq + Clone + Default + std::fmt::Debug> Ranking<T> {
|
||||||
|
pub fn is_valid(&self, participants: &HashSet<T>) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Ranking(v) => v.iter().all(|p| participants.contains(p)),
|
||||||
|
Self::Scores(m) => m.keys().all(|p| participants.contains(p)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct EventOutcome {
|
pub struct EventOutcome {
|
||||||
pub points: HashMap<String, i64>,
|
pub points: HashMap<User, i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EventOutcome {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
points: HashMap::<User, _>::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Event
|
/// # Event
|
||||||
|
@ -26,6 +70,7 @@ pub struct EventOutcome {
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebView))]
|
||||||
|
#[cfg_attr(feature = "sycamore", view(title = "name"))]
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
/// Has this event concluded?
|
/// Has this event concluded?
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
|
@ -104,6 +149,10 @@ macro_rules! events {
|
||||||
|
|
||||||
impl EventSpec {
|
impl EventSpec {
|
||||||
pub fn create_event(self) -> Result<Event, PartyError> {
|
pub fn create_event(self) -> Result<Event, PartyError> {
|
||||||
|
if self.name.is_empty() {
|
||||||
|
return Err(PartyError::Other("invalid name".into()))
|
||||||
|
}
|
||||||
|
|
||||||
let event_type = match self.event_type {
|
let event_type = match self.event_type {
|
||||||
$(EventTypeSpec::$name(s) => {
|
$(EventTypeSpec::$name(s) => {
|
||||||
EventType::$name($module::$name::from_spec(s))
|
EventType::$name($module::$name::from_spec(s))
|
||||||
|
@ -127,7 +176,7 @@ macro_rules! events {
|
||||||
$((EventType::$name(s), EventUpdate::$name(u)) => {
|
$((EventType::$name(s), EventUpdate::$name(u)) => {
|
||||||
s.apply_update(u)
|
s.apply_update(u)
|
||||||
})*
|
})*
|
||||||
_ => Err(PartyError::Unknown("invalid update".into())),
|
_ => Err(PartyError::Other("invalid update: update type does not match event type".into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,19 +253,17 @@ pub mod test {
|
||||||
|
|
||||||
fn outcome(&self) -> EventOutcome {
|
fn outcome(&self) -> EventOutcome {
|
||||||
let mut points = HashMap::new();
|
let mut points = HashMap::new();
|
||||||
points.insert("420".into(), self.num_players);
|
points.insert("420".to_string().into(), self.num_players);
|
||||||
EventOutcome { points }
|
EventOutcome { points }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod team_game {
|
pub mod team_game {
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
free_for_all_game::{
|
free_for_all_game::{FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate},
|
||||||
FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate,
|
|
||||||
FreeForAllGameUpdateParticipants, FreeForAllGameUpdateRanking,
|
|
||||||
FreeForAllGameUpdateRewards,
|
|
||||||
},
|
|
||||||
*,
|
*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -226,7 +273,7 @@ pub mod team_game {
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub struct TeamGame {
|
pub struct TeamGame {
|
||||||
/// Map of teams with a name as key and an array of players as value
|
/// Map of teams with a name as key and an array of players as value
|
||||||
pub teams: HashMap<String, Vec<String>>,
|
pub teams: HashMap<Team, Vec<User>>,
|
||||||
|
|
||||||
#[cfg_attr(feature = "serde", serde(flatten))]
|
#[cfg_attr(feature = "serde", serde(flatten))]
|
||||||
pub ffa_game: FreeForAllGame,
|
pub ffa_game: FreeForAllGame,
|
||||||
|
@ -238,7 +285,7 @@ pub mod team_game {
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub struct TeamGameSpec {
|
pub struct TeamGameSpec {
|
||||||
/// Map of teams with a name as key and an array of players as value
|
/// Map of teams with a name as key and an array of players as value
|
||||||
pub teams: HashMap<String, Vec<String>>,
|
pub teams: HashMap<Team, Vec<User>>,
|
||||||
|
|
||||||
/// Rewards for winning the game (first element for first place, second element for second
|
/// Rewards for winning the game (first element for first place, second element for second
|
||||||
/// place, etc.)
|
/// place, etc.)
|
||||||
|
@ -261,44 +308,8 @@ pub mod team_game {
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub struct TeamGameUpdateSetTeam {
|
pub struct TeamGameUpdateSetTeam {
|
||||||
pub team: String,
|
pub team: Team,
|
||||||
pub members: Vec<String>,
|
pub members: Vec<User>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum TeamGameUpdateInner {
|
|
||||||
/// Add or replace a team with the given name and array of members
|
|
||||||
SetTeam(TeamGameUpdateSetTeam),
|
|
||||||
|
|
||||||
/// Remove team with given name
|
|
||||||
RemoveTeam(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TeamGameUpdateInner {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::SetTeam(TeamGameUpdateSetTeam::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum TeamGameFfaInheritedUpdate {
|
|
||||||
/// Change the ranking and scores
|
|
||||||
Ranking(FreeForAllGameUpdateRanking),
|
|
||||||
/// Update rewards
|
|
||||||
Rewards(FreeForAllGameUpdateRewards),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TeamGameFfaInheritedUpdate {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Ranking(FreeForAllGameUpdateRanking::default())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -307,19 +318,34 @@ pub mod team_game {
|
||||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub enum TeamGameUpdate {
|
pub enum TeamGameUpdate {
|
||||||
/// # Team
|
/// Add or replace a team with the given name and array of members
|
||||||
///
|
SetTeam(TeamGameUpdateSetTeam),
|
||||||
/// Team specific updates
|
/// Remove team with given name
|
||||||
Team(TeamGameUpdateInner),
|
RemoveTeam(Team),
|
||||||
/// # Other
|
/// Replace the current ranking with the given ranking
|
||||||
///
|
SetRanking(Ranking<Team>),
|
||||||
/// Inherited from FreeForAllGame
|
/// If the current ranking is of type `Scores`, apply the given score deltas
|
||||||
Ffa(TeamGameFfaInheritedUpdate),
|
ScoreDelta(HashMap<Team, i64>),
|
||||||
|
/// Set rewards for winning the game
|
||||||
|
SetWinRewards(Vec<i64>),
|
||||||
|
/// Set rewards for losing the game
|
||||||
|
SetLoseRewards(Vec<i64>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TeamGameUpdate {
|
impl Default for TeamGameUpdate {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
TeamGameUpdate::Team(TeamGameUpdateInner::default())
|
TeamGameUpdate::SetTeam(TeamGameUpdateSetTeam::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Ranking<Team>> for Ranking<User> {
|
||||||
|
fn from(r: Ranking<Team>) -> Self {
|
||||||
|
match r {
|
||||||
|
Ranking::Scores(s) => {
|
||||||
|
Ranking::Scores(s.into_iter().map(|(k, v)| (k.into(), v)).collect())
|
||||||
|
}
|
||||||
|
Ranking::Ranking(s) => Ranking::Ranking(s.into_iter().map(|k| k.into()).collect()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,7 +355,7 @@ pub mod team_game {
|
||||||
|
|
||||||
fn from_spec(spec: TeamGameSpec) -> Self {
|
fn from_spec(spec: TeamGameSpec) -> Self {
|
||||||
let ffa_game_spec = FreeForAllGameSpec {
|
let ffa_game_spec = FreeForAllGameSpec {
|
||||||
participants: spec.teams.keys().cloned().collect(),
|
participants: spec.teams.keys().map(|k| User::from(k.clone())).collect(),
|
||||||
win_rewards: spec.win_rewards,
|
win_rewards: spec.win_rewards,
|
||||||
lose_rewards: spec.lose_rewards,
|
lose_rewards: spec.lose_rewards,
|
||||||
};
|
};
|
||||||
|
@ -342,32 +368,42 @@ pub mod team_game {
|
||||||
|
|
||||||
fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> {
|
fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> {
|
||||||
match update {
|
match update {
|
||||||
TeamGameUpdate::Ffa(update) => match update {
|
TeamGameUpdate::SetRanking(x) => self
|
||||||
TeamGameFfaInheritedUpdate::Ranking(u) => {
|
.ffa_game
|
||||||
self.ffa_game.apply_update(FreeForAllGameUpdate::Ranking(u))
|
.apply_update(FreeForAllGameUpdate::SetRanking(x.into())),
|
||||||
|
TeamGameUpdate::ScoreDelta(x) => {
|
||||||
|
self.ffa_game.apply_update(FreeForAllGameUpdate::ScoreDelta(
|
||||||
|
x.into_iter().map(|(k, v)| (k.into(), v)).collect(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
TeamGameFfaInheritedUpdate::Rewards(u) => {
|
TeamGameUpdate::SetWinRewards(x) => self
|
||||||
self.ffa_game.apply_update(FreeForAllGameUpdate::Rewards(u))
|
.ffa_game
|
||||||
}
|
.apply_update(FreeForAllGameUpdate::SetWinRewards(x)),
|
||||||
},
|
TeamGameUpdate::SetLoseRewards(x) => self
|
||||||
TeamGameUpdate::Team(update) => match update {
|
.ffa_game
|
||||||
TeamGameUpdateInner::SetTeam(u) => {
|
.apply_update(FreeForAllGameUpdate::SetLoseRewards(x)),
|
||||||
|
TeamGameUpdate::SetTeam(u) => {
|
||||||
self.ffa_game
|
self.ffa_game
|
||||||
.apply_update(FreeForAllGameUpdate::Participants(
|
.apply_update(FreeForAllGameUpdate::AddParticipant(
|
||||||
FreeForAllGameUpdateParticipants::AddParticipant(u.team.clone()),
|
u.team.clone().into(),
|
||||||
))?;
|
))?;
|
||||||
self.teams.insert(u.team, u.members);
|
self.teams.insert(
|
||||||
|
u.team,
|
||||||
|
u.members
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| (t.clone()).deref().clone().into())
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
TeamGameUpdateInner::RemoveTeam(team) => {
|
TeamGameUpdate::RemoveTeam(team) => {
|
||||||
self.ffa_game
|
self.ffa_game
|
||||||
.apply_update(FreeForAllGameUpdate::Participants(
|
.apply_update(FreeForAllGameUpdate::RemoveParticipant(
|
||||||
FreeForAllGameUpdateParticipants::RemoveParticipant(team.clone()),
|
team.clone().into(),
|
||||||
))?;
|
))?;
|
||||||
self.teams.remove(&team);
|
self.teams.remove(&team);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,10 +413,10 @@ pub mod team_game {
|
||||||
let mut points = HashMap::new();
|
let mut points = HashMap::new();
|
||||||
|
|
||||||
for (team, reward) in ffa_outcome.points.iter() {
|
for (team, reward) in ffa_outcome.points.iter() {
|
||||||
if let Some(team) = self.teams.get(team) {
|
if let Some(team) = self.teams.get(&*team.to_string()) {
|
||||||
for player in team {
|
for player in team {
|
||||||
let score = points.get(player).unwrap_or(&0);
|
let score = points.get(&player.clone().into()).unwrap_or(&0);
|
||||||
points.insert(player.clone(), score + reward);
|
points.insert(User::from(player.clone()), score + reward);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -395,32 +431,15 @@ pub mod free_for_all_game {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
/*
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub enum FreeForAllGameRanking {
|
pub struct User {
|
||||||
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
name: String,
|
||||||
/// place, etc.)
|
|
||||||
Ranking(Vec<String>),
|
|
||||||
/// Score based ranking of participants/teams
|
|
||||||
Scores(HashMap<String, i64>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FreeForAllGameRanking {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Ranking(Vec::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FreeForAllGameRanking {
|
|
||||||
pub fn is_valid(&self, participants: &HashSet<String>) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Ranking(v) => v.iter().all(|p| participants.contains(p)),
|
|
||||||
Self::Scores(m) => m.keys().all(|p| participants.contains(p)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
|
@ -429,7 +448,7 @@ pub mod free_for_all_game {
|
||||||
pub struct FreeForAllGame {
|
pub struct FreeForAllGame {
|
||||||
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
||||||
/// place, etc.)
|
/// place, etc.)
|
||||||
pub ranking: Option<FreeForAllGameRanking>,
|
pub ranking: Option<Ranking<User>>,
|
||||||
|
|
||||||
/// Specification of the game
|
/// Specification of the game
|
||||||
#[cfg_attr(feature = "serde", serde(flatten))]
|
#[cfg_attr(feature = "serde", serde(flatten))]
|
||||||
|
@ -450,7 +469,7 @@ pub mod free_for_all_game {
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub struct FreeForAllGameSpec {
|
pub struct FreeForAllGameSpec {
|
||||||
/// Array of user ids that participate in the game
|
/// Array of user ids that participate in the game
|
||||||
pub participants: HashSet<String>,
|
pub participants: HashSet<User>,
|
||||||
|
|
||||||
/// Rewards for winning the game (first element for first place, second element for second
|
/// Rewards for winning the game (first element for first place, second element for second
|
||||||
/// place, etc.)
|
/// place, etc.)
|
||||||
|
@ -462,80 +481,31 @@ pub mod free_for_all_game {
|
||||||
pub lose_rewards: Vec<i64>,
|
pub lose_rewards: Vec<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum FreeForAllGameUpdateRanking {
|
|
||||||
/// Replace the current ranking with the given ranking
|
|
||||||
SetRanking(FreeForAllGameRanking),
|
|
||||||
|
|
||||||
/// If the current ranking is of type `Scores`, apply the given score deltas
|
|
||||||
ScoreDelta(HashMap<String, i64>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FreeForAllGameUpdateRanking {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::SetRanking(FreeForAllGameRanking::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum FreeForAllGameUpdateRewards {
|
|
||||||
/// Set rewards for winning the game
|
|
||||||
SetWinRewards(Vec<i64>),
|
|
||||||
|
|
||||||
/// Set rewards for losing the game
|
|
||||||
SetLoseRewards(Vec<i64>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FreeForAllGameUpdateRewards {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::SetWinRewards(Vec::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum FreeForAllGameUpdateParticipants {
|
|
||||||
/// Set list of participants participating in the game
|
|
||||||
SetParticipants(HashSet<String>),
|
|
||||||
|
|
||||||
/// Add participant by name
|
|
||||||
AddParticipant(String),
|
|
||||||
|
|
||||||
/// Remove participant by name
|
|
||||||
RemoveParticipant(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FreeForAllGameUpdateParticipants {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::SetParticipants(HashSet::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub enum FreeForAllGameUpdate {
|
pub enum FreeForAllGameUpdate {
|
||||||
/// Change the ranking and scores
|
/// Replace the current ranking with the given ranking
|
||||||
Ranking(FreeForAllGameUpdateRanking),
|
SetRanking(Ranking<User>),
|
||||||
/// Update rewards
|
/// If the current ranking is of type `Scores`, apply the given score deltas
|
||||||
Rewards(FreeForAllGameUpdateRewards),
|
ScoreDelta(HashMap<User, i64>),
|
||||||
/// Update participants
|
/// Set rewards for winning the game
|
||||||
Participants(FreeForAllGameUpdateParticipants),
|
SetWinRewards(Vec<i64>),
|
||||||
|
/// Set rewards for losing the game
|
||||||
|
SetLoseRewards(Vec<i64>),
|
||||||
|
/// Set list of participants participating in the game
|
||||||
|
SetParticipants(HashSet<User>),
|
||||||
|
/// Add participant by name
|
||||||
|
AddParticipant(User),
|
||||||
|
/// Remove participant by name
|
||||||
|
RemoveParticipant(User),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FreeForAllGameUpdate {
|
impl Default for FreeForAllGameUpdate {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Ranking(FreeForAllGameUpdateRanking::default())
|
Self::AddParticipant(String::new().into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -552,31 +522,28 @@ pub mod free_for_all_game {
|
||||||
|
|
||||||
fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> {
|
fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> {
|
||||||
match update {
|
match update {
|
||||||
FreeForAllGameUpdate::Ranking(update) => match update {
|
FreeForAllGameUpdate::SetRanking(r) => {
|
||||||
FreeForAllGameUpdateRanking::SetRanking(r) => {
|
|
||||||
if !r.is_valid(&self.spec.participants) {
|
if !r.is_valid(&self.spec.participants) {
|
||||||
return Err(PartyError::Unknown("invalid ranking, all participants mentioned in ranking must be participating".into()));
|
return Err(PartyError::Other("invalid ranking, all participants mentioned in ranking must be participating".into()));
|
||||||
}
|
}
|
||||||
self.ranking = Some(r)
|
self.ranking = Some(r)
|
||||||
}
|
}
|
||||||
FreeForAllGameUpdateRanking::ScoreDelta(d) => match &mut self.ranking {
|
FreeForAllGameUpdate::ScoreDelta(d) => match &mut self.ranking {
|
||||||
Some(FreeForAllGameRanking::Ranking(_)) | None => {
|
Some(Ranking::Ranking(_)) => {
|
||||||
return Err(PartyError::Unknown("cannot apply score delta".into()))
|
return Err(PartyError::Other("cannot apply score delta".into()))
|
||||||
}
|
}
|
||||||
Some(FreeForAllGameRanking::Scores(s)) => {
|
None => self.ranking = Some(Ranking::Scores(d)),
|
||||||
|
Some(Ranking::Scores(s)) => {
|
||||||
for (participant, delta) in d.iter() {
|
for (participant, delta) in d.iter() {
|
||||||
if let Some(value) = s.get(participant) {
|
let value = s.get(participant).unwrap_or(&0);
|
||||||
s.insert(participant.clone(), value + delta);
|
s.insert(participant.clone(), value + delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
FreeForAllGameUpdate::AddParticipant(name) => {
|
||||||
FreeForAllGameUpdate::Participants(update) => match update {
|
|
||||||
FreeForAllGameUpdateParticipants::AddParticipant(name) => {
|
|
||||||
self.spec.participants.insert(name);
|
self.spec.participants.insert(name);
|
||||||
}
|
}
|
||||||
FreeForAllGameUpdateParticipants::RemoveParticipant(name) => {
|
FreeForAllGameUpdate::RemoveParticipant(name) => {
|
||||||
self.spec.participants.remove(&name);
|
self.spec.participants.remove(&name);
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
|
@ -586,37 +553,34 @@ pub mod free_for_all_game {
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
{
|
{
|
||||||
self.spec.participants.insert(name);
|
self.spec.participants.insert(name);
|
||||||
return Err(PartyError::Unknown("cannot remove participant, all participants mentioned in ranking must be participating".into()));
|
return Err(PartyError::Other("cannot remove participant, all participants mentioned in ranking must be participating".into()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FreeForAllGameUpdateParticipants::SetParticipants(participants) => {
|
FreeForAllGameUpdate::SetParticipants(participants) => {
|
||||||
if !self
|
if !self
|
||||||
.ranking
|
.ranking
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|r| r.is_valid(&participants))
|
.map(|r| r.is_valid(&participants))
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
{
|
{
|
||||||
return Err(PartyError::Unknown("invalid list of participants, all participants mentioned in ranking must be participating".into()));
|
return Err(PartyError::Other("invalid list of participants, all participants mentioned in ranking must be participating".into()));
|
||||||
}
|
}
|
||||||
self.spec.participants = participants;
|
self.spec.participants = participants;
|
||||||
}
|
}
|
||||||
},
|
FreeForAllGameUpdate::SetWinRewards(rewards) => {
|
||||||
FreeForAllGameUpdate::Rewards(update) => match update {
|
|
||||||
FreeForAllGameUpdateRewards::SetWinRewards(rewards) => {
|
|
||||||
self.spec.win_rewards = rewards;
|
self.spec.win_rewards = rewards;
|
||||||
}
|
}
|
||||||
FreeForAllGameUpdateRewards::SetLoseRewards(rewards) => {
|
FreeForAllGameUpdate::SetLoseRewards(rewards) => {
|
||||||
self.spec.lose_rewards = rewards;
|
self.spec.lose_rewards = rewards;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn outcome(&self) -> EventOutcome {
|
fn outcome(&self) -> EventOutcome {
|
||||||
let ranking = match &self.ranking {
|
let ranking = match &self.ranking {
|
||||||
Some(FreeForAllGameRanking::Ranking(r)) => r.clone(),
|
Some(Ranking::Ranking(r)) => r.clone(),
|
||||||
Some(FreeForAllGameRanking::Scores(s)) => {
|
Some(Ranking::Scores(s)) => {
|
||||||
let mut results: Vec<(_, _)> = s.iter().collect();
|
let mut results: Vec<(_, _)> = s.iter().collect();
|
||||||
results.sort_by(|a, b| b.1.cmp(a.1));
|
results.sort_by(|a, b| b.1.cmp(a.1));
|
||||||
results.into_iter().map(|(k, _)| k.clone()).collect()
|
results.into_iter().map(|(k, _)| k.clone()).collect()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
pub mod state;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
#[cfg(feature = "sycamore")]
|
||||||
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
|
use crate::{event::Event, user::User};
|
||||||
|
|
||||||
|
#[cfg(feature = "sycamore")]
|
||||||
|
use crate::util::ContextOptions;
|
||||||
|
|
||||||
|
#[cfg(feature = "sycamore")]
|
||||||
|
#[derive(Clone, PartialEq, Default)]
|
||||||
|
pub struct Users(pub RcSignal<Vec<User>>);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "sycamore"))]
|
||||||
|
#[derive(Clone, PartialEq, Default)]
|
||||||
|
pub struct Users;
|
||||||
|
|
||||||
|
#[cfg(feature = "sycamore")]
|
||||||
|
impl Users {
|
||||||
|
pub fn get(&self) -> &RcSignal<Vec<User>> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sycamore")]
|
||||||
|
impl ContextOptions<String> for Users {
|
||||||
|
fn options<'a>(&'a self, cx: Scope<'a>) -> &'a ReadSignal<Vec<String>> {
|
||||||
|
self.get()
|
||||||
|
.map(cx, |v| v.iter().map(|u| u.name.clone()).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sycamore")]
|
||||||
|
#[derive(Clone, PartialEq, Default)]
|
||||||
|
pub struct Events(pub RcSignal<Vec<Event>>);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "sycamore"))]
|
||||||
|
#[derive(Clone, PartialEq, Default)]
|
||||||
|
pub struct Events;
|
||||||
|
|
||||||
|
#[cfg(feature = "sycamore")]
|
||||||
|
impl Events {
|
||||||
|
pub fn get(&self) -> &RcSignal<Vec<Event>> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sycamore")]
|
||||||
|
impl ContextOptions<String> for Events {
|
||||||
|
fn options<'a>(&'a self, cx: Scope<'a>) -> &'a ReadSignal<Vec<String>> {
|
||||||
|
self.get()
|
||||||
|
.map(cx, |v| v.iter().map(|u| u.name.clone()).collect())
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,12 @@ use rocket::FromFormField;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
hash::Hash,
|
||||||
|
marker::PhantomData,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -12,8 +18,8 @@ pub enum PartyError {
|
||||||
UserNotFound(String),
|
UserNotFound(String),
|
||||||
#[error("event `{0}` does not exist")]
|
#[error("event `{0}` does not exist")]
|
||||||
EventNotFound(String),
|
EventNotFound(String),
|
||||||
#[error("unknown error: {0}")]
|
#[error("{0}")]
|
||||||
Unknown(String),
|
Other(String),
|
||||||
#[error("invalid parameter: {0}")]
|
#[error("invalid parameter: {0}")]
|
||||||
InvalidParameter(String),
|
InvalidParameter(String),
|
||||||
}
|
}
|
||||||
|
@ -42,3 +48,86 @@ impl ToString for Ordering {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct WithContext<Ctx, T> {
|
||||||
|
#[serde(skip)]
|
||||||
|
_ctx: PhantomData<Ctx>,
|
||||||
|
inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
impl<Ctx, T> JsonSchema for WithContext<Ctx, T>
|
||||||
|
where
|
||||||
|
T: JsonSchema,
|
||||||
|
{
|
||||||
|
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||||
|
T::json_schema(gen)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema_name() -> String {
|
||||||
|
T::schema_name()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx, T> Debug for WithContext<Ctx, T>
|
||||||
|
where
|
||||||
|
T: Debug,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.inner.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx, T> Hash for WithContext<Ctx, T>
|
||||||
|
where
|
||||||
|
T: Hash,
|
||||||
|
{
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.inner.hash(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx, T> PartialEq for WithContext<Ctx, T>
|
||||||
|
where
|
||||||
|
T: PartialEq,
|
||||||
|
{
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.inner == other.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx, T> Eq for WithContext<Ctx, T> where T: Eq {}
|
||||||
|
|
||||||
|
impl<Ctx, T> Deref for WithContext<Ctx, T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx, T> DerefMut for WithContext<Ctx, T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx, T> From<T> for WithContext<Ctx, T> {
|
||||||
|
fn from(inner: T) -> Self {
|
||||||
|
Self {
|
||||||
|
_ctx: PhantomData,
|
||||||
|
inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sycamore")]
|
||||||
|
use sycamore::reactive::{ReadSignal, Scope};
|
||||||
|
|
||||||
|
#[cfg(feature = "sycamore")]
|
||||||
|
pub trait ContextOptions<T> {
|
||||||
|
fn options<'a>(&'a self, cx: Scope<'a>) -> &'a ReadSignal<Vec<T>>;
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
|
ops::Deref,
|
||||||
};
|
};
|
||||||
use sycamore::view::IntoView as _;
|
use sycamore::view::IntoView as _;
|
||||||
|
|
||||||
|
@ -14,7 +15,10 @@ pub mod prelude {
|
||||||
pub use sycamore::prelude::*;
|
pub use sycamore::prelude::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::components::{Block, BlockProps};
|
use crate::{
|
||||||
|
components::{Block, BlockProps},
|
||||||
|
util::WithContext,
|
||||||
|
};
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! viewable {
|
macro_rules! viewable {
|
||||||
|
@ -174,6 +178,7 @@ impl<'a, G: Html, A: for<'b> Viewable<'b, G> + Clone, B: for<'b> Viewable<'b, G>
|
||||||
view! { cx,
|
view! { cx,
|
||||||
Block(title="Tuple".into()) {
|
Block(title="Tuple".into()) {
|
||||||
(props.state.0.view(cx))
|
(props.state.0.view(cx))
|
||||||
|
br()
|
||||||
(props.state.1.view(cx))
|
(props.state.1.view(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,3 +208,17 @@ impl<'a, G: Html, T: for<'b> Viewable<'b, G> + Clone> Viewer<'a, G, Option<T>> f
|
||||||
impl<'a, G: Html, T: for<'b> Viewable<'b, G> + Clone> Viewable<'a, G> for Option<T> {
|
impl<'a, G: Html, T: for<'b> Viewable<'b, G> + Clone> Viewable<'a, G> for Option<T> {
|
||||||
type Viewer = OptionView;
|
type Viewer = OptionView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ContextView;
|
||||||
|
|
||||||
|
impl<'a, G: Html, Ctx, T: for<'b> Viewable<'b, G> + Clone> Viewer<'a, G, WithContext<Ctx, T>>
|
||||||
|
for ContextView
|
||||||
|
{
|
||||||
|
fn view(cx: Scope<'a>, props: ViewProps<'a, WithContext<Ctx, T>>) -> View<G> {
|
||||||
|
props.state.deref().view(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, G: Html, Ctx, T: for<'b> Viewable<'b, G> + Clone> Viewable<'a, G> for WithContext<Ctx, T> {
|
||||||
|
type Viewer = ContextView;
|
||||||
|
}
|
||||||
|
|
|
@ -14,4 +14,5 @@ quote = "1.0"
|
||||||
sycamore = { version = "0.8.1", features = ["serde", "suspense"] }
|
sycamore = { version = "0.8.1", features = ["serde", "suspense"] }
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
convert_case = "0.6"
|
convert_case = "0.6"
|
||||||
|
darling = "0.14"
|
||||||
#lan_party_core = { path = "../core", features = ["sycamore"] }
|
#lan_party_core = { path = "../core", features = ["sycamore"] }
|
||||||
|
|
|
@ -1,9 +1,20 @@
|
||||||
mod edit;
|
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::{__private::TokenStream as TokenStream2, format_ident, quote};
|
use quote::{__private::TokenStream as TokenStream2, format_ident, quote};
|
||||||
use syn::{token::Do, Attribute, DataEnum, DataStruct, Fields, Ident, Path, Type};
|
use syn::{
|
||||||
|
parse::Parse, parse_str, token::Do, Attribute, DataEnum, DataStruct, Fields, Generics, Ident,
|
||||||
|
LifetimeDef, Lit, MetaNameValue, Path, PredicateType, Type, TypeParam,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate darling;
|
||||||
|
|
||||||
|
enum ParsedAttribute {
|
||||||
|
Documentation(Documentation),
|
||||||
|
View(ViewAttribute),
|
||||||
|
Serde(SerdeAttribute),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Documentation {
|
enum Documentation {
|
||||||
|
@ -12,6 +23,16 @@ enum Documentation {
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ViewAttribute {
|
||||||
|
Title(Ident),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SerdeAttribute {
|
||||||
|
Untagged,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
impl Documentation {
|
impl Documentation {
|
||||||
fn parse(attr: &Attribute) -> Documentation {
|
fn parse(attr: &Attribute) -> Documentation {
|
||||||
if !attr.path.is_ident("doc") {
|
if !attr.path.is_ident("doc") {
|
||||||
|
@ -30,14 +51,80 @@ impl Documentation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_title_description(attrs: &[Attribute]) -> (Option<String>, Option<String>) {
|
impl ViewAttribute {
|
||||||
let docs: Vec<_> = attrs.iter().map(Documentation::parse).collect();
|
fn parse(attr: &Attribute) -> ViewAttribute {
|
||||||
|
if !attr.path.is_ident("view") {
|
||||||
|
return Self::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: Result<MetaNameValue, _> = parse_str(
|
||||||
|
attr.tokens
|
||||||
|
.to_string()
|
||||||
|
.trim_matches(|c: char| c == '(' || c == ')'),
|
||||||
|
);
|
||||||
|
|
||||||
|
match parsed {
|
||||||
|
Ok(p) => match (p.path.get_ident().unwrap().to_string().as_str(), p.lit) {
|
||||||
|
("title", Lit::Str(ls)) => Self::Title(format_ident!("{}", ls.value())),
|
||||||
|
_ => Self::None,
|
||||||
|
},
|
||||||
|
Err(_) => Self::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerdeAttribute {
|
||||||
|
fn parse(attr: &Attribute) -> SerdeAttribute {
|
||||||
|
if !attr.path.is_ident("serde") {
|
||||||
|
return Self::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: Result<Ident, _> = parse_str(
|
||||||
|
attr.tokens
|
||||||
|
.to_string()
|
||||||
|
.trim_matches(|c: char| c == '(' || c == ')'),
|
||||||
|
);
|
||||||
|
|
||||||
|
match parsed {
|
||||||
|
Ok(p) => match p.to_string().as_str() {
|
||||||
|
"untagged" => Self::Untagged,
|
||||||
|
_ => Self::None,
|
||||||
|
},
|
||||||
|
Err(_) => Self::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParsedAttribute {
|
||||||
|
fn parse(attr: &Attribute) -> ParsedAttribute {
|
||||||
|
match attr.path.get_ident() {
|
||||||
|
Some(i) if i.to_string() == "doc" => Self::Documentation(Documentation::parse(attr)),
|
||||||
|
Some(i) if i.to_string() == "view" => Self::View(ViewAttribute::parse(attr)),
|
||||||
|
Some(i) if i.to_string() == "serde" => Self::Serde(SerdeAttribute::parse(attr)),
|
||||||
|
_ => Self::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Attributes {
|
||||||
|
title: Option<String>,
|
||||||
|
title_field: Option<Ident>,
|
||||||
|
description: Option<String>,
|
||||||
|
untagged: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Attributes {
|
||||||
|
pub fn parse(attrs: &[Attribute]) -> Self {
|
||||||
|
let parsed: Vec<_> = attrs.iter().map(ParsedAttribute::parse).collect();
|
||||||
|
|
||||||
let mut title = None;
|
let mut title = None;
|
||||||
|
let mut title_field = None;
|
||||||
let mut description: Option<String> = None;
|
let mut description: Option<String> = None;
|
||||||
|
let mut untagged = false;
|
||||||
|
|
||||||
for doc in docs {
|
for attr in parsed {
|
||||||
match doc {
|
match attr {
|
||||||
|
ParsedAttribute::Documentation(doc) => match doc {
|
||||||
Documentation::Title(t) => title = Some(t),
|
Documentation::Title(t) => title = Some(t),
|
||||||
Documentation::Description(d) => {
|
Documentation::Description(d) => {
|
||||||
if description.is_some() {
|
if description.is_some() {
|
||||||
|
@ -48,16 +135,32 @@ fn get_title_description(attrs: &[Attribute]) -> (Option<String>, Option<String>
|
||||||
description.as_mut().unwrap().push_str(&d);
|
description.as_mut().unwrap().push_str(&d);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
},
|
||||||
|
ParsedAttribute::View(v) => match v {
|
||||||
|
ViewAttribute::Title(t) => title_field = Some(t),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
ParsedAttribute::Serde(s) => match s {
|
||||||
|
SerdeAttribute::Untagged => untagged = true,
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(title, description)
|
Self {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
title_field,
|
||||||
|
untagged,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ItemProps {
|
struct ItemProps {
|
||||||
name: Ident,
|
name: Ident,
|
||||||
title: String,
|
attributes: Attributes,
|
||||||
description: Option<String>,
|
generics: Generics,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StructField {
|
struct StructField {
|
||||||
|
@ -78,9 +181,12 @@ struct EnumVariant {
|
||||||
fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
let ItemProps {
|
let ItemProps {
|
||||||
name,
|
name,
|
||||||
title,
|
attributes: Attributes {
|
||||||
description,
|
title, description, ..
|
||||||
|
},
|
||||||
|
generics,
|
||||||
} = props;
|
} = props;
|
||||||
|
let title = title.clone().unwrap_or(name.to_string());
|
||||||
|
|
||||||
let fields = s.fields.iter().map(|f| {
|
let fields = s.fields.iter().map(|f| {
|
||||||
let name = f
|
let name = f
|
||||||
|
@ -89,7 +195,12 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
.expect("each struct field must be named")
|
.expect("each struct field must be named")
|
||||||
.clone();
|
.clone();
|
||||||
let name_str = name.to_string();
|
let name_str = name.to_string();
|
||||||
let (title, description) = get_title_description(&f.attrs);
|
let Attributes {
|
||||||
|
title,
|
||||||
|
title_field: _,
|
||||||
|
description,
|
||||||
|
..
|
||||||
|
} = Attributes::parse(&f.attrs);
|
||||||
|
|
||||||
StructField {
|
StructField {
|
||||||
name_str,
|
name_str,
|
||||||
|
@ -159,9 +270,12 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
let ItemProps {
|
let ItemProps {
|
||||||
name,
|
name,
|
||||||
title,
|
attributes: Attributes {
|
||||||
description,
|
title, description, ..
|
||||||
|
},
|
||||||
|
generics,
|
||||||
} = props;
|
} = props;
|
||||||
|
let title = title.clone().unwrap_or(name.to_string());
|
||||||
|
|
||||||
let variants = e.variants.iter().map(|v| {
|
let variants = e.variants.iter().map(|v| {
|
||||||
let variant = v.ident.clone();
|
let variant = v.ident.clone();
|
||||||
|
@ -171,7 +285,12 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (title, description) = get_title_description(&v.attrs);
|
let Attributes {
|
||||||
|
title,
|
||||||
|
title_field: _,
|
||||||
|
description,
|
||||||
|
..
|
||||||
|
} = Attributes::parse(&v.attrs);
|
||||||
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
||||||
EnumVariant {
|
EnumVariant {
|
||||||
variant_lower,
|
variant_lower,
|
||||||
|
@ -274,6 +393,7 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
#(#view_description,)*
|
#(#view_description,)*
|
||||||
_ => view! { cx, }
|
_ => view! { cx, }
|
||||||
})
|
})
|
||||||
|
br()
|
||||||
(match selected.get().as_str() {
|
(match selected.get().as_str() {
|
||||||
#(#view_match,)*
|
#(#view_match,)*
|
||||||
_ => view! { cx, }
|
_ => view! { cx, }
|
||||||
|
@ -286,9 +406,16 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
let ItemProps {
|
let ItemProps {
|
||||||
name,
|
name,
|
||||||
|
attributes:
|
||||||
|
Attributes {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
title_field,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
generics,
|
||||||
} = props;
|
} = props;
|
||||||
|
let title = title.clone().unwrap_or(name.to_string());
|
||||||
|
|
||||||
let fields = s.fields.iter().map(|f| {
|
let fields = s.fields.iter().map(|f| {
|
||||||
let name = f
|
let name = f
|
||||||
|
@ -297,7 +424,12 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
.expect("each struct field must be named")
|
.expect("each struct field must be named")
|
||||||
.clone();
|
.clone();
|
||||||
let name_str = name.to_string();
|
let name_str = name.to_string();
|
||||||
let (title, description) = get_title_description(&f.attrs);
|
let Attributes {
|
||||||
|
title,
|
||||||
|
title_field: _,
|
||||||
|
description,
|
||||||
|
..
|
||||||
|
} = Attributes::parse(&f.attrs);
|
||||||
|
|
||||||
StructField {
|
StructField {
|
||||||
name_str,
|
name_str,
|
||||||
|
@ -334,6 +466,16 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let title = if let Some(title_field) = title_field {
|
||||||
|
quote! {
|
||||||
|
state.#title_field
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
#title
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
let state = props.state;
|
let state = props.state;
|
||||||
|
|
||||||
|
@ -353,9 +495,12 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
let ItemProps {
|
let ItemProps {
|
||||||
name,
|
name,
|
||||||
title,
|
attributes: Attributes {
|
||||||
description,
|
title, description, ..
|
||||||
|
},
|
||||||
|
generics,
|
||||||
} = props;
|
} = props;
|
||||||
|
let title = title.clone().unwrap_or(name.to_string());
|
||||||
|
|
||||||
let variants = e.variants.iter().map(|v| {
|
let variants = e.variants.iter().map(|v| {
|
||||||
let variant = v.ident.clone();
|
let variant = v.ident.clone();
|
||||||
|
@ -365,7 +510,12 @@ fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (title, description) = get_title_description(&v.attrs);
|
let Attributes {
|
||||||
|
title,
|
||||||
|
title_field: _,
|
||||||
|
description,
|
||||||
|
..
|
||||||
|
} = Attributes::parse(&v.attrs);
|
||||||
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
||||||
EnumVariant {
|
EnumVariant {
|
||||||
variant_lower,
|
variant_lower,
|
||||||
|
@ -429,15 +579,12 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
||||||
let name = input.ident;
|
let name = input.ident;
|
||||||
let edit_ident = format_ident!("{}Edit", name);
|
let edit_ident = format_ident!("{}Edit", name);
|
||||||
|
|
||||||
let (t, d) = get_title_description(&input.attrs);
|
let attrs = Attributes::parse(&input.attrs);
|
||||||
|
|
||||||
let title = t.unwrap_or(name.to_string());
|
|
||||||
let description = d;
|
|
||||||
|
|
||||||
let props = ItemProps {
|
let props = ItemProps {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
title: title.clone(),
|
attributes: attrs,
|
||||||
description: description.clone(),
|
generics: input.generics.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = match input.data {
|
let inner = match input.data {
|
||||||
|
@ -446,16 +593,43 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut generics = input.generics.clone();
|
||||||
|
let input_generics = input.generics;
|
||||||
|
|
||||||
|
//if generics.type_params().count() == 0 {
|
||||||
|
generics
|
||||||
|
.params
|
||||||
|
.push(syn::GenericParam::Lifetime(syn::parse_str("'a").unwrap()));
|
||||||
|
generics
|
||||||
|
.params
|
||||||
|
.push(syn::GenericParam::Type(syn::parse_str("G: Html").unwrap()));
|
||||||
|
//}
|
||||||
|
|
||||||
|
let (input_impl_generics, input_ty_generics, input_where_clause) =
|
||||||
|
input_generics.split_for_impl();
|
||||||
|
|
||||||
|
for ty_param in input_generics.type_params() {
|
||||||
|
generics.make_where_clause().predicates.push(
|
||||||
|
syn::parse_str(&format!(
|
||||||
|
"{}: for<'b> Editable<'b, G>",
|
||||||
|
ty_param.ident.to_string()
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||||
|
|
||||||
let res = quote! {
|
let res = quote! {
|
||||||
pub struct #edit_ident;
|
pub struct #edit_ident;
|
||||||
|
|
||||||
impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident {
|
impl #impl_generics Editor<'a, G, #name #input_ty_generics> for #edit_ident #where_clause {
|
||||||
fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View<G> {
|
fn edit(cx: Scope<'a>, props: EditProps<'a, #name #input_ty_generics>) -> View<G> {
|
||||||
#inner
|
#inner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, G: Html> Editable<'a, G> for #name {
|
impl #impl_generics Editable<'a, G> for #name #input_ty_generics #where_clause {
|
||||||
type Editor = #edit_ident;
|
type Editor = #edit_ident;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -463,22 +637,19 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
||||||
TokenStream::from(res)
|
TokenStream::from(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(WebView)]
|
#[proc_macro_derive(WebView, attributes(view))]
|
||||||
pub fn web_view(tokens: TokenStream) -> TokenStream {
|
pub fn web_view(tokens: TokenStream) -> TokenStream {
|
||||||
let input: syn::DeriveInput = syn::parse(tokens).unwrap();
|
let input: syn::DeriveInput = syn::parse(tokens).unwrap();
|
||||||
|
|
||||||
let name = input.ident;
|
let name = input.ident;
|
||||||
let view_ident = format_ident!("{}View", name);
|
let view_ident = format_ident!("{}View", name);
|
||||||
|
|
||||||
let (t, d) = get_title_description(&input.attrs);
|
let attrs = Attributes::parse(&input.attrs);
|
||||||
|
|
||||||
let title = t.unwrap_or(name.to_string());
|
|
||||||
let description = d;
|
|
||||||
|
|
||||||
let props = ItemProps {
|
let props = ItemProps {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
title: title.clone(),
|
attributes: attrs,
|
||||||
description: description.clone(),
|
generics: input.generics.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = match input.data {
|
let inner = match input.data {
|
||||||
|
@ -487,16 +658,45 @@ pub fn web_view(tokens: TokenStream) -> TokenStream {
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut generics = input.generics.clone();
|
||||||
|
let input_generics = input.generics;
|
||||||
|
|
||||||
|
//if generics.type_params().count() == 0 {
|
||||||
|
generics
|
||||||
|
.params
|
||||||
|
.push(syn::GenericParam::Lifetime(syn::parse_str("'a").unwrap()));
|
||||||
|
generics
|
||||||
|
.params
|
||||||
|
.push(syn::GenericParam::Type(syn::parse_str("G: Html").unwrap()));
|
||||||
|
//}
|
||||||
|
|
||||||
|
let (input_impl_generics, input_ty_generics, input_where_clause) =
|
||||||
|
input_generics.split_for_impl();
|
||||||
|
|
||||||
|
for ty_param in input_generics.type_params() {
|
||||||
|
generics.make_where_clause().predicates.push(
|
||||||
|
syn::parse_str(&format!(
|
||||||
|
"{}: for<'b> Viewable<'b, G>",
|
||||||
|
ty_param.ident.to_string()
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||||
|
|
||||||
|
//println!("{}", &inner.to_string());
|
||||||
|
|
||||||
let res = quote! {
|
let res = quote! {
|
||||||
pub struct #view_ident;
|
pub struct #view_ident;
|
||||||
|
|
||||||
impl<'a, G: Html> Viewer<'a, G, #name> for #view_ident {
|
impl #impl_generics Viewer<'a, G, #name #input_ty_generics> for #view_ident #where_clause {
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, #name>) -> View<G> {
|
fn view(cx: Scope<'a>, props: ViewProps<'a, #name #input_ty_generics>) -> View<G> {
|
||||||
#inner
|
#inner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, G: Html> Viewable<'a, G> for #name {
|
impl #impl_generics Viewable<'a, G> for #name #input_ty_generics #where_clause {
|
||||||
type Viewer = #view_ident;
|
type Viewer = #view_ident;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
podman run -d --rm --name lan_party_db \
|
podman run -d --rm --name lan_party_db \
|
||||||
-v lan-party-db:/data/db:z \
|
-v lan-party-db-2:/data/db:z \
|
||||||
-p "27017:27017" \
|
-p "27017:27017" \
|
||||||
-e MONGO_INITDB_ROOT_USERNAME=root \
|
-e MONGO_INITDB_ROOT_USERNAME=root \
|
||||||
-e MONGO_INITDB_ROOT_PASSWORD=example \
|
-e MONGO_INITDB_ROOT_PASSWORD=example \
|
||||||
|
|
|
@ -27,6 +27,6 @@ sycamore-router = "0.8.0"
|
||||||
reqwasm = "0.5"
|
reqwasm = "0.5"
|
||||||
console_log = "0.2"
|
console_log = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
gloo-timers = "0.2"
|
||||||
|
async-trait = "0.1"
|
||||||
|
futures = "0.3"
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<!--<link data-trunk href="tailwind.css" rel="css">-->
|
<!--<link data-trunk href="tailwind.css" rel="css">-->
|
||||||
<link rel="stylesheet" href="/simple.min-d15f5ff500b4c62a.css">
|
<link rel="stylesheet" href="/simple.min-d15f5ff500b4c62a.css">
|
||||||
<link rel="stylesheet" href="/style-5e1a36be9e479fbe.css">
|
<link rel="stylesheet" href="/style-95f29b132ea1fddf.css">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" rel="stylesheet">
|
||||||
<title>LAN Party</title>
|
<title>LAN Party</title>
|
||||||
|
|
||||||
<link rel="preload" href="/index-61979c95126900f4_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
<link rel="preload" href="/index-1e8437b6157bd12b_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||||
<link rel="modulepreload" href="/index-61979c95126900f4.js"></head>
|
<link rel="modulepreload" href="/index-1e8437b6157bd12b.js"></head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module">import init from '/index-61979c95126900f4.js';init('/index-61979c95126900f4_bg.wasm');</script></body></html>
|
<script type="module">import init from '/index-1e8437b6157bd12b.js';init('/index-1e8437b6157bd12b_bg.wasm');</script></body></html>
|
139
web/src/'
139
web/src/'
|
@ -1,139 +0,0 @@
|
||||||
use lan_party_core::user::User;
|
|
||||||
use log::debug;
|
|
||||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
|
||||||
use web_sys::Event;
|
|
||||||
|
|
||||||
use crate::util::api_request;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
|
||||||
let users = create_signal(cx, Vec::<User>::new());
|
|
||||||
|
|
||||||
spawn_local_scoped(cx, async move {
|
|
||||||
users.set(
|
|
||||||
api_request::<_, Vec<User>>(reqwasm::http::Method::GET, "/user", Option::<()>::None)
|
|
||||||
.await
|
|
||||||
.map(|inner| inner.unwrap())
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let handle_click = move |event: Event| {
|
|
||||||
debug!("{:#?}", event);
|
|
||||||
};
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
Page {
|
|
||||||
ul {
|
|
||||||
Keyed(
|
|
||||||
iterable=users,
|
|
||||||
view=|cx, user| view! { cx,
|
|
||||||
li { (user.name) }
|
|
||||||
},
|
|
||||||
key=|user| user.name.clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(onclick=handle_click,text="Click me".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temp
|
|
||||||
|
|
||||||
#[derive(Prop)]
|
|
||||||
pub struct PageProps<'a, G: Html> {
|
|
||||||
pub children: Children<'a, G>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Page<'a, G: Html>(cx: Scope<'a>, props: PageProps<'a, G>) -> View<G> {
|
|
||||||
let children = props.children.call(cx);
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
div(class="max-w-7xl mx-auto") { (children) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Prop)]
|
|
||||||
pub struct Props<F: FnMut(Event)> {
|
|
||||||
pub onclick: F,
|
|
||||||
#[builder(default)]
|
|
||||||
pub text: String,
|
|
||||||
#[builder(default)]
|
|
||||||
pub icon: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Button<'a, G: Html, F: 'a + FnMut(Event)>(cx: Scope<'a>, props: Props<F>) -> View<G> {
|
|
||||||
let mut icon_class = String::from("mdi text-lg ");
|
|
||||||
|
|
||||||
if !props.icon.is_empty() {
|
|
||||||
icon_class.push_str(&props.icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
button(class="bg-gray-700 hover:bg-gray-800 text-gray-400 font-bold py-2 px-2 rounded inline-flex items-center",on:click=props.onclick) {
|
|
||||||
span(class=icon_class)
|
|
||||||
span { (props.text) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Prop)]
|
|
||||||
pub struct TableProps<'a, G: Html> {
|
|
||||||
pub headers: Vec<String>,
|
|
||||||
pub rows: Vec<Vec<String>>,
|
|
||||||
pub loading: bool,
|
|
||||||
|
|
||||||
#[builder(default)]
|
|
||||||
pub children: Children<'a, G>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn table<'a, G: Html>(props: TableProps<'a, G>) -> View<G> {
|
|
||||||
let children = props.children.call(cx);
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
div(class="inline-block min-w-full py-2 align-middle") {
|
|
||||||
div(class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg") {
|
|
||||||
table(class="min-w-full divide-y divide-gray-600") {
|
|
||||||
thead(class="bg-gray-800") {
|
|
||||||
tr {
|
|
||||||
(props.headers.iter().map(|header| view! { cx,
|
|
||||||
th(
|
|
||||||
scope="col",
|
|
||||||
class="px-3 py-3.5 text-left
|
|
||||||
text-sm font-semibold text-gray-400"
|
|
||||||
) {
|
|
||||||
(header)
|
|
||||||
}
|
|
||||||
}).collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tbody(class="divide-y divide-gray-600 bg-gray-700") {
|
|
||||||
(props.rows.iter().map(|row| view! { cx,
|
|
||||||
tr {
|
|
||||||
{row.iter().map(|value| html! {
|
|
||||||
td(class="whitespace-nowrap px-3 py-4 text-sm text-gray-400"){
|
|
||||||
(value)
|
|
||||||
}
|
|
||||||
}).collect()}
|
|
||||||
}
|
|
||||||
}).collect())
|
|
||||||
(children)
|
|
||||||
(if props.loading == true { view! {
|
|
||||||
tr {
|
|
||||||
td(colspan={(props.headers.len() + 1).to_string()} class="py-3") {
|
|
||||||
div(class="grid place-items-center") {
|
|
||||||
"Loading ..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}} else { view! {} })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
use gloo_timers::future::TimeoutFuture;
|
||||||
|
use log::debug;
|
||||||
|
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
|
||||||
|
const MESSAGE_TIME: u32 = 5000;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct Message {
|
||||||
|
id: usize,
|
||||||
|
title: String,
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
pub fn new(title: String, text: String) -> Self {
|
||||||
|
Self { id: 0, title, text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct Messenger {
|
||||||
|
counter: RcSignal<usize>,
|
||||||
|
messages: RcSignal<Vec<Message>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Messenger {
|
||||||
|
pub fn add_message(&self, mut message: Message) {
|
||||||
|
message.id = *self.counter.get_untracked();
|
||||||
|
self.counter.set(*self.counter.get_untracked() + 1);
|
||||||
|
self.messages.modify().push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info(&self, title: impl Into<String>, text: impl Into<String>) {
|
||||||
|
self.add_message(Message::new(title.into(), text.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_result<T>(
|
||||||
|
&self,
|
||||||
|
result: anyhow::Result<T>,
|
||||||
|
success: Option<impl Into<String>>,
|
||||||
|
fail: Option<impl Into<String>>,
|
||||||
|
) {
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
if let Some(success) = success {
|
||||||
|
self.add_message(Message::new(success.into(), String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if let Some(fail) = fail {
|
||||||
|
self.add_message(Message::new(fail.into(), e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_message(&self) {
|
||||||
|
self.messages.modify().remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.messages.get_untracked().len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Messages<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
|
let messages = use_context::<Messenger>(cx);
|
||||||
|
|
||||||
|
create_effect(cx, move || {
|
||||||
|
messages.messages.track();
|
||||||
|
|
||||||
|
if messages.len() > 0 {
|
||||||
|
spawn_local_scoped(cx, async move {
|
||||||
|
debug!("Spawn");
|
||||||
|
TimeoutFuture::new(MESSAGE_TIME).await;
|
||||||
|
|
||||||
|
debug!("Remove");
|
||||||
|
messages.remove_message();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
div(class="messages") {
|
||||||
|
Keyed(
|
||||||
|
iterable=&messages.messages,
|
||||||
|
view=move |cx, message| {
|
||||||
|
view! { cx,
|
||||||
|
div(class="message") {
|
||||||
|
p(class="message-title") {
|
||||||
|
(message.title)
|
||||||
|
}
|
||||||
|
p(class="message-text") {
|
||||||
|
(message.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key=|message| (message.id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,31 +1,9 @@
|
||||||
use sycamore::prelude::*;
|
pub mod messages;
|
||||||
|
|
||||||
|
pub use lan_party_core::components::Button;
|
||||||
|
use sycamore::{builder::prelude::*, prelude::*};
|
||||||
use web_sys::Event;
|
use web_sys::Event;
|
||||||
|
|
||||||
#[derive(Prop)]
|
|
||||||
pub struct ButtonProps<F: FnMut(Event)> {
|
|
||||||
pub onclick: F,
|
|
||||||
#[builder(default)]
|
|
||||||
pub text: String,
|
|
||||||
#[builder(default)]
|
|
||||||
pub icon: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Button<'a, G: Html, F: 'a + FnMut(Event)>(cx: Scope<'a>, props: ButtonProps<F>) -> View<G> {
|
|
||||||
let mut icon_class = String::from("mdi ");
|
|
||||||
|
|
||||||
if !props.icon.is_empty() {
|
|
||||||
icon_class.push_str(&props.icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
button(on:click=props.onclick) {
|
|
||||||
span(class=icon_class)
|
|
||||||
span { (props.text) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Prop)]
|
#[derive(Prop)]
|
||||||
pub struct TableProps<'a, G: Html> {
|
pub struct TableProps<'a, G: Html> {
|
||||||
pub headers: Vec<String>,
|
pub headers: Vec<String>,
|
||||||
|
@ -56,19 +34,32 @@ pub fn Table<'a, G: Html>(cx: Scope<'a>, props: TableProps<'a, G>) -> View<G> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Prop)]
|
#[derive(Prop)]
|
||||||
pub struct BlockProps<'a, G: Html> {
|
pub struct ModalProps<'a, G: Html> {
|
||||||
|
pub open: &'a Signal<bool>,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub children: Children<'a, G>,
|
pub children: Children<'a, G>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View<G> {
|
pub fn Modal<'a, G: Html>(cx: Scope<'a>, props: ModalProps<'a, G>) -> View<G> {
|
||||||
let children = props.children.call(cx);
|
let children = props.children.call(cx);
|
||||||
|
|
||||||
|
let class = create_memo(cx, || {
|
||||||
|
if *props.open.get() {
|
||||||
|
"modal"
|
||||||
|
} else {
|
||||||
|
"modal hidden"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
details {
|
div(class=class) {
|
||||||
summary { (props.title) }
|
h4 { (props.title) }
|
||||||
p { (children) }
|
div(class="modal-close") {
|
||||||
|
Button(icon="mdi-close".into(), onclick=move |_| props.open.set(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
(children)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,448 +0,0 @@
|
||||||
use crate::components::*;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
use lan_party_core::event::{
|
|
||||||
free_for_all_game::{FreeForAllGame, FreeForAllGameRanking, FreeForAllGameSpec},
|
|
||||||
team_game::{TeamGame, TeamGameSpec},
|
|
||||||
test::{Test, TestSpec},
|
|
||||||
*,
|
|
||||||
};
|
|
||||||
use paste::paste;
|
|
||||||
use wasm_bindgen::JsValue;
|
|
||||||
use yew::prelude::*;
|
|
||||||
|
|
||||||
use crate::{bind, bind_change, bind_value, clone, clone_cb};
|
|
||||||
|
|
||||||
macro_rules! view_fields {
|
|
||||||
($(($name:expr, $prop:expr)),* $(,)?) => {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
$(
|
|
||||||
<p>
|
|
||||||
{ $name }
|
|
||||||
{ $prop.view() }
|
|
||||||
</p>
|
|
||||||
)*
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! view_struct {
|
|
||||||
($struct:path: $title:expr => $(($name:expr, $prop:ident)),* $(,)?) => {
|
|
||||||
impl View for $struct {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
html! {
|
|
||||||
<Block title={$title}>
|
|
||||||
{ view_fields!(
|
|
||||||
$(($name, self.$prop),)*
|
|
||||||
)}
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($struct:path as $self:ident => $body:expr) => {
|
|
||||||
impl View for $struct {
|
|
||||||
fn view(&$self) -> Html {
|
|
||||||
$body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! view_enum_simple {
|
|
||||||
($enum:path: $($variant:ident),* $(,)?) => {
|
|
||||||
impl View for $enum {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
html! {
|
|
||||||
{match self {
|
|
||||||
$(
|
|
||||||
Self::$variant(i) => html! {
|
|
||||||
<>
|
|
||||||
{ i.view() }
|
|
||||||
</>
|
|
||||||
},
|
|
||||||
)*
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Properties, PartialEq)]
|
|
||||||
pub struct BlockProps {
|
|
||||||
title: String,
|
|
||||||
children: Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component(Block)]
|
|
||||||
pub fn block(props: &BlockProps) -> Html {
|
|
||||||
let open = use_state(|| false);
|
|
||||||
let mut class = classes!("overflow-hidden");
|
|
||||||
|
|
||||||
if !*open {
|
|
||||||
class.push("max-h-1");
|
|
||||||
}
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<div class="px-3 py-3 rounded-lg border-solid border-gray-800 border-2 my-3">
|
|
||||||
<p class="cursor-pointer text-lg" onclick={clone_cb!(open => move |_| open.set(!*open))}>{ &props.title }</p>
|
|
||||||
<div {class}>
|
|
||||||
<br />
|
|
||||||
<div>
|
|
||||||
{for props.children.iter()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait View {
|
|
||||||
fn view(&self) -> Html;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for bool {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
html! {
|
|
||||||
<input type="checkbox" value={self.to_string()} disabled={true} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ViewPlain: Into<Html> + std::fmt::Display {}
|
|
||||||
|
|
||||||
impl<T> View for T
|
|
||||||
where
|
|
||||||
T: ViewPlain,
|
|
||||||
{
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
self.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ViewPlain for i64 {}
|
|
||||||
impl ViewPlain for i32 {}
|
|
||||||
impl ViewPlain for isize {}
|
|
||||||
|
|
||||||
impl ViewPlain for u64 {}
|
|
||||||
impl ViewPlain for u32 {}
|
|
||||||
impl ViewPlain for usize {}
|
|
||||||
|
|
||||||
impl ViewPlain for f64 {}
|
|
||||||
impl ViewPlain for f32 {}
|
|
||||||
|
|
||||||
impl ViewPlain for String {}
|
|
||||||
impl<'a> ViewPlain for &'a str {}
|
|
||||||
|
|
||||||
macro_rules! view_iter {
|
|
||||||
($t:ident => $type:ty) => {
|
|
||||||
impl<$t: View> View for $type {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
html! {
|
|
||||||
<Block title="List">
|
|
||||||
<ul>
|
|
||||||
{ self.iter().map(|x| html! { <li>{ x.view() }</li> }).collect::<Html>() }
|
|
||||||
</ul>
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
view_iter!(T => Vec<T>);
|
|
||||||
view_iter!(T => HashSet<T>);
|
|
||||||
view_iter!(T => &[T]);
|
|
||||||
|
|
||||||
impl<T: View> View for Option<T> {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
match self {
|
|
||||||
Some(content) => content.view(),
|
|
||||||
None => html! { "None" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K: View, V: View> View for HashMap<K, V> {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
html! {
|
|
||||||
<Block title={"Map"}>
|
|
||||||
{self.iter().map(|(k, v)| {
|
|
||||||
html! { <p><span>{k.view()}</span>{ ": " }<span>{v.view()}</span></p> }
|
|
||||||
}).collect::<Html>()}
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view_struct!(
|
|
||||||
lan_party_core::event::Event: "Event" =>
|
|
||||||
("Name: ", name),
|
|
||||||
("Description: ", description),
|
|
||||||
("Concluded: ", concluded),
|
|
||||||
("Event type: ", event_type),
|
|
||||||
);
|
|
||||||
|
|
||||||
view_enum_simple!(
|
|
||||||
lan_party_core::event::EventType: Test,
|
|
||||||
TeamGame,
|
|
||||||
FreeForAllGame
|
|
||||||
);
|
|
||||||
|
|
||||||
view_struct!(
|
|
||||||
lan_party_core::event::test::Test: "Test" =>
|
|
||||||
("Number of players: ", num_players)
|
|
||||||
);
|
|
||||||
|
|
||||||
view_struct!(FreeForAllGame as self =>
|
|
||||||
html! {
|
|
||||||
<Block title={"FreeForAllGame"}>
|
|
||||||
{ view_fields!(("Ranking: ", self.ranking)) }
|
|
||||||
{ view_fields!(
|
|
||||||
("Participants: ", self.spec.participants),
|
|
||||||
("Win rewards: ", self.spec.win_rewards),
|
|
||||||
("Lose rewards: ", self.spec.lose_rewards),
|
|
||||||
) }
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
view_enum_simple!(
|
|
||||||
lan_party_core::event::free_for_all_game::FreeForAllGameRanking: Ranking,
|
|
||||||
Scores
|
|
||||||
);
|
|
||||||
|
|
||||||
view_struct!(TeamGame as self =>
|
|
||||||
html! {
|
|
||||||
<Block title={"TeamGame"}>
|
|
||||||
{ view_fields!(("Teams: ", self.teams)) }
|
|
||||||
{ view_fields!(("Ranking: ", self.ffa_game.ranking)) }
|
|
||||||
{ view_fields!(
|
|
||||||
("Participants: ", self.ffa_game.spec.participants),
|
|
||||||
("Win rewards: ", self.ffa_game.spec.win_rewards),
|
|
||||||
("Lose rewards: ", self.ffa_game.spec.lose_rewards),
|
|
||||||
) }
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
macro_rules! edit_fields {
|
|
||||||
($(($name:expr, $prop:expr)),* $(,)?) => {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
$(
|
|
||||||
<p>
|
|
||||||
{ $name }
|
|
||||||
{ $prop.edit() }
|
|
||||||
</p>
|
|
||||||
)*
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! link_fields {
|
|
||||||
($($field:ident),* $(,)? => $state:ident as $t:ident) => {
|
|
||||||
$(let $field = use_state(|| $state.$field.clone());)*
|
|
||||||
|
|
||||||
use_effect_with_deps(
|
|
||||||
clone!($($field,)* $state =>
|
|
||||||
move |_| {
|
|
||||||
$state.set($t {
|
|
||||||
$($field: (*$field).clone(),)*
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|| { $(drop($field);)* drop($state); }
|
|
||||||
}
|
|
||||||
),
|
|
||||||
($($field.clone(),)*),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! link_variants {
|
|
||||||
($selected:ident => $($index:literal: $var_name:ident = $variant:ident: $var_type:ty),* $(,)? => $state:ident as $t:ident) => {
|
|
||||||
let $selected = use_state(|| 0 as usize);
|
|
||||||
$(let $var_name = if let $t::$variant(v) = &*$state {
|
|
||||||
use_state(|| v.clone())
|
|
||||||
} else {
|
|
||||||
use_state(|| <$var_type>::default())
|
|
||||||
};)*
|
|
||||||
|
|
||||||
use_effect_with_deps(
|
|
||||||
clone!($($var_name,)* $state, $selected =>
|
|
||||||
move |_| {
|
|
||||||
match *$selected {
|
|
||||||
$($index => $state.set($t::$variant((*$var_name).clone())),)*
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
|| { $(drop($var_name);)* drop($selected); drop($state); }
|
|
||||||
}
|
|
||||||
),
|
|
||||||
($($var_name.clone(),)* $selected.clone()),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! edit_struct {
|
|
||||||
($struct:ident => $(($name:expr, $prop:ident)),* $(,)?) => {
|
|
||||||
paste! {
|
|
||||||
#[function_component([<$struct Edit>])]
|
|
||||||
pub fn [<$struct:lower _edit>](props: &EditProps<$struct>) -> Html {
|
|
||||||
let state = props.state.clone();
|
|
||||||
link_fields!($($prop,)* => state as $struct);
|
|
||||||
html! {
|
|
||||||
<Block title={stringify!($struct)}>
|
|
||||||
{ edit_fields!($(($name, $prop)),*) }
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Editable for $struct {
|
|
||||||
type Edit = [<$struct Edit>];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! edit_enum {
|
|
||||||
($enum:ident => $selected:ident => $($index:literal: $var_name:ident = $variant:ident: $var_type:ty),* $(,)?) => {
|
|
||||||
paste! {
|
|
||||||
#[function_component([<$enum Edit>])]
|
|
||||||
pub fn [<$enum:lower _edit>](props: &EditProps<$enum>) -> Html {
|
|
||||||
let state = props.state.clone();
|
|
||||||
|
|
||||||
link_variants!($selected =>
|
|
||||||
$($index: $var_name = $variant: $var_type,)*
|
|
||||||
=> state as $enum
|
|
||||||
);
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<Block title={stringify!($enum)}>
|
|
||||||
<Select class="" bind={bind!($selected)}>
|
|
||||||
$(<option value={stringify!($index)}>{ stringify!($variant) }</option>)*
|
|
||||||
</Select>
|
|
||||||
{ match &*state {
|
|
||||||
$($enum::$variant(_) => $var_name.edit(),)*
|
|
||||||
}}
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Editable for $enum {
|
|
||||||
type Edit = [<$enum Edit>];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Properties)]
|
|
||||||
pub struct EditProps<T: PartialEq> {
|
|
||||||
pub state: UseStateHandle<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Edit {
|
|
||||||
fn edit(&self) -> Html;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Comp: Component<Properties = EditProps<Type>>, Type: Editable<Edit = Comp> + PartialEq> Edit
|
|
||||||
for UseStateHandle<Type>
|
|
||||||
{
|
|
||||||
fn edit(&self) -> Html {
|
|
||||||
html!(<Comp state={self.clone()} />)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Editable {
|
|
||||||
type Edit: Component;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component(InputEdit)]
|
|
||||||
pub fn input_edit<T>(props: &EditProps<T>) -> Html
|
|
||||||
where
|
|
||||||
T: PartialEq + Clone + 'static,
|
|
||||||
Binding<String>: From<Binding<T>>,
|
|
||||||
{
|
|
||||||
let string = props.state.clone();
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<TextInput
|
|
||||||
class={"mx-2 appearance-none block bg-gray-700 text-slate-400 border border-gray-600 rounded py-2 px-2 w-50 leading-tight focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"}
|
|
||||||
bind={bind!(string)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Editable for T
|
|
||||||
where
|
|
||||||
T: PartialEq + Clone + 'static,
|
|
||||||
Binding<String>: From<Binding<T>>,
|
|
||||||
{
|
|
||||||
type Edit = InputEdit<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component(Stub)]
|
|
||||||
pub fn stub<T: PartialEq>(props: &EditProps<T>) -> Html {
|
|
||||||
html! { "stub" }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Editable for Vec<String> {
|
|
||||||
type Edit = Stub<Vec<String>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Editable for HashMap<String, i64> {
|
|
||||||
type Edit = Stub<HashMap<String, i64>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
edit_struct!(EventSpec => ("Name: ", name), ("Description: ", description), ("Event type: ", event_type));
|
|
||||||
edit_struct!(TestSpec => ("Number of players: ", num_players));
|
|
||||||
edit_struct!(TeamGameSpec => );
|
|
||||||
edit_struct!(FreeForAllGameSpec => );
|
|
||||||
|
|
||||||
edit_enum!(EventTypeSpec => selected =>
|
|
||||||
0: test = Test: TestSpec,
|
|
||||||
1: team_game = TeamGame: TeamGameSpec,
|
|
||||||
2: free_for_all_game = FreeForAllGame: FreeForAllGameSpec
|
|
||||||
);
|
|
||||||
|
|
||||||
edit_enum!(FreeForAllGameRanking => selected =>
|
|
||||||
0: ranking = Ranking: Vec<String>,
|
|
||||||
1: scores = Scores: HashMap<String, i64>,
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
#[function_component(EventTypeSpecEdit)]
|
|
||||||
pub fn event_type_spec_edit(props: &EditProps<EventTypeSpec>) -> Html {
|
|
||||||
let state = props.state.clone();
|
|
||||||
|
|
||||||
link_variants!(selected =>
|
|
||||||
0: test = Test: TestSpec,
|
|
||||||
1: team_game = TeamGame: TeamGameSpec,
|
|
||||||
2: free_for_all_game = FreeForAllGame: FreeForAllGameSpec
|
|
||||||
=> state as EventTypeSpec
|
|
||||||
);
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<Block title={"EventTypeSpec"}>
|
|
||||||
<Select class="" bind={bind!(selected)}>
|
|
||||||
<option value="0">{ "Test" }</option>
|
|
||||||
<option value="1">{ "TeamGame" }</option>
|
|
||||||
<option value="2">{ "FreeForAllGame" }</option>
|
|
||||||
</Select>
|
|
||||||
{ match &*state {
|
|
||||||
EventTypeSpec::Test(_) => test.edit(),
|
|
||||||
EventTypeSpec::TeamGame(_) => team_game.edit(),
|
|
||||||
EventTypeSpec::FreeForAllGame(_) => free_for_all_game.edit(),
|
|
||||||
}}
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Editable for EventTypeSpec {
|
|
||||||
type Edit = EventTypeSpecEdit;
|
|
||||||
}
|
|
||||||
*/
|
|
|
@ -1,9 +1,14 @@
|
||||||
mod components;
|
mod components;
|
||||||
mod pages;
|
mod pages;
|
||||||
|
pub mod state;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
use components::messages::{Messages, Messenger};
|
||||||
|
use futures::join;
|
||||||
|
use lan_party_core::state::{Events, Users};
|
||||||
use pages::{EventsPage, UsersPage};
|
use pages::{EventsPage, UsersPage};
|
||||||
use sycamore::prelude::*;
|
use state::{EventsExt, UsersExt};
|
||||||
|
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
use sycamore_router::{HistoryIntegration, Route, Router};
|
use sycamore_router::{HistoryIntegration, Route, Router};
|
||||||
|
|
||||||
#[derive(Route)]
|
#[derive(Route)]
|
||||||
|
@ -62,11 +67,55 @@ fn main() {
|
||||||
];
|
];
|
||||||
|
|
||||||
sycamore::render(move |cx| {
|
sycamore::render(move |cx| {
|
||||||
|
let messenger = Messenger::default();
|
||||||
|
provide_context(cx, messenger);
|
||||||
|
|
||||||
|
let users = Users::default();
|
||||||
|
provide_context(cx, users);
|
||||||
|
|
||||||
|
let events = Events::default();
|
||||||
|
provide_context(cx, events);
|
||||||
|
|
||||||
|
spawn_local_scoped(cx, async move {
|
||||||
|
let (users_res, events_res) = join!(
|
||||||
|
use_context::<Users>(cx).load(),
|
||||||
|
use_context::<Events>(cx).load(),
|
||||||
|
);
|
||||||
|
use_context::<Messenger>(cx).add_result(
|
||||||
|
users_res,
|
||||||
|
Option::<String>::None,
|
||||||
|
Some("failed to load users"),
|
||||||
|
);
|
||||||
|
use_context::<Messenger>(cx).add_result(
|
||||||
|
events_res,
|
||||||
|
Option::<String>::None,
|
||||||
|
Some("failed to load events"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
let messages = use_context::<MessagesState>(cx);
|
||||||
|
|
||||||
|
spawn_local_scoped(cx, async move {
|
||||||
|
TimeoutFuture::new(1000).await;
|
||||||
|
|
||||||
|
messages.add_message(Message::new(
|
||||||
|
"Hello there".into(),
|
||||||
|
"This is a message".into(),
|
||||||
|
));
|
||||||
|
|
||||||
|
TimeoutFuture::new(1000).await;
|
||||||
|
|
||||||
|
messages.add_message(Message::new("Oh look!".into(), "Another message".into()));
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
Router(
|
Router(
|
||||||
integration=HistoryIntegration::new(),
|
integration=HistoryIntegration::new(),
|
||||||
view=|cx, route: &ReadSignal<AppRoutes>| {
|
view=|cx, route: &ReadSignal<AppRoutes>| {
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
Messages()
|
||||||
header {
|
header {
|
||||||
Navbar(pages=pages) {
|
Navbar(pages=pages) {
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use lan_party_core::{
|
use lan_party_core::{
|
||||||
|
components::Block,
|
||||||
edit::IntoEdit,
|
edit::IntoEdit,
|
||||||
event::{Event, EventSpec, EventUpdate},
|
event::{Event, EventOutcome, EventSpec, EventUpdate},
|
||||||
|
util::WithContext,
|
||||||
view::IntoView,
|
view::IntoView,
|
||||||
};
|
};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
@ -8,63 +13,182 @@ use reqwasm::http::Method;
|
||||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{Block, Button},
|
components::{messages::Messenger, Button, Modal, Table},
|
||||||
|
state::{EventsExt, UsersExt},
|
||||||
util::api_request,
|
util::api_request,
|
||||||
};
|
};
|
||||||
|
use lan_party_core::state::{Events, Users};
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
Reload,
|
||||||
|
Add(EventSpec),
|
||||||
|
Update(String, EventUpdate),
|
||||||
|
Delete(String),
|
||||||
|
Stop(String),
|
||||||
|
ViewOutcome(String),
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
|
let messenger = use_context::<Messenger>(cx);
|
||||||
|
|
||||||
let event_spec = create_signal(cx, EventSpec::default());
|
let event_spec = create_signal(cx, EventSpec::default());
|
||||||
let event_update = create_signal(cx, EventUpdate::default());
|
let event_update = create_signal(cx, EventUpdate::default());
|
||||||
|
let event_update_name = create_signal(cx, WithContext::<Events, String>::from(String::new()));
|
||||||
|
|
||||||
let events: &'a Signal<Vec<Event>> = create_signal(cx, Vec::<Event>::new());
|
let current_event = create_signal(cx, String::new());
|
||||||
|
let show_outcome = create_signal(cx, false);
|
||||||
|
let event_outcome = create_signal(cx, EventOutcome::default());
|
||||||
|
|
||||||
|
let events = use_context::<Events>(cx);
|
||||||
|
let users = use_context::<Users>(cx);
|
||||||
|
|
||||||
|
let dispatch = move |msg: Msg| {
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
events.set(
|
match msg {
|
||||||
api_request::<_, Vec<Event>>(Method::GET, "/event", Option::<()>::None)
|
Msg::Add(event_spec) => {
|
||||||
.await
|
let name = event_spec.name.clone();
|
||||||
.map(|inner| inner.unwrap())
|
messenger.add_result(
|
||||||
.unwrap(),
|
events.add(event_spec).await,
|
||||||
|
Some(format!("Created a new event with name \"{}\"", name)),
|
||||||
|
Some("Error when adding an event"),
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
Msg::Reload => messenger.add_result(
|
||||||
let onadd = move |_| {
|
events.load().await,
|
||||||
spawn_local_scoped(cx, async move {
|
Option::<String>::None,
|
||||||
let new_event = api_request::<EventSpec, Event>(
|
Some("Failed to load events"),
|
||||||
Method::POST,
|
),
|
||||||
"/event",
|
Msg::Update(event_update_name, event_update) => {
|
||||||
Some((*event_spec).get().as_ref().clone()),
|
messenger.add_result(
|
||||||
|
events.update_event(&event_update_name, event_update).await,
|
||||||
|
Some(format!("Updated event with name \"{}\"", event_update_name)),
|
||||||
|
Some("Error when updating event"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Msg::Delete(event_name) => {
|
||||||
|
messenger.add_result(
|
||||||
|
events.delete(&event_name).await,
|
||||||
|
Some(format!("Deleted event with name \"{}\"", event_name)),
|
||||||
|
Some("Error when deleting event"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Msg::ViewOutcome(event_name) => {
|
||||||
|
show_outcome.set(true);
|
||||||
|
current_event.set(event_name.clone());
|
||||||
|
let res = api_request::<(), EventOutcome>(
|
||||||
|
Method::GET,
|
||||||
|
&format!("/event/{}/outcome", event_name),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.and_then(|inner| inner.ok_or(anyhow!("missing body")));
|
||||||
debug!("{:#?}", new_event);
|
|
||||||
events.modify().push(new_event.unwrap());
|
if let Ok(outcome) = res {
|
||||||
|
event_outcome.set(outcome);
|
||||||
|
} else {
|
||||||
|
messenger.add_result(
|
||||||
|
res,
|
||||||
|
Option::<String>::None,
|
||||||
|
Some("Failed to load outcome"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg::Stop(event_name) => {
|
||||||
|
let res = api_request::<(), EventOutcome>(
|
||||||
|
Method::POST,
|
||||||
|
&format!("/event/{}/stop", event_name),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
messenger.add_result(
|
||||||
|
res,
|
||||||
|
Some("Event finished, scores were applied"),
|
||||||
|
Some("Unable to finish event"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = users.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dispatch(Msg::Reload);
|
||||||
|
|
||||||
|
let outcome_points = create_memo(cx, || {
|
||||||
|
let cloned = event_outcome.get().as_ref().clone();
|
||||||
|
let mut vec = cloned.points.into_iter().collect::<Vec<_>>();
|
||||||
|
vec.sort_by_key(|(_, s)| -*s);
|
||||||
|
vec
|
||||||
|
});
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
Modal(open=show_outcome, title="Event outcome".into()) {
|
||||||
|
Table(headers=vec!["Username".into(), "Score".into()]) {
|
||||||
|
Indexed(
|
||||||
|
iterable=&outcome_points,
|
||||||
|
view=move |cx, (k, v)| {
|
||||||
|
view! { cx,
|
||||||
|
tr {
|
||||||
|
td { (*k.clone()) }
|
||||||
|
td { (v.clone()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| dispatch(Msg::ViewOutcome(current_event.get().as_ref().clone())))
|
||||||
|
}
|
||||||
|
div(class="events-cols") {
|
||||||
|
div {
|
||||||
Block(title="Events".into()) {
|
Block(title="Events".into()) {
|
||||||
Keyed(
|
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| dispatch(Msg::Reload))
|
||||||
iterable=&events,
|
Indexed(
|
||||||
|
iterable=&events.get(),
|
||||||
view=move |cx, event| {
|
view=move |cx, event| {
|
||||||
let event = create_ref(cx, event);
|
let event = create_ref(cx, event);
|
||||||
view! { cx,
|
view! { cx,
|
||||||
(event.view(cx))
|
Block(title=event.name.clone()) {
|
||||||
|
(event.description)
|
||||||
|
br()
|
||||||
|
span(class="event-action") {
|
||||||
|
Button(text="Delete".into(), icon="mdi-delete".into(), onclick=move |_| dispatch(Msg::Delete(event.name.clone())))
|
||||||
|
}
|
||||||
|
span(class="event-action") {
|
||||||
|
Button(text="View outcome".into(), onclick=move |_| dispatch(Msg::ViewOutcome(event.name.clone())))
|
||||||
|
}
|
||||||
|
span(class="event-action") {
|
||||||
|
Button(text="Finish".into(), onclick=move |_| dispatch(Msg::Stop(event.name.clone())))
|
||||||
|
}
|
||||||
|
br()
|
||||||
|
(event.event_type.view(cx))
|
||||||
|
}
|
||||||
br()
|
br()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
key=|event| (event.name.clone()),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
br()
|
}
|
||||||
|
div(class="events-right") {
|
||||||
Block(title="Create new event".into()) {
|
Block(title="Create new event".into()) {
|
||||||
(event_spec.edit(cx))
|
(event_spec.edit(cx))
|
||||||
Button(icon="mdi-check".into(), onclick=onadd)
|
Button(icon="mdi-check".into(), onclick=move |_| dispatch(Msg::Add(event_spec.get().as_ref().clone())))
|
||||||
}
|
}
|
||||||
br()
|
br()
|
||||||
Block(title="Update an event".into()) {
|
Block(title="Update an event".into()) {
|
||||||
|
label { "Event name" }
|
||||||
|
(event_update_name.edit(cx))
|
||||||
(event_update.edit(cx))
|
(event_update.edit(cx))
|
||||||
Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_update.get()))
|
Button(
|
||||||
|
icon="mdi-check".into(),
|
||||||
|
onclick=move |_| dispatch(Msg::Update(
|
||||||
|
event_update_name.get().as_ref().deref().clone(),
|
||||||
|
event_update.get().as_ref().clone()
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
use crate::{
|
|
||||||
components::{
|
|
||||||
event::{Edit, EventSpecEdit},
|
|
||||||
Button, Page, View,
|
|
||||||
},
|
|
||||||
init,
|
|
||||||
};
|
|
||||||
use lan_party_core::event::EventSpec;
|
|
||||||
use wasm_bindgen::JsValue;
|
|
||||||
use wasm_bindgen_futures::spawn_local;
|
|
||||||
use yew::prelude::*;
|
|
||||||
use yew_hooks::*;
|
|
||||||
|
|
||||||
use crate::{clone, clone_cb, util::api_request};
|
|
||||||
|
|
||||||
#[function_component(EventsPage)]
|
|
||||||
pub fn events_page() -> Html {
|
|
||||||
let events = use_state(|| Vec::new());
|
|
||||||
|
|
||||||
init!(events => {
|
|
||||||
events.set(api_request::<_, Vec<lan_party_core::event::Event>>(reqwasm::http::Method::GET, "/event", Option::<()>::None)
|
|
||||||
.await
|
|
||||||
.map(|inner| inner.unwrap())
|
|
||||||
.unwrap())
|
|
||||||
});
|
|
||||||
|
|
||||||
//let edit_event = use_state(|| EventSpecEditHandle::to_edit(EventSpec::default()));
|
|
||||||
let event_spec = use_state(|| EventSpec::default());
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<Page>
|
|
||||||
{ events.view() }
|
|
||||||
|
|
||||||
{ event_spec.edit() }
|
|
||||||
|
|
||||||
<Button text="Create" onclick={clone_cb!(event_spec => move |_| web_sys::console::log_1(&JsValue::from_serde(&*event_spec).unwrap()))} />
|
|
||||||
</Page>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +1,55 @@
|
||||||
use crate::components::{Button, Table};
|
use crate::{
|
||||||
use lan_party_core::user::User;
|
components::{messages::Messenger, Button, Table},
|
||||||
use log::debug;
|
state::{EventsExt, UsersExt},
|
||||||
use reqwasm::http::Method;
|
};
|
||||||
|
use lan_party_core::state::{Events, Users};
|
||||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
use web_sys::Event;
|
|
||||||
|
|
||||||
use crate::util::api_request;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
let users = create_signal(cx, Vec::<User>::new());
|
//let users = create_signal(cx, Vec::<User>::new());
|
||||||
|
let messenger = use_context::<Messenger>(cx);
|
||||||
|
let users = use_context::<Users>(cx);
|
||||||
let headers = vec!["Username".into(), "Score".into(), "".into()];
|
let headers = vec!["Username".into(), "Score".into(), "".into()];
|
||||||
|
|
||||||
let score_edit = create_signal(cx, Option::<String>::None);
|
let score_edit = create_signal(cx, Option::<String>::None);
|
||||||
let new_score = create_signal(cx, String::new());
|
let new_score = create_signal(cx, String::new());
|
||||||
let new_username = create_signal(cx, String::new());
|
let new_username = create_signal(cx, String::new());
|
||||||
|
|
||||||
|
let reload = move || {
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
users.set(
|
messenger.add_result(
|
||||||
api_request::<_, Vec<User>>(Method::GET, "/user", Option::<()>::None)
|
users.load().await,
|
||||||
.await
|
Option::<String>::None,
|
||||||
.map(|inner| inner.unwrap())
|
Some("Failed to load users"),
|
||||||
.unwrap(),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reload();
|
||||||
|
|
||||||
let ondelete = move |name: String| {
|
let ondelete = move |name: String| {
|
||||||
move |event: Event| {
|
move |_| {
|
||||||
let name = name.clone();
|
let name = name.clone();
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
debug!("Delete {:#?}", event);
|
messenger.add_result(
|
||||||
let users_ref = users.get();
|
users.delete(&name).await,
|
||||||
let user: &User = users_ref.iter().find(|user| user.name == name).unwrap();
|
Some(format!("Deleted user {}", name)),
|
||||||
api_request::<_, ()>(
|
Some("Failed to delete user"),
|
||||||
Method::DELETE,
|
);
|
||||||
&format!("/user/{}", user.name),
|
|
||||||
Option::<()>::None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let cloned = (*users_ref)
|
|
||||||
.clone()
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.filter(|u| u.name != user.name)
|
|
||||||
.collect();
|
|
||||||
users.set(cloned);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let oncheck = move |_| {
|
let oncheck = move |_| {
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
if let (Some(score_edit), Ok(score)) =
|
if let (Some(name), Ok(score)) = (score_edit.get().as_ref(), new_score.get().parse()) {
|
||||||
(score_edit.get().as_ref(), new_score.get().parse())
|
|
||||||
{
|
|
||||||
let score: i64 = score;
|
let score: i64 = score;
|
||||||
let users_ref = users.get();
|
messenger.add_result(
|
||||||
let user: &User = users_ref
|
users.update_score(&name, score).await,
|
||||||
.iter()
|
Some(format!("Updated score for user {}", name)),
|
||||||
.find(|user| &user.name == score_edit)
|
Some("Failed to delete user"),
|
||||||
.unwrap();
|
);
|
||||||
api_request::<_, ()>(
|
|
||||||
Method::POST,
|
|
||||||
&format!("/user/{}/score", user.name),
|
|
||||||
Some(score),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let cloned = (*users_ref).clone();
|
|
||||||
let new_users: Vec<_> = cloned
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut user| {
|
|
||||||
if &user.name == score_edit {
|
|
||||||
user.score = score
|
|
||||||
}
|
|
||||||
user
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
users.set(new_users);
|
|
||||||
}
|
}
|
||||||
score_edit.set(None);
|
score_edit.set(None);
|
||||||
})
|
})
|
||||||
|
@ -86,21 +57,19 @@ pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
|
|
||||||
let onadd = move |_| {
|
let onadd = move |_| {
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
let user = api_request::<String, User>(
|
messenger.add_result(
|
||||||
Method::POST,
|
users.add(&new_username.get()).await,
|
||||||
"/user",
|
Some(format!("Added new user {}", new_username.get())),
|
||||||
Some((*new_username).get().as_ref().clone()),
|
Some("Failed to add user"),
|
||||||
)
|
);
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
users.modify().push(user.unwrap());
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| reload())
|
||||||
Table(headers=headers) {
|
Table(headers=headers) {
|
||||||
Keyed(
|
Keyed(
|
||||||
iterable=users,
|
iterable=users.get(),
|
||||||
view=move |cx, user| {
|
view=move |cx, user| {
|
||||||
let user = create_ref(cx, user);
|
let user = create_ref(cx, user);
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
use crate::{
|
|
||||||
bind, bind_change, bind_value, clone, clone_cb, clone_cb_spawn,
|
|
||||||
components::{Binding, Button, Loading, Page, Table, TextInput},
|
|
||||||
init,
|
|
||||||
util::api_request,
|
|
||||||
};
|
|
||||||
use lan_party_core::user::User;
|
|
||||||
use wasm_bindgen_futures::spawn_local;
|
|
||||||
use yew::prelude::*;
|
|
||||||
use yew_hooks::*;
|
|
||||||
|
|
||||||
#[function_component(UsersPage)]
|
|
||||||
pub fn users_page() -> Html {
|
|
||||||
let headers = vec!["Username".into(), "Score".into(), "".into()];
|
|
||||||
|
|
||||||
let new_username = use_state(|| String::new());
|
|
||||||
let score_edit: UseStateHandle<Option<usize>> = use_state(|| Option::None);
|
|
||||||
let current_score = use_state(|| String::new());
|
|
||||||
let users = use_state(|| Vec::new());
|
|
||||||
|
|
||||||
init!(users => {
|
|
||||||
users.set(api_request::<_, Vec<User>>(reqwasm::http::Method::GET, "/user", Option::<()>::None)
|
|
||||||
.await
|
|
||||||
.map(|inner| inner.unwrap())
|
|
||||||
.unwrap());
|
|
||||||
});
|
|
||||||
|
|
||||||
let oncheck = clone_cb_spawn!(score_edit, current_score, users => {
|
|
||||||
if let (Some(score_edit), Ok(score)) = (*score_edit, current_score.parse()) {
|
|
||||||
let user: &User = &users[score_edit];
|
|
||||||
api_request::<_, ()>(reqwasm::http::Method::POST, &format!("/user/{}/score", user.name), Some(score))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let mut cloned = (*users).clone();
|
|
||||||
cloned[score_edit].score = score;
|
|
||||||
users.set(cloned);
|
|
||||||
}
|
|
||||||
score_edit.set(None);
|
|
||||||
});
|
|
||||||
|
|
||||||
let onedit = clone_cb!(current_score, score_edit, users => i => move |_| {
|
|
||||||
let user: &User = &users[i];
|
|
||||||
current_score.set(user.score.to_string());
|
|
||||||
score_edit.set(Some(i));
|
|
||||||
});
|
|
||||||
|
|
||||||
let ondelete = clone_cb_spawn!(users => i => {
|
|
||||||
let user: &User = &users[i];
|
|
||||||
api_request::<_, ()>(reqwasm::http::Method::DELETE, &format!("/user/{}", user.name), Option::<()>::None).await.unwrap();
|
|
||||||
let cloned = users.iter().cloned().filter(|u| u.name != user.name).collect();
|
|
||||||
users.set(cloned);
|
|
||||||
});
|
|
||||||
|
|
||||||
let onadd = clone_cb_spawn!(new_username, users => {
|
|
||||||
let user = api_request::<String, User>(reqwasm::http::Method::POST, "/user", Some((*new_username).clone())).await.unwrap();
|
|
||||||
let mut cloned = (*users).clone();
|
|
||||||
cloned.push(user.unwrap());
|
|
||||||
users.set(cloned);
|
|
||||||
});
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<Page>
|
|
||||||
<Table headers={headers.clone()} loading=false rows={vec![]}>
|
|
||||||
{users.iter().enumerate().map(move |(i, user)| html! {
|
|
||||||
<tr>
|
|
||||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-slate-400">{&user.name}</td>
|
|
||||||
<td class="whitespace-nowrap px-3 text-sm text-slate-400">
|
|
||||||
{if Some(i) == *score_edit.clone() { html! {
|
|
||||||
<>
|
|
||||||
<span class="inline-block">
|
|
||||||
<TextInput
|
|
||||||
class={"mx-2 appearance-none block bg-gray-700 text-slate-400 border border-gray-600 rounded py-2 px-2 w-20 leading-tight focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"}
|
|
||||||
bind={bind!(current_score)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<Button icon={"mdi-check"} onclick={oncheck.clone()} />
|
|
||||||
</>
|
|
||||||
}} else { html! {
|
|
||||||
<>
|
|
||||||
<span class="my-3 w-20">
|
|
||||||
{user.score}
|
|
||||||
</span>
|
|
||||||
<Button icon={"mdi-pencil"} onclick={onedit.clone()(i)} />
|
|
||||||
</>
|
|
||||||
}}}
|
|
||||||
</td>
|
|
||||||
<td class="whitespace-nowrap py-4 text-sm text-slate-400">
|
|
||||||
<Button icon={"mdi-delete"} onclick={ondelete.clone()(i)} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}).collect::<Html>()}
|
|
||||||
<tr>
|
|
||||||
<td class="whitespace-nowrap px-3 text-sm text-slate-400">
|
|
||||||
<span class="inline-block">
|
|
||||||
<TextInput
|
|
||||||
class={"mx-2 appearance-none block bg-gray-700 text-slate-400 border border-gray-600 rounded py-2 px-2 w-50 leading-tight focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"}
|
|
||||||
bind={bind!(new_username)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-slate-400"></td>
|
|
||||||
<td class="whitespace-nowrap py-4 text-sm text-slate-400">
|
|
||||||
<Button icon={"mdi-plus"} onclick={onadd} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</Table>
|
|
||||||
</Page>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use lan_party_core::{
|
||||||
|
event::{Event, EventSpec, EventUpdate},
|
||||||
|
state::{Events, Users},
|
||||||
|
user::User,
|
||||||
|
};
|
||||||
|
use reqwasm::http::Method;
|
||||||
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
|
use crate::util::api_request;
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
pub trait UsersExt {
|
||||||
|
async fn load(&self) -> Result<()>;
|
||||||
|
|
||||||
|
async fn delete(&self, name: &str) -> Result<()>;
|
||||||
|
|
||||||
|
async fn update_score(&self, name: &str, score: i64) -> Result<()>;
|
||||||
|
|
||||||
|
async fn add(&self, name: &str) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl UsersExt for Users {
|
||||||
|
async fn load(&self) -> Result<()> {
|
||||||
|
self.0.set(
|
||||||
|
api_request::<_, Vec<User>>(
|
||||||
|
Method::GET,
|
||||||
|
"/user?sort=score&order=desc",
|
||||||
|
Option::<()>::None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.and_then(|inner| inner.ok_or(anyhow!("missing body")))?,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, name: &str) -> Result<()> {
|
||||||
|
let users_ref = self.0.get();
|
||||||
|
let user: &User = users_ref.iter().find(|user| user.name == name).unwrap();
|
||||||
|
api_request::<_, ()>(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("/user/{}", user.name),
|
||||||
|
Option::<()>::None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let cloned = (*users_ref)
|
||||||
|
.clone()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.filter(|u| u.name != user.name)
|
||||||
|
.collect();
|
||||||
|
self.0.set(cloned);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_score(&self, name: &str, score: i64) -> Result<()> {
|
||||||
|
let users_ref = self.0.get();
|
||||||
|
let user: &User = users_ref.iter().find(|user| &user.name == name).unwrap();
|
||||||
|
api_request::<_, ()>(
|
||||||
|
Method::POST,
|
||||||
|
&format!("/user/{}/score", user.name),
|
||||||
|
Some(score),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let cloned = (*users_ref).clone();
|
||||||
|
let new_users: Vec<_> = cloned
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut user| {
|
||||||
|
if &user.name == name {
|
||||||
|
user.score = score
|
||||||
|
}
|
||||||
|
user
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
self.0.set(new_users);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add(&self, name: &str) -> Result<()> {
|
||||||
|
let user = api_request::<&str, User>(Method::POST, "/user", Some(name)).await?;
|
||||||
|
self.0.modify().push(user.ok_or(anyhow!("missing body"))?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
pub trait EventsExt {
|
||||||
|
async fn load(&self) -> Result<()>;
|
||||||
|
|
||||||
|
async fn add(&self, event_spec: EventSpec) -> Result<()>;
|
||||||
|
|
||||||
|
async fn update_event(&self, event_name: &str, event_update: EventUpdate) -> Result<()>;
|
||||||
|
|
||||||
|
async fn delete(&self, event_name: &str) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl EventsExt for Events {
|
||||||
|
async fn load(&self) -> Result<()> {
|
||||||
|
self.0.set(
|
||||||
|
api_request::<_, Vec<Event>>(Method::GET, "/event?concluded=false", Option::<()>::None)
|
||||||
|
.await
|
||||||
|
.and_then(|inner| inner.ok_or(anyhow!("missing body")))?,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add(&self, event_spec: EventSpec) -> Result<()> {
|
||||||
|
let new_event =
|
||||||
|
api_request::<EventSpec, Event>(Method::POST, "/event", Some(event_spec)).await?;
|
||||||
|
|
||||||
|
self.0
|
||||||
|
.modify()
|
||||||
|
.push(new_event.ok_or(anyhow!("missing body"))?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_event(&self, event_name: &str, event_update: EventUpdate) -> Result<()> {
|
||||||
|
api_request::<EventUpdate, ()>(
|
||||||
|
Method::POST,
|
||||||
|
&format!("/event/{}", event_name),
|
||||||
|
Some(event_update),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.load().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, event_name: &str) -> Result<()> {
|
||||||
|
api_request::<(), ()>(Method::DELETE, &format!("/event/{}", event_name), None).await?;
|
||||||
|
|
||||||
|
self.load().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,11 +33,19 @@ pub async fn api_request<B: Serialize, R: for<'a> Deserialize<'a>>(
|
||||||
|
|
||||||
let res = req.send().await?;
|
let res = req.send().await?;
|
||||||
|
|
||||||
|
if res.ok() {
|
||||||
if let Ok(json) = res.json().await {
|
if let Ok(json) = res.json().await {
|
||||||
Ok(Some(json))
|
Ok(Some(json))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if let Ok(text) = res.text().await {
|
||||||
|
Err(anyhow!("Request failed: {}", text.to_string()))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Request failed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
|
|
@ -64,5 +64,84 @@ textarea:focus, input:focus{
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
grid-template-columns: 1fr min(60rem, 90%) 1fr;
|
grid-template-columns: 1fr min(100rem, 90%) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
top: 0.5em;
|
||||||
|
left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin: 0.5em;
|
||||||
|
background-color: var(--accent);
|
||||||
|
padding: 0.5em;
|
||||||
|
border-radius: 5px;
|
||||||
|
min-width: 30em;
|
||||||
|
max-width: 30em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-title {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-cols {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-cols > * {
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-cols:nth-child(1) {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-cols:nth-child(2) {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-action {
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
z-index: 5;
|
||||||
|
left: 15vw;
|
||||||
|
top: 10vh;
|
||||||
|
position: fixed;
|
||||||
|
width: 70vw;
|
||||||
|
height: 80vh;
|
||||||
|
padding: 1em;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal > h4 {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 1em;
|
||||||
|
top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close > * {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue