Frontend refactor and generics in macros
This commit is contained in:
parent
59eeabc888
commit
3ef2c282b0
|
@ -263,7 +263,7 @@ pub struct VecEdit;
|
||||||
impl<'a, G, T, I> Editor<'a, G, I> for VecEdit
|
impl<'a, G, T, I> Editor<'a, G, I> for VecEdit
|
||||||
where
|
where
|
||||||
G: Html,
|
G: Html,
|
||||||
T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + std::fmt::Debug + 'a,
|
T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + 'a,
|
||||||
I: IntoIterator<Item = T> + FromIterator<T> + Clone,
|
I: IntoIterator<Item = T> + FromIterator<T> + Clone,
|
||||||
{
|
{
|
||||||
fn edit(cx: Scope<'a>, props: EditProps<'a, I>) -> View<G> {
|
fn edit(cx: Scope<'a>, props: EditProps<'a, I>) -> View<G> {
|
||||||
|
@ -332,7 +332,7 @@ where
|
||||||
impl<'a, G, T> Editable<'a, G> for Vec<T>
|
impl<'a, G, T> Editable<'a, G> for Vec<T>
|
||||||
where
|
where
|
||||||
G: Html,
|
G: Html,
|
||||||
T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + std::fmt::Debug + 'a,
|
T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + 'a,
|
||||||
{
|
{
|
||||||
type Editor = VecEdit;
|
type Editor = VecEdit;
|
||||||
}
|
}
|
||||||
|
@ -340,7 +340,7 @@ where
|
||||||
impl<'a, G, T> Editable<'a, G> for HashSet<T>
|
impl<'a, G, T> Editable<'a, G> for HashSet<T>
|
||||||
where
|
where
|
||||||
G: Html,
|
G: Html,
|
||||||
T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + std::fmt::Debug + 'a,
|
T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a,
|
||||||
{
|
{
|
||||||
type Editor = VecEdit;
|
type Editor = VecEdit;
|
||||||
}
|
}
|
||||||
|
@ -350,8 +350,7 @@ where
|
||||||
G: Html,
|
G: Html,
|
||||||
K: Clone + Hash + Eq,
|
K: Clone + Hash + Eq,
|
||||||
V: Clone,
|
V: Clone,
|
||||||
(K, V):
|
(K, V): for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a,
|
||||||
for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + std::fmt::Debug + 'a,
|
|
||||||
{
|
{
|
||||||
type Editor = VecEdit;
|
type Editor = VecEdit;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,37 @@ use paste::paste;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
hash::Hash,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, 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> {
|
||||||
|
/// 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> Default for Ranking<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Ranking(Vec::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Hash + PartialEq + Eq + Clone + Default> 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, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
|
@ -105,6 +135,10 @@ macro_rules! events {
|
||||||
|
|
||||||
impl EventSpec {
|
impl EventSpec {
|
||||||
pub fn create_event(self) -> Result<Event, PartyError> {
|
pub fn create_event(self) -> Result<Event, PartyError> {
|
||||||
|
if self.name.is_empty() {
|
||||||
|
return Err(PartyError::Unknown("invalid name".into()))
|
||||||
|
}
|
||||||
|
|
||||||
let event_type = match self.event_type {
|
let event_type = match self.event_type {
|
||||||
$(EventTypeSpec::$name(s) => {
|
$(EventTypeSpec::$name(s) => {
|
||||||
EventType::$name($module::$name::from_spec(s))
|
EventType::$name($module::$name::from_spec(s))
|
||||||
|
@ -213,11 +247,7 @@ pub mod test {
|
||||||
|
|
||||||
pub mod team_game {
|
pub mod team_game {
|
||||||
use super::{
|
use super::{
|
||||||
free_for_all_game::{
|
free_for_all_game::{FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate},
|
||||||
FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate,
|
|
||||||
FreeForAllGameUpdateParticipants, FreeForAllGameUpdateRanking,
|
|
||||||
FreeForAllGameUpdateRewards,
|
|
||||||
},
|
|
||||||
*,
|
*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -266,40 +296,12 @@ pub mod team_game {
|
||||||
pub members: Vec<String>,
|
pub members: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub enum TeamGameUpdateInner {
|
pub struct Team {
|
||||||
/// Add or replace a team with the given name and array of members
|
name: String,
|
||||||
SetTeam(TeamGameUpdateSetTeam),
|
|
||||||
|
|
||||||
/// Remove team with given name
|
|
||||||
RemoveTeam(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TeamGameUpdateInner {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::SetTeam(TeamGameUpdateSetTeam::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum TeamGameFfaInheritedUpdate {
|
|
||||||
/// Change the ranking and scores
|
|
||||||
Ranking(FreeForAllGameUpdateRanking),
|
|
||||||
/// Update rewards
|
|
||||||
Rewards(FreeForAllGameUpdateRewards),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TeamGameFfaInheritedUpdate {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Ranking(FreeForAllGameUpdateRanking::default())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -308,19 +310,23 @@ pub mod team_game {
|
||||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub enum TeamGameUpdate {
|
pub enum TeamGameUpdate {
|
||||||
/// # Team
|
/// Add or replace a team with the given name and array of members
|
||||||
///
|
SetTeam(TeamGameUpdateSetTeam),
|
||||||
/// Team specific updates
|
/// Remove team with given name
|
||||||
Team(TeamGameUpdateInner),
|
RemoveTeam(String),
|
||||||
/// # Other
|
/// Replace the current ranking with the given ranking
|
||||||
///
|
SetRanking(Ranking<String>),
|
||||||
/// Inherited from FreeForAllGame
|
/// If the current ranking is of type `Scores`, apply the given score deltas
|
||||||
Ffa(TeamGameFfaInheritedUpdate),
|
ScoreDelta(HashMap<String, i64>),
|
||||||
|
/// Set rewards for winning the game
|
||||||
|
SetWinRewards(Vec<i64>),
|
||||||
|
/// Set rewards for losing the game
|
||||||
|
SetLoseRewards(Vec<i64>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TeamGameUpdate {
|
impl Default for TeamGameUpdate {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
TeamGameUpdate::Team(TeamGameUpdateInner::default())
|
TeamGameUpdate::SetTeam(TeamGameUpdateSetTeam::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,32 +349,30 @@ pub mod team_game {
|
||||||
|
|
||||||
fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> {
|
fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> {
|
||||||
match update {
|
match update {
|
||||||
TeamGameUpdate::Ffa(update) => match update {
|
TeamGameUpdate::SetRanking(x) => self
|
||||||
TeamGameFfaInheritedUpdate::Ranking(u) => {
|
.ffa_game
|
||||||
self.ffa_game.apply_update(FreeForAllGameUpdate::Ranking(u))
|
.apply_update(FreeForAllGameUpdate::SetRanking(x)),
|
||||||
}
|
TeamGameUpdate::ScoreDelta(x) => self
|
||||||
TeamGameFfaInheritedUpdate::Rewards(u) => {
|
.ffa_game
|
||||||
self.ffa_game.apply_update(FreeForAllGameUpdate::Rewards(u))
|
.apply_update(FreeForAllGameUpdate::ScoreDelta(x)),
|
||||||
}
|
TeamGameUpdate::SetWinRewards(x) => self
|
||||||
},
|
.ffa_game
|
||||||
TeamGameUpdate::Team(update) => match update {
|
.apply_update(FreeForAllGameUpdate::SetWinRewards(x)),
|
||||||
TeamGameUpdateInner::SetTeam(u) => {
|
TeamGameUpdate::SetLoseRewards(x) => self
|
||||||
|
.ffa_game
|
||||||
|
.apply_update(FreeForAllGameUpdate::SetLoseRewards(x)),
|
||||||
|
TeamGameUpdate::SetTeam(u) => {
|
||||||
self.ffa_game
|
self.ffa_game
|
||||||
.apply_update(FreeForAllGameUpdate::Participants(
|
.apply_update(FreeForAllGameUpdate::AddParticipant(u.team.clone()))?;
|
||||||
FreeForAllGameUpdateParticipants::AddParticipant(u.team.clone()),
|
|
||||||
))?;
|
|
||||||
self.teams.insert(u.team, u.members);
|
self.teams.insert(u.team, u.members);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
TeamGameUpdateInner::RemoveTeam(team) => {
|
TeamGameUpdate::RemoveTeam(team) => {
|
||||||
self.ffa_game
|
self.ffa_game
|
||||||
.apply_update(FreeForAllGameUpdate::Participants(
|
.apply_update(FreeForAllGameUpdate::RemoveParticipant(team.clone()))?;
|
||||||
FreeForAllGameUpdateParticipants::RemoveParticipant(team.clone()),
|
|
||||||
))?;
|
|
||||||
self.teams.remove(&team);
|
self.teams.remove(&team);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,31 +400,12 @@ pub mod free_for_all_game {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub enum FreeForAllGameRanking {
|
pub struct User {
|
||||||
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
name: String,
|
||||||
/// place, etc.)
|
|
||||||
Ranking(Vec<String>),
|
|
||||||
/// Score based ranking of participants/teams
|
|
||||||
Scores(HashMap<String, i64>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FreeForAllGameRanking {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Ranking(Vec::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FreeForAllGameRanking {
|
|
||||||
pub fn is_valid(&self, participants: &HashSet<String>) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Ranking(v) => v.iter().all(|p| participants.contains(p)),
|
|
||||||
Self::Scores(m) => m.keys().all(|p| participants.contains(p)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
@ -430,7 +415,7 @@ pub mod free_for_all_game {
|
||||||
pub struct FreeForAllGame {
|
pub struct FreeForAllGame {
|
||||||
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
||||||
/// place, etc.)
|
/// place, etc.)
|
||||||
pub ranking: Option<FreeForAllGameRanking>,
|
pub ranking: Option<Ranking<String>>,
|
||||||
|
|
||||||
/// Specification of the game
|
/// Specification of the game
|
||||||
#[cfg_attr(feature = "serde", serde(flatten))]
|
#[cfg_attr(feature = "serde", serde(flatten))]
|
||||||
|
@ -466,77 +451,28 @@ pub mod free_for_all_game {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub enum FreeForAllGameUpdateRanking {
|
pub enum FreeForAllGameUpdate {
|
||||||
/// Replace the current ranking with the given ranking
|
/// Replace the current ranking with the given ranking
|
||||||
SetRanking(FreeForAllGameRanking),
|
SetRanking(Ranking<String>),
|
||||||
|
|
||||||
/// If the current ranking is of type `Scores`, apply the given score deltas
|
/// If the current ranking is of type `Scores`, apply the given score deltas
|
||||||
ScoreDelta(HashMap<String, i64>),
|
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
|
/// Set rewards for winning the game
|
||||||
SetWinRewards(Vec<i64>),
|
SetWinRewards(Vec<i64>),
|
||||||
|
|
||||||
/// Set rewards for losing the game
|
/// Set rewards for losing the game
|
||||||
SetLoseRewards(Vec<i64>),
|
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
|
/// Set list of participants participating in the game
|
||||||
SetParticipants(HashSet<String>),
|
SetParticipants(HashSet<String>),
|
||||||
|
|
||||||
/// Add participant by name
|
/// Add participant by name
|
||||||
AddParticipant(String),
|
AddParticipant(String),
|
||||||
|
|
||||||
/// Remove participant by name
|
/// Remove participant by name
|
||||||
RemoveParticipant(String),
|
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 {
|
|
||||||
/// Change the ranking and scores
|
|
||||||
Ranking(FreeForAllGameUpdateRanking),
|
|
||||||
/// Update rewards
|
|
||||||
Rewards(FreeForAllGameUpdateRewards),
|
|
||||||
/// Update participants
|
|
||||||
Participants(FreeForAllGameUpdateParticipants),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FreeForAllGameUpdate {
|
impl Default for FreeForAllGameUpdate {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Ranking(FreeForAllGameUpdateRanking::default())
|
Self::AddParticipant(String::new())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -553,18 +489,17 @@ pub mod free_for_all_game {
|
||||||
|
|
||||||
fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> {
|
fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> {
|
||||||
match update {
|
match update {
|
||||||
FreeForAllGameUpdate::Ranking(update) => match update {
|
FreeForAllGameUpdate::SetRanking(r) => {
|
||||||
FreeForAllGameUpdateRanking::SetRanking(r) => {
|
|
||||||
if !r.is_valid(&self.spec.participants) {
|
if !r.is_valid(&self.spec.participants) {
|
||||||
return Err(PartyError::Unknown("invalid ranking, all participants mentioned in ranking must be participating".into()));
|
return Err(PartyError::Unknown("invalid ranking, all participants mentioned in ranking must be participating".into()));
|
||||||
}
|
}
|
||||||
self.ranking = Some(r)
|
self.ranking = Some(r)
|
||||||
}
|
}
|
||||||
FreeForAllGameUpdateRanking::ScoreDelta(d) => match &mut self.ranking {
|
FreeForAllGameUpdate::ScoreDelta(d) => match &mut self.ranking {
|
||||||
Some(FreeForAllGameRanking::Ranking(_)) | None => {
|
Some(Ranking::Ranking(_)) | None => {
|
||||||
return Err(PartyError::Unknown("cannot apply score delta".into()))
|
return Err(PartyError::Unknown("cannot apply score delta".into()))
|
||||||
}
|
}
|
||||||
Some(FreeForAllGameRanking::Scores(s)) => {
|
Some(Ranking::Scores(s)) => {
|
||||||
for (participant, delta) in d.iter() {
|
for (participant, delta) in d.iter() {
|
||||||
if let Some(value) = s.get(participant) {
|
if let Some(value) = s.get(participant) {
|
||||||
s.insert(participant.clone(), value + delta);
|
s.insert(participant.clone(), value + delta);
|
||||||
|
@ -572,12 +507,10 @@ pub mod free_for_all_game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
FreeForAllGameUpdate::AddParticipant(name) => {
|
||||||
FreeForAllGameUpdate::Participants(update) => match update {
|
|
||||||
FreeForAllGameUpdateParticipants::AddParticipant(name) => {
|
|
||||||
self.spec.participants.insert(name);
|
self.spec.participants.insert(name);
|
||||||
}
|
}
|
||||||
FreeForAllGameUpdateParticipants::RemoveParticipant(name) => {
|
FreeForAllGameUpdate::RemoveParticipant(name) => {
|
||||||
self.spec.participants.remove(&name);
|
self.spec.participants.remove(&name);
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
|
@ -590,7 +523,7 @@ pub mod free_for_all_game {
|
||||||
return Err(PartyError::Unknown("cannot remove participant, all participants mentioned in ranking must be participating".into()));
|
return Err(PartyError::Unknown("cannot remove participant, all participants mentioned in ranking must be participating".into()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FreeForAllGameUpdateParticipants::SetParticipants(participants) => {
|
FreeForAllGameUpdate::SetParticipants(participants) => {
|
||||||
if !self
|
if !self
|
||||||
.ranking
|
.ranking
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -601,23 +534,20 @@ pub mod free_for_all_game {
|
||||||
}
|
}
|
||||||
self.spec.participants = participants;
|
self.spec.participants = participants;
|
||||||
}
|
}
|
||||||
},
|
FreeForAllGameUpdate::SetWinRewards(rewards) => {
|
||||||
FreeForAllGameUpdate::Rewards(update) => match update {
|
|
||||||
FreeForAllGameUpdateRewards::SetWinRewards(rewards) => {
|
|
||||||
self.spec.win_rewards = rewards;
|
self.spec.win_rewards = rewards;
|
||||||
}
|
}
|
||||||
FreeForAllGameUpdateRewards::SetLoseRewards(rewards) => {
|
FreeForAllGameUpdate::SetLoseRewards(rewards) => {
|
||||||
self.spec.lose_rewards = rewards;
|
self.spec.lose_rewards = rewards;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn outcome(&self) -> EventOutcome {
|
fn outcome(&self) -> EventOutcome {
|
||||||
let ranking = match &self.ranking {
|
let ranking = match &self.ranking {
|
||||||
Some(FreeForAllGameRanking::Ranking(r)) => r.clone(),
|
Some(Ranking::Ranking(r)) => r.clone(),
|
||||||
Some(FreeForAllGameRanking::Scores(s)) => {
|
Some(Ranking::Scores(s)) => {
|
||||||
let mut results: Vec<(_, _)> = s.iter().collect();
|
let mut results: Vec<(_, _)> = s.iter().collect();
|
||||||
results.sort_by(|a, b| b.1.cmp(a.1));
|
results.sort_by(|a, b| b.1.cmp(a.1));
|
||||||
results.into_iter().map(|(k, _)| k.clone()).collect()
|
results.into_iter().map(|(k, _)| k.clone()).collect()
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
mod edit;
|
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::{__private::TokenStream as TokenStream2, format_ident, quote};
|
use quote::{__private::TokenStream as TokenStream2, format_ident, quote};
|
||||||
use syn::{
|
use syn::{
|
||||||
parse::Parse, parse_str, token::Do, Attribute, DataEnum, DataStruct, Fields, Ident, Lit,
|
parse::Parse, parse_str, token::Do, Attribute, DataEnum, DataStruct, Fields, Generics, Ident,
|
||||||
MetaNameValue, Path, Type,
|
LifetimeDef, Lit, MetaNameValue, Path, PredicateType, Type, TypeParam,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ParsedAttribute {
|
enum ParsedAttribute {
|
||||||
Documentation(Documentation),
|
Documentation(Documentation),
|
||||||
View(ViewAttribute),
|
View(ViewAttribute),
|
||||||
|
Serde(SerdeAttribute),
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +25,11 @@ enum ViewAttribute {
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SerdeAttribute {
|
||||||
|
Untagged,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
impl Documentation {
|
impl Documentation {
|
||||||
fn parse(attr: &Attribute) -> Documentation {
|
fn parse(attr: &Attribute) -> Documentation {
|
||||||
if !attr.path.is_ident("doc") {
|
if !attr.path.is_ident("doc") {
|
||||||
|
@ -56,8 +60,6 @@ impl ViewAttribute {
|
||||||
.trim_matches(|c: char| c == '(' || c == ')'),
|
.trim_matches(|c: char| c == '(' || c == ')'),
|
||||||
);
|
);
|
||||||
|
|
||||||
dbg!(&parsed);
|
|
||||||
|
|
||||||
match parsed {
|
match parsed {
|
||||||
Ok(p) => match (p.path.get_ident().unwrap().to_string().as_str(), p.lit) {
|
Ok(p) => match (p.path.get_ident().unwrap().to_string().as_str(), p.lit) {
|
||||||
("title", Lit::Str(ls)) => Self::Title(format_ident!("{}", ls.value())),
|
("title", Lit::Str(ls)) => Self::Title(format_ident!("{}", ls.value())),
|
||||||
|
@ -68,11 +70,34 @@ impl ViewAttribute {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
impl ParsedAttribute {
|
||||||
fn parse(attr: &Attribute) -> ParsedAttribute {
|
fn parse(attr: &Attribute) -> ParsedAttribute {
|
||||||
match attr.path.get_ident() {
|
match attr.path.get_ident() {
|
||||||
Some(i) if i.to_string() == "doc" => Self::Documentation(Documentation::parse(attr)),
|
Some(i) if i.to_string() == "doc" => Self::Documentation(Documentation::parse(attr)),
|
||||||
Some(i) if i.to_string() == "web_view_attr" => Self::View(ViewAttribute::parse(attr)),
|
Some(i) if i.to_string() == "web_view_attr" => Self::View(ViewAttribute::parse(attr)),
|
||||||
|
Some(i) if i.to_string() == "serde" => Self::Serde(SerdeAttribute::parse(attr)),
|
||||||
_ => Self::None,
|
_ => Self::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,6 +107,7 @@ struct Attributes {
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
title_field: Option<Ident>,
|
title_field: Option<Ident>,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
untagged: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Attributes {
|
impl Attributes {
|
||||||
|
@ -91,6 +117,7 @@ impl Attributes {
|
||||||
let mut title = None;
|
let mut title = None;
|
||||||
let mut title_field = None;
|
let mut title_field = None;
|
||||||
let mut description: Option<String> = None;
|
let mut description: Option<String> = None;
|
||||||
|
let mut untagged = false;
|
||||||
|
|
||||||
for attr in parsed {
|
for attr in parsed {
|
||||||
match attr {
|
match attr {
|
||||||
|
@ -110,6 +137,10 @@ impl Attributes {
|
||||||
ViewAttribute::Title(t) => title_field = Some(t),
|
ViewAttribute::Title(t) => title_field = Some(t),
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
ParsedAttribute::Serde(s) => match s {
|
||||||
|
SerdeAttribute::Untagged => untagged = true,
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,6 +149,7 @@ impl Attributes {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
title_field,
|
title_field,
|
||||||
|
untagged,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,9 +161,8 @@ pub fn web_view_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
|
||||||
struct ItemProps {
|
struct ItemProps {
|
||||||
name: Ident,
|
name: Ident,
|
||||||
title: String,
|
attributes: Attributes,
|
||||||
title_field: Option<Ident>,
|
generics: Generics,
|
||||||
description: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StructField {
|
struct StructField {
|
||||||
|
@ -152,10 +183,12 @@ struct EnumVariant {
|
||||||
fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
let ItemProps {
|
let ItemProps {
|
||||||
name,
|
name,
|
||||||
title,
|
attributes: Attributes {
|
||||||
description,
|
title, description, ..
|
||||||
..
|
},
|
||||||
|
generics,
|
||||||
} = props;
|
} = props;
|
||||||
|
let title = title.clone().unwrap_or(name.to_string());
|
||||||
|
|
||||||
let fields = s.fields.iter().map(|f| {
|
let fields = s.fields.iter().map(|f| {
|
||||||
let name = f
|
let name = f
|
||||||
|
@ -168,6 +201,7 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
title,
|
title,
|
||||||
title_field: _,
|
title_field: _,
|
||||||
description,
|
description,
|
||||||
|
..
|
||||||
} = Attributes::parse(&f.attrs);
|
} = Attributes::parse(&f.attrs);
|
||||||
|
|
||||||
StructField {
|
StructField {
|
||||||
|
@ -238,10 +272,12 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
let ItemProps {
|
let ItemProps {
|
||||||
name,
|
name,
|
||||||
title,
|
attributes: Attributes {
|
||||||
description,
|
title, description, ..
|
||||||
..
|
},
|
||||||
|
generics,
|
||||||
} = props;
|
} = props;
|
||||||
|
let title = title.clone().unwrap_or(name.to_string());
|
||||||
|
|
||||||
let variants = e.variants.iter().map(|v| {
|
let variants = e.variants.iter().map(|v| {
|
||||||
let variant = v.ident.clone();
|
let variant = v.ident.clone();
|
||||||
|
@ -255,6 +291,7 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
title,
|
title,
|
||||||
title_field: _,
|
title_field: _,
|
||||||
description,
|
description,
|
||||||
|
..
|
||||||
} = Attributes::parse(&v.attrs);
|
} = Attributes::parse(&v.attrs);
|
||||||
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
||||||
EnumVariant {
|
EnumVariant {
|
||||||
|
@ -358,6 +395,7 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
#(#view_description,)*
|
#(#view_description,)*
|
||||||
_ => view! { cx, }
|
_ => view! { cx, }
|
||||||
})
|
})
|
||||||
|
br()
|
||||||
(match selected.get().as_str() {
|
(match selected.get().as_str() {
|
||||||
#(#view_match,)*
|
#(#view_match,)*
|
||||||
_ => view! { cx, }
|
_ => view! { cx, }
|
||||||
|
@ -370,10 +408,16 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
let ItemProps {
|
let ItemProps {
|
||||||
name,
|
name,
|
||||||
|
attributes:
|
||||||
|
Attributes {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
title_field,
|
title_field,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
generics,
|
||||||
} = props;
|
} = props;
|
||||||
|
let title = title.clone().unwrap_or(name.to_string());
|
||||||
|
|
||||||
let fields = s.fields.iter().map(|f| {
|
let fields = s.fields.iter().map(|f| {
|
||||||
let name = f
|
let name = f
|
||||||
|
@ -386,6 +430,7 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
title,
|
title,
|
||||||
title_field: _,
|
title_field: _,
|
||||||
description,
|
description,
|
||||||
|
..
|
||||||
} = Attributes::parse(&f.attrs);
|
} = Attributes::parse(&f.attrs);
|
||||||
|
|
||||||
StructField {
|
StructField {
|
||||||
|
@ -449,13 +494,41 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enum_fields<'a>(e: &'a DataEnum) -> impl Iterator<Item = EnumVariant> + 'a {
|
||||||
|
e.variants.iter().map(|v| {
|
||||||
|
let variant = v.ident.clone();
|
||||||
|
|
||||||
|
let inner = match &v.fields {
|
||||||
|
Fields::Unnamed(u) => u.unnamed.first().expect("the should be a field").ty.clone(),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let Attributes {
|
||||||
|
title,
|
||||||
|
title_field: _,
|
||||||
|
description,
|
||||||
|
..
|
||||||
|
} = Attributes::parse(&v.attrs);
|
||||||
|
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
||||||
|
EnumVariant {
|
||||||
|
variant_lower,
|
||||||
|
variant,
|
||||||
|
inner,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
let ItemProps {
|
let ItemProps {
|
||||||
name,
|
name,
|
||||||
title,
|
attributes: Attributes {
|
||||||
description,
|
title, description, ..
|
||||||
..
|
},
|
||||||
|
generics,
|
||||||
} = props;
|
} = props;
|
||||||
|
let title = title.clone().unwrap_or(name.to_string());
|
||||||
|
|
||||||
let variants = e.variants.iter().map(|v| {
|
let variants = e.variants.iter().map(|v| {
|
||||||
let variant = v.ident.clone();
|
let variant = v.ident.clone();
|
||||||
|
@ -469,6 +542,7 @@ fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||||
title,
|
title,
|
||||||
title_field: _,
|
title_field: _,
|
||||||
description,
|
description,
|
||||||
|
..
|
||||||
} = Attributes::parse(&v.attrs);
|
} = Attributes::parse(&v.attrs);
|
||||||
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
||||||
EnumVariant {
|
EnumVariant {
|
||||||
|
@ -533,20 +607,12 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
||||||
let name = input.ident;
|
let name = input.ident;
|
||||||
let edit_ident = format_ident!("{}Edit", name);
|
let edit_ident = format_ident!("{}Edit", name);
|
||||||
|
|
||||||
let Attributes {
|
let attrs = Attributes::parse(&input.attrs);
|
||||||
title: t,
|
|
||||||
title_field,
|
|
||||||
description: d,
|
|
||||||
} = Attributes::parse(&input.attrs);
|
|
||||||
|
|
||||||
let title = t.unwrap_or(name.to_string());
|
|
||||||
let description = d;
|
|
||||||
|
|
||||||
let props = ItemProps {
|
let props = ItemProps {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
title: title.clone(),
|
attributes: attrs,
|
||||||
description: description.clone(),
|
generics: input.generics.clone(),
|
||||||
title_field: title_field.clone(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = match input.data {
|
let inner = match input.data {
|
||||||
|
@ -555,20 +621,49 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut generics = input.generics.clone();
|
||||||
|
let input_generics = input.generics;
|
||||||
|
|
||||||
|
//if generics.type_params().count() == 0 {
|
||||||
|
generics
|
||||||
|
.params
|
||||||
|
.push(syn::GenericParam::Lifetime(syn::parse_str("'a").unwrap()));
|
||||||
|
generics
|
||||||
|
.params
|
||||||
|
.push(syn::GenericParam::Type(syn::parse_str("G: Html").unwrap()));
|
||||||
|
//}
|
||||||
|
|
||||||
|
let (input_impl_generics, input_ty_generics, input_where_clause) =
|
||||||
|
input_generics.split_for_impl();
|
||||||
|
|
||||||
|
for ty_param in input_generics.type_params() {
|
||||||
|
generics.make_where_clause().predicates.push(
|
||||||
|
syn::parse_str(&format!(
|
||||||
|
"{}: for<'b> Editable<'b, G>",
|
||||||
|
ty_param.ident.to_string()
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||||
|
|
||||||
let res = quote! {
|
let res = quote! {
|
||||||
pub struct #edit_ident;
|
pub struct #edit_ident;
|
||||||
|
|
||||||
impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident {
|
impl #impl_generics Editor<'a, G, #name #input_ty_generics> for #edit_ident #where_clause {
|
||||||
fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View<G> {
|
fn edit(cx: Scope<'a>, props: EditProps<'a, #name #input_ty_generics>) -> View<G> {
|
||||||
#inner
|
#inner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, G: Html> Editable<'a, G> for #name {
|
impl #impl_generics Editable<'a, G> for #name #input_ty_generics #where_clause {
|
||||||
type Editor = #edit_ident;
|
type Editor = #edit_ident;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
println!("{}", &res.to_string());
|
||||||
|
|
||||||
TokenStream::from(res)
|
TokenStream::from(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -579,20 +674,12 @@ pub fn web_view(tokens: TokenStream) -> TokenStream {
|
||||||
let name = input.ident;
|
let name = input.ident;
|
||||||
let view_ident = format_ident!("{}View", name);
|
let view_ident = format_ident!("{}View", name);
|
||||||
|
|
||||||
let Attributes {
|
let attrs = Attributes::parse(&input.attrs);
|
||||||
title: t,
|
|
||||||
title_field,
|
|
||||||
description: d,
|
|
||||||
} = Attributes::parse(&input.attrs);
|
|
||||||
|
|
||||||
let title = t.unwrap_or(name.to_string());
|
|
||||||
let description = d;
|
|
||||||
|
|
||||||
let props = ItemProps {
|
let props = ItemProps {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
title: title.clone(),
|
attributes: attrs,
|
||||||
description: description.clone(),
|
generics: input.generics.clone(),
|
||||||
title_field: title_field.clone(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = match input.data {
|
let inner = match input.data {
|
||||||
|
@ -601,19 +688,50 @@ pub fn web_view(tokens: TokenStream) -> TokenStream {
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut generics = input.generics.clone();
|
||||||
|
let input_generics = input.generics;
|
||||||
|
|
||||||
|
//if generics.type_params().count() == 0 {
|
||||||
|
generics
|
||||||
|
.params
|
||||||
|
.push(syn::GenericParam::Lifetime(syn::parse_str("'a").unwrap()));
|
||||||
|
generics
|
||||||
|
.params
|
||||||
|
.push(syn::GenericParam::Type(syn::parse_str("G: Html").unwrap()));
|
||||||
|
//}
|
||||||
|
|
||||||
|
let (input_impl_generics, input_ty_generics, input_where_clause) =
|
||||||
|
input_generics.split_for_impl();
|
||||||
|
|
||||||
|
for ty_param in input_generics.type_params() {
|
||||||
|
generics.make_where_clause().predicates.push(
|
||||||
|
syn::parse_str(&format!(
|
||||||
|
"{}: for<'b> Viewable<'b, G>",
|
||||||
|
ty_param.ident.to_string()
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||||
|
|
||||||
|
//println!("{}", &inner.to_string());
|
||||||
|
|
||||||
let res = quote! {
|
let res = quote! {
|
||||||
pub struct #view_ident;
|
pub struct #view_ident;
|
||||||
|
|
||||||
impl<'a, G: Html> Viewer<'a, G, #name> for #view_ident {
|
impl #impl_generics Viewer<'a, G, #name #input_ty_generics> for #view_ident #where_clause {
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, #name>) -> View<G> {
|
fn view(cx: Scope<'a>, props: ViewProps<'a, #name #input_ty_generics>) -> View<G> {
|
||||||
#inner
|
#inner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, G: Html> Viewable<'a, G> for #name {
|
impl #impl_generics Viewable<'a, G> for #name #input_ty_generics #where_clause {
|
||||||
type Viewer = #view_ident;
|
type Viewer = #view_ident;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
println!("{}", &res.to_string());
|
||||||
|
|
||||||
TokenStream::from(res)
|
TokenStream::from(res)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<!--<link data-trunk href="tailwind.css" rel="css">-->
|
<!--<link data-trunk href="tailwind.css" rel="css">-->
|
||||||
<link rel="stylesheet" href="/simple.min-d15f5ff500b4c62a.css">
|
<link rel="stylesheet" href="/simple.min-d15f5ff500b4c62a.css">
|
||||||
<link rel="stylesheet" href="/style-2f979acf99c8ad73.css">
|
<link rel="stylesheet" href="/style-95f29b132ea1fddf.css">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" rel="stylesheet">
|
||||||
<title>LAN Party</title>
|
<title>LAN Party</title>
|
||||||
|
|
||||||
<link rel="preload" href="/index-b0d435005316ee2a_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
<link rel="preload" href="/index-45b72451f5518e3c_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||||
<link rel="modulepreload" href="/index-b0d435005316ee2a.js"></head>
|
<link rel="modulepreload" href="/index-45b72451f5518e3c.js"></head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module">import init from '/index-b0d435005316ee2a.js';init('/index-b0d435005316ee2a_bg.wasm');</script></body></html>
|
<script type="module">import init from '/index-45b72451f5518e3c.js';init('/index-45b72451f5518e3c_bg.wasm');</script></body></html>
|
|
@ -35,6 +35,26 @@ impl Messenger {
|
||||||
self.add_message(Message::new(title.into(), text.into()));
|
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) {
|
pub fn remove_message(&self) {
|
||||||
self.messages.modify().remove(0);
|
self.messages.modify().remove(0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,9 @@
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
|
||||||
|
pub use lan_party_core::components::Button;
|
||||||
use sycamore::{builder::prelude::*, prelude::*};
|
use sycamore::{builder::prelude::*, prelude::*};
|
||||||
use web_sys::Event;
|
use web_sys::Event;
|
||||||
|
|
||||||
#[derive(Prop)]
|
|
||||||
pub struct ButtonProps<F: FnMut(Event)> {
|
|
||||||
pub onclick: F,
|
|
||||||
#[builder(default)]
|
|
||||||
pub text: String,
|
|
||||||
#[builder(default)]
|
|
||||||
pub icon: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Button<'a, G: Html, F: 'a + FnMut(Event)>(cx: Scope<'a>, props: ButtonProps<F>) -> View<G> {
|
|
||||||
let mut icon_class = String::from("mdi ");
|
|
||||||
|
|
||||||
if !props.icon.is_empty() {
|
|
||||||
icon_class.push_str(&props.icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
button(on:click=props.onclick) {
|
|
||||||
span(class=icon_class)
|
|
||||||
span { (props.text) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Prop)]
|
#[derive(Prop)]
|
||||||
pub struct TableProps<'a, G: Html> {
|
pub struct TableProps<'a, G: Html> {
|
||||||
pub headers: Vec<String>,
|
pub headers: Vec<String>,
|
||||||
|
@ -58,64 +34,32 @@ pub fn Table<'a, G: Html>(cx: Scope<'a>, props: TableProps<'a, G>) -> View<G> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Prop)]
|
#[derive(Prop)]
|
||||||
pub struct BlockProps<'a, G: Html> {
|
pub struct ModalProps<'a, G: Html> {
|
||||||
|
pub open: &'a Signal<bool>,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub children: Children<'a, G>,
|
pub children: Children<'a, G>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View<G> {
|
pub fn Modal<'a, G: Html>(cx: Scope<'a>, props: ModalProps<'a, G>) -> View<G> {
|
||||||
let children = props.children.call(cx);
|
let children = props.children.call(cx);
|
||||||
|
|
||||||
|
let class = create_memo(cx, || {
|
||||||
|
if *props.open.get() {
|
||||||
|
"modal"
|
||||||
|
} else {
|
||||||
|
"modal hidden"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
details {
|
div(class=class) {
|
||||||
summary { (props.title) }
|
h4 { (props.title) }
|
||||||
p { (children) }
|
div(class="modal-close") {
|
||||||
|
Button(icon="mdi-close".into(), onclick=move |_| props.open.set(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
(children)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
#[derive(Prop)]
|
|
||||||
pub struct TestProps<'a> {
|
|
||||||
pub text: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Test<'a, G: Html>(cx: Scope<'a>, props: TestProps<'a>) -> View<G> {
|
|
||||||
let text = create_ref(cx, props.text.clone());
|
|
||||||
|
|
||||||
// This is okay, but I don't know why
|
|
||||||
create_child_scope(cx, move |_| {
|
|
||||||
println!("{}", props.text);
|
|
||||||
drop(props.text);
|
|
||||||
});
|
|
||||||
|
|
||||||
// This is fine
|
|
||||||
create_child_scope(cx, move |_| {
|
|
||||||
println!("{}", text);
|
|
||||||
drop(text);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Builders always seem to work just fine
|
|
||||||
let _: View<G> = div().c(p().t(text)).view(cx);
|
|
||||||
let _: View<G> = div().dyn_c_scoped(|cx| p().t(text).view(cx)).view(cx);
|
|
||||||
let _: View<G> = div()
|
|
||||||
.dyn_c_scoped(|cx| p().t(props.text).view(cx))
|
|
||||||
.t(props.text)
|
|
||||||
.view(cx);
|
|
||||||
|
|
||||||
// error[E0521]: borrowed data escapes outside of function
|
|
||||||
let _: View<G> = view! { cx,
|
|
||||||
p { (text) }
|
|
||||||
};
|
|
||||||
|
|
||||||
// error[E0521]: borrowed data escapes outside of function
|
|
||||||
let _: View<G> = view! { cx,
|
|
||||||
p { (props.text) }
|
|
||||||
p { (props.text) }
|
|
||||||
};
|
|
||||||
|
|
||||||
view! { cx, }
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
mod components;
|
mod components;
|
||||||
mod pages;
|
mod pages;
|
||||||
|
pub mod state;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
use components::messages::{Messages, Messenger};
|
use components::messages::{Messages, Messenger};
|
||||||
use pages::{EventsPage, UsersPage};
|
use pages::{EventsPage, UsersPage};
|
||||||
|
use state::{Events, Users};
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use sycamore_router::{HistoryIntegration, Route, Router};
|
use sycamore_router::{HistoryIntegration, Route, Router};
|
||||||
|
|
||||||
|
@ -66,6 +68,12 @@ fn main() {
|
||||||
let messenger = Messenger::default();
|
let messenger = Messenger::default();
|
||||||
provide_context(cx, messenger);
|
provide_context(cx, messenger);
|
||||||
|
|
||||||
|
let users = Users::default();
|
||||||
|
provide_context(cx, users);
|
||||||
|
|
||||||
|
let events = Events::default();
|
||||||
|
provide_context(cx, events);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
let messages = use_context::<MessagesState>(cx);
|
let messages = use_context::<MessagesState>(cx);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
use lan_party_core::{
|
use lan_party_core::{
|
||||||
components::Block,
|
components::Block,
|
||||||
edit::IntoEdit,
|
edit::IntoEdit,
|
||||||
event::{Event, EventSpec, EventUpdate},
|
event::{Event, EventOutcome, EventSpec, EventUpdate},
|
||||||
view::IntoView,
|
view::IntoView,
|
||||||
};
|
};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
@ -9,10 +10,20 @@ use reqwasm::http::Method;
|
||||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{messages::Messenger, Button},
|
components::{messages::Messenger, Button, Modal, Table},
|
||||||
|
state::Events,
|
||||||
util::api_request,
|
util::api_request,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
Reload,
|
||||||
|
Add(EventSpec),
|
||||||
|
Update(String, EventUpdate),
|
||||||
|
Delete(String),
|
||||||
|
Stop(String),
|
||||||
|
ViewOutcome(String),
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
let messenger = use_context::<Messenger>(cx);
|
let messenger = use_context::<Messenger>(cx);
|
||||||
|
@ -21,119 +32,120 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
let event_update = create_signal(cx, EventUpdate::default());
|
let event_update = create_signal(cx, EventUpdate::default());
|
||||||
let event_update_name = create_signal(cx, String::new());
|
let event_update_name = create_signal(cx, String::new());
|
||||||
|
|
||||||
let events: &'a Signal<Vec<Event>> = create_signal(cx, Vec::<Event>::new());
|
let current_event = create_signal(cx, String::new());
|
||||||
|
let show_outcome = create_signal(cx, false);
|
||||||
|
let event_outcome = create_signal(cx, EventOutcome::default());
|
||||||
|
|
||||||
let update_events = move || async move {
|
let events = use_context::<Events>(cx);
|
||||||
events.set(
|
|
||||||
api_request::<_, Vec<Event>>(Method::GET, "/event", Option::<()>::None)
|
let dispatch = move |msg: Msg| {
|
||||||
|
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
|
.await
|
||||||
.map(|inner| inner.unwrap())
|
.and_then(|inner| inner.ok_or(anyhow!("missing body")));
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
spawn_local_scoped(cx, update_events());
|
if let Ok(outcome) = res {
|
||||||
|
debug!("{:#?}", outcome);
|
||||||
let onadd = move |_| {
|
event_outcome.set(outcome);
|
||||||
spawn_local_scoped(cx, async move {
|
} else {
|
||||||
let new_event = api_request::<EventSpec, Event>(
|
debug!("oh no");
|
||||||
Method::POST,
|
messenger.add_result(
|
||||||
"/event",
|
res,
|
||||||
Some((*event_spec).get().as_ref().clone()),
|
Option::<String>::None,
|
||||||
|
Some("Failed to load outcome"),
|
||||||
)
|
)
|
||||||
.await;
|
}
|
||||||
|
}
|
||||||
if let Ok(Some(new_event)) = new_event {
|
_ => {}
|
||||||
messenger.info(
|
|
||||||
"Added new event",
|
|
||||||
format!(
|
|
||||||
"Successfully created a new event with name \"{}\"",
|
|
||||||
new_event.name
|
|
||||||
),
|
|
||||||
);
|
|
||||||
events.modify().push(new_event);
|
|
||||||
} else {
|
|
||||||
messenger.info("Error when adding an event", "Unable to create a new event");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let onupdate = move |_| {
|
dispatch(Msg::Reload);
|
||||||
spawn_local_scoped(cx, async move {
|
|
||||||
let res = api_request::<EventUpdate, ()>(
|
|
||||||
Method::POST,
|
|
||||||
&format!("/event/{}", event_update_name),
|
|
||||||
Some((*event_update).get().as_ref().clone()),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(_) = res {
|
let outcome_points = create_memo(cx, || {
|
||||||
update_events().await;
|
let cloned = event_outcome.get().as_ref().clone();
|
||||||
messenger.info(
|
let mut vec = cloned.points.into_iter().collect::<Vec<_>>();
|
||||||
"Updated event",
|
vec.sort_by_key(|(_, s)| -*s);
|
||||||
format!(
|
vec
|
||||||
"Successfully updated event with name \"{}\"",
|
|
||||||
event_update_name
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
messenger.info(
|
|
||||||
"Error when updating event",
|
|
||||||
format!("Unable to update event with name \"{}\"", event_update_name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
let onviewoutcome = move |event_name: String| {
|
|
||||||
move |_| {
|
|
||||||
let event_name = event_name.clone();
|
|
||||||
spawn_local_scoped(cx, async move {});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let onstop = move |event_name: String| {
|
|
||||||
move |_| {
|
|
||||||
let event_name = event_name.clone();
|
|
||||||
spawn_local_scoped(cx, async move {});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let ondelete = move |event_name: String| {
|
|
||||||
move |_| {
|
|
||||||
let event_name = event_name.clone();
|
|
||||||
spawn_local_scoped(cx, async move {
|
|
||||||
let res =
|
|
||||||
api_request::<(), ()>(Method::DELETE, &format!("/event/{}", event_name), None)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(_) = res {
|
|
||||||
update_events().await;
|
|
||||||
messenger.info(
|
|
||||||
"Removed event",
|
|
||||||
format!("Successfully removed event with name \"{}\"", event_name),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
messenger.info(
|
|
||||||
"Error when removing event",
|
|
||||||
format!("Unable to remove event with name \"{}\"", event_name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
Modal(open=show_outcome, title="Event outcome".into()) {
|
||||||
|
Table(headers=vec!["Username".into(), "Score".into()]) {
|
||||||
|
Indexed(
|
||||||
|
iterable=&outcome_points,
|
||||||
|
view=move |cx, (k, v)| {
|
||||||
|
view! { cx,
|
||||||
|
tr {
|
||||||
|
td { (k.clone()) }
|
||||||
|
td { (v.clone()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| dispatch(Msg::ViewOutcome(current_event.get().as_ref().clone())))
|
||||||
|
}
|
||||||
div(class="events-cols") {
|
div(class="events-cols") {
|
||||||
div {
|
div {
|
||||||
Block(title="Events".into()) {
|
Block(title="Events".into()) {
|
||||||
|
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| dispatch(Msg::Reload))
|
||||||
Indexed(
|
Indexed(
|
||||||
iterable=&events,
|
iterable=&events.get(),
|
||||||
view=move |cx, event| {
|
view=move |cx, event| {
|
||||||
let event = create_ref(cx, event);
|
let event = create_ref(cx, event);
|
||||||
view! { cx,
|
view! { cx,
|
||||||
//(event.view(cx))
|
Block(title=event.name.clone()) {
|
||||||
EventView(event=event, ondelete=ondelete(event.name.clone()), onviewoutcome=onviewoutcome(event.name.clone()), onstop=onstop(event.name.clone()))
|
(event.description)
|
||||||
|
br()
|
||||||
|
span(class="event-action") {
|
||||||
|
Button(text="Delete".into(), icon="mdi-delete".into(), onclick=move |_| dispatch(Msg::Delete(event.name.clone())))
|
||||||
|
}
|
||||||
|
span(class="event-action") {
|
||||||
|
Button(text="View outcome".into(), onclick=move |_| dispatch(Msg::ViewOutcome(event.name.clone())))
|
||||||
|
}
|
||||||
|
span(class="event-action") {
|
||||||
|
Button(text="Finish".into(), onclick=move |_| dispatch(Msg::Stop(event.name.clone())))
|
||||||
|
}
|
||||||
|
br()
|
||||||
|
(event.event_type.view(cx))
|
||||||
|
}
|
||||||
br()
|
br()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -143,14 +155,14 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
div(class="events-right") {
|
div(class="events-right") {
|
||||||
Block(title="Create new event".into()) {
|
Block(title="Create new event".into()) {
|
||||||
(event_spec.edit(cx))
|
(event_spec.edit(cx))
|
||||||
Button(icon="mdi-check".into(), onclick=onadd)
|
Button(icon="mdi-check".into(), onclick=move |_| dispatch(Msg::Add(event_spec.get().as_ref().clone())))
|
||||||
}
|
}
|
||||||
br()
|
br()
|
||||||
Block(title="Update an event".into()) {
|
Block(title="Update an event".into()) {
|
||||||
label { "Event name" }
|
label { "Event name" }
|
||||||
select(bind:value=event_update_name) {
|
select(bind:value=event_update_name) {
|
||||||
Indexed(
|
Indexed(
|
||||||
iterable=&events,
|
iterable=&events.get(),
|
||||||
view=move |cx, event| {
|
view=move |cx, event| {
|
||||||
let event = create_ref(cx, event);
|
let event = create_ref(cx, event);
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
@ -160,44 +172,15 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
(event_update.edit(cx))
|
(event_update.edit(cx))
|
||||||
Button(icon="mdi-check".into(), onclick=onupdate)
|
Button(
|
||||||
|
icon="mdi-check".into(),
|
||||||
|
onclick=move |_| dispatch(Msg::Update(
|
||||||
|
event_update_name.get().as_ref().clone(),
|
||||||
|
event_update.get().as_ref().clone()
|
||||||
|
))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Prop)]
|
|
||||||
struct EventViewProps<'a, F1, F2, F3>
|
|
||||||
where
|
|
||||||
F1: FnMut(web_sys::Event),
|
|
||||||
F2: FnMut(web_sys::Event),
|
|
||||||
F3: FnMut(web_sys::Event),
|
|
||||||
{
|
|
||||||
pub event: &'a Event,
|
|
||||||
|
|
||||||
pub ondelete: F1,
|
|
||||||
pub onviewoutcome: F2,
|
|
||||||
pub onstop: F3,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn EventView<'a, G, F1, F2, F3>(cx: Scope<'a>, props: EventViewProps<'a, F1, F2, F3>) -> View<G>
|
|
||||||
where
|
|
||||||
F1: FnMut(web_sys::Event) + 'a,
|
|
||||||
F2: FnMut(web_sys::Event) + 'a,
|
|
||||||
F3: FnMut(web_sys::Event) + 'a,
|
|
||||||
G: Html,
|
|
||||||
{
|
|
||||||
view! { cx,
|
|
||||||
Block(title=props.event.name.clone()) {
|
|
||||||
(props.event.description)
|
|
||||||
br()
|
|
||||||
Button(text="Delete".into(), icon="mdi-delete".into(), onclick=props.ondelete)
|
|
||||||
Button(text="View outcome".into(), onclick=props.onviewoutcome)
|
|
||||||
Button(text="Finish".into(), onclick=props.onstop)
|
|
||||||
br()
|
|
||||||
(props.event.event_type.view(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,84 +1,54 @@
|
||||||
use crate::components::{Button, Table};
|
use crate::{
|
||||||
use lan_party_core::user::User;
|
components::{messages::Messenger, Button, Table},
|
||||||
use log::debug;
|
state::Users,
|
||||||
use reqwasm::http::Method;
|
};
|
||||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
use web_sys::Event;
|
|
||||||
|
|
||||||
use crate::util::api_request;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
let users = create_signal(cx, Vec::<User>::new());
|
//let users = create_signal(cx, Vec::<User>::new());
|
||||||
|
let messenger = use_context::<Messenger>(cx);
|
||||||
|
let users = use_context::<Users>(cx);
|
||||||
let headers = vec!["Username".into(), "Score".into(), "".into()];
|
let headers = vec!["Username".into(), "Score".into(), "".into()];
|
||||||
|
|
||||||
let score_edit = create_signal(cx, Option::<String>::None);
|
let score_edit = create_signal(cx, Option::<String>::None);
|
||||||
let new_score = create_signal(cx, String::new());
|
let new_score = create_signal(cx, String::new());
|
||||||
let new_username = create_signal(cx, String::new());
|
let new_username = create_signal(cx, String::new());
|
||||||
|
|
||||||
|
let reload = move || {
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
users.set(
|
messenger.add_result(
|
||||||
api_request::<_, Vec<User>>(Method::GET, "/user", Option::<()>::None)
|
users.load().await,
|
||||||
.await
|
Option::<String>::None,
|
||||||
.map(|inner| inner.unwrap())
|
Some("Failed to load users"),
|
||||||
.unwrap(),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reload();
|
||||||
|
|
||||||
let ondelete = move |name: String| {
|
let ondelete = move |name: String| {
|
||||||
move |event: Event| {
|
move |_| {
|
||||||
let name = name.clone();
|
let name = name.clone();
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
debug!("Delete {:#?}", event);
|
messenger.add_result(
|
||||||
let users_ref = users.get();
|
users.delete(&name).await,
|
||||||
let user: &User = users_ref.iter().find(|user| user.name == name).unwrap();
|
Some(format!("Deleted user {}", name)),
|
||||||
api_request::<_, ()>(
|
Some("Failed to delete user"),
|
||||||
Method::DELETE,
|
);
|
||||||
&format!("/user/{}", user.name),
|
|
||||||
Option::<()>::None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let cloned = (*users_ref)
|
|
||||||
.clone()
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.filter(|u| u.name != user.name)
|
|
||||||
.collect();
|
|
||||||
users.set(cloned);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let oncheck = move |_| {
|
let oncheck = move |_| {
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
if let (Some(score_edit), Ok(score)) =
|
if let (Some(name), Ok(score)) = (score_edit.get().as_ref(), new_score.get().parse()) {
|
||||||
(score_edit.get().as_ref(), new_score.get().parse())
|
|
||||||
{
|
|
||||||
let score: i64 = score;
|
let score: i64 = score;
|
||||||
let users_ref = users.get();
|
messenger.add_result(
|
||||||
let user: &User = users_ref
|
users.update_score(&name, score).await,
|
||||||
.iter()
|
Some(format!("Updated score for user {}", name)),
|
||||||
.find(|user| &user.name == score_edit)
|
Some("Failed to delete user"),
|
||||||
.unwrap();
|
);
|
||||||
api_request::<_, ()>(
|
|
||||||
Method::POST,
|
|
||||||
&format!("/user/{}/score", user.name),
|
|
||||||
Some(score),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let cloned = (*users_ref).clone();
|
|
||||||
let new_users: Vec<_> = cloned
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut user| {
|
|
||||||
if &user.name == score_edit {
|
|
||||||
user.score = score
|
|
||||||
}
|
|
||||||
user
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
users.set(new_users);
|
|
||||||
}
|
}
|
||||||
score_edit.set(None);
|
score_edit.set(None);
|
||||||
})
|
})
|
||||||
|
@ -86,21 +56,19 @@ pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
|
|
||||||
let onadd = move |_| {
|
let onadd = move |_| {
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
let user = api_request::<String, User>(
|
messenger.add_result(
|
||||||
Method::POST,
|
users.add(&new_username.get()).await,
|
||||||
"/user",
|
Some(format!("Added new user {}", new_username.get())),
|
||||||
Some((*new_username).get().as_ref().clone()),
|
Some("Failed to add user"),
|
||||||
)
|
);
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
users.modify().push(user.unwrap());
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| reload())
|
||||||
Table(headers=headers) {
|
Table(headers=headers) {
|
||||||
Keyed(
|
Keyed(
|
||||||
iterable=users,
|
iterable=users.get(),
|
||||||
view=move |cx, user| {
|
view=move |cx, user| {
|
||||||
let user = create_ref(cx, user);
|
let user = create_ref(cx, user);
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use lan_party_core::{
|
||||||
|
event::{Event, EventSpec, EventUpdate},
|
||||||
|
user::User,
|
||||||
|
};
|
||||||
|
use reqwasm::http::Method;
|
||||||
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
|
use crate::util::api_request;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Default)]
|
||||||
|
pub struct Users(RcSignal<Vec<User>>);
|
||||||
|
|
||||||
|
impl Users {
|
||||||
|
pub fn get(&self) -> &RcSignal<Vec<User>> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Default)]
|
||||||
|
pub struct Events(RcSignal<Vec<Event>>);
|
||||||
|
|
||||||
|
impl Events {
|
||||||
|
pub fn get(&self) -> &RcSignal<Vec<Event>> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load(&self) -> Result<()> {
|
||||||
|
self.0.set(
|
||||||
|
api_request::<_, Vec<Event>>(Method::GET, "/event", Option::<()>::None)
|
||||||
|
.await
|
||||||
|
.and_then(|inner| inner.ok_or(anyhow!("missing body")))?,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(&self, event_name: &str) -> Result<()> {
|
||||||
|
api_request::<(), ()>(Method::DELETE, &format!("/event/{}", event_name), None).await?;
|
||||||
|
|
||||||
|
self.load().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,3 +109,39 @@ body {
|
||||||
.events-cols:nth-child(2) {
|
.events-cols:nth-child(2) {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-action {
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
z-index: 5;
|
||||||
|
left: 15vw;
|
||||||
|
top: 10vh;
|
||||||
|
position: fixed;
|
||||||
|
width: 70vw;
|
||||||
|
height: 80vh;
|
||||||
|
padding: 1em;
|
||||||
|
background-color: var(--accent-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal > h4 {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 1em;
|
||||||
|
top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close > * {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue