diff --git a/assets/tailwind.css b/assets/tailwind.css index 8beadf2..4126de1 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -754,6 +754,14 @@ html { --tw-contain-style: ; } +@media (hover:hover) { + .table tr.hover:hover, + .table tr.hover:nth-child(even):hover { + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + } +} + .btn { display: inline-flex; height: 3rem; @@ -818,6 +826,57 @@ html { content: var(--tw-content); } +.card { + position: relative; + display: flex; + flex-direction: column; + border-radius: var(--rounded-box, 1rem); +} + +.card:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.card figure { + display: flex; + align-items: center; + justify-content: center; +} + +.card.image-full { + display: grid; +} + +.card.image-full:before { + position: relative; + content: ""; + z-index: 10; + border-radius: var(--rounded-box, 1rem); + --tw-bg-opacity: 1; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + opacity: 0.75; +} + +.card.image-full:before, + .card.image-full > * { + grid-column-start: 1; + grid-row-start: 1; +} + +.card.image-full > figure img { + height: 100%; + -o-object-fit: cover; + object-fit: cover; +} + +.card.image-full > .card-body { + position: relative; + z-index: 20; + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); +} + .dropdown { position: relative; display: inline-block; @@ -1137,6 +1196,39 @@ html { min-width: 4rem; } +.table { + position: relative; + width: 100%; + border-radius: var(--rounded-box, 1rem); + text-align: left; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.table :where(.table-pin-rows thead tr) { + position: sticky; + top: 0px; + z-index: 1; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + +.table :where(.table-pin-rows tfoot tr) { + position: sticky; + bottom: 0px; + z-index: 1; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + +.table :where(.table-pin-cols tr th) { + position: sticky; + left: 0px; + right: 0px; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + .btm-nav > *.disabled, .btm-nav > *[disabled] { pointer-events: none; @@ -1271,6 +1363,53 @@ html { } } +.card :where(figure:first-child) { + overflow: hidden; + border-start-start-radius: inherit; + border-start-end-radius: inherit; + border-end-start-radius: unset; + border-end-end-radius: unset; +} + +.card :where(figure:last-child) { + overflow: hidden; + border-start-start-radius: unset; + border-start-end-radius: unset; + border-end-start-radius: inherit; + border-end-end-radius: inherit; +} + +.card:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +.card.bordered { + border-width: 1px; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); +} + +.card.compact .card-body { + padding: 1rem; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.card-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.25rem; + line-height: 1.75rem; + font-weight: 600; +} + +.card.image-full :where(figure) { + overflow: hidden; + border-radius: inherit; +} + @keyframes checkmark { 0% { background-position-y: 5px; @@ -1643,6 +1782,45 @@ html { color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); } +.table:where([dir="rtl"], [dir="rtl"] *) { + text-align: right; +} + +.table :where(th, td) { + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.75rem; + padding-bottom: 0.75rem; + vertical-align: middle; +} + +.table tr.active, + .table tr.active:nth-child(even), + .table-zebra tbody tr:nth-child(even) { + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); +} + +.table :where(thead tr, tbody tr:not(:last-child), tbody tr:first-child:last-child) { + border-bottom-width: 1px; + --tw-border-opacity: 1; + border-bottom-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); +} + +.table :where(thead, tfoot) { + white-space: nowrap; + font-size: 0.75rem; + line-height: 1rem; + font-weight: 700; + color: var(--fallback-bc,oklch(var(--bc)/0.6)); +} + +.table :where(tfoot) { + border-top-width: 1px; + --tw-border-opacity: 1; + border-top-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); +} + @keyframes toast-pop { 0% { transform: scale(0.9); @@ -1737,6 +1915,14 @@ html { grid-template-rows: repeat(1, minmax(0, 1fr)); } +.card-compact .card-title { + margin-bottom: 0.25rem; +} + +.card-normal .card-title { + margin-bottom: 0.75rem; +} + .join.join-vertical > :where(*:not(:first-child)) { margin-left: 0px; margin-right: 0px; @@ -1809,8 +1995,12 @@ html { margin-right: auto; } -.mt-16 { - margin-top: 4rem; +.mb-1 { + margin-bottom: 0.25rem; +} + +.mb-5 { + margin-bottom: 1.25rem; } .mt-5 { @@ -1821,6 +2011,10 @@ html { display: flex; } +.table { + display: table; +} + .contents { display: contents; } @@ -1830,6 +2024,10 @@ html { height: 2rem; } +.h-\[30rem\] { + height: 30rem; +} + .h-full { height: 100%; } @@ -1847,6 +2045,10 @@ html { min-height: 4rem; } +.w-96 { + width: 24rem; +} + .w-full { width: 100%; } @@ -1867,6 +2069,10 @@ html { flex-direction: column; } +.flex-wrap { + flex-wrap: wrap; +} + .flex-nowrap { flex-wrap: nowrap; } @@ -1891,6 +2097,14 @@ html { gap: 0.5rem; } +.gap-5 { + gap: 1.25rem; +} + +.overflow-auto { + overflow: auto; +} + .overflow-y-auto { overflow-y: auto; } @@ -1900,6 +2114,10 @@ html { background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); } +.p-5 { + padding: 1.25rem; +} + .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -1920,8 +2138,8 @@ html { padding-bottom: 2.5rem; } -.pb-36 { - padding-bottom: 9rem; +.pb-10 { + padding-bottom: 2.5rem; } .text-xl { diff --git a/src/components/admin/members.rs b/src/components/admin/members.rs index 92618a8..8680c72 100644 --- a/src/components/admin/members.rs +++ b/src/components/admin/members.rs @@ -1,3 +1,4 @@ +use crate::util::model::member::{Member, MembersMigration}; use dioxus::prelude::{dioxus_elements::FileEngine, *}; use std::sync::Arc; @@ -17,12 +18,13 @@ enum Steps { #[component] pub fn Migration() -> Element { let step = use_signal(|| Steps::Upload); + let members_migration = use_signal(|| MembersMigration::new()); rsx! { div { - class: "flex flex-col items-center justify-center h-full py-10", + class: "flex flex-col items-center justify-center py-10", ul { - class: "steps pb-36", + class: "steps pb-10", li { class: "step step-primary", "Uploaden" }, li { class: "step", @@ -32,16 +34,16 @@ pub fn Migration() -> Element { li { class: "step", "Klaar" }, }, match *step.read() { - Steps::Upload => rsx! { Upload { step: step } }, - Steps::Verify => rsx! { Verify {} }, - Steps::Done => rsx! { Verify {} }, + Steps::Upload => rsx! { Upload { step: step, members_migration: members_migration } }, + Steps::Verify => rsx! { Verify { members_migration: members_migration} }, + Steps::Done => rsx! { Verify { members_migration: members_migration } }, } } } } #[component] -fn Upload(step: Signal) -> Element { +fn Upload(step: Signal, members_migration: Signal) -> Element { let mut file_uploaded = use_signal(|| None); let mut loading = use_signal(|| false); @@ -69,23 +71,25 @@ fn Upload(step: Signal) -> Element { Some(file) => { loading.set(true); - if let Ok(_response) = upload_members_list(file.contents.clone()).await { - tracing::info!("Done"); + if let Ok(response) = upload_members_list(file.contents.clone()).await { + members_migration.set(response); step.set(Steps::Verify); + loading.set(false); } loading.set(false); } None => tracing::info!("File doesn't exists"), } }; + rsx! { form { class: "flex flex-col items-center w-full h-full max-w-md mx-auto px-2", onsubmit: sumbit, - h2 { class: "text-xl", "Selecteer het ledenbestand" }, + h2 { class: "text-xl mb-5", "Selecteer de ledenlijst" }, input { r#type: "file", - class: "file-input file-input-bordered w-full mt-16", + class: "file-input file-input-bordered w-full", accept: ".csv", multiple: false, autocomplete: "off", @@ -99,32 +103,81 @@ fn Upload(step: Signal) -> Element { } "Uploaden" } - "{file_uploaded.read().is_none()}" } } } #[component] -fn Verify() -> Element { +fn Verify(members_migration: Signal) -> Element { + let mut loading = use_signal(|| false); + rsx! { div { - class: "flex flex-col items-center w-full h-full max-w-md mx-auto px-2", - h2 { class: "text-xl", "Controleer de actie" }, + class: "flex flex-col items-center justify-center w-full mx-auto px-2", + h2 { class: "text-xl mb-5", "Controleer de verandering" }, + div { + class: "flex flex-wrap gap-5", + div { + class: "card bg-base-200 p-5", + h2 { class: "card-title mb-1", "Toevoegen" } + MembersTable { members: members_migration.read().inserted.clone() } + } + div { + class: "card bg-base-200 p-5", + h2 { class: "card-title mb-1", "Verwijderen" } + MembersTable { members: members_migration.read().removed.clone() } + } + div { + class: "card bg-base-200 p-5", + h2 { class: "card-title mb-1", "Updaten" } + MembersTable { members: members_migration.read().updated.clone() } + } + } + button { + class: "btn btn-primary btn-wide mt-5", + disabled: loading(), + if loading() { + span { class: "loading loading-spinner" } + } + "Toepassen" + } + } + } +} + +#[component] +fn MembersTable(members: Vec) -> Element { + rsx! { + div { + class: "h-[30rem] w-96 overflow-auto font-normal", + table { + class: "table table-pin-rows", + thead { + tr { + th { "Relatiecode" } + th { "Naam" } + } + } + tbody { + for member in members { + tr { + th { "{member.id}" } + th { "{member.name.full}" } + } + } + } + } + } } } #[server] -async fn upload_members_list(input: String) -> Result { - use crate::util::model::member::Member; - - tracing::info!("Getting members..."); - - let result = Member::migrate_proposal(input).await; - - tracing::info!("{:?}", result); - - Ok("Whoo".to_string()) +async fn upload_members_list(input: String) -> Result { + match Member::migrate_proposal(input).await { + Ok(r) => Ok(r), + Err(err) => Err(ServerFnError::new(err.to_string())), + } } #[server] diff --git a/src/util/model.rs b/src/util/model.rs index 73dc5cd..0dd2a03 100644 --- a/src/util/model.rs +++ b/src/util/model.rs @@ -1,2 +1 @@ -#[cfg(feature = "server")] pub mod member; diff --git a/src/util/model/member.rs b/src/util/model/member.rs index 30bf640..759aeb9 100644 --- a/src/util/model/member.rs +++ b/src/util/model/member.rs @@ -1,24 +1,32 @@ #[cfg(feature = "server")] use crate::util::surrealdb::DB; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; +#[cfg(feature = "server")] mod migration; -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] pub struct Member { - id: String, - name: Name, - hours: BTreeSet, - groups: BTreeSet, - diploma: Option, - registration_token: Option, + pub id: String, + pub name: Name, + pub hours: BTreeSet, + pub groups: BTreeSet, + pub diploma: Option, + pub registration_token: Option, } -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] pub struct Name { - first: String, - full: String, + pub first: String, + pub full: String, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +pub struct MembersMigration { + pub inserted: Vec, + pub updated: Vec, + pub removed: Vec, } #[cfg(feature = "server")] @@ -31,3 +39,13 @@ impl Member { Ok(members) } } + +impl MembersMigration { + pub fn new() -> MembersMigration { + MembersMigration { + inserted: vec![], + updated: vec![], + removed: vec![], + } + } +} diff --git a/src/util/model/member/migration.rs b/src/util/model/member/migration.rs index 3743e60..800246b 100644 --- a/src/util/model/member/migration.rs +++ b/src/util/model/member/migration.rs @@ -1,9 +1,7 @@ -use crate::util::surrealdb::DB; - -use super::Member; +use super::{Member, MembersMigration}; use once_cell::sync::Lazy; -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap}; // Create a store for saving information when migrating to a new members list static MEMBERS_STORE: Lazy = Lazy::new(|| MembersStore::new()); @@ -129,11 +127,10 @@ fn rows_to_members(rows: Vec) -> Vec { } // Compare the new members list with the current -// (inserted, updated, removed) fn combine_members_lists( new_members_list: Vec, current_members_list: Vec, -) -> (Vec, Vec, Vec) { +) -> MembersMigration { tracing::info!( "Current: {}, New: {}", current_members_list.len(), @@ -183,13 +180,17 @@ fn combine_members_lists( } } - (inserted_members, updated_members, removed_members) + MembersMigration { + inserted: inserted_members, + updated: updated_members, + removed: removed_members, + } } impl Member { pub async fn migrate_proposal( csv: String, - ) -> Result<(Vec, Vec, Vec), Box> { + ) -> Result> { let rows = csv_to_rows(csv)?; let new_members = rows_to_members(rows);