Initial version with randomized questions and answers

master
Wynd 2025-06-14 15:43:12 +03:00
commit 6fff9e4450
11 changed files with 5855 additions and 0 deletions

9
.gitignore vendored 100644
View File

@ -0,0 +1,9 @@
# Generated by Cargo
# will have compiled files and executables
/target
.DS_Store
# These are backup files generated by rustfmt
**/*.rs.bk
questions.toml

5445
Cargo.lock generated 100644

File diff suppressed because it is too large Load Diff

31
Cargo.toml 100644
View File

@ -0,0 +1,31 @@
[package]
name = "flashcards"
version = "0.1.0"
authors = ["Wynd <wyndmaster@gmail.com>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { version = "0.6.0", features = [] }
toml = { version = "0.8" }
serde = { version = "1.0", features = ["derive"] }
rand = { version = "0.9" }
[features]
default = ["desktop"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]
[profile]
[profile.wasm-dev]
inherits = "dev"
opt-level = 1
[profile.server-dev]
inherits = "dev"
[profile.android-dev]
inherits = "dev"

21
Dioxus.toml 100644
View File

@ -0,0 +1,21 @@
[application]
[web.app]
# HTML title tag content
title = "flashcards"
# include `assets` in web platform
[web.resource]
# Additional CSS style files
style = []
# Additional JavaScript files
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []

25
README.md 100644
View File

@ -0,0 +1,25 @@
# Development
Your new bare-bones project includes minimal organization with a single `main.rs` file and a few assets.
```
project/
├─ assets/ # Any assets that are used by the app should be placed here
├─ src/
│ ├─ main.rs # main.rs is the entry point to your application and currently contains all components for the app
├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project
```
### Serving Your App
Run the following command in the root of your project to start developing with the default platform:
```bash
dx serve
```
To run for a different platform, use the `--platform platform` flag. E.g.
```bash
dx serve --platform desktop
```

BIN
assets/favicon.ico 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

20
assets/header.svg 100644

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

90
assets/main.css 100644
View File

@ -0,0 +1,90 @@
/* App-wide styling */
html {
}
body {
background-color: #0f1116;
color: #ffffff;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
}
form {
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
font-size: 24px;
user-select: none;
> div {
width: 100%;
height: 80px;
display: flex;
margin-top: 10px;
align-items: center;
align-self: center;
box-sizing: border-box;
border: solid 2px unset;
&.selected {
border: solid 2px goldenrod;
}
&:hover:not(.selected) {
border: solid 1px gray;
}
> input[type="checkbox"] {
display: none;
scale: 1.5;
margin-right: 10px;
}
> label {
flex-grow: 1;
}
}
> input[type="submit"] {
margin-top: 40px;
height: 70px;
background-color: transparent;
border: none;
color: white;
font-size: 42px;
&:hover {
font-size: 48px;
color: goldenrod;
}
}
}
#links {
width: 400px;
text-align: left;
font-size: x-large;
color: white;
display: flex;
flex-direction: column;
}
#links a {
color: white;
text-decoration: none;
margin-top: 20px;
margin: 10px 0px;
border: white 1px solid;
border-radius: 5px;
padding: 10px;
}
#links a:hover {
background-color: #1f1f1f;
cursor: pointer;
}
#header {
max-width: 1200px;
}

8
clippy.toml 100644
View File

@ -0,0 +1,8 @@
await-holding-invalid-types = [
"generational_box::GenerationalRef",
{ path = "generational_box::GenerationalRef", reason = "Reads should not be held over an await point. This will cause any writes to fail while the await is pending since the read borrow is still active." },
"generational_box::GenerationalRefMut",
{ path = "generational_box::GenerationalRefMut", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
"dioxus_signals::Write",
{ path = "dioxus_signals::Write", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
]

6
rustfmt.toml 100644
View File

@ -0,0 +1,6 @@
reorder_imports = true
hard_tabs = true
control_brace_style = "ClosingNextLine"
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
newline_style = "Unix"

200
src/main.rs 100644
View File

@ -0,0 +1,200 @@
use std::{
collections::HashMap,
ops::{Deref, DerefMut},
sync::LazyLock,
};
use dioxus::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)]
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)]
struct Question {
message: String,
answers: Vec<Answer>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
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::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));
}
}
},
onkeydown: move |event| {
dbg!(event.key());
if event.key() == Key::Enter {
dbg!("enter pressed");
}
},
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}"
},
}
}
}