Compare commits
No commits in common. "master" and "less-lifetimes" have entirely different histories.
master
...
less-lifet
|
@ -376,18 +376,8 @@ 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 0.13.4",
|
"darling_core",
|
||||||
"darling_macro 0.13.4",
|
"darling_macro",
|
||||||
]
|
|
||||||
|
|
||||||
[[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]]
|
||||||
|
@ -404,38 +394,13 @@ 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 0.13.4",
|
"darling_core",
|
||||||
"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",
|
||||||
]
|
]
|
||||||
|
@ -1208,7 +1173,6 @@ 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",
|
||||||
|
@ -1220,10 +1184,7 @@ 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",
|
||||||
|
@ -2003,7 +1964,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 0.13.4",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rocket_http",
|
"rocket_http",
|
||||||
|
@ -2251,7 +2212,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 0.13.4",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
|
|
|
@ -14,14 +14,13 @@ 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! { "name": player.to_string() },
|
doc! { "id": player },
|
||||||
doc! { "$inc": { "score": reward } },
|
doc! { "$inc": { "score": reward } },
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
@ -165,16 +164,3 @@ 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,10 +1,4 @@
|
||||||
use std::borrow::Borrow;
|
use rocket::{http::Status, response, response::Responder, Request};
|
||||||
|
|
||||||
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;
|
||||||
|
@ -36,7 +30,7 @@ impl ToString for Ordering {
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum PartyError {
|
pub enum PartyError {
|
||||||
#[error("{source}")]
|
#[error("internal error {source:?}")]
|
||||||
CoreError {
|
CoreError {
|
||||||
#[from]
|
#[from]
|
||||||
source: CoreError,
|
source: CoreError,
|
||||||
|
@ -74,16 +68,13 @@ 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.to_string());
|
println!("{:#?}", &self);
|
||||||
let message = self.to_string();
|
|
||||||
match self {
|
match self {
|
||||||
Self::CoreError { source } => match source {
|
Self::CoreError { source } => match source {
|
||||||
CoreError::UserNotFound(_) | CoreError::EventNotFound(_) => {
|
CoreError::UserNotFound(_) | CoreError::EventNotFound(_) => Status::NotFound,
|
||||||
(Status::NotFound, "not found".to_string())
|
_ => Status::InternalServerError,
|
||||||
}
|
|
||||||
_ => (Status::BadRequest, message),
|
|
||||||
},
|
},
|
||||||
_ => (Status::InternalServerError, "unknown error".to_string()),
|
_ => Status::InternalServerError,
|
||||||
}
|
}
|
||||||
.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(open=true) {
|
details {
|
||||||
summary { (props.title) }
|
summary { (props.title) }
|
||||||
p { (children) }
|
p { (children) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,7 @@ use std::{
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::components::Block;
|
||||||
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::*;
|
||||||
|
@ -288,6 +284,7 @@ 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(),
|
||||||
)
|
)
|
||||||
|
@ -462,58 +459,6 @@ 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,11 +1,8 @@
|
||||||
#[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;
|
||||||
|
@ -13,54 +10,13 @@ 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::{
|
use std::collections::HashMap;
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
hash::Hash,
|
|
||||||
};
|
|
||||||
|
|
||||||
type User = WithContext<Users, String>;
|
#[derive(Clone, Debug, Default)]
|
||||||
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<User, i64>,
|
pub points: HashMap<String, i64>,
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for EventOutcome {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
points: HashMap::<User, _>::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Event
|
/// # Event
|
||||||
|
@ -70,7 +26,6 @@ impl Default for 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))]
|
||||||
|
@ -149,10 +104,6 @@ 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))
|
||||||
|
@ -176,7 +127,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::Other("invalid update: update type does not match event type".into())),
|
_ => Err(PartyError::Unknown("invalid update".into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,17 +204,19 @@ pub mod test {
|
||||||
|
|
||||||
fn outcome(&self) -> EventOutcome {
|
fn outcome(&self) -> EventOutcome {
|
||||||
let mut points = HashMap::new();
|
let mut points = HashMap::new();
|
||||||
points.insert("420".to_string().into(), self.num_players);
|
points.insert("420".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::{FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate},
|
free_for_all_game::{
|
||||||
|
FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate,
|
||||||
|
FreeForAllGameUpdateParticipants, FreeForAllGameUpdateRanking,
|
||||||
|
FreeForAllGameUpdateRewards,
|
||||||
|
},
|
||||||
*,
|
*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -273,7 +226,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<Team, Vec<User>>,
|
pub teams: HashMap<String, Vec<String>>,
|
||||||
|
|
||||||
#[cfg_attr(feature = "serde", serde(flatten))]
|
#[cfg_attr(feature = "serde", serde(flatten))]
|
||||||
pub ffa_game: FreeForAllGame,
|
pub ffa_game: FreeForAllGame,
|
||||||
|
@ -285,7 +238,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<Team, Vec<User>>,
|
pub teams: HashMap<String, Vec<String>>,
|
||||||
|
|
||||||
/// 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.)
|
||||||
|
@ -308,8 +261,44 @@ 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: Team,
|
pub team: String,
|
||||||
pub members: Vec<User>,
|
pub members: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)]
|
||||||
|
@ -318,34 +307,19 @@ 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 {
|
||||||
/// Add or replace a team with the given name and array of members
|
/// # Team
|
||||||
SetTeam(TeamGameUpdateSetTeam),
|
///
|
||||||
/// Remove team with given name
|
/// Team specific updates
|
||||||
RemoveTeam(Team),
|
Team(TeamGameUpdateInner),
|
||||||
/// Replace the current ranking with the given ranking
|
/// # Other
|
||||||
SetRanking(Ranking<Team>),
|
///
|
||||||
/// If the current ranking is of type `Scores`, apply the given score deltas
|
/// Inherited from FreeForAllGame
|
||||||
ScoreDelta(HashMap<Team, i64>),
|
Ffa(TeamGameFfaInheritedUpdate),
|
||||||
/// 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::SetTeam(TeamGameUpdateSetTeam::default())
|
TeamGameUpdate::Team(TeamGameUpdateInner::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()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,7 +329,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().map(|k| User::from(k.clone())).collect(),
|
participants: spec.teams.keys().cloned().collect(),
|
||||||
win_rewards: spec.win_rewards,
|
win_rewards: spec.win_rewards,
|
||||||
lose_rewards: spec.lose_rewards,
|
lose_rewards: spec.lose_rewards,
|
||||||
};
|
};
|
||||||
|
@ -368,42 +342,32 @@ 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::SetRanking(x) => self
|
TeamGameUpdate::Ffa(update) => match update {
|
||||||
.ffa_game
|
TeamGameFfaInheritedUpdate::Ranking(u) => {
|
||||||
.apply_update(FreeForAllGameUpdate::SetRanking(x.into())),
|
self.ffa_game.apply_update(FreeForAllGameUpdate::Ranking(u))
|
||||||
TeamGameUpdate::ScoreDelta(x) => {
|
}
|
||||||
self.ffa_game.apply_update(FreeForAllGameUpdate::ScoreDelta(
|
TeamGameFfaInheritedUpdate::Rewards(u) => {
|
||||||
x.into_iter().map(|(k, v)| (k.into(), v)).collect(),
|
self.ffa_game.apply_update(FreeForAllGameUpdate::Rewards(u))
|
||||||
))
|
}
|
||||||
}
|
},
|
||||||
TeamGameUpdate::SetWinRewards(x) => self
|
TeamGameUpdate::Team(update) => match update {
|
||||||
.ffa_game
|
TeamGameUpdateInner::SetTeam(u) => {
|
||||||
.apply_update(FreeForAllGameUpdate::SetWinRewards(x)),
|
self.ffa_game
|
||||||
TeamGameUpdate::SetLoseRewards(x) => self
|
.apply_update(FreeForAllGameUpdate::Participants(
|
||||||
.ffa_game
|
FreeForAllGameUpdateParticipants::AddParticipant(u.team.clone()),
|
||||||
.apply_update(FreeForAllGameUpdate::SetLoseRewards(x)),
|
))?;
|
||||||
TeamGameUpdate::SetTeam(u) => {
|
self.teams.insert(u.team, u.members);
|
||||||
self.ffa_game
|
Ok(())
|
||||||
.apply_update(FreeForAllGameUpdate::AddParticipant(
|
}
|
||||||
u.team.clone().into(),
|
TeamGameUpdateInner::RemoveTeam(team) => {
|
||||||
))?;
|
self.ffa_game
|
||||||
self.teams.insert(
|
.apply_update(FreeForAllGameUpdate::Participants(
|
||||||
u.team,
|
FreeForAllGameUpdateParticipants::RemoveParticipant(team.clone()),
|
||||||
u.members
|
))?;
|
||||||
.into_iter()
|
self.teams.remove(&team);
|
||||||
.map(|t| (t.clone()).deref().clone().into())
|
Ok(())
|
||||||
.collect(),
|
}
|
||||||
);
|
},
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
TeamGameUpdate::RemoveTeam(team) => {
|
|
||||||
self.ffa_game
|
|
||||||
.apply_update(FreeForAllGameUpdate::RemoveParticipant(
|
|
||||||
team.clone().into(),
|
|
||||||
))?;
|
|
||||||
self.teams.remove(&team);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,10 +377,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.to_string()) {
|
if let Some(team) = self.teams.get(team) {
|
||||||
for player in team {
|
for player in team {
|
||||||
let score = points.get(&player.clone().into()).unwrap_or(&0);
|
let score = points.get(player).unwrap_or(&0);
|
||||||
points.insert(User::from(player.clone()), score + reward);
|
points.insert(player.clone(), score + reward);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -431,15 +395,32 @@ 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 struct User {
|
pub enum FreeForAllGameRanking {
|
||||||
name: String,
|
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
||||||
|
/// 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))]
|
||||||
|
@ -448,7 +429,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<Ranking<User>>,
|
pub ranking: Option<FreeForAllGameRanking>,
|
||||||
|
|
||||||
/// Specification of the game
|
/// Specification of the game
|
||||||
#[cfg_attr(feature = "serde", serde(flatten))]
|
#[cfg_attr(feature = "serde", serde(flatten))]
|
||||||
|
@ -469,7 +450,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<User>,
|
pub participants: HashSet<String>,
|
||||||
|
|
||||||
/// 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.)
|
||||||
|
@ -481,31 +462,80 @@ 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 {
|
||||||
/// Replace the current ranking with the given ranking
|
/// Change the ranking and scores
|
||||||
SetRanking(Ranking<User>),
|
Ranking(FreeForAllGameUpdateRanking),
|
||||||
/// If the current ranking is of type `Scores`, apply the given score deltas
|
/// Update rewards
|
||||||
ScoreDelta(HashMap<User, i64>),
|
Rewards(FreeForAllGameUpdateRewards),
|
||||||
/// Set rewards for winning the game
|
/// Update participants
|
||||||
SetWinRewards(Vec<i64>),
|
Participants(FreeForAllGameUpdateParticipants),
|
||||||
/// 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::AddParticipant(String::new().into())
|
Self::Ranking(FreeForAllGameUpdateRanking::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -522,65 +552,71 @@ 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::SetRanking(r) => {
|
FreeForAllGameUpdate::Ranking(update) => match update {
|
||||||
if !r.is_valid(&self.spec.participants) {
|
FreeForAllGameUpdateRanking::SetRanking(r) => {
|
||||||
return Err(PartyError::Other("invalid ranking, all participants mentioned in ranking must be participating".into()));
|
if !r.is_valid(&self.spec.participants) {
|
||||||
|
return Err(PartyError::Unknown("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 {
|
||||||
}
|
Some(FreeForAllGameRanking::Ranking(_)) | None => {
|
||||||
FreeForAllGameUpdate::ScoreDelta(d) => match &mut self.ranking {
|
return Err(PartyError::Unknown("cannot apply score delta".into()))
|
||||||
Some(Ranking::Ranking(_)) => {
|
}
|
||||||
return Err(PartyError::Other("cannot apply score delta".into()))
|
Some(FreeForAllGameRanking::Scores(s)) => {
|
||||||
|
for (participant, delta) in d.iter() {
|
||||||
|
if let Some(value) = s.get(participant) {
|
||||||
|
s.insert(participant.clone(), value + delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FreeForAllGameUpdate::Participants(update) => match update {
|
||||||
|
FreeForAllGameUpdateParticipants::AddParticipant(name) => {
|
||||||
|
self.spec.participants.insert(name);
|
||||||
}
|
}
|
||||||
None => self.ranking = Some(Ranking::Scores(d)),
|
FreeForAllGameUpdateParticipants::RemoveParticipant(name) => {
|
||||||
Some(Ranking::Scores(s)) => {
|
self.spec.participants.remove(&name);
|
||||||
for (participant, delta) in d.iter() {
|
|
||||||
let value = s.get(participant).unwrap_or(&0);
|
if !self
|
||||||
s.insert(participant.clone(), value + delta);
|
.ranking
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.is_valid(&self.spec.participants))
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
self.spec.participants.insert(name);
|
||||||
|
return Err(PartyError::Unknown("cannot remove participant, all participants mentioned in ranking must be participating".into()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
FreeForAllGameUpdateParticipants::SetParticipants(participants) => {
|
||||||
|
if !self
|
||||||
|
.ranking
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.is_valid(&participants))
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
return Err(PartyError::Unknown("invalid list of participants, all participants mentioned in ranking must be participating".into()));
|
||||||
|
}
|
||||||
|
self.spec.participants = participants;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
FreeForAllGameUpdate::AddParticipant(name) => {
|
FreeForAllGameUpdate::Rewards(update) => match update {
|
||||||
self.spec.participants.insert(name);
|
FreeForAllGameUpdateRewards::SetWinRewards(rewards) => {
|
||||||
}
|
self.spec.win_rewards = rewards;
|
||||||
FreeForAllGameUpdate::RemoveParticipant(name) => {
|
|
||||||
self.spec.participants.remove(&name);
|
|
||||||
|
|
||||||
if !self
|
|
||||||
.ranking
|
|
||||||
.as_ref()
|
|
||||||
.map(|r| r.is_valid(&self.spec.participants))
|
|
||||||
.unwrap_or(true)
|
|
||||||
{
|
|
||||||
self.spec.participants.insert(name);
|
|
||||||
return Err(PartyError::Other("cannot remove participant, all participants mentioned in ranking must be participating".into()));
|
|
||||||
}
|
}
|
||||||
}
|
FreeForAllGameUpdateRewards::SetLoseRewards(rewards) => {
|
||||||
FreeForAllGameUpdate::SetParticipants(participants) => {
|
self.spec.lose_rewards = rewards;
|
||||||
if !self
|
|
||||||
.ranking
|
|
||||||
.as_ref()
|
|
||||||
.map(|r| r.is_valid(&participants))
|
|
||||||
.unwrap_or(true)
|
|
||||||
{
|
|
||||||
return Err(PartyError::Other("invalid list of participants, all participants mentioned in ranking must be participating".into()));
|
|
||||||
}
|
}
|
||||||
self.spec.participants = participants;
|
},
|
||||||
}
|
|
||||||
FreeForAllGameUpdate::SetWinRewards(rewards) => {
|
|
||||||
self.spec.win_rewards = rewards;
|
|
||||||
}
|
|
||||||
FreeForAllGameUpdate::SetLoseRewards(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(Ranking::Ranking(r)) => r.clone(),
|
Some(FreeForAllGameRanking::Ranking(r)) => r.clone(),
|
||||||
Some(Ranking::Scores(s)) => {
|
Some(FreeForAllGameRanking::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,5 +1,4 @@
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod state;
|
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
#[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,12 +4,6 @@ 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)]
|
||||||
|
@ -18,8 +12,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("{0}")]
|
#[error("unknown error: {0}")]
|
||||||
Other(String),
|
Unknown(String),
|
||||||
#[error("invalid parameter: {0}")]
|
#[error("invalid parameter: {0}")]
|
||||||
InvalidParameter(String),
|
InvalidParameter(String),
|
||||||
}
|
}
|
||||||
|
@ -48,86 +42,3 @@ 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,7 +2,6 @@ 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 _;
|
||||||
|
|
||||||
|
@ -15,10 +14,7 @@ pub mod prelude {
|
||||||
pub use sycamore::prelude::*;
|
pub use sycamore::prelude::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::{
|
use crate::components::{Block, BlockProps};
|
||||||
components::{Block, BlockProps},
|
|
||||||
util::WithContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! viewable {
|
macro_rules! viewable {
|
||||||
|
@ -178,7 +174,6 @@ 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,17 +203,3 @@ 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,5 +14,4 @@ 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,20 +1,9 @@
|
||||||
|
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::{
|
use syn::{token::Do, Attribute, DataEnum, DataStruct, Fields, Ident, Path, Type};
|
||||||
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 {
|
||||||
|
@ -23,16 +12,6 @@ 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") {
|
||||||
|
@ -51,116 +30,34 @@ impl Documentation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewAttribute {
|
fn get_title_description(attrs: &[Attribute]) -> (Option<String>, Option<String>) {
|
||||||
fn parse(attr: &Attribute) -> ViewAttribute {
|
let docs: Vec<_> = attrs.iter().map(Documentation::parse).collect();
|
||||||
if !attr.path.is_ident("view") {
|
|
||||||
return Self::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed: Result<MetaNameValue, _> = parse_str(
|
let mut title = None;
|
||||||
attr.tokens
|
let mut description: Option<String> = None;
|
||||||
.to_string()
|
|
||||||
.trim_matches(|c: char| c == '(' || c == ')'),
|
|
||||||
);
|
|
||||||
|
|
||||||
match parsed {
|
for doc in docs {
|
||||||
Ok(p) => match (p.path.get_ident().unwrap().to_string().as_str(), p.lit) {
|
match doc {
|
||||||
("title", Lit::Str(ls)) => Self::Title(format_ident!("{}", ls.value())),
|
Documentation::Title(t) => title = Some(t),
|
||||||
_ => Self::None,
|
Documentation::Description(d) => {
|
||||||
},
|
if description.is_some() {
|
||||||
Err(_) => Self::None,
|
description.as_mut().unwrap().push(' ');
|
||||||
}
|
} else {
|
||||||
}
|
let _ = description.insert(String::new());
|
||||||
}
|
}
|
||||||
|
description.as_mut().unwrap().push_str(&d);
|
||||||
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_field = None;
|
|
||||||
let mut description: Option<String> = None;
|
|
||||||
let mut untagged = false;
|
|
||||||
|
|
||||||
for attr in parsed {
|
|
||||||
match attr {
|
|
||||||
ParsedAttribute::Documentation(doc) => match doc {
|
|
||||||
Documentation::Title(t) => title = Some(t),
|
|
||||||
Documentation::Description(d) => {
|
|
||||||
if description.is_some() {
|
|
||||||
description.as_mut().unwrap().push(' ');
|
|
||||||
} else {
|
|
||||||
let _ = description.insert(String::new());
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
_ => {}
|
||||||
|
|
||||||
Self {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
title_field,
|
|
||||||
untagged,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(title, description)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ItemProps {
|
struct ItemProps {
|
||||||
name: Ident,
|
name: Ident,
|
||||||
attributes: Attributes,
|
title: String,
|
||||||
generics: Generics,
|
description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StructField {
|
struct StructField {
|
||||||
|
@ -181,12 +78,9 @@ struct EnumVariant {
|
||||||
fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
let ItemProps {
|
let ItemProps {
|
||||||
name,
|
name,
|
||||||
attributes: Attributes {
|
title,
|
||||||
title, description, ..
|
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
|
||||||
|
@ -195,12 +89,7 @@ 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 Attributes {
|
let (title, description) = get_title_description(&f.attrs);
|
||||||
title,
|
|
||||||
title_field: _,
|
|
||||||
description,
|
|
||||||
..
|
|
||||||
} = Attributes::parse(&f.attrs);
|
|
||||||
|
|
||||||
StructField {
|
StructField {
|
||||||
name_str,
|
name_str,
|
||||||
|
@ -270,12 +159,9 @@ 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,
|
||||||
attributes: Attributes {
|
title,
|
||||||
title, description, ..
|
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();
|
||||||
|
@ -285,12 +171,7 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let Attributes {
|
let (title, description) = get_title_description(&v.attrs);
|
||||||
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,
|
||||||
|
@ -393,7 +274,6 @@ 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, }
|
||||||
|
@ -406,16 +286,9 @@ 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:
|
title,
|
||||||
Attributes {
|
description,
|
||||||
title,
|
|
||||||
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
|
||||||
|
@ -424,12 +297,7 @@ 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 Attributes {
|
let (title, description) = get_title_description(&f.attrs);
|
||||||
title,
|
|
||||||
title_field: _,
|
|
||||||
description,
|
|
||||||
..
|
|
||||||
} = Attributes::parse(&f.attrs);
|
|
||||||
|
|
||||||
StructField {
|
StructField {
|
||||||
name_str,
|
name_str,
|
||||||
|
@ -466,16 +334,6 @@ 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;
|
||||||
|
|
||||||
|
@ -495,12 +353,9 @@ 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,
|
||||||
attributes: Attributes {
|
title,
|
||||||
title, description, ..
|
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();
|
||||||
|
@ -510,12 +365,7 @@ fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let Attributes {
|
let (title, description) = get_title_description(&v.attrs);
|
||||||
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,
|
||||||
|
@ -579,12 +429,15 @@ 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 attrs = Attributes::parse(&input.attrs);
|
let (t, d) = get_title_description(&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(),
|
||||||
attributes: attrs,
|
title: title.clone(),
|
||||||
generics: input.generics.clone(),
|
description: description.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = match input.data {
|
let inner = match input.data {
|
||||||
|
@ -593,43 +446,16 @@ 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 #impl_generics Editor<'a, G, #name #input_ty_generics> for #edit_ident #where_clause {
|
impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident {
|
||||||
fn edit(cx: Scope<'a>, props: EditProps<'a, #name #input_ty_generics>) -> View<G> {
|
fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View<G> {
|
||||||
#inner
|
#inner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #impl_generics Editable<'a, G> for #name #input_ty_generics #where_clause {
|
impl<'a, G: Html> Editable<'a, G> for #name {
|
||||||
type Editor = #edit_ident;
|
type Editor = #edit_ident;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -637,19 +463,22 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
||||||
TokenStream::from(res)
|
TokenStream::from(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(WebView, attributes(view))]
|
#[proc_macro_derive(WebView)]
|
||||||
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 attrs = Attributes::parse(&input.attrs);
|
let (t, d) = get_title_description(&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(),
|
||||||
attributes: attrs,
|
title: title.clone(),
|
||||||
generics: input.generics.clone(),
|
description: description.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = match input.data {
|
let inner = match input.data {
|
||||||
|
@ -658,45 +487,16 @@ 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 #impl_generics Viewer<'a, G, #name #input_ty_generics> for #view_ident #where_clause {
|
impl<'a, G: Html> Viewer<'a, G, #name> for #view_ident {
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, #name #input_ty_generics>) -> View<G> {
|
fn view(cx: Scope<'a>, props: ViewProps<'a, #name>) -> View<G> {
|
||||||
#inner
|
#inner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #impl_generics Viewable<'a, G> for #name #input_ty_generics #where_clause {
|
impl<'a, G: Html> Viewable<'a, G> for #name {
|
||||||
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-2:/data/db:z \
|
-v lan-party-db:/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-95f29b132ea1fddf.css">
|
<link rel="stylesheet" href="/style-5e1a36be9e479fbe.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-1e8437b6157bd12b_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
<link rel="preload" href="/index-61979c95126900f4_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||||
<link rel="modulepreload" href="/index-1e8437b6157bd12b.js"></head>
|
<link rel="modulepreload" href="/index-61979c95126900f4.js"></head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module">import init from '/index-1e8437b6157bd12b.js';init('/index-1e8437b6157bd12b_bg.wasm');</script></body></html>
|
<script type="module">import init from '/index-61979c95126900f4.js';init('/index-61979c95126900f4_bg.wasm');</script></body></html>
|
|
@ -0,0 +1,139 @@
|
||||||
|
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! {} })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,105 +0,0 @@
|
||||||
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,9 +1,31 @@
|
||||||
pub mod messages;
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
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>,
|
||||||
|
@ -34,32 +56,19 @@ pub fn Table<'a, G: Html>(cx: Scope<'a>, props: TableProps<'a, G>) -> View<G> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Prop)]
|
#[derive(Prop)]
|
||||||
pub struct ModalProps<'a, G: Html> {
|
pub struct BlockProps<'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 Modal<'a, G: Html>(cx: Scope<'a>, props: ModalProps<'a, G>) -> View<G> {
|
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);
|
||||||
|
|
||||||
let class = create_memo(cx, || {
|
|
||||||
if *props.open.get() {
|
|
||||||
"modal"
|
|
||||||
} else {
|
|
||||||
"modal hidden"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
div(class=class) {
|
details {
|
||||||
h4 { (props.title) }
|
summary { (props.title) }
|
||||||
div(class="modal-close") {
|
p { (children) }
|
||||||
Button(icon="mdi-close".into(), onclick=move |_| props.open.set(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
(children)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,448 @@
|
||||||
|
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,14 +1,9 @@
|
||||||
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 state::{EventsExt, UsersExt};
|
use sycamore::prelude::*;
|
||||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
|
||||||
use sycamore_router::{HistoryIntegration, Route, Router};
|
use sycamore_router::{HistoryIntegration, Route, Router};
|
||||||
|
|
||||||
#[derive(Route)]
|
#[derive(Route)]
|
||||||
|
@ -67,55 +62,11 @@ 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,11 +1,6 @@
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use lan_party_core::{
|
use lan_party_core::{
|
||||||
components::Block,
|
|
||||||
edit::IntoEdit,
|
edit::IntoEdit,
|
||||||
event::{Event, EventOutcome, EventSpec, EventUpdate},
|
event::{Event, EventSpec, EventUpdate},
|
||||||
util::WithContext,
|
|
||||||
view::IntoView,
|
view::IntoView,
|
||||||
};
|
};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
@ -13,182 +8,63 @@ use reqwasm::http::Method;
|
||||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{messages::Messenger, Button, Modal, Table},
|
components::{Block, Button},
|
||||||
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 current_event = create_signal(cx, String::new());
|
let events: &'a Signal<Vec<Event>> = create_signal(cx, Vec::<Event>::new());
|
||||||
let show_outcome = create_signal(cx, false);
|
|
||||||
let event_outcome = create_signal(cx, EventOutcome::default());
|
|
||||||
|
|
||||||
let events = use_context::<Events>(cx);
|
spawn_local_scoped(cx, async move {
|
||||||
let users = use_context::<Users>(cx);
|
events.set(
|
||||||
|
api_request::<_, Vec<Event>>(Method::GET, "/event", Option::<()>::None)
|
||||||
|
.await
|
||||||
|
.map(|inner| inner.unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
let dispatch = move |msg: Msg| {
|
let onadd = move |_| {
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
match msg {
|
let new_event = api_request::<EventSpec, Event>(
|
||||||
Msg::Add(event_spec) => {
|
Method::POST,
|
||||||
let name = event_spec.name.clone();
|
"/event",
|
||||||
messenger.add_result(
|
Some((*event_spec).get().as_ref().clone()),
|
||||||
events.add(event_spec).await,
|
)
|
||||||
Some(format!("Created a new event with name \"{}\"", name)),
|
.await
|
||||||
Some("Error when adding an event"),
|
.unwrap();
|
||||||
);
|
debug!("{:#?}", new_event);
|
||||||
}
|
events.modify().push(new_event.unwrap());
|
||||||
Msg::Reload => messenger.add_result(
|
|
||||||
events.load().await,
|
|
||||||
Option::<String>::None,
|
|
||||||
Some("Failed to load events"),
|
|
||||||
),
|
|
||||||
Msg::Update(event_update_name, event_update) => {
|
|
||||||
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
|
|
||||||
.and_then(|inner| inner.ok_or(anyhow!("missing body")));
|
|
||||||
|
|
||||||
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()) {
|
Block(title="Events".into()) {
|
||||||
Table(headers=vec!["Username".into(), "Score".into()]) {
|
Keyed(
|
||||||
Indexed(
|
iterable=&events,
|
||||||
iterable=&outcome_points,
|
view=move |cx, event| {
|
||||||
view=move |cx, (k, v)| {
|
let event = create_ref(cx, event);
|
||||||
view! { cx,
|
view! { cx,
|
||||||
tr {
|
(event.view(cx))
|
||||||
td { (*k.clone()) }
|
br()
|
||||||
td { (v.clone()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
}
|
key=|event| (event.name.clone()),
|
||||||
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| dispatch(Msg::ViewOutcome(current_event.get().as_ref().clone())))
|
)
|
||||||
}
|
}
|
||||||
div(class="events-cols") {
|
br()
|
||||||
div {
|
Block(title="Create new event".into()) {
|
||||||
Block(title="Events".into()) {
|
(event_spec.edit(cx))
|
||||||
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| dispatch(Msg::Reload))
|
Button(icon="mdi-check".into(), onclick=onadd)
|
||||||
Indexed(
|
}
|
||||||
iterable=&events.get(),
|
br()
|
||||||
view=move |cx, event| {
|
Block(title="Update an event".into()) {
|
||||||
let event = create_ref(cx, event);
|
(event_update.edit(cx))
|
||||||
view! { cx,
|
Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_update.get()))
|
||||||
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()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div(class="events-right") {
|
|
||||||
Block(title="Create new event".into()) {
|
|
||||||
(event_spec.edit(cx))
|
|
||||||
Button(icon="mdi-check".into(), onclick=move |_| dispatch(Msg::Add(event_spec.get().as_ref().clone())))
|
|
||||||
}
|
|
||||||
br()
|
|
||||||
Block(title="Update an event".into()) {
|
|
||||||
label { "Event name" }
|
|
||||||
(event_update_name.edit(cx))
|
|
||||||
(event_update.edit(cx))
|
|
||||||
Button(
|
|
||||||
icon="mdi-check".into(),
|
|
||||||
onclick=move |_| dispatch(Msg::Update(
|
|
||||||
event_update_name.get().as_ref().deref().clone(),
|
|
||||||
event_update.get().as_ref().clone()
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
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,55 +1,84 @@
|
||||||
use crate::{
|
use crate::components::{Button, Table};
|
||||||
components::{messages::Messenger, Button, Table},
|
use lan_party_core::user::User;
|
||||||
state::{EventsExt, UsersExt},
|
use log::debug;
|
||||||
};
|
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 |_| {
|
move |event: Event| {
|
||||||
let name = name.clone();
|
let name = name.clone();
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
messenger.add_result(
|
debug!("Delete {:#?}", event);
|
||||||
users.delete(&name).await,
|
let users_ref = users.get();
|
||||||
Some(format!("Deleted user {}", name)),
|
let user: &User = users_ref.iter().find(|user| user.name == name).unwrap();
|
||||||
Some("Failed to delete user"),
|
api_request::<_, ()>(
|
||||||
);
|
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(name), Ok(score)) = (score_edit.get().as_ref(), new_score.get().parse()) {
|
if let (Some(score_edit), Ok(score)) =
|
||||||
|
(score_edit.get().as_ref(), new_score.get().parse())
|
||||||
|
{
|
||||||
let score: i64 = score;
|
let score: i64 = score;
|
||||||
messenger.add_result(
|
let users_ref = users.get();
|
||||||
users.update_score(&name, score).await,
|
let user: &User = users_ref
|
||||||
Some(format!("Updated score for user {}", name)),
|
.iter()
|
||||||
Some("Failed to delete user"),
|
.find(|user| &user.name == score_edit)
|
||||||
);
|
.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);
|
||||||
})
|
})
|
||||||
|
@ -57,19 +86,21 @@ 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 {
|
||||||
messenger.add_result(
|
let user = api_request::<String, User>(
|
||||||
users.add(&new_username.get()).await,
|
Method::POST,
|
||||||
Some(format!("Added new user {}", new_username.get())),
|
"/user",
|
||||||
Some("Failed to add user"),
|
Some((*new_username).get().as_ref().clone()),
|
||||||
);
|
)
|
||||||
|
.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.get(),
|
iterable=users,
|
||||||
view=move |cx, user| {
|
view=move |cx, user| {
|
||||||
let user = create_ref(cx, user);
|
let user = create_ref(cx, user);
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
140
web/src/state.rs
140
web/src/state.rs
|
@ -1,140 +0,0 @@
|
||||||
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,18 +33,10 @@ 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 {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if let Ok(text) = res.text().await {
|
Ok(None)
|
||||||
Err(anyhow!("Request failed: {}", text.to_string()))
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Request failed"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,84 +64,5 @@ textarea:focus, input:focus{
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
grid-template-columns: 1fr min(100rem, 90%) 1fr;
|
grid-template-columns: 1fr min(60rem, 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