Compare commits

..

No commits in common. "master" and "less-lifetimes" have entirely different histories.

28 changed files with 1190 additions and 1364 deletions

49
Cargo.lock generated
View File

@ -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",

View File

@ -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)
}

View File

@ -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)
} }

View File

@ -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) }
} }

View File

@ -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,

View File

@ -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()

View File

@ -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;

View File

@ -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())
}
}

View File

@ -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>>;
}

View File

@ -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;
}

View File

@ -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"] }

0
macros/src/edit.rs Normal file
View File

View File

@ -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;
} }
}; };

View File

@ -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 \

View File

@ -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"

8
web/dist/index.html vendored
View File

@ -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>

139
web/src/' Normal file
View File

@ -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! {} })
}
}
}
}
}
}

View File

@ -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),
)
}
}
}

View File

@ -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)
} }
} }
} }

448
web/src/event_old.rs Normal file
View File

@ -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;
}
*/

View File

@ -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) {

View File

@ -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()
))
)
}
}
} }
} }
} }

View File

@ -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>
}
}

View File

@ -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,

109
web/src/pages/users_old.rs Normal file
View File

@ -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>
}
}

View File

@ -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(())
}
}

View File

@ -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"))
}
} }
} }

View File

@ -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;
} }