From 4cfe28fb214f954facef4a40cc4db26f2179bff0 Mon Sep 17 00:00:00 2001 From: xeovalyte Date: Sat, 19 Oct 2024 16:38:03 +0200 Subject: [PATCH] Added member edit modal --- assets/tailwind.css | 401 ++++++++++++++++++++++++++++++++ src/components/admin/members.rs | 127 ++++++++-- 2 files changed, 515 insertions(+), 13 deletions(-) diff --git a/assets/tailwind.css b/assets/tailwind.css index aa7cfd4..c91ef18 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -787,6 +787,15 @@ html { color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); } + .menu li > *:not(ul, .menu-title, details, .btn):active, +.menu li > *:not(ul, .menu-title, details, .btn).active, +.menu li > details > summary:active { + --tw-bg-opacity: 1; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); + } + .tab:hover { --tw-text-opacity: 1; } @@ -1057,6 +1066,46 @@ html { transition-duration: 200ms; } +.dropdown-end .dropdown-content { + inset-inline-end: 0px; +} + +.dropdown-left .dropdown-content { + bottom: auto; + inset-inline-end: 100%; + top: 0px; + transform-origin: right; +} + +.dropdown-right .dropdown-content { + bottom: auto; + inset-inline-start: 100%; + top: 0px; + transform-origin: left; +} + +.dropdown-bottom .dropdown-content { + bottom: auto; + top: 100%; + transform-origin: top; +} + +.dropdown-top .dropdown-content { + bottom: 100%; + top: auto; + transform-origin: bottom; +} + +.dropdown-end.dropdown-right .dropdown-content { + bottom: 0px; + top: auto; +} + +.dropdown-end.dropdown-left .dropdown-content { + bottom: 0px; + top: auto; +} + .dropdown.dropdown-open .dropdown-content, .dropdown:not(.dropdown-hover):focus .dropdown-content, .dropdown:focus-within .dropdown-content { @@ -1240,6 +1289,18 @@ html { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } + :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { + cursor: pointer; + outline: 2px solid transparent; + outline-offset: 2px; + } + + @supports (color: oklch(0% 0 0)) { + :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { + background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); + } + } + .tab[disabled], .tab[disabled]:hover { cursor: not-allowed; @@ -1412,6 +1473,34 @@ html { } } +.menu { + display: flex; + flex-direction: column; + flex-wrap: wrap; + font-size: 0.875rem; + line-height: 1.25rem; + padding: 0.5rem; +} + +.menu :where(li ul) { + position: relative; + white-space: nowrap; + margin-inline-start: 1rem; + padding-inline-start: 0.5rem; +} + +.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), .menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { + display: grid; + grid-auto-flow: column; + align-content: flex-start; + align-items: center; + gap: 0.5rem; + grid-auto-columns: minmax(auto, max-content) auto max-content; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + .menu li.disabled { cursor: not-allowed; -webkit-user-select: none; @@ -1420,10 +1509,97 @@ html { color: var(--fallback-bc,oklch(var(--bc)/0.3)); } +.menu :where(li > .menu-dropdown:not(.menu-dropdown-show)) { + display: none; +} + +:where(.menu li) { + position: relative; + display: flex; + flex-shrink: 0; + flex-direction: column; + flex-wrap: wrap; + align-items: stretch; +} + :where(.menu li) .badge { justify-self: end; } +.modal { + pointer-events: none; + position: fixed; + inset: 0px; + margin: 0px; + display: grid; + height: 100%; + max-height: none; + width: 100%; + max-width: none; + justify-items: center; + padding: 0px; + opacity: 0; + overscroll-behavior: contain; + z-index: 999; + background-color: transparent; + color: inherit; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-property: transform, opacity, visibility; + overflow-y: hidden; +} + +:where(.modal) { + align-items: center; +} + +.modal-box { + max-height: calc(100vh - 5em); + grid-column-start: 1; + grid-row-start: 1; + width: 91.666667%; + max-width: 32rem; + --tw-scale-x: .9; + --tw-scale-y: .9; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-bottom-right-radius: var(--rounded-box, 1rem); + border-bottom-left-radius: var(--rounded-box, 1rem); + border-top-left-radius: var(--rounded-box, 1rem); + border-top-right-radius: var(--rounded-box, 1rem); + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); + padding: 1.5rem; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 200ms; + box-shadow: rgba(0, 0, 0, 0.25) 0px 25px 50px -12px; + overflow-y: auto; + overscroll-behavior: contain; +} + +.modal-open, +.modal:target, +.modal-toggle:checked + .modal, +.modal[open] { + pointer-events: auto; + visibility: visible; + opacity: 1; +} + +.modal-action { + display: flex; + margin-top: 1.5rem; + justify-content: flex-end; +} + +:root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])) { + overflow: hidden; + scrollbar-gutter: stable; +} + .navbar { display: flex; align-items: center; @@ -2216,6 +2392,88 @@ details.collapse summary::-webkit-details-marker { mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E"); } +:where(.menu li:empty) { + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + opacity: 0.1; + margin: 0.5rem 1rem; + height: 1px; +} + +.menu :where(li ul):before { + position: absolute; + bottom: 0.75rem; + inset-inline-start: 0px; + top: 0.75rem; + width: 1px; + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + opacity: 0.1; + content: ""; +} + +.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), +.menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { + border-radius: var(--rounded-btn, 0.5rem); + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + text-align: start; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 200ms; + text-wrap: balance; +} + +:where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):is(summary):not(.active, .btn):focus-visible, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):is(summary):not(.active, .btn):focus-visible { + cursor: pointer; + background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + outline: 2px solid transparent; + outline-offset: 2px; +} + +.menu li > *:not(ul, .menu-title, details, .btn):active, +.menu li > *:not(ul, .menu-title, details, .btn).active, +.menu li > details > summary:active { + --tw-bg-opacity: 1; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); +} + +.menu :where(li > details > summary)::-webkit-details-marker { + display: none; +} + +.menu :where(li > details > summary):after, +.menu :where(li > .menu-dropdown-toggle):after { + justify-self: end; + display: block; + margin-top: -0.5rem; + height: 0.5rem; + width: 0.5rem; + transform: rotate(45deg); + transition-property: transform, margin-top; + transition-duration: 0.3s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + content: ""; + transform-origin: 75% 75%; + box-shadow: 2px 2px; + pointer-events: none; +} + +.menu :where(li > details[open] > summary):after, +.menu :where(li > .menu-dropdown-toggle.menu-dropdown-show):after { + transform: rotate(225deg); + margin-top: 0; +} + .mockup-browser .mockup-browser-toolbar .input { position: relative; margin-left: auto; @@ -2262,6 +2520,38 @@ details.collapse summary::-webkit-details-marker { opacity: 0.6; } +.modal:not(dialog:not(.modal-open)), + .modal::backdrop { + background-color: #0006; + animation: modal-pop 0.2s ease-out; +} + +.modal-backdrop { + z-index: -1; + grid-column-start: 1; + grid-row-start: 1; + display: grid; + align-self: stretch; + justify-self: stretch; + color: transparent; +} + +.modal-open .modal-box, +.modal-toggle:checked + .modal .modal-box, +.modal:target .modal-box, +.modal[open] .modal-box { + --tw-translate-y: 0px; + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.modal-action > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + @keyframes modal-pop { 0% { opacity: 0; @@ -2756,6 +3046,10 @@ details.collapse summary::-webkit-details-marker { border-start-end-radius: inherit; } +.modal-bottom { + place-items: end; +} + .steps-horizontal .step { display: grid; grid-template-columns: repeat(1, minmax(0, 1fr)); @@ -2840,6 +3134,45 @@ details.collapse summary::-webkit-details-marker { margin-inline-start: calc(var(--border-btn) * -1); } +.modal-top :where(.modal-box) { + width: 100%; + max-width: none; + --tw-translate-y: -2.5rem; + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-bottom-right-radius: var(--rounded-box, 1rem); + border-bottom-left-radius: var(--rounded-box, 1rem); + border-top-left-radius: 0px; + border-top-right-radius: 0px; +} + +.modal-middle :where(.modal-box) { + width: 91.666667%; + max-width: 32rem; + --tw-translate-y: 0px; + --tw-scale-x: .9; + --tw-scale-y: .9; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-top-left-radius: var(--rounded-box, 1rem); + border-top-right-radius: var(--rounded-box, 1rem); + border-bottom-right-radius: var(--rounded-box, 1rem); + border-bottom-left-radius: var(--rounded-box, 1rem); +} + +.modal-bottom :where(.modal-box) { + width: 100%; + max-width: none; + --tw-translate-y: 2.5rem; + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-top-left-radius: var(--rounded-box, 1rem); + border-top-right-radius: var(--rounded-box, 1rem); + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; +} + .steps-horizontal .step { grid-template-rows: 40px 1fr; grid-template-columns: auto; @@ -2891,6 +3224,14 @@ details.collapse summary::-webkit-details-marker { position: static; } +.z-50 { + z-index: 50; +} + +.z-\[1\] { + z-index: 1; +} + .mx-1 { margin-left: 0.25rem; margin-right: 0.25rem; @@ -2959,6 +3300,10 @@ details.collapse summary::-webkit-details-marker { display: table; } +.grid { + display: grid; +} + .contents { display: contents; } @@ -3117,6 +3462,10 @@ details.collapse summary::-webkit-details-marker { white-space: nowrap; } +.rounded-box { + border-radius: var(--rounded-box, 1rem); +} + .rounded-b-none { border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; @@ -3127,6 +3476,11 @@ details.collapse summary::-webkit-details-marker { border-top-right-radius: 0px; } +.bg-base-100 { + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + .bg-base-200 { --tw-bg-opacity: 1; background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); @@ -3137,6 +3491,14 @@ details.collapse summary::-webkit-details-marker { background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); } +.bg-black\/50 { + background-color: rgb(0 0 0 / 0.5); +} + +.p-2 { + padding: 0.5rem; +} + .p-5 { padding: 1.25rem; } @@ -3219,6 +3581,12 @@ details.collapse summary::-webkit-details-marker { color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); } +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + .shadow-xl { --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); @@ -3229,6 +3597,25 @@ details.collapse summary::-webkit-details-marker { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } +@media (min-width: 640px) { + .sm\:modal-middle { + place-items: center; + } + + .sm\:modal-middle :where(.modal-box) { + width: 91.666667%; + max-width: 32rem; + --tw-translate-y: 0px; + --tw-scale-x: .9; + --tw-scale-y: .9; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-top-left-radius: var(--rounded-box, 1rem); + border-top-right-radius: var(--rounded-box, 1rem); + border-bottom-right-radius: var(--rounded-box, 1rem); + border-bottom-left-radius: var(--rounded-box, 1rem); + } +} + .hover\:cursor-pointer:hover { cursor: pointer; } @@ -3236,3 +3623,17 @@ details.collapse summary::-webkit-details-marker { .hover\:line-through:hover { text-decoration-line: line-through; } + +@media (min-width: 640px) { + .sm\:grid-flow-col { + grid-auto-flow: column; + } + + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .sm\:grid-rows-3 { + grid-template-rows: repeat(3, minmax(0, 1fr)); + } +} diff --git a/src/components/admin/members.rs b/src/components/admin/members.rs index 4659600..c0ccb78 100644 --- a/src/components/admin/members.rs +++ b/src/components/admin/members.rs @@ -7,6 +7,8 @@ use crate::util::model::{member::Member, session::Session}; pub fn Members() -> Element { let members = use_resource(fetch_members); + let modal_member: Signal> = use_signal(|| None); + rsx! { div { class: "max-w-3xl w-full", @@ -26,7 +28,7 @@ pub fn Members() -> Element { } tbody { for member in res { - MemberRow { member: member.clone() } + MemberRow { member: member.clone(), modal_member: modal_member } } } } @@ -35,26 +37,27 @@ pub fn Members() -> Element { Some(Err(_)) => rsx! { div { "Error while loading members" } }, None => rsx! { div { "Loading..." } }, } + MemberModal { member: modal_member } } } } -#[derive(Props, Clone, PartialEq)] -struct MemberRowProps { - member: Member, -} +#[component] +fn MemberRow(member: Member, modal_member: Signal>) -> Element { + let member_parse = member.clone(); -fn MemberRow(props: MemberRowProps) -> Element { - let registration_token = props - .member + let registration_token = member_parse .registration_token .unwrap_or("None".to_string()); rsx! { tr { class: "hover hover:cursor-pointer", - th { "{props.member.id}" } - td { "{props.member.name.full}" } + onclick: move |_| { + modal_member.set(Some(member.to_owned())) + }, + th { "{member.id}" } + td { "{member.name.full}" } td { "{registration_token}" } } } @@ -72,10 +75,108 @@ async fn fetch_members() -> Result, ServerFnError> { Ok(members) } -fn MemberModal(props: MemberRowProps) -> Element { +#[component] +fn MemberModal(member: Signal>) -> Element { + let mut groups_focus = use_signal(|| false); + rsx! { - div { - "BOe" + dialog { + class: "modal modal-bottom sm:modal-middle z-50", + open: member().is_some(), + div { + class: "modal-box", + h3 { class: "text-lg font-bold", "Lid bewerken" }, + if let Some(m) = member() { + div { + div { + class: "grid gap-3 sm:grid-flow-col sm:grid-rows-3 sm:grid-cols-2", + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Naam" } + } + b { class: "select-all", "{m.name.full}" }, + } + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Relatiecode" } + } + b { class: "select-all", "{m.id}" }, + } + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Registratiecode" } + } + b { class: "select-all", {m.registration_token.unwrap_or("geen".to_string())} }, + } + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Uur" } + } + b { class: "select-all", {m.hours.join(", ")} }, + } + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Diploma" } + } + b { class: "select-all", {m.diploma.unwrap_or("geen".to_string())} }, + } + } + div { + class: "mt-5", + div { + class: "dropdown w-full", + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Groepen" } + } + input { + r#type: "text", + class: "input input-bordered", + onfocusin: move |_| { + groups_focus.set(true); + }, + onfocusout: move |_| { + groups_focus.set(false); + }, + } + } + ul { + class: "dropdown-content menu bg-base-100 rounded-box z-[1] p-2 shadow w-full", + li { "Kader" } + li { "Wedstrijden" } + li { "Bestuur" } + } + } + } + } + } else { + "Geen lid geselecteerd" + } + div { + class: "modal-action", + button { + class: "btn", + onclick: move |_| member.set(None), + "Annuleren" + } + } + } + label { + class: "modal-backdrop bg-black/50", + onclick: move |_| member.set(None) + } } } }