flashcards/src/main.rs

208 lines
4.0 KiB
Rust
Raw Normal View History

use std::{
ops::{Deref, DerefMut},
sync::LazyLock,
};
use dioxus::{
desktop::{Config, WindowBuilder},
prelude::*,
};
use rand::seq::SliceRandom;
use serde::Deserialize;
// const FAVICON: Asset = asset!("/assets/favicon.ico");
const MAIN_CSS: Asset = asset!("/assets/main.css");
// const HEADER_SVG: Asset = asset!("/assets/header.svg");
#[derive(Debug, Deserialize)]
pub struct Questions {
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)]
pub struct Question {
message: String,
answers: Vec<Answer>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Answer {
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() {
dioxus::LaunchBuilder::new()
.with_cfg(
Config::default().with_menu(None).with_window(
WindowBuilder::new()
.with_maximized(true)
.with_title("Flashcards"),
),
)
.launch(App)
}
#[component]
fn App() -> Element {
rsx! {
// document::Link { rel: "icon", href: FAVICON }
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));
}
}
},
h1 { "{current().message}" }
{ answer_buttons }
input {
type: "submit",
}
}
}
}
#[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}"
},
}
}
}