Frontend refactor and generics in macros

This commit is contained in:
Daan Vanoverloop 2022-09-15 11:46:20 +02:00
parent 59eeabc888
commit 3ef2c282b0
Signed by: Danacus
GPG Key ID: F2272B50E129FC5C
12 changed files with 674 additions and 540 deletions

View File

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

View File

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

View File

View File

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

8
web/dist/index.html vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

128
web/src/state.rs Normal file
View File

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

View File

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