Compare commits

..

2 Commits

12 changed files with 945 additions and 584 deletions

1198
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ dioxus = { version = "0.5", features = ["fullstack", "router"] }
tokio = { version = "1.38", features = ["macros", "rt-multi-thread"], optional = true }
axum = { version = "0.7", optional = true }
once_cell = { version = "1.19", optional = true }
surrealdb = { version = "1.5", features = ["kv-speedb"], optional = true }
surrealdb = { version = "2.0", features = ["kv-rocksdb"], optional = true }
csv = { version = "1.3", optional = true }

View File

@ -1,5 +1,5 @@
/*
! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
*/
/*
@ -754,6 +754,33 @@ html {
--tw-contain-style: ;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
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;
height: 1.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
width: -moz-fit-content;
width: fit-content;
padding-left: 0.563rem;
padding-right: 0.563rem;
border-radius: var(--rounded-badge, 1.9rem);
border-width: 1px;
--tw-border-opacity: 1;
border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
--tw-bg-opacity: 1;
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
}
@media (hover:hover) {
.label a:hover {
--tw-text-opacity: 1;
@ -1227,6 +1254,10 @@ html {
color: var(--fallback-bc,oklch(var(--bc)/0.3));
}
:where(.menu li) .badge {
justify-self: end;
}
.navbar {
display: flex;
align-items: center;
@ -1364,6 +1395,20 @@ input.tab:checked + .tab-content,
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
}
.badge-neutral {
--tw-border-opacity: 1;
border-color: var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity)));
--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)));
}
.badge-outline.badge-neutral {
--tw-text-opacity: 1;
color: var(--fallback-n,oklch(var(--n)/var(--tw-text-opacity)));
}
.btm-nav > *.disabled,
.btm-nav > *[disabled] {
pointer-events: none;
@ -1400,6 +1445,10 @@ input.tab:checked + .tab-content,
.btn-primary {
--btn-color: var(--fallback-p);
}
.btn-neutral {
--btn-color: var(--fallback-n);
}
}
@supports (color: color-mix(in oklab, black, black)) {
@ -1425,6 +1474,16 @@ input.tab:checked + .tab-content,
.btn-primary {
--btn-color: var(--p);
}
.btn-neutral {
--btn-color: var(--n);
}
}
.btn-neutral {
--tw-text-opacity: 1;
color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
outline-color: var(--fallback-n,oklch(var(--n)/1));
}
.btn.glass {
@ -1691,6 +1750,10 @@ input.tab:checked + .tab-content,
margin-inline-start: calc(var(--border-btn) * -1);
}
.join-item:focus {
isolation: isolate;
}
.loading {
pointer-events: none;
display: inline-block;
@ -2125,6 +2188,14 @@ input.tab:checked + .tab-content,
}
}
.badge-md {
height: 1.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
padding-left: 0.563rem;
padding-right: 0.563rem;
}
.btn-wide {
width: 16rem;
}
@ -2345,6 +2416,10 @@ input.tab:checked + .tab-content,
margin-top: 2.5rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mt-20 {
margin-top: 5rem;
}
@ -2369,11 +2444,20 @@ input.tab:checked + .tab-content,
display: contents;
}
.size-6 {
width: 1.5rem;
height: 1.5rem;
}
.size-8 {
width: 2rem;
height: 2rem;
}
.h-8 {
height: 2rem;
}
.h-\[30rem\] {
height: 30rem;
}
@ -2475,6 +2559,10 @@ input.tab:checked + .tab-content,
overflow-y: auto;
}
.whitespace-nowrap {
white-space: nowrap;
}
.bg-base-200 {
--tw-bg-opacity: 1;
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
@ -2513,6 +2601,10 @@ input.tab:checked + .tab-content,
padding-bottom: 2.5rem;
}
.pb-3 {
padding-bottom: 0.75rem;
}
.text-center {
text-align: center;
}
@ -2558,3 +2650,7 @@ input.tab:checked + .tab-content,
.hover\:cursor-pointer:hover {
cursor: pointer;
}
.hover\:line-through:hover {
text-decoration-line: line-through;
}

View File

@ -3,11 +3,11 @@
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1725637114,
"lastModified": 1726826452,
"owner": "cachix",
"repo": "devenv",
"rev": "c31e347a96dbb7718a0279afa993752a7dfc6a39",
"treeHash": "e0dfcbbfb0974603336900406b364bd4d1308fa4",
"rev": "2bdf6461e88c7e93b94d72d8b11d5a61f167cbf5",
"treeHash": "e10f5d7f21ef64fb0a9c269c400af0cd8f43fb35",
"type": "github"
},
"original": {
@ -25,11 +25,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1725690497,
"lastModified": 1726986637,
"owner": "nix-community",
"repo": "fenix",
"rev": "4b8d964df93d1f918ee6c4f003b3548c432cc866",
"treeHash": "0b0da47066a273999c379058d109204c9fb5296a",
"rev": "f5a9b01cd81b46228b3737669e8275b1f2da7d35",
"treeHash": "97be3a5a79be99410ddfc81f61377daba4df0ec5",
"type": "github"
},
"original": {
@ -77,27 +77,27 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1725534445,
"lastModified": 1726838390,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9bb1e7571aadf31ddb4af77fc64b2d59580f9a39",
"treeHash": "42b4c844752e33185e2fac3d506d150637caae7d",
"rev": "944b2aea7f0a2d7c79f72468106bc5510cbf5101",
"treeHash": "5ff7724fe1272ea9b66c97c25e00bb1dc9782aaf",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1725407940,
"lastModified": 1726838390,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6f6c45b5134a8ee2e465164811e451dcb5ad86e3",
"treeHash": "f65d5344b23cfbe3695140b602bc463349b26638",
"rev": "944b2aea7f0a2d7c79f72468106bc5510cbf5101",
"treeHash": "5ff7724fe1272ea9b66c97c25e00bb1dc9782aaf",
"type": "github"
},
"original": {
@ -117,11 +117,11 @@
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1725513492,
"lastModified": 1726745158,
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "7570de7b9b504cfe92025dd1be797bf546f66528",
"treeHash": "4b46d77870afecd8f642541cb4f4927326343b59",
"rev": "4e743a6920eab45e8ba0fbe49dc459f1423a4b74",
"treeHash": "56fbe2a9610b3ad9163a74011131e7624f6b3b81",
"type": "github"
},
"original": {
@ -141,11 +141,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1725630423,
"lastModified": 1726443025,
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "08c7bbc2dbe4dcc8968484f1a0e1e6fe7a1d4f6d",
"treeHash": "0a74bbe7a31e26686dab5792be52a87dbd0d36b3",
"rev": "94b526fc86eaa0e90fb4d54a5ba6313aa1e9b269",
"treeHash": "3a9ddbe34b25b2921864f029988d2d48049b2d4a",
"type": "github"
},
"original": {

View File

@ -4,7 +4,7 @@
# https://devenv.sh/basics/
env.GREET = "devenv";
env.LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
env.BINDGEN_EXTRA_CLANG_ARGS = "-I ${pkgs.libclang.lib}/lib/clang/18/include";
env.BINDGEN_EXTRA_CLANG_ARGS = "-I ${pkgs.libclang.lib}/lib/clang/17/include";
# https://devenv.sh/packages/
packages = [ pkgs.openssl pkgs.git pkgs.tailwindcss pkgs.watchexec pkgs.libclang pkgs.clang ];

View File

@ -1,6 +1,6 @@
inputs:
nixpkgs:
url: github:NixOS/nixpkgs/nixpkgs-unstable
url: github:NixOS/nixpkgs/nixos-24.05
fenix:
url: github:nix-community/fenix
inputs:

View File

@ -35,6 +35,6 @@ pub fn Global() -> Element {
#[server]
async fn get_user_from_cookie() -> Result<(), ServerFnError> {
// Err(ServerFnError::new("Not authenticated"))
Ok(())
Err(ServerFnError::new("Not authenticated"))
// Ok(())
}

View File

@ -1,5 +1,9 @@
use dioxus::prelude::*;
use crate::components::layout::icons;
use crate::util::model::member::Member;
use std::collections::HashMap;
pub fn Auth() -> Element {
let mut register = use_signal(|| false);
@ -77,26 +81,66 @@ fn Login() -> Element {
}
fn Register() -> Element {
let mut members: Signal<HashMap<String, Member>> = use_signal(|| HashMap::new());
let mut input_registration_token = use_signal(|| "".to_string());
rsx! {
form {
label {
class: "form-control w-full",
class: "form-control w-full join-item",
div {
class: "label",
span { class: "label-text", "Registratie code" },
}
div {
class: "w-full join",
input {
r#type: "text",
class: "input input-bordered w-full",
class: "input input-bordered w-full join-item",
value: "{input_registration_token}",
oninput: move |event| input_registration_token.set(event.value())
},
button {
class: "btn join-item btn-neutral",
onclick: move |_| async move {
if let Ok(res) = fetch_member(input_registration_token()).await {
members.write().insert(res.id.clone(), res);
input_registration_token.set("".to_string());
}
},
icons::UserPlus {},
"Toevoegen",
}
}
div {
class: "flex gap-1 mt-2 flex-wrap pb-3",
for (id, member) in members() {
div {
class: "badge badge-md badge-neutral h-8 gap-1 hover:cursor-pointer hover:line-through whitespace-nowrap",
onclick: move |_| {
members.write().remove(&id);
},
"{member.name.full}"
icons::XMark {},
}
}
}
}
div {
class: "card-actions mt-8",
button {
class: "btn btn-primary",
"Registreren"
}
"Volgende"
}
}
}
}
#[server]
async fn fetch_member(registration_token: String) -> Result<Member, ServerFnError> {
let member = Member::fetch_from_registration_token(registration_token).await?;
match member {
Some(m) => Ok(m),
None => Err(ServerFnError::new("Lid niet gevonden")),
}
}

View File

@ -1,5 +1,7 @@
use dioxus::prelude::*;
// Icons are from heroicons (https://icones.js.org/collection/heroicons)
#[component]
pub fn Home() -> Element {
rsx! {
@ -80,3 +82,42 @@ pub fn Cog() -> Element {
}
}
}
#[component]
pub fn UserPlus() -> Element {
rsx! {
svg {
"viewBox": "0 0 24 24",
height: "32",
"xmlns": "http://www.w3.org/2000/svg",
width: "32",
path {
"fill": "none",
"stroke": "currentColor",
"stroke-linecap": "round",
"d": "M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0a3.375 3.375 0 0 1 6.75 0M3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.3 12.3 0 0 1 9.374 21C7.043 21 4.862 20.355 3 19.234",
"stroke-linejoin": "round",
"stroke-width": "1.5"
}
}
}
}
#[component]
pub fn XMark() -> Element {
rsx! {
svg {
"stroke": "currentColor",
"viewBox": "0 0 24 24",
"stroke-width": "1.5",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg",
class: "size-6",
path {
"d": "M6 18 18 6M6 6l12 12",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}
}
}
}

View File

@ -31,12 +31,29 @@ pub struct MembersMigration {
#[cfg(feature = "server")]
impl Member {
pub async fn fetch_all() -> Result<Vec<Self>, surrealdb::Error> {
let mut res = DB.query("SELECT meta::id(id) as id, name.first, name.full, hours, groups, diploma, registration_token FROM member").await?;
let mut res = DB.query("SELECT record::id(id) as id, name.first, name.full, hours, groups, diploma, registration_token FROM member").await?;
let members: Vec<Self> = res.take(0)?;
tracing::info!("{:?}", members);
Ok(members)
}
pub async fn fetch_from_registration_token(
registration_token: String,
) -> Result<Option<Self>, surrealdb::Error> {
let query = format!("SELECT record::id(id) as id, name.first, name.full, hours, groups, diploma, registration_token FROM ONLY member WHERE registration_token = $registration_token LIMIT 1");
let mut res = DB
.query(query)
.bind(("registration_token", registration_token))
.await?;
let member: Option<Self> = res.take(0)?;
Ok(member)
}
}
impl MembersMigration {

View File

@ -4,6 +4,7 @@ use super::{Member, MembersMigration};
use once_cell::sync::Lazy;
use std::collections::HashMap;
use surrealdb::sql::statements::{BeginStatement, CommitStatement};
use tokio::sync::Mutex;
// Create a store for saving information when migrating to a new members list
@ -135,12 +136,6 @@ impl MembersMigration {
new_members_list: Vec<Member>,
current_members_list: Vec<Member>,
) -> MembersMigration {
tracing::info!(
"Current: {}, New: {}",
current_members_list.len(),
new_members_list.len()
);
let current_members_map: HashMap<String, Member> = current_members_list
.clone()
.into_iter()
@ -214,37 +209,45 @@ impl MembersMigration {
None => return Err("Could not get members from store".into()),
};
let mut query = String::from("BEGIN TRANSACTION;");
let mut transaction = DB.query(BeginStatement::default());
for member in members_migration.updated.clone() {
// TODO add hours and diploma support
query = query
+ format!(
"UPDATE member:{} SET name.first = \"{}\", name.full = \"{}\", hours = {:?}, groups = {:?};",
member.id, member.name.first, member.name.full, member.hours, member.groups
)
.as_str();
let id = member.id.clone();
transaction = transaction.query(format!("UPDATE type::thing('member', $id_{id}) SET name.first = $name_first_{id}, name.full = $name_full_{id}, hours = $hours_{id}, groups = $groups_{id};"))
.bind((format!("id_{id}"), member.id))
.bind((format!("name_first_{id}"), member.name.first))
.bind((format!("name_full_{id}"), member.name.full))
.bind((format!("hours_{id}"), member.hours))
.bind((format!("groups_{id}"), member.groups));
}
for member in members_migration.inserted.clone() {
// TODO add hours and diploma support
let id = member.id.clone();
query = query
+ format!(
"CREATE member:{} SET name.first = \"{}\", name.full = \"{}\", hours = {:?}, groups = {:?}, registration_token = rand::string(16);",
member.id, member.name.first, member.name.full, member.hours, member.groups
)
.as_str();
transaction = transaction.query(format!("CREATE type::thing('member', $id_{id}) SET name.first = $name_first_{id}, name.full = $name_full_{id}, hours = $hours_{id}, groups = $groups_{id}, registration_token = rand::string(16);"))
.bind((format!("id_{id}"), member.id))
.bind((format!("name_first_{id}"), member.name.first))
.bind((format!("name_full_{id}"), member.name.full))
.bind((format!("hours_{id}"), member.hours))
.bind((format!("groups_{id}"), member.groups));
}
for member in members_migration.removed.clone() {
// TODO add hours and diploma support
query = query + format!("DELETE member:{};", member.id).as_str();
let id = member.id.clone();
transaction = transaction
.query(format!("DELETE type::thing('member', $id_{id});"))
.bind((format!("id_{id}"), member.id));
}
query = query + "COMMIT TRANSACTION;";
DB.query(query).await?;
transaction
.query(CommitStatement::default())
.await?
.check()?;
Ok(())
}

View File

@ -1,11 +1,11 @@
use once_cell::sync::Lazy;
use surrealdb::engine::local::{Db, SpeeDb};
use surrealdb::engine::local::{Db, RocksDb};
use surrealdb::Surreal;
pub static DB: Lazy<Surreal<Db>> = Lazy::new(|| Surreal::init());
pub async fn initialize() -> surrealdb::Result<()> {
DB.connect::<SpeeDb>("./database").await?;
DB.connect::<RocksDb>("./database").await?;
DB.use_ns("xvmcmm").use_db("xvmcmm").await?;