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"
|
||||
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
||||
dependencies = [
|
||||
"darling_core 0.13.4",
|
||||
"darling_macro 0.13.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
|
||||
dependencies = [
|
||||
"darling_core 0.14.1",
|
||||
"darling_macro 0.14.1",
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -404,38 +394,13 @@ dependencies = [
|
|||
"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]]
|
||||
name = "darling_macro"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
||||
dependencies = [
|
||||
"darling_core 0.13.4",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
|
||||
dependencies = [
|
||||
"darling_core 0.14.1",
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
@ -1208,7 +1173,6 @@ name = "lan_party_macros"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"darling 0.14.1",
|
||||
"paste",
|
||||
"quote",
|
||||
"sycamore",
|
||||
|
@ -1220,10 +1184,7 @@ name = "lan_party_web"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"console_log",
|
||||
"futures",
|
||||
"gloo-timers",
|
||||
"js-sys",
|
||||
"lan_party_core",
|
||||
"log",
|
||||
|
@ -2003,7 +1964,7 @@ version = "0.8.0-rc.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54f94d1ffe41472e08463d7a2674f1db04dc4df745285e8369b33d3cfd6b0308"
|
||||
dependencies = [
|
||||
"darling 0.13.4",
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rocket_http",
|
||||
|
@ -2251,7 +2212,7 @@ version = "1.5.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
|
||||
dependencies = [
|
||||
"darling 0.13.4",
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
|
|
|
@ -14,14 +14,13 @@ api_routes!(
|
|||
update_event,
|
||||
get_all_events,
|
||||
event_outcome,
|
||||
delete_event,
|
||||
);
|
||||
|
||||
pub async fn apply_outcome(outcome: &EventOutcome, db: &Connection<Db>) -> Result<(), PartyError> {
|
||||
for (player, reward) in outcome.points.iter() {
|
||||
db.users()
|
||||
.update_one(
|
||||
doc! { "name": player.to_string() },
|
||||
doc! { "id": player },
|
||||
doc! { "$inc": { "score": reward } },
|
||||
None,
|
||||
)
|
||||
|
@ -165,16 +164,3 @@ pub async fn stop_event(
|
|||
|
||||
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::{self, status, Responder},
|
||||
Request,
|
||||
};
|
||||
use rocket::{http::Status, response, response::Responder, Request};
|
||||
use rocket_db_pools::mongodb;
|
||||
use rocket_okapi::response::OpenApiResponderInner;
|
||||
use schemars::JsonSchema;
|
||||
|
@ -36,7 +30,7 @@ impl ToString for Ordering {
|
|||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PartyError {
|
||||
#[error("{source}")]
|
||||
#[error("internal error {source:?}")]
|
||||
CoreError {
|
||||
#[from]
|
||||
source: CoreError,
|
||||
|
@ -74,16 +68,13 @@ impl OpenApiResponderInner for PartyError {
|
|||
|
||||
impl<'r, 'o: 'r> Responder<'r, 'o> for PartyError {
|
||||
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> {
|
||||
println!("{}", self.to_string());
|
||||
let message = self.to_string();
|
||||
println!("{:#?}", &self);
|
||||
match self {
|
||||
Self::CoreError { source } => match source {
|
||||
CoreError::UserNotFound(_) | CoreError::EventNotFound(_) => {
|
||||
(Status::NotFound, "not found".to_string())
|
||||
}
|
||||
_ => (Status::BadRequest, message),
|
||||
CoreError::UserNotFound(_) | CoreError::EventNotFound(_) => Status::NotFound,
|
||||
_ => Status::InternalServerError,
|
||||
},
|
||||
_ => (Status::InternalServerError, "unknown error".to_string()),
|
||||
_ => Status::InternalServerError,
|
||||
}
|
||||
.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);
|
||||
|
||||
view! { cx,
|
||||
details(open=true) {
|
||||
details {
|
||||
summary { (props.title) }
|
||||
p { (children) }
|
||||
}
|
||||
|
|
|
@ -5,11 +5,7 @@ use std::{
|
|||
str::FromStr,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
components::Block,
|
||||
util::{ContextOptions, WithContext},
|
||||
view::Viewable,
|
||||
};
|
||||
use crate::components::Block;
|
||||
use log::debug;
|
||||
use paste::paste;
|
||||
use sycamore::prelude::*;
|
||||
|
@ -288,6 +284,7 @@ where
|
|||
vec.get()
|
||||
.as_ref()
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|x| x.get().as_ref().clone())
|
||||
.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)]
|
||||
pub struct Test {
|
||||
inner: TestInner,
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
#[cfg(feature = "sycamore")]
|
||||
use crate::edit::prelude::*;
|
||||
use crate::util::PartyError;
|
||||
#[cfg(feature = "sycamore")]
|
||||
use crate::view::prelude::*;
|
||||
use crate::{
|
||||
state::Users,
|
||||
util::{PartyError, WithContext},
|
||||
};
|
||||
#[cfg(feature = "sycamore")]
|
||||
use lan_party_macros::{WebEdit, WebView};
|
||||
use paste::paste;
|
||||
|
@ -13,54 +10,13 @@ use paste::paste;
|
|||
use schemars::JsonSchema;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
hash::Hash,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
type User = WithContext<Users, String>;
|
||||
type Team = String;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||
pub enum Ranking<T: Hash + PartialEq + Eq + Clone + Default + std::fmt::Debug> {
|
||||
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
||||
/// place, etc.)
|
||||
Ranking(Vec<T>),
|
||||
/// Score based ranking of participants/teams
|
||||
Scores(HashMap<T, i64>),
|
||||
}
|
||||
|
||||
impl<T: Hash + PartialEq + Eq + Clone + Default + std::fmt::Debug> Default for Ranking<T> {
|
||||
fn default() -> Self {
|
||||
Self::Ranking(Vec::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash + PartialEq + Eq + Clone + Default + std::fmt::Debug> Ranking<T> {
|
||||
pub fn is_valid(&self, participants: &HashSet<T>) -> bool {
|
||||
match self {
|
||||
Self::Ranking(v) => v.iter().all(|p| participants.contains(p)),
|
||||
Self::Scores(m) => m.keys().all(|p| participants.contains(p)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct EventOutcome {
|
||||
pub points: HashMap<User, i64>,
|
||||
}
|
||||
|
||||
impl Default for EventOutcome {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
points: HashMap::<User, _>::default(),
|
||||
}
|
||||
}
|
||||
pub points: HashMap<String, i64>,
|
||||
}
|
||||
|
||||
/// # Event
|
||||
|
@ -70,7 +26,6 @@ impl Default for EventOutcome {
|
|||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebView))]
|
||||
#[cfg_attr(feature = "sycamore", view(title = "name"))]
|
||||
pub struct Event {
|
||||
/// Has this event concluded?
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
|
@ -149,10 +104,6 @@ macro_rules! events {
|
|||
|
||||
impl EventSpec {
|
||||
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 {
|
||||
$(EventTypeSpec::$name(s) => {
|
||||
EventType::$name($module::$name::from_spec(s))
|
||||
|
@ -176,7 +127,7 @@ macro_rules! events {
|
|||
$((EventType::$name(s), EventUpdate::$name(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 {
|
||||
let mut points = HashMap::new();
|
||||
points.insert("420".to_string().into(), self.num_players);
|
||||
points.insert("420".into(), self.num_players);
|
||||
EventOutcome { points }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod team_game {
|
||||
use std::ops::Deref;
|
||||
|
||||
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))]
|
||||
pub struct TeamGame {
|
||||
/// 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))]
|
||||
pub ffa_game: FreeForAllGame,
|
||||
|
@ -285,7 +238,7 @@ pub mod team_game {
|
|||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||
pub struct TeamGameSpec {
|
||||
/// 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
|
||||
/// place, etc.)
|
||||
|
@ -308,8 +261,44 @@ pub mod team_game {
|
|||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||
pub struct TeamGameUpdateSetTeam {
|
||||
pub team: Team,
|
||||
pub members: Vec<User>,
|
||||
pub team: String,
|
||||
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)]
|
||||
|
@ -318,34 +307,19 @@ pub mod team_game {
|
|||
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||
pub enum TeamGameUpdate {
|
||||
/// Add or replace a team with the given name and array of members
|
||||
SetTeam(TeamGameUpdateSetTeam),
|
||||
/// Remove team with given name
|
||||
RemoveTeam(Team),
|
||||
/// Replace the current ranking with the given ranking
|
||||
SetRanking(Ranking<Team>),
|
||||
/// If the current ranking is of type `Scores`, apply the given score deltas
|
||||
ScoreDelta(HashMap<Team, i64>),
|
||||
/// Set rewards for winning the game
|
||||
SetWinRewards(Vec<i64>),
|
||||
/// Set rewards for losing the game
|
||||
SetLoseRewards(Vec<i64>),
|
||||
/// # Team
|
||||
///
|
||||
/// Team specific updates
|
||||
Team(TeamGameUpdateInner),
|
||||
/// # Other
|
||||
///
|
||||
/// Inherited from FreeForAllGame
|
||||
Ffa(TeamGameFfaInheritedUpdate),
|
||||
}
|
||||
|
||||
impl Default for TeamGameUpdate {
|
||||
fn default() -> Self {
|
||||
TeamGameUpdate::SetTeam(TeamGameUpdateSetTeam::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ranking<Team>> for Ranking<User> {
|
||||
fn from(r: Ranking<Team>) -> Self {
|
||||
match r {
|
||||
Ranking::Scores(s) => {
|
||||
Ranking::Scores(s.into_iter().map(|(k, v)| (k.into(), v)).collect())
|
||||
}
|
||||
Ranking::Ranking(s) => Ranking::Ranking(s.into_iter().map(|k| k.into()).collect()),
|
||||
}
|
||||
TeamGameUpdate::Team(TeamGameUpdateInner::default())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -355,7 +329,7 @@ pub mod team_game {
|
|||
|
||||
fn from_spec(spec: TeamGameSpec) -> Self {
|
||||
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,
|
||||
lose_rewards: spec.lose_rewards,
|
||||
};
|
||||
|
@ -368,42 +342,32 @@ pub mod team_game {
|
|||
|
||||
fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> {
|
||||
match update {
|
||||
TeamGameUpdate::SetRanking(x) => self
|
||||
.ffa_game
|
||||
.apply_update(FreeForAllGameUpdate::SetRanking(x.into())),
|
||||
TeamGameUpdate::ScoreDelta(x) => {
|
||||
self.ffa_game.apply_update(FreeForAllGameUpdate::ScoreDelta(
|
||||
x.into_iter().map(|(k, v)| (k.into(), v)).collect(),
|
||||
))
|
||||
}
|
||||
TeamGameUpdate::SetWinRewards(x) => self
|
||||
.ffa_game
|
||||
.apply_update(FreeForAllGameUpdate::SetWinRewards(x)),
|
||||
TeamGameUpdate::SetLoseRewards(x) => self
|
||||
.ffa_game
|
||||
.apply_update(FreeForAllGameUpdate::SetLoseRewards(x)),
|
||||
TeamGameUpdate::SetTeam(u) => {
|
||||
self.ffa_game
|
||||
.apply_update(FreeForAllGameUpdate::AddParticipant(
|
||||
u.team.clone().into(),
|
||||
))?;
|
||||
self.teams.insert(
|
||||
u.team,
|
||||
u.members
|
||||
.into_iter()
|
||||
.map(|t| (t.clone()).deref().clone().into())
|
||||
.collect(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
TeamGameUpdate::RemoveTeam(team) => {
|
||||
self.ffa_game
|
||||
.apply_update(FreeForAllGameUpdate::RemoveParticipant(
|
||||
team.clone().into(),
|
||||
))?;
|
||||
self.teams.remove(&team);
|
||||
Ok(())
|
||||
}
|
||||
TeamGameUpdate::Ffa(update) => match update {
|
||||
TeamGameFfaInheritedUpdate::Ranking(u) => {
|
||||
self.ffa_game.apply_update(FreeForAllGameUpdate::Ranking(u))
|
||||
}
|
||||
TeamGameFfaInheritedUpdate::Rewards(u) => {
|
||||
self.ffa_game.apply_update(FreeForAllGameUpdate::Rewards(u))
|
||||
}
|
||||
},
|
||||
TeamGameUpdate::Team(update) => match update {
|
||||
TeamGameUpdateInner::SetTeam(u) => {
|
||||
self.ffa_game
|
||||
.apply_update(FreeForAllGameUpdate::Participants(
|
||||
FreeForAllGameUpdateParticipants::AddParticipant(u.team.clone()),
|
||||
))?;
|
||||
self.teams.insert(u.team, u.members);
|
||||
Ok(())
|
||||
}
|
||||
TeamGameUpdateInner::RemoveTeam(team) => {
|
||||
self.ffa_game
|
||||
.apply_update(FreeForAllGameUpdate::Participants(
|
||||
FreeForAllGameUpdateParticipants::RemoveParticipant(team.clone()),
|
||||
))?;
|
||||
self.teams.remove(&team);
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -413,10 +377,10 @@ pub mod team_game {
|
|||
let mut points = HashMap::new();
|
||||
|
||||
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 {
|
||||
let score = points.get(&player.clone().into()).unwrap_or(&0);
|
||||
points.insert(User::from(player.clone()), score + reward);
|
||||
let score = points.get(player).unwrap_or(&0);
|
||||
points.insert(player.clone(), score + reward);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -431,15 +395,32 @@ pub mod free_for_all_game {
|
|||
|
||||
use super::*;
|
||||
|
||||
/*
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
#[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 struct User {
|
||||
name: String,
|
||||
pub enum FreeForAllGameRanking {
|
||||
/// 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)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
|
@ -448,7 +429,7 @@ pub mod free_for_all_game {
|
|||
pub struct FreeForAllGame {
|
||||
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
||||
/// place, etc.)
|
||||
pub ranking: Option<Ranking<User>>,
|
||||
pub ranking: Option<FreeForAllGameRanking>,
|
||||
|
||||
/// Specification of the game
|
||||
#[cfg_attr(feature = "serde", serde(flatten))]
|
||||
|
@ -469,7 +450,7 @@ pub mod free_for_all_game {
|
|||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||
pub struct FreeForAllGameSpec {
|
||||
/// 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
|
||||
/// place, etc.)
|
||||
|
@ -481,31 +462,80 @@ pub mod free_for_all_game {
|
|||
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)]
|
||||
#[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 FreeForAllGameUpdate {
|
||||
/// Replace the current ranking with the given ranking
|
||||
SetRanking(Ranking<User>),
|
||||
/// If the current ranking is of type `Scores`, apply the given score deltas
|
||||
ScoreDelta(HashMap<User, i64>),
|
||||
/// Set rewards for winning the game
|
||||
SetWinRewards(Vec<i64>),
|
||||
/// Set rewards for losing the game
|
||||
SetLoseRewards(Vec<i64>),
|
||||
/// Set list of participants participating in the game
|
||||
SetParticipants(HashSet<User>),
|
||||
/// Add participant by name
|
||||
AddParticipant(User),
|
||||
/// Remove participant by name
|
||||
RemoveParticipant(User),
|
||||
/// Change the ranking and scores
|
||||
Ranking(FreeForAllGameUpdateRanking),
|
||||
/// Update rewards
|
||||
Rewards(FreeForAllGameUpdateRewards),
|
||||
/// Update participants
|
||||
Participants(FreeForAllGameUpdateParticipants),
|
||||
}
|
||||
|
||||
impl Default for FreeForAllGameUpdate {
|
||||
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> {
|
||||
match update {
|
||||
FreeForAllGameUpdate::SetRanking(r) => {
|
||||
if !r.is_valid(&self.spec.participants) {
|
||||
return Err(PartyError::Other("invalid ranking, all participants mentioned in ranking must be participating".into()));
|
||||
FreeForAllGameUpdate::Ranking(update) => match update {
|
||||
FreeForAllGameUpdateRanking::SetRanking(r) => {
|
||||
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)
|
||||
}
|
||||
FreeForAllGameUpdate::ScoreDelta(d) => match &mut self.ranking {
|
||||
Some(Ranking::Ranking(_)) => {
|
||||
return Err(PartyError::Other("cannot apply score delta".into()))
|
||||
FreeForAllGameUpdateRanking::ScoreDelta(d) => match &mut self.ranking {
|
||||
Some(FreeForAllGameRanking::Ranking(_)) | None => {
|
||||
return Err(PartyError::Unknown("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)),
|
||||
Some(Ranking::Scores(s)) => {
|
||||
for (participant, delta) in d.iter() {
|
||||
let value = s.get(participant).unwrap_or(&0);
|
||||
s.insert(participant.clone(), value + delta);
|
||||
FreeForAllGameUpdateParticipants::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::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) => {
|
||||
self.spec.participants.insert(name);
|
||||
}
|
||||
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()));
|
||||
FreeForAllGameUpdate::Rewards(update) => match update {
|
||||
FreeForAllGameUpdateRewards::SetWinRewards(rewards) => {
|
||||
self.spec.win_rewards = rewards;
|
||||
}
|
||||
}
|
||||
FreeForAllGameUpdate::SetParticipants(participants) => {
|
||||
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()));
|
||||
FreeForAllGameUpdateRewards::SetLoseRewards(rewards) => {
|
||||
self.spec.lose_rewards = rewards;
|
||||
}
|
||||
self.spec.participants = participants;
|
||||
}
|
||||
FreeForAllGameUpdate::SetWinRewards(rewards) => {
|
||||
self.spec.win_rewards = rewards;
|
||||
}
|
||||
FreeForAllGameUpdate::SetLoseRewards(rewards) => {
|
||||
self.spec.lose_rewards = rewards;
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn outcome(&self) -> EventOutcome {
|
||||
let ranking = match &self.ranking {
|
||||
Some(Ranking::Ranking(r)) => r.clone(),
|
||||
Some(Ranking::Scores(s)) => {
|
||||
Some(FreeForAllGameRanking::Ranking(r)) => r.clone(),
|
||||
Some(FreeForAllGameRanking::Scores(s)) => {
|
||||
let mut results: Vec<(_, _)> = s.iter().collect();
|
||||
results.sort_by(|a, b| b.1.cmp(a.1));
|
||||
results.into_iter().map(|(k, _)| k.clone()).collect()
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
pub mod event;
|
||||
pub mod state;
|
||||
pub mod user;
|
||||
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;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
hash::Hash,
|
||||
marker::PhantomData,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -18,8 +12,8 @@ pub enum PartyError {
|
|||
UserNotFound(String),
|
||||
#[error("event `{0}` does not exist")]
|
||||
EventNotFound(String),
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
#[error("unknown error: {0}")]
|
||||
Unknown(String),
|
||||
#[error("invalid parameter: {0}")]
|
||||
InvalidParameter(String),
|
||||
}
|
||||
|
@ -48,86 +42,3 @@ impl ToString for Ordering {
|
|||
.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},
|
||||
hash::Hash,
|
||||
marker::PhantomData,
|
||||
ops::Deref,
|
||||
};
|
||||
use sycamore::view::IntoView as _;
|
||||
|
||||
|
@ -15,10 +14,7 @@ pub mod prelude {
|
|||
pub use sycamore::prelude::*;
|
||||
}
|
||||
|
||||
use crate::{
|
||||
components::{Block, BlockProps},
|
||||
util::WithContext,
|
||||
};
|
||||
use crate::components::{Block, BlockProps};
|
||||
|
||||
#[macro_export]
|
||||
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,
|
||||
Block(title="Tuple".into()) {
|
||||
(props.state.0.view(cx))
|
||||
br()
|
||||
(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> {
|
||||
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"] }
|
||||
paste = "1.0"
|
||||
convert_case = "0.6"
|
||||
darling = "0.14"
|
||||
#lan_party_core = { path = "../core", features = ["sycamore"] }
|
||||
|
|
|
@ -1,20 +1,9 @@
|
|||
mod edit;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{__private::TokenStream as TokenStream2, format_ident, quote};
|
||||
use syn::{
|
||||
parse::Parse, parse_str, token::Do, Attribute, DataEnum, DataStruct, Fields, Generics, Ident,
|
||||
LifetimeDef, Lit, MetaNameValue, Path, PredicateType, Type, TypeParam,
|
||||
};
|
||||
|
||||
#[macro_use]
|
||||
extern crate darling;
|
||||
|
||||
enum ParsedAttribute {
|
||||
Documentation(Documentation),
|
||||
View(ViewAttribute),
|
||||
Serde(SerdeAttribute),
|
||||
None,
|
||||
}
|
||||
use syn::{token::Do, Attribute, DataEnum, DataStruct, Fields, Ident, Path, Type};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Documentation {
|
||||
|
@ -23,16 +12,6 @@ enum Documentation {
|
|||
None,
|
||||
}
|
||||
|
||||
enum ViewAttribute {
|
||||
Title(Ident),
|
||||
None,
|
||||
}
|
||||
|
||||
enum SerdeAttribute {
|
||||
Untagged,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Documentation {
|
||||
fn parse(attr: &Attribute) -> Documentation {
|
||||
if !attr.path.is_ident("doc") {
|
||||
|
@ -51,116 +30,34 @@ impl Documentation {
|
|||
}
|
||||
}
|
||||
|
||||
impl ViewAttribute {
|
||||
fn parse(attr: &Attribute) -> ViewAttribute {
|
||||
if !attr.path.is_ident("view") {
|
||||
return Self::None;
|
||||
}
|
||||
fn get_title_description(attrs: &[Attribute]) -> (Option<String>, Option<String>) {
|
||||
let docs: Vec<_> = attrs.iter().map(Documentation::parse).collect();
|
||||
|
||||
let parsed: Result<MetaNameValue, _> = parse_str(
|
||||
attr.tokens
|
||||
.to_string()
|
||||
.trim_matches(|c: char| c == '(' || c == ')'),
|
||||
);
|
||||
let mut title = None;
|
||||
let mut description: Option<String> = None;
|
||||
|
||||
match parsed {
|
||||
Ok(p) => match (p.path.get_ident().unwrap().to_string().as_str(), p.lit) {
|
||||
("title", Lit::Str(ls)) => Self::Title(format_ident!("{}", ls.value())),
|
||||
_ => Self::None,
|
||||
},
|
||||
Err(_) => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SerdeAttribute {
|
||||
fn parse(attr: &Attribute) -> SerdeAttribute {
|
||||
if !attr.path.is_ident("serde") {
|
||||
return Self::None;
|
||||
}
|
||||
|
||||
let parsed: Result<Ident, _> = parse_str(
|
||||
attr.tokens
|
||||
.to_string()
|
||||
.trim_matches(|c: char| c == '(' || c == ')'),
|
||||
);
|
||||
|
||||
match parsed {
|
||||
Ok(p) => match p.to_string().as_str() {
|
||||
"untagged" => Self::Untagged,
|
||||
_ => Self::None,
|
||||
},
|
||||
Err(_) => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParsedAttribute {
|
||||
fn parse(attr: &Attribute) -> ParsedAttribute {
|
||||
match attr.path.get_ident() {
|
||||
Some(i) if i.to_string() == "doc" => Self::Documentation(Documentation::parse(attr)),
|
||||
Some(i) if i.to_string() == "view" => Self::View(ViewAttribute::parse(attr)),
|
||||
Some(i) if i.to_string() == "serde" => Self::Serde(SerdeAttribute::parse(attr)),
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Attributes {
|
||||
title: Option<String>,
|
||||
title_field: Option<Ident>,
|
||||
description: Option<String>,
|
||||
untagged: bool,
|
||||
}
|
||||
|
||||
impl Attributes {
|
||||
pub fn parse(attrs: &[Attribute]) -> Self {
|
||||
let parsed: Vec<_> = attrs.iter().map(ParsedAttribute::parse).collect();
|
||||
|
||||
let mut title = None;
|
||||
let mut title_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,
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
for doc in docs {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
title,
|
||||
description,
|
||||
title_field,
|
||||
untagged,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(title, description)
|
||||
}
|
||||
|
||||
struct ItemProps {
|
||||
name: Ident,
|
||||
attributes: Attributes,
|
||||
generics: Generics,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
struct StructField {
|
||||
|
@ -181,12 +78,9 @@ struct EnumVariant {
|
|||
fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||
let ItemProps {
|
||||
name,
|
||||
attributes: Attributes {
|
||||
title, description, ..
|
||||
},
|
||||
generics,
|
||||
title,
|
||||
description,
|
||||
} = props;
|
||||
let title = title.clone().unwrap_or(name.to_string());
|
||||
|
||||
let fields = s.fields.iter().map(|f| {
|
||||
let name = f
|
||||
|
@ -195,12 +89,7 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
.expect("each struct field must be named")
|
||||
.clone();
|
||||
let name_str = name.to_string();
|
||||
let Attributes {
|
||||
title,
|
||||
title_field: _,
|
||||
description,
|
||||
..
|
||||
} = Attributes::parse(&f.attrs);
|
||||
let (title, description) = get_title_description(&f.attrs);
|
||||
|
||||
StructField {
|
||||
name_str,
|
||||
|
@ -270,12 +159,9 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||
let ItemProps {
|
||||
name,
|
||||
attributes: Attributes {
|
||||
title, description, ..
|
||||
},
|
||||
generics,
|
||||
title,
|
||||
description,
|
||||
} = props;
|
||||
let title = title.clone().unwrap_or(name.to_string());
|
||||
|
||||
let variants = e.variants.iter().map(|v| {
|
||||
let variant = v.ident.clone();
|
||||
|
@ -285,12 +171,7 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
let Attributes {
|
||||
title,
|
||||
title_field: _,
|
||||
description,
|
||||
..
|
||||
} = Attributes::parse(&v.attrs);
|
||||
let (title, description) = get_title_description(&v.attrs);
|
||||
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
||||
EnumVariant {
|
||||
variant_lower,
|
||||
|
@ -393,7 +274,6 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
#(#view_description,)*
|
||||
_ => view! { cx, }
|
||||
})
|
||||
br()
|
||||
(match selected.get().as_str() {
|
||||
#(#view_match,)*
|
||||
_ => view! { cx, }
|
||||
|
@ -406,16 +286,9 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||
let ItemProps {
|
||||
name,
|
||||
attributes:
|
||||
Attributes {
|
||||
title,
|
||||
description,
|
||||
title_field,
|
||||
..
|
||||
},
|
||||
generics,
|
||||
title,
|
||||
description,
|
||||
} = props;
|
||||
let title = title.clone().unwrap_or(name.to_string());
|
||||
|
||||
let fields = s.fields.iter().map(|f| {
|
||||
let name = f
|
||||
|
@ -424,12 +297,7 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
.expect("each struct field must be named")
|
||||
.clone();
|
||||
let name_str = name.to_string();
|
||||
let Attributes {
|
||||
title,
|
||||
title_field: _,
|
||||
description,
|
||||
..
|
||||
} = Attributes::parse(&f.attrs);
|
||||
let (title, description) = get_title_description(&f.attrs);
|
||||
|
||||
StructField {
|
||||
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! {
|
||||
let state = props.state;
|
||||
|
||||
|
@ -495,12 +353,9 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||
let ItemProps {
|
||||
name,
|
||||
attributes: Attributes {
|
||||
title, description, ..
|
||||
},
|
||||
generics,
|
||||
title,
|
||||
description,
|
||||
} = props;
|
||||
let title = title.clone().unwrap_or(name.to_string());
|
||||
|
||||
let variants = e.variants.iter().map(|v| {
|
||||
let variant = v.ident.clone();
|
||||
|
@ -510,12 +365,7 @@ fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
let Attributes {
|
||||
title,
|
||||
title_field: _,
|
||||
description,
|
||||
..
|
||||
} = Attributes::parse(&v.attrs);
|
||||
let (title, description) = get_title_description(&v.attrs);
|
||||
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
||||
EnumVariant {
|
||||
variant_lower,
|
||||
|
@ -579,12 +429,15 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
|||
let name = input.ident;
|
||||
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 {
|
||||
name: name.clone(),
|
||||
attributes: attrs,
|
||||
generics: input.generics.clone(),
|
||||
title: title.clone(),
|
||||
description: description.clone(),
|
||||
};
|
||||
|
||||
let inner = match input.data {
|
||||
|
@ -593,43 +446,16 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
|||
_ => 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! {
|
||||
pub struct #edit_ident;
|
||||
|
||||
impl #impl_generics Editor<'a, G, #name #input_ty_generics> for #edit_ident #where_clause {
|
||||
fn edit(cx: Scope<'a>, props: EditProps<'a, #name #input_ty_generics>) -> View<G> {
|
||||
impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident {
|
||||
fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View<G> {
|
||||
#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;
|
||||
}
|
||||
};
|
||||
|
@ -637,19 +463,22 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
|||
TokenStream::from(res)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(WebView, attributes(view))]
|
||||
#[proc_macro_derive(WebView)]
|
||||
pub fn web_view(tokens: TokenStream) -> TokenStream {
|
||||
let input: syn::DeriveInput = syn::parse(tokens).unwrap();
|
||||
|
||||
let name = input.ident;
|
||||
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 {
|
||||
name: name.clone(),
|
||||
attributes: attrs,
|
||||
generics: input.generics.clone(),
|
||||
title: title.clone(),
|
||||
description: description.clone(),
|
||||
};
|
||||
|
||||
let inner = match input.data {
|
||||
|
@ -658,45 +487,16 @@ pub fn web_view(tokens: TokenStream) -> TokenStream {
|
|||
_ => 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! {
|
||||
pub struct #view_ident;
|
||||
|
||||
impl #impl_generics Viewer<'a, G, #name #input_ty_generics> for #view_ident #where_clause {
|
||||
fn view(cx: Scope<'a>, props: ViewProps<'a, #name #input_ty_generics>) -> View<G> {
|
||||
impl<'a, G: Html> Viewer<'a, G, #name> for #view_ident {
|
||||
fn view(cx: Scope<'a>, props: ViewProps<'a, #name>) -> View<G> {
|
||||
#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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
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" \
|
||||
-e MONGO_INITDB_ROOT_USERNAME=root \
|
||||
-e MONGO_INITDB_ROOT_PASSWORD=example \
|
||||
|
|
|
@ -27,6 +27,6 @@ sycamore-router = "0.8.0"
|
|||
reqwasm = "0.5"
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
gloo-timers = "0.2"
|
||||
async-trait = "0.1"
|
||||
futures = "0.3"
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<meta charset="utf-8">
|
||||
<!--<link data-trunk href="tailwind.css" rel="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">
|
||||
<title>LAN Party</title>
|
||||
|
||||
<link rel="preload" href="/index-1e8437b6157bd12b_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<link rel="modulepreload" href="/index-1e8437b6157bd12b.js"></head>
|
||||
<link rel="preload" href="/index-61979c95126900f4_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<link rel="modulepreload" href="/index-61979c95126900f4.js"></head>
|
||||
<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;
|
||||
|
||||
pub use lan_party_core::components::Button;
|
||||
use sycamore::{builder::prelude::*, prelude::*};
|
||||
use sycamore::prelude::*;
|
||||
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)]
|
||||
pub struct TableProps<'a, G: Html> {
|
||||
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)]
|
||||
pub struct ModalProps<'a, G: Html> {
|
||||
pub open: &'a Signal<bool>,
|
||||
pub struct BlockProps<'a, G: Html> {
|
||||
pub title: String,
|
||||
pub children: Children<'a, G>,
|
||||
}
|
||||
|
||||
#[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 class = create_memo(cx, || {
|
||||
if *props.open.get() {
|
||||
"modal"
|
||||
} else {
|
||||
"modal hidden"
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
div(class=class) {
|
||||
h4 { (props.title) }
|
||||
div(class="modal-close") {
|
||||
Button(icon="mdi-close".into(), onclick=move |_| props.open.set(false))
|
||||
}
|
||||
|
||||
(children)
|
||||
details {
|
||||
summary { (props.title) }
|
||||
p { (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 pages;
|
||||
pub mod state;
|
||||
pub mod util;
|
||||
|
||||
use components::messages::{Messages, Messenger};
|
||||
use futures::join;
|
||||
use lan_party_core::state::{Events, Users};
|
||||
use pages::{EventsPage, UsersPage};
|
||||
use state::{EventsExt, UsersExt};
|
||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||
use sycamore::prelude::*;
|
||||
use sycamore_router::{HistoryIntegration, Route, Router};
|
||||
|
||||
#[derive(Route)]
|
||||
|
@ -67,55 +62,11 @@ fn main() {
|
|||
];
|
||||
|
||||
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,
|
||||
Router(
|
||||
integration=HistoryIntegration::new(),
|
||||
view=|cx, route: &ReadSignal<AppRoutes>| {
|
||||
view! { cx,
|
||||
Messages()
|
||||
header {
|
||||
Navbar(pages=pages) {
|
||||
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use lan_party_core::{
|
||||
components::Block,
|
||||
edit::IntoEdit,
|
||||
event::{Event, EventOutcome, EventSpec, EventUpdate},
|
||||
util::WithContext,
|
||||
event::{Event, EventSpec, EventUpdate},
|
||||
view::IntoView,
|
||||
};
|
||||
use log::debug;
|
||||
|
@ -13,182 +8,63 @@ use reqwasm::http::Method;
|
|||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||
|
||||
use crate::{
|
||||
components::{messages::Messenger, Button, Modal, Table},
|
||||
state::{EventsExt, UsersExt},
|
||||
components::{Block, Button},
|
||||
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]
|
||||
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_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 show_outcome = create_signal(cx, false);
|
||||
let event_outcome = create_signal(cx, EventOutcome::default());
|
||||
let events: &'a Signal<Vec<Event>> = create_signal(cx, Vec::<Event>::new());
|
||||
|
||||
let events = use_context::<Events>(cx);
|
||||
let users = use_context::<Users>(cx);
|
||||
spawn_local_scoped(cx, async move {
|
||||
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 {
|
||||
match msg {
|
||||
Msg::Add(event_spec) => {
|
||||
let name = event_spec.name.clone();
|
||||
messenger.add_result(
|
||||
events.add(event_spec).await,
|
||||
Some(format!("Created a new event with name \"{}\"", name)),
|
||||
Some("Error when adding an event"),
|
||||
);
|
||||
}
|
||||
Msg::Reload => messenger.add_result(
|
||||
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();
|
||||
}
|
||||
}
|
||||
let new_event = api_request::<EventSpec, Event>(
|
||||
Method::POST,
|
||||
"/event",
|
||||
Some((*event_spec).get().as_ref().clone()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
debug!("{:#?}", new_event);
|
||||
events.modify().push(new_event.unwrap());
|
||||
});
|
||||
};
|
||||
|
||||
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,
|
||||
Modal(open=show_outcome, title="Event outcome".into()) {
|
||||
Table(headers=vec!["Username".into(), "Score".into()]) {
|
||||
Indexed(
|
||||
iterable=&outcome_points,
|
||||
view=move |cx, (k, v)| {
|
||||
view! { cx,
|
||||
tr {
|
||||
td { (*k.clone()) }
|
||||
td { (v.clone()) }
|
||||
}
|
||||
}
|
||||
Block(title="Events".into()) {
|
||||
Keyed(
|
||||
iterable=&events,
|
||||
view=move |cx, event| {
|
||||
let event = create_ref(cx, event);
|
||||
view! { cx,
|
||||
(event.view(cx))
|
||||
br()
|
||||
}
|
||||
)
|
||||
}
|
||||
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| dispatch(Msg::ViewOutcome(current_event.get().as_ref().clone())))
|
||||
},
|
||||
key=|event| (event.name.clone()),
|
||||
)
|
||||
}
|
||||
div(class="events-cols") {
|
||||
div {
|
||||
Block(title="Events".into()) {
|
||||
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| dispatch(Msg::Reload))
|
||||
Indexed(
|
||||
iterable=&events.get(),
|
||||
view=move |cx, event| {
|
||||
let event = create_ref(cx, event);
|
||||
view! { cx,
|
||||
Block(title=event.name.clone()) {
|
||||
(event.description)
|
||||
br()
|
||||
span(class="event-action") {
|
||||
Button(text="Delete".into(), icon="mdi-delete".into(), onclick=move |_| dispatch(Msg::Delete(event.name.clone())))
|
||||
}
|
||||
span(class="event-action") {
|
||||
Button(text="View outcome".into(), onclick=move |_| dispatch(Msg::ViewOutcome(event.name.clone())))
|
||||
}
|
||||
span(class="event-action") {
|
||||
Button(text="Finish".into(), onclick=move |_| dispatch(Msg::Stop(event.name.clone())))
|
||||
}
|
||||
br()
|
||||
(event.event_type.view(cx))
|
||||
}
|
||||
br()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
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()
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
br()
|
||||
Block(title="Create new event".into()) {
|
||||
(event_spec.edit(cx))
|
||||
Button(icon="mdi-check".into(), onclick=onadd)
|
||||
}
|
||||
br()
|
||||
Block(title="Update an event".into()) {
|
||||
(event_update.edit(cx))
|
||||
Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_update.get()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::{
|
||||
components::{messages::Messenger, Button, Table},
|
||||
state::{EventsExt, UsersExt},
|
||||
};
|
||||
use lan_party_core::state::{Events, Users};
|
||||
use crate::components::{Button, Table};
|
||||
use lan_party_core::user::User;
|
||||
use log::debug;
|
||||
use reqwasm::http::Method;
|
||||
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());
|
||||
let messenger = use_context::<Messenger>(cx);
|
||||
let users = use_context::<Users>(cx);
|
||||
let users = create_signal(cx, Vec::<User>::new());
|
||||
let headers = vec!["Username".into(), "Score".into(), "".into()];
|
||||
|
||||
let score_edit = create_signal(cx, Option::<String>::None);
|
||||
let new_score = create_signal(cx, String::new());
|
||||
let new_username = create_signal(cx, String::new());
|
||||
|
||||
let reload = move || {
|
||||
spawn_local_scoped(cx, async move {
|
||||
messenger.add_result(
|
||||
users.load().await,
|
||||
Option::<String>::None,
|
||||
Some("Failed to load users"),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
reload();
|
||||
spawn_local_scoped(cx, async move {
|
||||
users.set(
|
||||
api_request::<_, Vec<User>>(Method::GET, "/user", Option::<()>::None)
|
||||
.await
|
||||
.map(|inner| inner.unwrap())
|
||||
.unwrap(),
|
||||
);
|
||||
});
|
||||
|
||||
let ondelete = move |name: String| {
|
||||
move |_| {
|
||||
move |event: Event| {
|
||||
let name = name.clone();
|
||||
spawn_local_scoped(cx, async move {
|
||||
messenger.add_result(
|
||||
users.delete(&name).await,
|
||||
Some(format!("Deleted user {}", name)),
|
||||
Some("Failed to delete user"),
|
||||
);
|
||||
debug!("Delete {:#?}", event);
|
||||
let users_ref = users.get();
|
||||
let user: &User = users_ref.iter().find(|user| user.name == name).unwrap();
|
||||
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 |_| {
|
||||
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;
|
||||
messenger.add_result(
|
||||
users.update_score(&name, score).await,
|
||||
Some(format!("Updated score for user {}", name)),
|
||||
Some("Failed to delete user"),
|
||||
);
|
||||
let users_ref = users.get();
|
||||
let user: &User = users_ref
|
||||
.iter()
|
||||
.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);
|
||||
})
|
||||
|
@ -57,19 +86,21 @@ pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
|||
|
||||
let onadd = move |_| {
|
||||
spawn_local_scoped(cx, async move {
|
||||
messenger.add_result(
|
||||
users.add(&new_username.get()).await,
|
||||
Some(format!("Added new user {}", new_username.get())),
|
||||
Some("Failed to add user"),
|
||||
);
|
||||
let user = api_request::<String, User>(
|
||||
Method::POST,
|
||||
"/user",
|
||||
Some((*new_username).get().as_ref().clone()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
users.modify().push(user.unwrap());
|
||||
});
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| reload())
|
||||
Table(headers=headers) {
|
||||
Keyed(
|
||||
iterable=users.get(),
|
||||
iterable=users,
|
||||
view=move |cx, user| {
|
||||
let user = create_ref(cx, user);
|
||||
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?;
|
||||
|
||||
if res.ok() {
|
||||
if let Ok(json) = res.json().await {
|
||||
Ok(Some(json))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
if let Ok(json) = res.json().await {
|
||||
Ok(Some(json))
|
||||
} else {
|
||||
if let Ok(text) = res.text().await {
|
||||
Err(anyhow!("Request failed: {}", text.to_string()))
|
||||
} else {
|
||||
Err(anyhow!("Request failed"))
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,84 +64,5 @@ textarea:focus, input:focus{
|
|||
}
|
||||
|
||||
body {
|
||||
grid-template-columns: 1fr min(100rem, 90%) 1fr;
|
||||
}
|
||||
|
||||
.messages {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0.5em;
|
||||
background-color: var(--accent);
|
||||
padding: 0.5em;
|
||||
border-radius: 5px;
|
||||
min-width: 30em;
|
||||
max-width: 30em;
|
||||
}
|
||||
|
||||
.message-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
margin-bottom: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.events-cols {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
}
|
||||
|
||||
.events-cols > * {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.events-cols:nth-child(1) {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.events-cols:nth-child(2) {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.event-action {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 5;
|
||||
left: 15vw;
|
||||
top: 10vh;
|
||||
position: fixed;
|
||||
width: 70vw;
|
||||
height: 80vh;
|
||||
padding: 1em;
|
||||
background-color: var(--accent-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.modal > h4 {
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
top: 1em;
|
||||
}
|
||||
|
||||
.modal-close > * {
|
||||
margin: 0;
|
||||
grid-template-columns: 1fr min(60rem, 90%) 1fr;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue