2025-06-14 15:43:12 +03:00
|
|
|
use std::{
|
|
|
|
ops::{Deref, DerefMut},
|
|
|
|
sync::LazyLock,
|
|
|
|
};
|
|
|
|
|
2025-06-14 18:33:30 +03:00
|
|
|
use dioxus::{
|
|
|
|
desktop::{Config, WindowBuilder},
|
|
|
|
prelude::*,
|
|
|
|
};
|
2025-06-14 15:43:12 +03:00
|
|
|
use rand::seq::SliceRandom;
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
2025-06-14 18:33:30 +03:00
|
|
|
// const FAVICON: Asset = asset!("/assets/favicon.ico");
|
2025-06-14 15:43:12 +03:00
|
|
|
const MAIN_CSS: Asset = asset!("/assets/main.css");
|
2025-06-14 18:33:30 +03:00
|
|
|
// const HEADER_SVG: Asset = asset!("/assets/header.svg");
|
2025-06-14 15:43:12 +03:00
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
2025-06-14 18:33:30 +03:00
|
|
|
pub struct Questions {
|
2025-06-14 15:43:12 +03:00
|
|
|
questions: Vec<Question>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Deref for Questions {
|
|
|
|
type Target = Vec<Question>;
|
|
|
|
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
&self.questions
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DerefMut for Questions {
|
|
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
|
|
&mut self.questions
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
2025-06-14 18:33:30 +03:00
|
|
|
pub struct Question {
|
2025-06-14 15:43:12 +03:00
|
|
|
message: String,
|
|
|
|
answers: Vec<Answer>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
2025-06-14 18:33:30 +03:00
|
|
|
pub struct Answer {
|
2025-06-14 15:43:12 +03:00
|
|
|
message: String,
|
|
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
is_correct: Option<bool>,
|
|
|
|
|
|
|
|
#[serde(default, skip)]
|
|
|
|
checked: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Props, PartialEq, Clone)]
|
|
|
|
struct QuestionProps {
|
|
|
|
current: Question,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub static QUESTIONS: LazyLock<Questions> = LazyLock::new(|| {
|
|
|
|
let questions_str = std::fs::read_to_string("questions.toml").unwrap();
|
|
|
|
let questions: Questions = toml::from_str(&questions_str).unwrap();
|
|
|
|
questions
|
|
|
|
});
|
|
|
|
|
|
|
|
fn get_questions() -> Vec<Question> {
|
|
|
|
let mut questions = QUESTIONS.questions.clone();
|
|
|
|
let mut rng = rand::rng();
|
|
|
|
|
|
|
|
// Randomize the answers
|
|
|
|
questions
|
|
|
|
.iter_mut()
|
|
|
|
.for_each(|q| q.answers.shuffle(&mut rng));
|
|
|
|
|
|
|
|
// Randomize the questions list
|
|
|
|
questions.shuffle(&mut rng);
|
|
|
|
|
|
|
|
questions
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
2025-06-14 18:33:30 +03:00
|
|
|
dioxus::LaunchBuilder::new()
|
|
|
|
.with_cfg(
|
|
|
|
Config::default().with_menu(None).with_window(
|
|
|
|
WindowBuilder::new()
|
|
|
|
.with_maximized(true)
|
|
|
|
.with_title("Flashcards"),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.launch(App)
|
2025-06-14 15:43:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
#[component]
|
|
|
|
fn App() -> Element {
|
|
|
|
rsx! {
|
2025-06-14 18:33:30 +03:00
|
|
|
// document::Link { rel: "icon", href: FAVICON }
|
2025-06-14 15:43:12 +03:00
|
|
|
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
|
|
|
QuestionPrompt {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[component]
|
|
|
|
pub fn QuestionPrompt() -> Element {
|
|
|
|
let mut questions = use_signal(get_questions);
|
|
|
|
let mut current = use_signal(move || questions.remove(0));
|
|
|
|
|
|
|
|
let total_correct = use_memo(move || {
|
|
|
|
current
|
|
|
|
.read()
|
|
|
|
.answers
|
|
|
|
.iter()
|
|
|
|
.filter(|a| a.is_correct.unwrap_or_default())
|
|
|
|
.count()
|
|
|
|
});
|
|
|
|
let actual_correct = use_memo(move || {
|
|
|
|
current
|
|
|
|
.read()
|
|
|
|
.answers
|
|
|
|
.iter()
|
|
|
|
.filter(|a| a.is_correct.unwrap_or_default() && a.checked)
|
|
|
|
.count()
|
|
|
|
});
|
|
|
|
|
|
|
|
let total_questions = QUESTIONS.len();
|
|
|
|
let current_question = use_memo(move || questions().len());
|
|
|
|
let left_questions = use_memo(move || total_questions - current_question());
|
|
|
|
|
|
|
|
let answer_buttons = current()
|
|
|
|
.answers
|
|
|
|
.into_iter()
|
|
|
|
.enumerate()
|
|
|
|
.map(|(i, _answer)| {
|
|
|
|
rsx! {
|
|
|
|
AnswerCheckbox { current, id: i }
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
rsx! {
|
|
|
|
div { "{left_questions}/{total_questions}" },
|
|
|
|
form {
|
|
|
|
id: "form",
|
|
|
|
onsubmit: move |_| {
|
|
|
|
if actual_correct() == total_correct() {
|
|
|
|
current().answers.iter_mut().for_each(|a| a.checked = false);
|
|
|
|
if !questions().is_empty() {
|
|
|
|
current.set(questions.remove(0));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
questions.set(get_questions());
|
|
|
|
current.set(questions.remove(0));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2025-06-14 18:33:30 +03:00
|
|
|
|
2025-06-14 15:43:12 +03:00
|
|
|
h1 { "{current().message}" }
|
|
|
|
{ answer_buttons }
|
2025-06-14 18:33:30 +03:00
|
|
|
input {
|
|
|
|
type: "submit",
|
|
|
|
}
|
2025-06-14 15:43:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[component]
|
|
|
|
pub fn AnswerCheckbox(current: Signal<Question>, id: usize) -> Element {
|
|
|
|
let message = use_memo(move || {
|
|
|
|
current
|
|
|
|
.read()
|
|
|
|
.answers
|
|
|
|
.get(id)
|
|
|
|
.map(|a| a.message.clone())
|
|
|
|
.unwrap_or("???".to_string())
|
|
|
|
});
|
|
|
|
|
|
|
|
let checked = use_memo(move || {
|
|
|
|
current
|
|
|
|
.read()
|
|
|
|
.answers
|
|
|
|
.get(id)
|
|
|
|
.map(|a| a.checked)
|
|
|
|
.unwrap_or(false)
|
|
|
|
});
|
|
|
|
|
|
|
|
rsx! {
|
|
|
|
div {
|
|
|
|
class: if checked() { "selected" } else { "" },
|
|
|
|
onclick: move |_| {
|
|
|
|
let mut write_lock = current.write();
|
|
|
|
if let Some(answer) = write_lock.answers.get_mut(id) {
|
|
|
|
answer.checked = !answer.checked;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
input {
|
|
|
|
id: "a{id}",
|
|
|
|
name: "a{id}",
|
|
|
|
type: "checkbox",
|
|
|
|
value: "{id}",
|
|
|
|
checked: "{checked}",
|
|
|
|
onclick: move |event| event.prevent_default(),
|
|
|
|
}
|
|
|
|
label {
|
|
|
|
for: "a{id}",
|
|
|
|
onclick: move |event| event.prevent_default(),
|
|
|
|
"{message}"
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|