Compare commits

...

20 Commits

Author SHA1 Message Date
7637606d9c Merge pull request 'Fixed issue that loading state does not reset when database restarts' (#12) from feature/surrealdb-session into main
Reviewed-on: #12
2024-04-03 13:28:49 +02:00
bcf843765a
Fixed issue that loading state does not reset when database restarts 2024-04-03 13:27:27 +02:00
d9ed4ba626 Merge pull request 'Added storage for auth keys' (#11) from feature/surrealdb-session into main
Reviewed-on: #11
2024-04-02 19:51:03 +02:00
6aadc89530
Added storage for auth keys 2024-04-02 19:50:03 +02:00
14bad374b0
Added documentation 2024-04-02 18:47:40 +02:00
a744e56537
Removed unused code 2024-04-02 18:43:37 +02:00
edde7f6604
Added system to add Times to database 2024-04-02 18:40:06 +02:00
01e9a65463
Added time UI 2024-03-26 18:47:19 +01:00
ead2852922
Fixed button styling 2024-03-23 19:36:30 +01:00
ac92e9f16a
Started working on selection system 2024-03-20 15:56:10 +01:00
e41dfc72de
Added sorting function 2024-03-20 12:54:38 +01:00
86de475a2e
Added live subscription to persons 2024-03-19 20:16:27 +01:00
afd5de8f02
fixed conflicts 2024-03-19 18:42:45 +01:00
59d6f7b37e
Merge new-person into main 2024-03-19 18:39:21 +01:00
0cbbafb6ec Merge pull request 'Added toast function for creating toast notifications' (#9) from toast-notification into main
Reviewed-on: #9
2024-03-12 16:29:06 +01:00
e00f70e1f7
Added timeout function for toast notifications 2024-03-12 16:28:20 +01:00
e697272e26 Merge branch 'main' into toast-notification 2024-03-12 15:51:58 +01:00
6098b7460b
Added a toast notification for DB sigin 2024-03-12 15:51:23 +01:00
ed2a93548a
Merge branch 'toast-notification'
Merged toast-notification into main
2024-03-12 15:46:25 +01:00
57aa1a356c
Added toast function for creating toast notifications 2024-03-12 15:42:40 +01:00
11 changed files with 595 additions and 46 deletions

View File

@ -16,16 +16,28 @@ console_error_panic_hook = "0.1"
leptos-use = "0.10.2"
serde = "1.0.196"
serde_json = "1.0.113"
rand = "0.8.5"
gloo-timers = "0.3.0"
strsim = "0.11.0"
# utils
# strum = { version = "0.25", features = ["derive", "strum_macros"] }
# strum_macros = "0.25"
[dependencies.web-sys]
version = "0.3"
features = [
"Document",
"Window",
"Element",
"ScrollIntoViewOptions",
"ScrollLogicalPosition",
"ScrollBehavior",
]
[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3"
web-sys = { version = "0.3", features = ["Document", "Window"] }
[profile.release]

View File

@ -13,6 +13,8 @@ html,
body {
height: 100vh;
max-width: 100vw;
display: flex;
flex-direction: column;
margin: 0;
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
}
@ -25,6 +27,7 @@ body {
main {
height: 100%;
width: 100%;
padding: 20px;
display: flex;
flex-direction: column;
@ -110,8 +113,84 @@ form {
width: 400px;
}
.error {
color: red;
text-align: center;
font-size: 1.2em;
.toastcontainer {
display: flex;
flex-direction: column-reverse;
gap: 10px;
position: fixed;
top: 0;
left: 0;
z-index: 999;
padding: 10px;
}
.toastcontainer span {
padding: 10px 20px;
border-radius: 10px;
width: 300px;
font-weight: bold;
}
.toastcontainer span:hover {
cursor: pointer;
}
.warning {
background-color: #f29826;
}
.error {
background-color: #d9534f;
}
.info {
background-color: #2181d4;
}
.success {
background-color: #4bb24b;
}
.participants-container {
height: 200px;
overflow: scroll;
display: flex;
flex-direction: column;
gap: 10px;
list-style-type: none;
margin-top: 0;
padding: 0;
}
.participants-container li {
text-align: left;
background-color: $secondary-bg-color-light;
padding: 5px 10px;
border-radius: 5px;
}
.participants-container li:hover {
cursor: pointer;
background-color: $secondary-bg-color-lighter;
}
.participants-container .selected {
border: 1px solid $primary-color;
}
.time-input-container {
display: flex;
gap: 10px;
margin: 10px 0px;
}
.time-input-container input[type=number] {
width: 30px;
text-align: center;
-moz-appearance: textfield;
}
.time-input-container input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

View File

@ -1 +1,2 @@
pub mod navbar;
pub mod toast;

View File

@ -0,0 +1,18 @@
use crate::util;
use leptos::*;
/// Navigation bar
#[component]
pub fn Toasts() -> impl IntoView {
let notifications = expect_context::<util::toast::NotificationsContext>();
view! {
<div class="toastcontainer">
{move || notifications.notifications.get().into_iter()
.map(|n| view! {
<span on:click=move |_| util::toast::remove_toast((*n.id).to_string()) class=n.option>{n.text}</span>
}
).collect_view()}
</div>
}
}

View File

@ -20,6 +20,7 @@ pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
util::surrealdb::init_surrealdb();
util::toast::init_toast();
let websocket = expect_context::<util::surrealdb::SurrealContext>();
let _participants = use_context::<util::surrealdb::ParticipantsContext>()
@ -58,14 +59,17 @@ pub fn App() -> impl IntoView {
}
>
<Router>
<components::toast::Toasts />
<components::navbar::Navbar />
<main>
<Routes>
<Route path="/" view=move || {
view! {
// only show the outlet if the signin was successfull
<Show when=move || websocket.authenticated.get() fallback=login::Login>
<Outlet/>
<Show when=move || !websocket.loading.get() fallback=|| view! { <p>"Connection to database..."</p>}>
<Show when=move || websocket.authenticated.get() fallback=login::Login>
<Outlet/>
</Show>
</Show>
}
}>
@ -73,6 +77,7 @@ pub fn App() -> impl IntoView {
<Route path="/participants" view=participants::Participants />
<Route path="/participants/add" view=participants::add::Add />
<Route path="/times" view=times::Times />
<Route path="/times/add" view=times::add::Add />
<Route path="/*" view=NotFound />
</Route>
</Routes>

View File

@ -1,4 +1,5 @@
use leptos::*;
use leptos_router::*;
pub mod add;
@ -6,10 +7,6 @@ pub mod add;
#[component]
pub fn Times() -> impl IntoView {
view! {
<div class="container">
<h1>"Tijden"</h1>
</div>
<A class="btn-add-link" href="/times/add">"Tijd toevoegen"</A>
}
}

View File

@ -1,9 +1,209 @@
use leptos::*;
use crate::util::surrealdb;
use leptos::{ev::keydown, *};
use leptos_use::*;
use strsim::normalized_damerau_levenshtein;
use web_sys::{ScrollIntoViewOptions, ScrollLogicalPosition};
use crate::util::surrealdb::Participant;
// Functions to sort all the participants and include a search term
fn sort_participants(participants: Vec<Participant>, search: String) -> Vec<Participant> {
let mut filtered_sorted_list: Vec<(Participant, f64)> = participants
.into_iter()
.map(|participant| {
(
participant.clone(),
normalized_damerau_levenshtein(
&participant.name.to_lowercase(),
&search.to_lowercase(),
),
)
})
.collect();
filtered_sorted_list.sort_by(|a, b| {
let (_, sim_score_a) = a;
let (_, sim_score_b) = b;
sim_score_b
.partial_cmp(sim_score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
filtered_sorted_list
.into_iter()
.map(|(item, _)| item)
.collect()
}
#[component]
pub fn SelectOption(is: &'static str, value: ReadSignal<String>) -> impl IntoView {
view! {
<option
value=is
selected=move || value.get() == is
>
{is}
</option>
}
}
/// Navigation bar
#[component]
pub fn Add() -> impl IntoView {
let websocket = expect_context::<crate::util::surrealdb::SurrealContext>();
let participants = expect_context::<crate::util::surrealdb::ParticipantsContext>();
let container_ref: NodeRef<html::Ul> = create_node_ref();
let name_ref: NodeRef<html::Input> = create_node_ref();
let (name, set_name) = create_signal(String::from(""));
let (event, set_event) = create_signal(String::from("lifesaver"));
let (error, set_error) = create_signal(String::from(""));
let (minutes, set_minutes) = create_signal::<u64>(0);
let (seconds, set_seconds) = create_signal::<u64>(0);
let (miliseconds, set_miliseconds) = create_signal::<u64>(0);
let (selected, set_selected) = create_signal::<usize>(0);
let participants_sorted =
create_memo(move |_| sort_participants(participants.read.get(), name.get()));
let _ = use_event_listener(use_document(), keydown, move |evt| {
if evt.key() == "ArrowDown".to_string() {
set_selected.update(|x| {
let len = participants.read.get_untracked().len();
if *x != len {
*x += 1;
}
});
} else if evt.key() == "ArrowUp".to_string() {
set_selected.update(|x| {
if *x != 0 {
*x -= 1
}
});
}
let el: web_sys::Element = container_ref
.get_untracked()
.unwrap()
.children()
.item(selected.get_untracked().try_into().unwrap())
.expect("No element found");
el.scroll_into_view_with_scroll_into_view_options(
&ScrollIntoViewOptions::new().block(ScrollLogicalPosition::Center),
);
});
let on_submit = move |ev: leptos::ev::SubmitEvent| {
ev.prevent_default();
set_error.set(String::from(""));
let person = &participants_sorted.get()[selected.get()];
match websocket.add_time(
surrealdb::CreateTimeParam::new(
person.clone().id,
minutes.get(),
seconds.get(),
miliseconds.get(),
),
event.get(),
) {
Ok(_) => {
set_name.set(String::from(""));
set_minutes.set(0);
set_seconds.set(0);
set_miliseconds.set(0);
let _ = name_ref.get().unwrap().focus();
}
Err(err) => set_error.set(err),
}
};
view! {
<h1>"Deelnemer toevoegen"</h1>
<h1>"Tijd toevoegen"</h1>
<form class="add" on:submit=on_submit>
<div class="time-input-container">
<span>"Onderdeel:"</span>
<select
on:change=move |ev| {
set_event.set(event_target_value(&ev));
}
>
<SelectOption value=event is="lifesaver"/>
<SelectOption value=event is="popduiken"/>
<SelectOption value=event is="hindernis"/>
</select>
</div>
<input type="text"
placeholder="Name"
tabindex=0
node_ref=name_ref
on:input=move |ev| {
set_name.set(event_target_value(&ev));
set_selected.set(0);
}
prop:value=name
/>
<ul class="participants-container" tabindex=-1 node_ref=container_ref>
{move || participants_sorted.get().into_iter().enumerate().map(|(i, participant)| view! {
<li on:click=move |_| set_selected.set(i) class:selected=move || selected.get() == i>{participant.name + " " + "(" + &participant.group[6..].to_string() + ")" }</li>
}).collect_view()}
</ul>
<div class="time-input-container">
<span>"Tijd:"</span>
<input
type="number"
tabindex=0
placeholder="mm"
min=0
max=99
on:input=move |ev| {
let x = event_target_value(&ev).parse::<u64>();
match x {
Ok(x) => set_minutes.set(x),
Err(_) => set_minutes.set(0),
}
}
prop:value=minutes
/>
<input
type="number"
tabindex=0
placeholder="ss"
min=0
max=59
on:input=move |ev| {
let x = event_target_value(&ev).parse::<u64>();
match x {
Ok(x) => set_seconds.set(x),
Err(_) => set_seconds.set(0),
}
}
prop:value=seconds
/>
<input
type="number"
tabindex=0
placeholder="ms"
min=0
max=99
on:input=move |ev| {
let x = event_target_value(&ev).parse::<u64>();
match x {
Ok(x) => set_miliseconds.set(x),
Err(_) => set_miliseconds.set(0),
}
}
prop:value=miliseconds
/>
</div>
<input type="submit" tabindex=0 value="Submit" />
</form>
<p class="error">
{ error }
</p>
}
}

View File

@ -1 +1,2 @@
pub mod surrealdb;
pub mod toast;

View File

@ -1,12 +1,26 @@
use crate::util;
use leptos::*;
use leptos_use::{core::ConnectionReadyState, use_websocket, UseWebsocketReturn};
use leptos_use::{
core::ConnectionReadyState,
storage::use_local_storage,
use_websocket,
utils::{FromToStringCodec, StringCodec},
UseWebsocketReturn,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::rc::Rc;
#[derive(Serialize)]
#[serde(untagged)]
enum SurrealId {
String(String),
Integer(u32),
}
#[derive(Serialize)]
struct SurrealRequest {
id: u32,
id: SurrealId,
method: String,
params: Vec<SurrealParams>,
}
@ -17,6 +31,7 @@ enum SurrealParams {
Participant,
SigninParam(SigninParam),
CreatePersonParam(CreatePersonParam),
CreateTimeParam(CreateTimeParam),
String(String),
}
@ -32,14 +47,33 @@ struct CreatePersonParam {
group: String,
}
#[derive(Clone, Debug)]
#[derive(Serialize)]
pub struct CreateTimeParam {
person_id: String,
time: u64,
}
impl CreateTimeParam {
pub fn new(person_id: String, minutes: u64, seconds: u64, miliseconds: u64) -> Self {
Self {
person_id,
time: minutes * 60 * 1000 + seconds * 1000 + miliseconds * 10,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Participant {
name: String,
group: String,
pub id: String,
pub name: String,
pub group: String,
}
#[derive(Clone, Debug)]
pub struct ParticipantsContext(pub ReadSignal<Vec<Participant>>);
pub struct ParticipantsContext {
pub read: ReadSignal<Vec<Participant>>,
write: WriteSignal<Vec<Participant>>,
}
#[derive(Clone)]
pub struct SurrealContext {
@ -48,18 +82,41 @@ pub struct SurrealContext {
pub ready_state: Signal<ConnectionReadyState>,
pub authenticated: ReadSignal<bool>,
pub set_authenticated: WriteSignal<bool>,
pub loading: ReadSignal<bool>,
pub set_loading: WriteSignal<bool>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
struct SurrealResponse {
id: u32,
id: Option<u32>,
result: SurrealResult,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
struct SurrealResultError {
code: i32,
message: String,
}
#[derive(Serialize, Deserialize, Clone)]
struct SurrealResponseError {
id: u32,
error: SurrealResultError,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct SurrealAction {
id: String,
action: String,
result: Participant,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(untagged)]
enum SurrealResult {
String(String),
Participants(Vec<Participant>),
Action(SurrealAction),
Null,
}
@ -70,6 +127,8 @@ impl SurrealContext {
ready_state: Signal<ConnectionReadyState>,
authenticated: ReadSignal<bool>,
set_authenticated: WriteSignal<bool>,
loading: ReadSignal<bool>,
set_loading: WriteSignal<bool>,
) -> Self {
Self {
message,
@ -77,6 +136,8 @@ impl SurrealContext {
ready_state,
authenticated,
set_authenticated,
loading,
set_loading,
}
}
@ -91,7 +152,7 @@ impl SurrealContext {
log::debug!("Signing into surrealdb");
let request = SurrealRequest {
id: 0,
id: SurrealId::Integer(0),
method: String::from("signin"),
params: vec![SurrealParams::SigninParam(SigninParam {
user: String::from("root"),
@ -108,7 +169,7 @@ impl SurrealContext {
}
let request = SurrealRequest {
id: 1,
id: SurrealId::Integer(10),
method: String::from("create"),
params: vec![
SurrealParams::String(String::from("person")),
@ -121,6 +182,19 @@ impl SurrealContext {
Ok(self.send(&json!(request).to_string()))
}
pub fn add_time(&self, body: CreateTimeParam, event: String) -> Result<(), String> {
let request = SurrealRequest {
id: SurrealId::Integer(20),
method: String::from("create"),
params: vec![
SurrealParams::String(event),
SurrealParams::CreateTimeParam(body),
],
};
Ok(self.send(&json!(request).to_string()))
}
}
pub fn init_surrealdb() {
@ -133,8 +207,9 @@ pub fn init_surrealdb() {
} = use_websocket("ws://localhost:80/rpc");
// Create reactive signals for the websocket connection to surrealDB
let (participants, _set_participants) = create_signal::<Vec<Participant>>(vec![]);
let (participants, set_participants) = create_signal::<Vec<Participant>>(vec![]);
let (authenticated, set_authenticated) = create_signal::<bool>(false);
let (loading, set_loading) = create_signal::<bool>(true);
// Bind the websocket connection to a context
let websocket = SurrealContext::new(
@ -143,10 +218,38 @@ pub fn init_surrealdb() {
ready_state,
authenticated,
set_authenticated,
loading,
set_loading,
);
provide_context(websocket);
provide_context(ParticipantsContext(participants));
provide_context(websocket.clone());
provide_context(ParticipantsContext {
read: participants,
write: set_participants,
});
create_effect(move |prev_value| {
let status = ready_state.get();
if prev_value != Some(status.clone()) && status == ConnectionReadyState::Open {
let (token, _, _) = use_local_storage::<String, FromToStringCodec>("surrealdb-token");
if token.get().is_empty() {
set_loading.set(false);
return status;
}
let request = SurrealRequest {
id: SurrealId::Integer(0),
method: String::from("authenticate"),
params: vec![SurrealParams::String(token.get())],
};
websocket.send(&json!(request).to_string());
};
status
});
// Watch for a message recieved from the surrealDB connection
create_effect(move |prev_value| {
@ -169,6 +272,31 @@ pub fn init_surrealdb() {
/// Function to execute when SurrealDB returns a message
fn surrealdb_response(response: String) {
let response: SurrealResponse = match serde_json::from_str(&response) {
Ok(res) => res,
Err(_) => {
handle_error(response);
return;
}
};
match response.id {
Some(0) => use_surrealdb(response.clone()),
Some(1) => get_participants(),
Some(2) => log::debug!("Subscribed to live timings"),
Some(5) => got_participants(response.result.clone()),
Some(_) => (),
None => (),
}
match response.result {
SurrealResult::Action(action) => handle_action(action),
_ => (),
}
}
/// Function to execute when an error is returned from Surrealdb
fn handle_error(response: String) {
let response: SurrealResponseError = match serde_json::from_str(&response) {
Ok(res) => res,
Err(err) => {
log::warn!("{}", err);
@ -176,21 +304,37 @@ fn surrealdb_response(response: String) {
}
};
match response.id {
0 => use_surrealdb(response),
1 => log::debug!("Succesfully execute use wrb timings"),
_ => return,
if response.id == 0 && response.error.code == -32000 {
let (_, _, remove_token) =
use_local_storage::<String, FromToStringCodec>("surrealdb-token");
let websocket = expect_context::<SurrealContext>();
remove_token();
websocket.set_loading.set(false);
}
}
/// Function to execute when DB signin is succesful
fn use_surrealdb(_response: SurrealResponse) {
fn use_surrealdb(response: SurrealResponse) {
util::toast::add_toast(
"Succesfully signed into DB".to_string(),
"success".to_string(),
);
let (_, set_token, _) = use_local_storage::<String, FromToStringCodec>("surrealdb-token");
match response.result {
SurrealResult::String(str) => set_token.set(str),
_ => (),
};
let websocket = expect_context::<SurrealContext>();
websocket.set_authenticated.set(true);
websocket.set_loading.set(false);
let request = SurrealRequest {
id: 1,
id: SurrealId::Integer(1),
method: String::from("use"),
params: vec![
SurrealParams::String(String::from("wrb")),
@ -200,3 +344,44 @@ fn use_surrealdb(_response: SurrealResponse) {
websocket.send(&json!(request).to_string())
}
/// Function to get all participants and subscribe to changes
fn get_participants() {
let websocket = expect_context::<SurrealContext>();
let request = SurrealRequest {
id: SurrealId::Integer(5),
method: String::from("select"),
params: vec![SurrealParams::String(String::from("person"))],
};
websocket.send(&json!(request).to_string());
let request = SurrealRequest {
id: SurrealId::Integer(2),
method: String::from("live"),
params: vec![SurrealParams::String(String::from("person"))],
};
websocket.send(&json!(request).to_string());
}
/// Function that will execute when participants are recieved
fn got_participants(result: SurrealResult) {
let participants_context = expect_context::<ParticipantsContext>();
if let SurrealResult::Participants(mut value) = result {
let mut participants = participants_context.read.get();
participants.append(&mut value);
participants_context.write.set(participants);
}
}
/// Function to call when an action is recieved from surrealDB
fn handle_action(action: SurrealAction) {
let participants_context = expect_context::<ParticipantsContext>();
let mut participants = participants_context.read.get();
participants.push(action.result);
participants_context.write.set(participants);
}

View File

@ -0,0 +1,51 @@
use gloo_timers::future::TimeoutFuture;
use leptos::*;
use rand::distributions::{Alphanumeric, DistString};
#[derive(Clone)]
pub struct NotificationsContext {
pub notifications: ReadSignal<Vec<ToastNotification>>,
pub set_notifications: WriteSignal<Vec<ToastNotification>>,
}
#[derive(Clone)]
pub struct ToastNotification {
pub text: String,
pub option: String, // error, warning, info, success
pub id: String,
}
pub fn init_toast() {
let (notifications, set_notifications) = create_signal::<Vec<ToastNotification>>(vec![]);
provide_context(NotificationsContext {
notifications,
set_notifications,
});
}
pub fn add_toast(text: String, option: String) {
let context = expect_context::<NotificationsContext>();
let id = Alphanumeric.sample_string(&mut rand::thread_rng(), 4);
let mut vec = context.notifications.get();
vec.push(ToastNotification {
text,
option,
id: id.clone(),
});
context.set_notifications.set(vec);
spawn_local(async {
TimeoutFuture::new(5000).await;
remove_toast(id);
});
}
pub fn remove_toast(id: String) {
let context = expect_context::<NotificationsContext>();
let mut vec = context.notifications.get_untracked();
vec.retain(|x| x.id != id);
context.set_notifications.set(vec);
}

View File

@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
@ -38,11 +38,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1707514827,
"narHash": "sha256-Y+wqFkvikpE1epCx57PsGw+M1hX5aY5q/xgk+ebDwxI=",
"lastModified": 1710951922,
"narHash": "sha256-FOOBJ3DQenLpTNdxMHR2CpGZmYuctb92gF0lpiirZ30=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "20f65b86b6485decb43c5498780c223571dd56ef",
"rev": "f091af045dff8347d66d186a62d42aceff159456",
"type": "github"
},
"original": {
@ -54,11 +54,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1707546158,
"narHash": "sha256-nYYJTpzfPMDxI8mzhQsYjIUX+grorqjKEU9Np6Xwy/0=",
"lastModified": 1711001935,
"narHash": "sha256-URtGpHue7HHZK0mrHnSf8wJ6OmMKYSsoLmJybrOLFSQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d934204a0f8d9198e1e4515dd6fec76a139c87f0",
"rev": "20f77aa09916374aa3141cbc605c955626762c9a",
"type": "github"
},
"original": {
@ -98,11 +98,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1707703915,
"narHash": "sha256-Vej69igzNr3eVDca6+32uO+TXjVWx6ZUwwy3iZuzhJ4=",
"lastModified": 1711073443,
"narHash": "sha256-PpNb4xq7U5Q/DdX40qe7CijUsqhVVM3VZrhN0+c6Lcw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "e6679d2ff9136d00b3a7168d2bf1dff9e84c5758",
"rev": "eec55ba9fcde6be4c63942827247e42afef7fafe",
"type": "github"
},
"original": {