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
|
||||
where
|
||||
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,
|
||||
{
|
||||
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>
|
||||
where
|
||||
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;
|
||||
}
|
||||
|
@ -340,7 +340,7 @@ where
|
|||
impl<'a, G, T> Editable<'a, G> for HashSet<T>
|
||||
where
|
||||
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;
|
||||
}
|
||||
|
@ -350,8 +350,7 @@ where
|
|||
G: Html,
|
||||
K: Clone + Hash + Eq,
|
||||
V: Clone,
|
||||
(K, V):
|
||||
for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + std::fmt::Debug + 'a,
|
||||
(K, V): for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a,
|
||||
{
|
||||
type Editor = VecEdit;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,37 @@ use paste::paste;
|
|||
use schemars::JsonSchema;
|
||||
#[cfg(feature = "serde")]
|
||||
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)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
|
@ -105,6 +135,10 @@ macro_rules! events {
|
|||
|
||||
impl EventSpec {
|
||||
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 {
|
||||
$(EventTypeSpec::$name(s) => {
|
||||
EventType::$name($module::$name::from_spec(s))
|
||||
|
@ -213,11 +247,7 @@ pub mod test {
|
|||
|
||||
pub mod team_game {
|
||||
use super::{
|
||||
free_for_all_game::{
|
||||
FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate,
|
||||
FreeForAllGameUpdateParticipants, FreeForAllGameUpdateRanking,
|
||||
FreeForAllGameUpdateRewards,
|
||||
},
|
||||
free_for_all_game::{FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate},
|
||||
*,
|
||||
};
|
||||
|
||||
|
@ -266,40 +296,12 @@ pub mod team_game {
|
|||
pub members: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
#[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())
|
||||
}
|
||||
pub struct Team {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -308,19 +310,23 @@ pub mod team_game {
|
|||
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||
pub enum TeamGameUpdate {
|
||||
/// # Team
|
||||
///
|
||||
/// Team specific updates
|
||||
Team(TeamGameUpdateInner),
|
||||
/// # Other
|
||||
///
|
||||
/// Inherited from FreeForAllGame
|
||||
Ffa(TeamGameFfaInheritedUpdate),
|
||||
/// Add or replace a team with the given name and array of members
|
||||
SetTeam(TeamGameUpdateSetTeam),
|
||||
/// Remove team with given name
|
||||
RemoveTeam(String),
|
||||
/// Replace the current ranking with the given ranking
|
||||
SetRanking(Ranking<String>),
|
||||
/// If the current ranking is of type `Scores`, apply the given score deltas
|
||||
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 {
|
||||
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> {
|
||||
match update {
|
||||
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(())
|
||||
}
|
||||
},
|
||||
TeamGameUpdate::SetRanking(x) => self
|
||||
.ffa_game
|
||||
.apply_update(FreeForAllGameUpdate::SetRanking(x)),
|
||||
TeamGameUpdate::ScoreDelta(x) => self
|
||||
.ffa_game
|
||||
.apply_update(FreeForAllGameUpdate::ScoreDelta(x)),
|
||||
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()))?;
|
||||
self.teams.insert(u.team, u.members);
|
||||
Ok(())
|
||||
}
|
||||
TeamGameUpdate::RemoveTeam(team) => {
|
||||
self.ffa_game
|
||||
.apply_update(FreeForAllGameUpdate::RemoveParticipant(team.clone()))?;
|
||||
self.teams.remove(&team);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -396,31 +400,12 @@ pub mod free_for_all_game {
|
|||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||
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)),
|
||||
}
|
||||
}
|
||||
pub struct User {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
|
@ -430,7 +415,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<FreeForAllGameRanking>,
|
||||
pub ranking: Option<Ranking<String>>,
|
||||
|
||||
/// Specification of the game
|
||||
#[cfg_attr(feature = "serde", serde(flatten))]
|
||||
|
@ -466,77 +451,28 @@ pub mod free_for_all_game {
|
|||
#[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 FreeForAllGameUpdateRanking {
|
||||
pub enum FreeForAllGameUpdate {
|
||||
/// 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
|
||||
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 {
|
||||
/// Change the ranking and scores
|
||||
Ranking(FreeForAllGameUpdateRanking),
|
||||
/// Update rewards
|
||||
Rewards(FreeForAllGameUpdateRewards),
|
||||
/// Update participants
|
||||
Participants(FreeForAllGameUpdateParticipants),
|
||||
}
|
||||
|
||||
impl Default for FreeForAllGameUpdate {
|
||||
fn default() -> Self {
|
||||
Self::Ranking(FreeForAllGameUpdateRanking::default())
|
||||
Self::AddParticipant(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -553,71 +489,65 @@ pub mod free_for_all_game {
|
|||
|
||||
fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> {
|
||||
match update {
|
||||
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)
|
||||
FreeForAllGameUpdate::SetRanking(r) => {
|
||||
if !r.is_valid(&self.spec.participants) {
|
||||
return Err(PartyError::Unknown("invalid ranking, all participants mentioned in ranking must be participating".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);
|
||||
}
|
||||
self.ranking = Some(r)
|
||||
}
|
||||
FreeForAllGameUpdate::ScoreDelta(d) => match &mut self.ranking {
|
||||
Some(Ranking::Ranking(_)) | None => {
|
||||
return Err(PartyError::Unknown("cannot apply score delta".into()))
|
||||
}
|
||||
Some(Ranking::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);
|
||||
}
|
||||
FreeForAllGameUpdateParticipants::RemoveParticipant(name) => {
|
||||
self.spec.participants.remove(&name);
|
||||
},
|
||||
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::Unknown("cannot remove participant, all participants mentioned in ranking must be participating".into()));
|
||||
}
|
||||
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::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()));
|
||||
}
|
||||
},
|
||||
FreeForAllGameUpdate::Rewards(update) => match update {
|
||||
FreeForAllGameUpdateRewards::SetWinRewards(rewards) => {
|
||||
self.spec.win_rewards = rewards;
|
||||
}
|
||||
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(FreeForAllGameRanking::Ranking(r)) => r.clone(),
|
||||
Some(FreeForAllGameRanking::Scores(s)) => {
|
||||
Some(Ranking::Ranking(r)) => r.clone(),
|
||||
Some(Ranking::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,16 +1,15 @@
|
|||
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, Ident, Lit,
|
||||
MetaNameValue, Path, Type,
|
||||
parse::Parse, parse_str, token::Do, Attribute, DataEnum, DataStruct, Fields, Generics, Ident,
|
||||
LifetimeDef, Lit, MetaNameValue, Path, PredicateType, Type, TypeParam,
|
||||
};
|
||||
|
||||
enum ParsedAttribute {
|
||||
Documentation(Documentation),
|
||||
View(ViewAttribute),
|
||||
Serde(SerdeAttribute),
|
||||
None,
|
||||
}
|
||||
|
||||
|
@ -26,6 +25,11 @@ enum ViewAttribute {
|
|||
None,
|
||||
}
|
||||
|
||||
enum SerdeAttribute {
|
||||
Untagged,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Documentation {
|
||||
fn parse(attr: &Attribute) -> Documentation {
|
||||
if !attr.path.is_ident("doc") {
|
||||
|
@ -56,8 +60,6 @@ impl ViewAttribute {
|
|||
.trim_matches(|c: char| c == '(' || c == ')'),
|
||||
);
|
||||
|
||||
dbg!(&parsed);
|
||||
|
||||
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())),
|
||||
|
@ -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 {
|
||||
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() == "web_view_attr" => Self::View(ViewAttribute::parse(attr)),
|
||||
Some(i) if i.to_string() == "serde" => Self::Serde(SerdeAttribute::parse(attr)),
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +107,7 @@ struct Attributes {
|
|||
title: Option<String>,
|
||||
title_field: Option<Ident>,
|
||||
description: Option<String>,
|
||||
untagged: bool,
|
||||
}
|
||||
|
||||
impl Attributes {
|
||||
|
@ -91,6 +117,7 @@ impl Attributes {
|
|||
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 {
|
||||
|
@ -110,6 +137,10 @@ impl Attributes {
|
|||
ViewAttribute::Title(t) => title_field = Some(t),
|
||||
_ => {}
|
||||
},
|
||||
ParsedAttribute::Serde(s) => match s {
|
||||
SerdeAttribute::Untagged => untagged = true,
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -118,6 +149,7 @@ impl Attributes {
|
|||
title,
|
||||
description,
|
||||
title_field,
|
||||
untagged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -129,9 +161,8 @@ pub fn web_view_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||
|
||||
struct ItemProps {
|
||||
name: Ident,
|
||||
title: String,
|
||||
title_field: Option<Ident>,
|
||||
description: Option<String>,
|
||||
attributes: Attributes,
|
||||
generics: Generics,
|
||||
}
|
||||
|
||||
struct StructField {
|
||||
|
@ -152,10 +183,12 @@ struct EnumVariant {
|
|||
fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||
let ItemProps {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
..
|
||||
attributes: Attributes {
|
||||
title, description, ..
|
||||
},
|
||||
generics,
|
||||
} = props;
|
||||
let title = title.clone().unwrap_or(name.to_string());
|
||||
|
||||
let fields = s.fields.iter().map(|f| {
|
||||
let name = f
|
||||
|
@ -168,6 +201,7 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
title,
|
||||
title_field: _,
|
||||
description,
|
||||
..
|
||||
} = Attributes::parse(&f.attrs);
|
||||
|
||||
StructField {
|
||||
|
@ -238,10 +272,12 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
||||
let ItemProps {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
..
|
||||
attributes: Attributes {
|
||||
title, description, ..
|
||||
},
|
||||
generics,
|
||||
} = props;
|
||||
let title = title.clone().unwrap_or(name.to_string());
|
||||
|
||||
let variants = e.variants.iter().map(|v| {
|
||||
let variant = v.ident.clone();
|
||||
|
@ -255,6 +291,7 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
title,
|
||||
title_field: _,
|
||||
description,
|
||||
..
|
||||
} = Attributes::parse(&v.attrs);
|
||||
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
||||
EnumVariant {
|
||||
|
@ -358,6 +395,7 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
#(#view_description,)*
|
||||
_ => view! { cx, }
|
||||
})
|
||||
br()
|
||||
(match selected.get().as_str() {
|
||||
#(#view_match,)*
|
||||
_ => view! { cx, }
|
||||
|
@ -370,10 +408,16 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
||||
let ItemProps {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
title_field,
|
||||
attributes:
|
||||
Attributes {
|
||||
title,
|
||||
description,
|
||||
title_field,
|
||||
..
|
||||
},
|
||||
generics,
|
||||
} = props;
|
||||
let title = title.clone().unwrap_or(name.to_string());
|
||||
|
||||
let fields = s.fields.iter().map(|f| {
|
||||
let name = f
|
||||
|
@ -386,6 +430,7 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
title,
|
||||
title_field: _,
|
||||
description,
|
||||
..
|
||||
} = Attributes::parse(&f.attrs);
|
||||
|
||||
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 {
|
||||
let ItemProps {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
..
|
||||
attributes: Attributes {
|
||||
title, description, ..
|
||||
},
|
||||
generics,
|
||||
} = props;
|
||||
let title = title.clone().unwrap_or(name.to_string());
|
||||
|
||||
let variants = e.variants.iter().map(|v| {
|
||||
let variant = v.ident.clone();
|
||||
|
@ -469,6 +542,7 @@ fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
title,
|
||||
title_field: _,
|
||||
description,
|
||||
..
|
||||
} = Attributes::parse(&v.attrs);
|
||||
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
||||
EnumVariant {
|
||||
|
@ -533,20 +607,12 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
|||
let name = input.ident;
|
||||
let edit_ident = format_ident!("{}Edit", name);
|
||||
|
||||
let Attributes {
|
||||
title: t,
|
||||
title_field,
|
||||
description: d,
|
||||
} = Attributes::parse(&input.attrs);
|
||||
|
||||
let title = t.unwrap_or(name.to_string());
|
||||
let description = d;
|
||||
let attrs = Attributes::parse(&input.attrs);
|
||||
|
||||
let props = ItemProps {
|
||||
name: name.clone(),
|
||||
title: title.clone(),
|
||||
description: description.clone(),
|
||||
title_field: title_field.clone(),
|
||||
attributes: attrs,
|
||||
generics: input.generics.clone(),
|
||||
};
|
||||
|
||||
let inner = match input.data {
|
||||
|
@ -555,20 +621,49 @@ 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<'a, G: Html> Editor<'a, G, #name> for #edit_ident {
|
||||
fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View<G> {
|
||||
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> {
|
||||
#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;
|
||||
}
|
||||
};
|
||||
|
||||
println!("{}", &res.to_string());
|
||||
|
||||
TokenStream::from(res)
|
||||
}
|
||||
|
||||
|
@ -579,20 +674,12 @@ pub fn web_view(tokens: TokenStream) -> TokenStream {
|
|||
let name = input.ident;
|
||||
let view_ident = format_ident!("{}View", name);
|
||||
|
||||
let Attributes {
|
||||
title: t,
|
||||
title_field,
|
||||
description: d,
|
||||
} = Attributes::parse(&input.attrs);
|
||||
|
||||
let title = t.unwrap_or(name.to_string());
|
||||
let description = d;
|
||||
let attrs = Attributes::parse(&input.attrs);
|
||||
|
||||
let props = ItemProps {
|
||||
name: name.clone(),
|
||||
title: title.clone(),
|
||||
description: description.clone(),
|
||||
title_field: title_field.clone(),
|
||||
attributes: attrs,
|
||||
generics: input.generics.clone(),
|
||||
};
|
||||
|
||||
let inner = match input.data {
|
||||
|
@ -601,19 +688,50 @@ 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<'a, G: Html> Viewer<'a, G, #name> for #view_ident {
|
||||
fn view(cx: Scope<'a>, props: ViewProps<'a, #name>) -> View<G> {
|
||||
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> {
|
||||
#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;
|
||||
}
|
||||
};
|
||||
|
||||
println!("{}", &res.to_string());
|
||||
|
||||
TokenStream::from(res)
|
||||
}
|
||||
|
|
|
@ -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-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">
|
||||
<title>LAN Party</title>
|
||||
|
||||
<link rel="preload" href="/index-b0d435005316ee2a_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<link rel="modulepreload" href="/index-b0d435005316ee2a.js"></head>
|
||||
<link rel="preload" href="/index-45b72451f5518e3c_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<link rel="modulepreload" href="/index-45b72451f5518e3c.js"></head>
|
||||
<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()));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,33 +1,9 @@
|
|||
pub mod messages;
|
||||
|
||||
pub use lan_party_core::components::Button;
|
||||
use sycamore::{builder::prelude::*, 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>,
|
||||
|
@ -58,64 +34,32 @@ pub fn Table<'a, G: Html>(cx: Scope<'a>, props: TableProps<'a, G>) -> View<G> {
|
|||
}
|
||||
|
||||
#[derive(Prop)]
|
||||
pub struct BlockProps<'a, G: Html> {
|
||||
pub struct ModalProps<'a, G: Html> {
|
||||
pub open: &'a Signal<bool>,
|
||||
pub title: String,
|
||||
pub children: Children<'a, G>,
|
||||
}
|
||||
|
||||
#[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 class = create_memo(cx, || {
|
||||
if *props.open.get() {
|
||||
"modal"
|
||||
} else {
|
||||
"modal hidden"
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
details {
|
||||
summary { (props.title) }
|
||||
p { (children) }
|
||||
div(class=class) {
|
||||
h4 { (props.title) }
|
||||
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 pages;
|
||||
pub mod state;
|
||||
pub mod util;
|
||||
|
||||
use components::messages::{Messages, Messenger};
|
||||
use pages::{EventsPage, UsersPage};
|
||||
use state::{Events, Users};
|
||||
use sycamore::prelude::*;
|
||||
use sycamore_router::{HistoryIntegration, Route, Router};
|
||||
|
||||
|
@ -66,6 +68,12 @@ fn main() {
|
|||
let messenger = Messenger::default();
|
||||
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);
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use anyhow::anyhow;
|
||||
use lan_party_core::{
|
||||
components::Block,
|
||||
edit::IntoEdit,
|
||||
event::{Event, EventSpec, EventUpdate},
|
||||
event::{Event, EventOutcome, EventSpec, EventUpdate},
|
||||
view::IntoView,
|
||||
};
|
||||
use log::debug;
|
||||
|
@ -9,10 +10,20 @@ use reqwasm::http::Method;
|
|||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||
|
||||
use crate::{
|
||||
components::{messages::Messenger, Button},
|
||||
components::{messages::Messenger, Button, Modal, Table},
|
||||
state::Events,
|
||||
util::api_request,
|
||||
};
|
||||
|
||||
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);
|
||||
|
@ -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_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 {
|
||||
events.set(
|
||||
api_request::<_, Vec<Event>>(Method::GET, "/event", Option::<()>::None)
|
||||
.await
|
||||
.map(|inner| inner.unwrap())
|
||||
.unwrap(),
|
||||
);
|
||||
};
|
||||
let events = use_context::<Events>(cx);
|
||||
|
||||
spawn_local_scoped(cx, update_events());
|
||||
|
||||
let onadd = move |_| {
|
||||
let dispatch = move |msg: Msg| {
|
||||
spawn_local_scoped(cx, async move {
|
||||
let new_event = api_request::<EventSpec, Event>(
|
||||
Method::POST,
|
||||
"/event",
|
||||
Some((*event_spec).get().as_ref().clone()),
|
||||
)
|
||||
.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 |_| {
|
||||
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 {
|
||||
update_events().await;
|
||||
messenger.info(
|
||||
"Updated event",
|
||||
format!(
|
||||
"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),
|
||||
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 {
|
||||
debug!("{:#?}", outcome);
|
||||
event_outcome.set(outcome);
|
||||
} else {
|
||||
debug!("oh no");
|
||||
messenger.add_result(
|
||||
res,
|
||||
Option::<String>::None,
|
||||
Some("Failed to load outcome"),
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| dispatch(Msg::ViewOutcome(current_event.get().as_ref().clone())))
|
||||
}
|
||||
div(class="events-cols") {
|
||||
div {
|
||||
Block(title="Events".into()) {
|
||||
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| dispatch(Msg::Reload))
|
||||
Indexed(
|
||||
iterable=&events,
|
||||
iterable=&events.get(),
|
||||
view=move |cx, event| {
|
||||
let event = create_ref(cx, event);
|
||||
view! { cx,
|
||||
//(event.view(cx))
|
||||
EventView(event=event, ondelete=ondelete(event.name.clone()), onviewoutcome=onviewoutcome(event.name.clone()), onstop=onstop(event.name.clone()))
|
||||
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()
|
||||
}
|
||||
},
|
||||
|
@ -143,14 +155,14 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
|||
div(class="events-right") {
|
||||
Block(title="Create new event".into()) {
|
||||
(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()
|
||||
Block(title="Update an event".into()) {
|
||||
label { "Event name" }
|
||||
select(bind:value=event_update_name) {
|
||||
Indexed(
|
||||
iterable=&events,
|
||||
iterable=&events.get(),
|
||||
view=move |cx, event| {
|
||||
let event = create_ref(cx, event);
|
||||
view! { cx,
|
||||
|
@ -160,44 +172,15 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
|||
)
|
||||
}
|
||||
(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 lan_party_core::user::User;
|
||||
use log::debug;
|
||||
use reqwasm::http::Method;
|
||||
use crate::{
|
||||
components::{messages::Messenger, Button, Table},
|
||||
state::Users,
|
||||
};
|
||||
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 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 score_edit = create_signal(cx, Option::<String>::None);
|
||||
let new_score = create_signal(cx, String::new());
|
||||
let new_username = create_signal(cx, String::new());
|
||||
|
||||
spawn_local_scoped(cx, async move {
|
||||
users.set(
|
||||
api_request::<_, Vec<User>>(Method::GET, "/user", Option::<()>::None)
|
||||
.await
|
||||
.map(|inner| inner.unwrap())
|
||||
.unwrap(),
|
||||
);
|
||||
});
|
||||
let reload = move || {
|
||||
spawn_local_scoped(cx, async move {
|
||||
messenger.add_result(
|
||||
users.load().await,
|
||||
Option::<String>::None,
|
||||
Some("Failed to load users"),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
reload();
|
||||
|
||||
let ondelete = move |name: String| {
|
||||
move |event: Event| {
|
||||
move |_| {
|
||||
let name = name.clone();
|
||||
spawn_local_scoped(cx, async move {
|
||||
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);
|
||||
messenger.add_result(
|
||||
users.delete(&name).await,
|
||||
Some(format!("Deleted user {}", name)),
|
||||
Some("Failed to delete user"),
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let oncheck = move |_| {
|
||||
spawn_local_scoped(cx, async move {
|
||||
if let (Some(score_edit), Ok(score)) =
|
||||
(score_edit.get().as_ref(), new_score.get().parse())
|
||||
{
|
||||
if let (Some(name), Ok(score)) = (score_edit.get().as_ref(), new_score.get().parse()) {
|
||||
let score: i64 = score;
|
||||
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);
|
||||
messenger.add_result(
|
||||
users.update_score(&name, score).await,
|
||||
Some(format!("Updated score for user {}", name)),
|
||||
Some("Failed to delete user"),
|
||||
);
|
||||
}
|
||||
score_edit.set(None);
|
||||
})
|
||||
|
@ -86,21 +56,19 @@ pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
|||
|
||||
let onadd = move |_| {
|
||||
spawn_local_scoped(cx, async move {
|
||||
let user = api_request::<String, User>(
|
||||
Method::POST,
|
||||
"/user",
|
||||
Some((*new_username).get().as_ref().clone()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
users.modify().push(user.unwrap());
|
||||
messenger.add_result(
|
||||
users.add(&new_username.get()).await,
|
||||
Some(format!("Added new user {}", new_username.get())),
|
||||
Some("Failed to add user"),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
Button(text="Reload".into(), icon="mdi-refresh".into(), onclick=move |_| reload())
|
||||
Table(headers=headers) {
|
||||
Keyed(
|
||||
iterable=users,
|
||||
iterable=users.get(),
|
||||
view=move |cx, user| {
|
||||
let user = create_ref(cx, user);
|
||||
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) {
|
||||
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