Temp
This commit is contained in:
parent
44745b252a
commit
59eeabc888
|
@ -14,6 +14,7 @@ api_routes!(
|
|||
update_event,
|
||||
get_all_events,
|
||||
event_outcome,
|
||||
delete_event,
|
||||
);
|
||||
|
||||
pub async fn apply_outcome(outcome: &EventOutcome, db: &Connection<Db>) -> Result<(), PartyError> {
|
||||
|
@ -164,3 +165,16 @@ pub async fn stop_event(
|
|||
|
||||
Ok(Json(outcome))
|
||||
}
|
||||
|
||||
/// # Delete event by name
|
||||
#[openapi(tag = "Event")]
|
||||
#[delete("/<name>")]
|
||||
pub async fn delete_event(
|
||||
_api_key: ApiKey,
|
||||
db: Connection<Db>,
|
||||
name: String,
|
||||
) -> Result<Status, PartyError> {
|
||||
db.events().delete_one(doc! { "name": name }, None).await?;
|
||||
|
||||
Ok(Status::Ok)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::util::PartyError;
|
|||
#[cfg(feature = "sycamore")]
|
||||
use crate::view::prelude::*;
|
||||
#[cfg(feature = "sycamore")]
|
||||
use lan_party_macros::{WebEdit, WebView};
|
||||
use lan_party_macros::{web_view_attr, WebEdit, WebView};
|
||||
use paste::paste;
|
||||
#[cfg(feature = "openapi")]
|
||||
use schemars::JsonSchema;
|
||||
|
@ -26,6 +26,7 @@ pub struct EventOutcome {
|
|||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebView))]
|
||||
#[cfg_attr(feature = "sycamore", web_view_attr(title = "name"))]
|
||||
pub struct Event {
|
||||
/// Has this event concluded?
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
|
|
|
@ -174,6 +174,7 @@ impl<'a, G: Html, A: for<'b> Viewable<'b, G> + Clone, B: for<'b> Viewable<'b, G>
|
|||
view! { cx,
|
||||
Block(title="Tuple".into()) {
|
||||
(props.state.0.view(cx))
|
||||
br()
|
||||
(props.state.1.view(cx))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,16 @@ mod edit;
|
|||
use convert_case::{Case, Casing};
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{__private::TokenStream as TokenStream2, format_ident, quote};
|
||||
use syn::{token::Do, Attribute, DataEnum, DataStruct, Fields, Ident, Path, Type};
|
||||
use syn::{
|
||||
parse::Parse, parse_str, token::Do, Attribute, DataEnum, DataStruct, Fields, Ident, Lit,
|
||||
MetaNameValue, Path, Type,
|
||||
};
|
||||
|
||||
enum ParsedAttribute {
|
||||
Documentation(Documentation),
|
||||
View(ViewAttribute),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Documentation {
|
||||
|
@ -12,6 +21,11 @@ enum Documentation {
|
|||
None,
|
||||
}
|
||||
|
||||
enum ViewAttribute {
|
||||
Title(Ident),
|
||||
None,
|
||||
}
|
||||
|
||||
impl Documentation {
|
||||
fn parse(attr: &Attribute) -> Documentation {
|
||||
if !attr.path.is_ident("doc") {
|
||||
|
@ -30,14 +44,57 @@ impl Documentation {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_title_description(attrs: &[Attribute]) -> (Option<String>, Option<String>) {
|
||||
let docs: Vec<_> = attrs.iter().map(Documentation::parse).collect();
|
||||
impl ViewAttribute {
|
||||
fn parse(attr: &Attribute) -> ViewAttribute {
|
||||
if !attr.path.is_ident("web_view_attr") {
|
||||
return Self::None;
|
||||
}
|
||||
|
||||
let parsed: Result<MetaNameValue, _> = parse_str(
|
||||
attr.tokens
|
||||
.to_string()
|
||||
.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())),
|
||||
_ => 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)),
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Attributes {
|
||||
title: Option<String>,
|
||||
title_field: Option<Ident>,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
impl Attributes {
|
||||
pub fn parse(attrs: &[Attribute]) -> Self {
|
||||
let parsed: Vec<_> = attrs.iter().map(ParsedAttribute::parse).collect();
|
||||
|
||||
let mut title = None;
|
||||
let mut title_field = None;
|
||||
let mut description: Option<String> = None;
|
||||
|
||||
for doc in docs {
|
||||
match doc {
|
||||
for attr in parsed {
|
||||
match attr {
|
||||
ParsedAttribute::Documentation(doc) => match doc {
|
||||
Documentation::Title(t) => title = Some(t),
|
||||
Documentation::Description(d) => {
|
||||
if description.is_some() {
|
||||
|
@ -48,15 +105,32 @@ fn get_title_description(attrs: &[Attribute]) -> (Option<String>, Option<String>
|
|||
description.as_mut().unwrap().push_str(&d);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ParsedAttribute::View(v) => match v {
|
||||
ViewAttribute::Title(t) => title_field = Some(t),
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(title, description)
|
||||
Self {
|
||||
title,
|
||||
description,
|
||||
title_field,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn web_view_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
item
|
||||
}
|
||||
|
||||
struct ItemProps {
|
||||
name: Ident,
|
||||
title: String,
|
||||
title_field: Option<Ident>,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
|
@ -80,6 +154,7 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
name,
|
||||
title,
|
||||
description,
|
||||
..
|
||||
} = props;
|
||||
|
||||
let fields = s.fields.iter().map(|f| {
|
||||
|
@ -89,7 +164,11 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
.expect("each struct field must be named")
|
||||
.clone();
|
||||
let name_str = name.to_string();
|
||||
let (title, description) = get_title_description(&f.attrs);
|
||||
let Attributes {
|
||||
title,
|
||||
title_field: _,
|
||||
description,
|
||||
} = Attributes::parse(&f.attrs);
|
||||
|
||||
StructField {
|
||||
name_str,
|
||||
|
@ -161,6 +240,7 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
name,
|
||||
title,
|
||||
description,
|
||||
..
|
||||
} = props;
|
||||
|
||||
let variants = e.variants.iter().map(|v| {
|
||||
|
@ -171,7 +251,11 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
let (title, description) = get_title_description(&v.attrs);
|
||||
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,
|
||||
|
@ -288,6 +372,7 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
name,
|
||||
title,
|
||||
description,
|
||||
title_field,
|
||||
} = props;
|
||||
|
||||
let fields = s.fields.iter().map(|f| {
|
||||
|
@ -297,7 +382,11 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
.expect("each struct field must be named")
|
||||
.clone();
|
||||
let name_str = name.to_string();
|
||||
let (title, description) = get_title_description(&f.attrs);
|
||||
let Attributes {
|
||||
title,
|
||||
title_field: _,
|
||||
description,
|
||||
} = Attributes::parse(&f.attrs);
|
||||
|
||||
StructField {
|
||||
name_str,
|
||||
|
@ -334,6 +423,16 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|||
}
|
||||
});
|
||||
|
||||
let title = if let Some(title_field) = title_field {
|
||||
quote! {
|
||||
state.#title_field
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#title
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
let state = props.state;
|
||||
|
||||
|
@ -355,6 +454,7 @@ fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
name,
|
||||
title,
|
||||
description,
|
||||
..
|
||||
} = props;
|
||||
|
||||
let variants = e.variants.iter().map(|v| {
|
||||
|
@ -365,7 +465,11 @@ fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
let (title, description) = get_title_description(&v.attrs);
|
||||
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,
|
||||
|
@ -429,7 +533,11 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
|||
let name = input.ident;
|
||||
let edit_ident = format_ident!("{}Edit", name);
|
||||
|
||||
let (t, d) = get_title_description(&input.attrs);
|
||||
let Attributes {
|
||||
title: t,
|
||||
title_field,
|
||||
description: d,
|
||||
} = Attributes::parse(&input.attrs);
|
||||
|
||||
let title = t.unwrap_or(name.to_string());
|
||||
let description = d;
|
||||
|
@ -438,6 +546,7 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
|||
name: name.clone(),
|
||||
title: title.clone(),
|
||||
description: description.clone(),
|
||||
title_field: title_field.clone(),
|
||||
};
|
||||
|
||||
let inner = match input.data {
|
||||
|
@ -470,7 +579,11 @@ pub fn web_view(tokens: TokenStream) -> TokenStream {
|
|||
let name = input.ident;
|
||||
let view_ident = format_ident!("{}View", name);
|
||||
|
||||
let (t, d) = get_title_description(&input.attrs);
|
||||
let Attributes {
|
||||
title: t,
|
||||
title_field,
|
||||
description: d,
|
||||
} = Attributes::parse(&input.attrs);
|
||||
|
||||
let title = t.unwrap_or(name.to_string());
|
||||
let description = d;
|
||||
|
@ -479,6 +592,7 @@ pub fn web_view(tokens: TokenStream) -> TokenStream {
|
|||
name: name.clone(),
|
||||
title: title.clone(),
|
||||
description: description.clone(),
|
||||
title_field: title_field.clone(),
|
||||
};
|
||||
|
||||
let inner = match input.data {
|
||||
|
|
|
@ -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-d252283caa890f82.css">
|
||||
<link rel="stylesheet" href="/style-2f979acf99c8ad73.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-50210a46cec614af_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<link rel="modulepreload" href="/index-50210a46cec614af.js"></head>
|
||||
<link rel="preload" href="/index-b0d435005316ee2a_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<link rel="modulepreload" href="/index-b0d435005316ee2a.js"></head>
|
||||
<body>
|
||||
<script type="module">import init from '/index-50210a46cec614af.js';init('/index-50210a46cec614af_bg.wasm');</script></body></html>
|
||||
<script type="module">import init from '/index-b0d435005316ee2a.js';init('/index-b0d435005316ee2a_bg.wasm');</script></body></html>
|
|
@ -1,6 +1,6 @@
|
|||
pub mod messages;
|
||||
|
||||
use sycamore::prelude::*;
|
||||
use sycamore::{builder::prelude::*, prelude::*};
|
||||
use web_sys::Event;
|
||||
|
||||
#[derive(Prop)]
|
||||
|
@ -74,3 +74,48 @@ pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View<G> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[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,4 +1,5 @@
|
|||
use lan_party_core::{
|
||||
components::Block,
|
||||
edit::IntoEdit,
|
||||
event::{Event, EventSpec, EventUpdate},
|
||||
view::IntoView,
|
||||
|
@ -8,7 +9,7 @@ use reqwasm::http::Method;
|
|||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||
|
||||
use crate::{
|
||||
components::{messages::Messenger, Block, Button},
|
||||
components::{messages::Messenger, Button},
|
||||
util::api_request,
|
||||
};
|
||||
|
||||
|
@ -18,17 +19,20 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
|||
|
||||
let event_spec = create_signal(cx, EventSpec::default());
|
||||
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());
|
||||
|
||||
spawn_local_scoped(cx, async move {
|
||||
let update_events = move || async move {
|
||||
events.set(
|
||||
api_request::<_, Vec<Event>>(Method::GET, "/event", Option::<()>::None)
|
||||
.await
|
||||
.map(|inner| inner.unwrap())
|
||||
.unwrap(),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
spawn_local_scoped(cx, update_events());
|
||||
|
||||
let onadd = move |_| {
|
||||
spawn_local_scoped(cx, async move {
|
||||
|
@ -54,29 +58,146 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
|||
});
|
||||
};
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
div(class="events-cols") {
|
||||
div {
|
||||
Block(title="Events".into()) {
|
||||
Keyed(
|
||||
Indexed(
|
||||
iterable=&events,
|
||||
view=move |cx, event| {
|
||||
let event = create_ref(cx, event);
|
||||
view! { 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()))
|
||||
br()
|
||||
}
|
||||
},
|
||||
key=|event| (event.name.clone()),
|
||||
)
|
||||
}
|
||||
br()
|
||||
}
|
||||
div(class="events-right") {
|
||||
Block(title="Create new event".into()) {
|
||||
(event_spec.edit(cx))
|
||||
Button(icon="mdi-check".into(), onclick=onadd)
|
||||
}
|
||||
br()
|
||||
Block(title="Update an event".into()) {
|
||||
label { "Event name" }
|
||||
select(bind:value=event_update_name) {
|
||||
Indexed(
|
||||
iterable=&events,
|
||||
view=move |cx, event| {
|
||||
let event = create_ref(cx, event);
|
||||
view! { cx,
|
||||
option(value=event.name) { (event.name) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
(event_update.edit(cx))
|
||||
Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_update.get()))
|
||||
Button(icon="mdi-check".into(), onclick=onupdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,11 +33,15 @@ pub async fn api_request<B: Serialize, R: for<'a> Deserialize<'a>>(
|
|||
|
||||
let res = req.send().await?;
|
||||
|
||||
if res.ok() {
|
||||
if let Ok(json) = res.json().await {
|
||||
Ok(Some(json))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!("Request failed"))
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
|
|
@ -64,7 +64,7 @@ textarea:focus, input:focus{
|
|||
}
|
||||
|
||||
body {
|
||||
grid-template-columns: 1fr min(60rem, 90%) 1fr;
|
||||
grid-template-columns: 1fr min(100rem, 90%) 1fr;
|
||||
}
|
||||
|
||||
.messages {
|
||||
|
@ -91,3 +91,21 @@ body {
|
|||
margin-bottom: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.events-cols {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
}
|
||||
|
||||
.events-cols > * {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.events-cols:nth-child(1) {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.events-cols:nth-child(2) {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue