diff --git a/assets/tailwind.css b/assets/tailwind.css index cec965f..d4e1986 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -943,6 +943,70 @@ html { color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); } +.collapse:not(td):not(tr):not(colgroup) { + visibility: visible; +} + +.collapse { + position: relative; + display: grid; + overflow: hidden; + grid-template-rows: auto 0fr; + transition: grid-template-rows 0.2s; + width: 100%; + border-radius: var(--rounded-box, 1rem); +} + +.collapse-title, +.collapse > input[type="checkbox"], +.collapse > input[type="radio"], +.collapse-content { + grid-column-start: 1; + grid-row-start: 1; +} + +.collapse > input[type="checkbox"], +.collapse > input[type="radio"] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + opacity: 0; +} + +.collapse-content { + visibility: hidden; + grid-column-start: 1; + grid-row-start: 2; + min-height: 0px; + transition: visibility 0.2s; + transition: padding 0.2s ease-out, + background-color 0.2s ease-out; + padding-left: 1rem; + padding-right: 1rem; + cursor: unset; +} + +.collapse[open], +.collapse-open, +.collapse:focus:not(.collapse-close) { + grid-template-rows: auto 1fr; +} + +.collapse:not(.collapse-close):has(> input[type="checkbox"]:checked), +.collapse:not(.collapse-close):has(> input[type="radio"]:checked) { + grid-template-rows: auto 1fr; +} + +.collapse[open] > .collapse-content, +.collapse-open > .collapse-content, +.collapse:focus:not(.collapse-close) > .collapse-content, +.collapse:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-content, +.collapse:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-content { + visibility: visible; + min-height: -moz-fit-content; + min-height: fit-content; +} + .dropdown { position: relative; display: inline-block; @@ -1039,6 +1103,15 @@ html { } } + .btn-outline:hover { + --tw-border-opacity: 1; + border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity))); + } + .btn-outline.btn-primary:hover { --tw-text-opacity: 1; color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); @@ -1051,6 +1124,78 @@ html { } } + .btn-outline.btn-secondary:hover { + --tw-text-opacity: 1; + color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-secondary:hover { + background-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); + } + } + + .btn-outline.btn-accent:hover { + --tw-text-opacity: 1; + color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-accent:hover { + background-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); + } + } + + .btn-outline.btn-success:hover { + --tw-text-opacity: 1; + color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-success:hover { + background-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); + } + } + + .btn-outline.btn-info:hover { + --tw-text-opacity: 1; + color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-info:hover { + background-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); + } + } + + .btn-outline.btn-warning:hover { + --tw-text-opacity: 1; + color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-warning:hover { + background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + } + } + + .btn-outline.btn-error:hover { + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-error:hover { + background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + } + } + .btn-disabled:hover, .btn[disabled]:hover, .btn:disabled:hover { @@ -1395,6 +1540,30 @@ input.tab:checked + .tab-content, background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); } +.toggle { + flex-shrink: 0; + --tglbg: var(--fallback-b1,oklch(var(--b1)/1)); + --handleoffset: 1.5rem; + --handleoffsetcalculator: calc(var(--handleoffset) * -1); + --togglehandleborder: 0 0; + height: 1.5rem; + width: 3rem; + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border-radius: var(--rounded-badge, 1.9rem); + border-width: 1px; + border-color: currentColor; + background-color: currentColor; + color: var(--fallback-bc,oklch(var(--bc)/0.5)); + transition: background, + box-shadow var(--animation-input, 0.2s) ease-out; + box-shadow: var(--handleoffsetcalculator) 0 0 2px var(--tglbg) inset, + 0 0 0 2px var(--tglbg) inset, + var(--togglehandleborder); +} + .badge-neutral { --tw-border-opacity: 1; border-color: var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity))); @@ -1449,6 +1618,10 @@ input.tab:checked + .tab-content, .btn-neutral { --btn-color: var(--fallback-n); } + + .btn-error { + --btn-color: var(--fallback-er); + } } @supports (color: color-mix(in oklab, black, black)) { @@ -1456,6 +1629,36 @@ input.tab:checked + .tab-content, background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); } + + .btn-outline.btn-secondary.btn-active { + background-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); + } + + .btn-outline.btn-accent.btn-active { + background-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); + } + + .btn-outline.btn-success.btn-active { + background-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); + } + + .btn-outline.btn-info.btn-active { + background-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); + } + + .btn-outline.btn-warning.btn-active { + background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + } + + .btn-outline.btn-error.btn-active { + background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + } } .btn:focus-visible { @@ -1478,6 +1681,10 @@ input.tab:checked + .tab-content, .btn-neutral { --btn-color: var(--n); } + + .btn-error { + --btn-color: var(--er); + } } .btn-neutral { @@ -1486,6 +1693,12 @@ input.tab:checked + .tab-content, outline-color: var(--fallback-n,oklch(var(--n)/1)); } +.btn-error { + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); + outline-color: var(--fallback-er,oklch(var(--er)/1)); +} + .btn.glass { --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; @@ -1514,6 +1727,25 @@ input.tab:checked + .tab-content, background-color: var(--fallback-bc,oklch(var(--bc)/0.2)); } +.btn-outline { + border-color: currentColor; + background-color: transparent; + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.btn-outline.btn-active { + --tw-border-opacity: 1; + border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity))); +} + .btn-outline.btn-primary { --tw-text-opacity: 1; color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); @@ -1524,6 +1756,66 @@ input.tab:checked + .tab-content, color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); } +.btn-outline.btn-secondary { + --tw-text-opacity: 1; + color: var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity))); +} + +.btn-outline.btn-secondary.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity))); +} + +.btn-outline.btn-accent { + --tw-text-opacity: 1; + color: var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity))); +} + +.btn-outline.btn-accent.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity))); +} + +.btn-outline.btn-success { + --tw-text-opacity: 1; + color: var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity))); +} + +.btn-outline.btn-success.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity))); +} + +.btn-outline.btn-info { + --tw-text-opacity: 1; + color: var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity))); +} + +.btn-outline.btn-info.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); +} + +.btn-outline.btn-warning { + --tw-text-opacity: 1; + color: var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity))); +} + +.btn-outline.btn-warning.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); +} + +.btn-outline.btn-error { + --tw-text-opacity: 1; + color: var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity))); +} + +.btn-outline.btn-error.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); +} + .btn.btn-disabled, .btn[disabled], .btn:disabled { @@ -1623,6 +1915,130 @@ input.tab:checked + .tab-content, } } +details.collapse { + width: 100%; +} + +details.collapse summary { + position: relative; + display: block; + outline: 2px solid transparent; + outline-offset: 2px; +} + +details.collapse summary::-webkit-details-marker { + display: none; +} + +.collapse:focus-visible { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/1)); +} + +.collapse:has(.collapse-title:focus-visible), +.collapse:has(> input[type="checkbox"]:focus-visible), +.collapse:has(> input[type="radio"]:focus-visible) { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/1)); +} + +.collapse-arrow > .collapse-title:after { + position: absolute; + display: block; + height: 0.5rem; + width: 0.5rem; + --tw-translate-y: -100%; + --tw-rotate: 45deg; + 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)); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 150ms; + transition-duration: 0.2s; + top: 1.9rem; + inset-inline-end: 1.4rem; + content: ""; + transform-origin: 75% 75%; + box-shadow: 2px 2px; + pointer-events: none; +} + +.collapse-plus > .collapse-title:after { + position: absolute; + display: block; + height: 0.5rem; + width: 0.5rem; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 300ms; + top: 0.9rem; + inset-inline-end: 1.4rem; + content: "+"; + pointer-events: none; +} + +.collapse:not(.collapse-open):not(.collapse-close) > input[type="checkbox"], +.collapse:not(.collapse-open):not(.collapse-close) > input[type="radio"]:not(:checked), +.collapse:not(.collapse-open):not(.collapse-close) > .collapse-title { + cursor: pointer; +} + +.collapse:focus:not(.collapse-open):not(.collapse-close):not(.collapse[open]) > .collapse-title { + cursor: unset; +} + +.collapse-title { + position: relative; +} + +:where(.collapse > input[type="checkbox"]), +:where(.collapse > input[type="radio"]) { + z-index: 1; +} + +.collapse-title, +:where(.collapse > input[type="checkbox"]), +:where(.collapse > input[type="radio"]) { + width: 100%; + padding: 1rem; + padding-inline-end: 3rem; + min-height: 3.75rem; + transition: background-color 0.2s ease-out; +} + +.collapse[open] > :where(.collapse-content), +.collapse-open > :where(.collapse-content), +.collapse:focus:not(.collapse-close) > :where(.collapse-content), +.collapse:not(.collapse-close) > :where(input[type="checkbox"]:checked ~ .collapse-content), +.collapse:not(.collapse-close) > :where(input[type="radio"]:checked ~ .collapse-content) { + padding-bottom: 1rem; + transition: padding 0.2s ease-out, + background-color 0.2s ease-out; +} + +.collapse[open].collapse-arrow > .collapse-title:after, +.collapse-open.collapse-arrow > .collapse-title:after, +.collapse-arrow:focus:not(.collapse-close) > .collapse-title:after, +.collapse-arrow:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-title:after, +.collapse-arrow:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after { + --tw-translate-y: -50%; + --tw-rotate: 225deg; + 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)); +} + +.collapse[open].collapse-plus > .collapse-title:after, +.collapse-open.collapse-plus > .collapse-title:after, +.collapse-plus:focus:not(.collapse-close) > .collapse-title:after, +.collapse-plus:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-title:after, +.collapse-plus:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after { + content: "−"; +} + .dropdown.dropdown-open .dropdown-content, .dropdown:focus .dropdown-content, .dropdown:focus-within .dropdown-content { @@ -2188,6 +2604,57 @@ input.tab:checked + .tab-content, } } +[dir="rtl"] .toggle { + --handleoffsetcalculator: calc(var(--handleoffset) * 1); +} + +.toggle:focus-visible { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.toggle:hover { + background-color: currentColor; +} + +.toggle:checked, + .toggle[aria-checked="true"] { + background-image: none; + --handleoffsetcalculator: var(--handleoffset); + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); +} + +[dir="rtl"] .toggle:checked, [dir="rtl"] .toggle[aria-checked="true"] { + --handleoffsetcalculator: calc(var(--handleoffset) * -1); +} + +.toggle:indeterminate { + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + box-shadow: calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset, + calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset, + 0 0 0 2px var(--tglbg) inset; +} + +[dir="rtl"] .toggle:indeterminate { + box-shadow: calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset, + calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset, + 0 0 0 2px var(--tglbg) inset; +} + +.toggle:disabled { + cursor: not-allowed; + --tw-border-opacity: 1; + border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); + background-color: transparent; + opacity: 0.3; + --togglehandleborder: 0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset, + var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset; +} + .badge-md { height: 1.25rem; font-size: 0.875rem; @@ -2391,6 +2858,10 @@ input.tab:checked + .tab-content, 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)); } +.collapse { + visibility: collapse; +} + .static { position: static; } @@ -2412,6 +2883,10 @@ input.tab:checked + .tab-content, margin-bottom: 1.5rem; } +.ml-auto { + margin-left: auto; +} + .mt-10 { margin-top: 2.5rem; } @@ -2491,6 +2966,10 @@ input.tab:checked + .tab-content, width: 100%; } +.max-w-2xl { + max-width: 42rem; +} + .max-w-4xl { max-width: 56rem; } @@ -2511,6 +2990,12 @@ input.tab:checked + .tab-content, flex: none; } +.select-none { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + .flex-col { flex-direction: column; } @@ -2543,10 +3028,20 @@ input.tab:checked + .tab-content, gap: 0.5rem; } +.gap-3 { + gap: 0.75rem; +} + .gap-5 { gap: 1.25rem; } +.space-y-3 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); +} + .overflow-auto { overflow: auto; } @@ -2597,10 +3092,19 @@ input.tab:checked + .tab-content, padding-bottom: 2.5rem; } +.py-5 { + padding-top: 1.25rem; + padding-bottom: 1.25rem; +} + .pb-10 { padding-bottom: 2.5rem; } +.pl-2 { + padding-left: 0.5rem; +} + .pt-2 { padding-top: 0.5rem; } @@ -2628,6 +3132,10 @@ input.tab:checked + .tab-content, font-weight: 700; } +.font-medium { + font-weight: 500; +} + .font-normal { font-weight: 400; } diff --git a/src/components/layout.rs b/src/components/layout.rs index 2de2c7d..dc98666 100644 --- a/src/components/layout.rs +++ b/src/components/layout.rs @@ -13,18 +13,27 @@ use dioxus::prelude::*; #[component] pub fn Global() -> Element { let user = use_resource(get_user_from_cookie); + let mut user_state: Signal> = use_signal(|| None); - use_context_provider(|| user); + use_context_provider(|| user_state); + + use_effect(move || { + if let Some(Ok(u)) = &*user.read_unchecked() { + user_state.set(Some(u.clone())); + }; + }); rsx! { match &*user.read_unchecked() { Some(Ok(_)) => rsx! { - crate::components::layout::topbar::Topbar {} - main { - class: "h-full overflow-y-auto", - Outlet:: {} + if user_state().is_some() { + crate::components::layout::topbar::Topbar {} + main { + class: "h-full overflow-y-auto flex justify-center px-2 py-5", + Outlet:: {} + } + crate::components::layout::navbar::Navbar {} } - crate::components::layout::navbar::Navbar {} }, Some(Err(_)) => rsx! { Auth { } @@ -38,8 +47,7 @@ pub fn Global() -> Element { #[server] async fn get_user_from_cookie() -> Result { - let token = Session::get_token_from_cookie().await?; - let user = Session::fetch_user_from_token(token).await?; + let user = Session::fetch_current_user().await?; Ok(user) } diff --git a/src/components/settings.rs b/src/components/settings.rs index 21bc29f..e06a0c4 100644 --- a/src/components/settings.rs +++ b/src/components/settings.rs @@ -1,10 +1,158 @@ use dioxus::prelude::*; +use crate::util::model::{session::Session, user::User}; + #[component] pub fn Settings() -> Element { rsx! { div { - h1 { class: "text-xl font-bold text-primary", "Settings" } + class: "w-full max-w-2xl space-y-3", + Account {}, + Password {}, } } } + +fn Account() -> Element { + let user_state = use_context::>>(); + let user = user_state().unwrap(); + + let mut is_open = use_signal(|| false); + let mut input_email = use_signal(|| user.email); + + let submit = move |_| async move { + if let Ok(_) = change_email(input_email()).await { + tracing::info!("User email changed"); + }; + }; + + rsx! { + div { + class: "collapse collapse-arrow bg-base-200", + class: if is_open() { "collapse-open" }, + div { + class: "collapse-title text-lg font-medium hover:cursor-pointer select-none", + onclick: move |_| is_open.toggle(), + "Account", + }, + div { + class: "collapse-content", + div { + class: "pl-2 flex flex-col gap-3", + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Account ID" } + } + b { "{user.id}" }, + } + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Email" } + } + input { + r#type: "text", + class: "input input-bordered w-full", + oninput: move |event| input_email.set(event.value()), + value: "{input_email}", + } + } + div { + class: "w-full flex mt-5", + button { + class: "btn btn-outline btn-error", + onclick: move |_| async move { + if let Ok(_) = logout().await { + let window = web_sys::window().expect("Could not find window"); + window.location().reload().expect("Could not reload window"); + } + }, + "Uitloggen", + } + button { + class: "ml-auto btn btn-primary", + onclick: submit, + "Opslaan", + } + } + } + }, + }, + } +} + +#[server] +async fn logout() -> Result<(), ServerFnError> { + let session_token = Session::get_token_from_cookie().await?; + Session::delete_session(session_token).await?; + Session::delete_cookie().await?; + + Ok(()) +} + +#[server] +async fn change_email(new_email: String) -> Result<(), ServerFnError> { + let user = Session::fetch_current_user().await?; + + user.change_email(new_email).await?; + + Ok(()) +} + +fn Password() -> Element { + let mut is_open = use_signal(|| false); + + rsx! { + div { + class: "collapse collapse-arrow bg-base-200", + class: if is_open() { "collapse-open" }, + div { + class: "collapse-title text-lg font-medium hover:cursor-pointer select-none", + onclick: move |_| is_open.toggle(), + "Wachtwoord", + }, + div { + class: "collapse-content", + div { + class: "pl-2 space-y-3", + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Huidig wachtwoord" } + } + input { + r#type: "text", + class: "input input-bordered w-full", + } + } + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Nieuw wachtwoord" } + } + input { + r#type: "text", + class: "input input-bordered w-full", + } + } + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Herhaal nieuw wachtwoord" } + } + input { + r#type: "text", + class: "input input-bordered w-full", + } + } + } + }, + }, + } +} diff --git a/src/util/model/member.rs b/src/util/model/member.rs index d24a1dd..95fede8 100644 --- a/src/util/model/member.rs +++ b/src/util/model/member.rs @@ -35,8 +35,6 @@ impl Member { let members: Vec = res.take(0)?; - tracing::info!("{:?}", members); - Ok(members) } diff --git a/src/util/model/session.rs b/src/util/model/session.rs index 489246b..3b92313 100644 --- a/src/util/model/session.rs +++ b/src/util/model/session.rs @@ -81,4 +81,44 @@ impl Session { None => Err(crate::Error::NoSessionCookie), } } + + pub async fn delete_cookie() -> Result<(), crate::Error> { + use axum::http::{header, HeaderValue}; + use axum_extra::extract::cookie::{Cookie, SameSite}; + use dioxus::prelude::server_context; + use time::Duration; + + let mut cookie = Cookie::build(("session_token", "")) + .max_age(Duration::seconds(1)) + .build(); + + cookie.set_same_site(SameSite::Strict); + + server_context() + .response_parts_mut() + .unwrap() + .headers + .insert( + header::SET_COOKIE, + HeaderValue::from_str(&cookie.to_string())?, + ); + + Ok(()) + } + + pub async fn delete_session(user_id: String) -> Result<(), crate::Error> { + DB.query("DELETE ONLY session WHERE user = type::thing('user', $user_id)") + .bind(("user_id", user_id)) + .await?; + + Ok(()) + } + + // Fetches the current user from cookie headers + pub async fn fetch_current_user() -> Result { + let session_token = Self::get_token_from_cookie().await?; + let user = Self::fetch_user_from_token(session_token).await?; + + Ok(user) + } } diff --git a/src/util/model/user.rs b/src/util/model/user.rs index 43d16b6..4e0f379 100644 --- a/src/util/model/user.rs +++ b/src/util/model/user.rs @@ -8,7 +8,7 @@ use surrealdb::sql::statements::{BeginStatement, CommitStatement}; #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] pub struct User { pub id: String, - email: String, + pub email: String, password: Option, } @@ -60,4 +60,19 @@ impl User { None => Err(crate::Error::NoDocument), } } + + pub async fn change_email(&self, new_email: String) -> Result { + let mut res = DB + .query("UPDATE type::thing('user', $user_id) SET email = $email RETURN VALUE email") + .bind(("user_id", self.id.to_string())) + .bind(("email", new_email)) + .await?; + + let email: Option = res.take(0)?; + + match email { + Some(e) => Ok(e), + None => Err(crate::Error::NoDocument), + } + } }