Initial version with randomized questions and answers
commit
6fff9e4450
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
|
@ -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 = []
|
|
@ -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
|
||||
```
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 130 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 23 KiB |
|
@ -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;
|
||||
}
|
|
@ -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." },
|
||||
]
|
|
@ -0,0 +1,6 @@
|
|||
reorder_imports = true
|
||||
hard_tabs = true
|
||||
control_brace_style = "ClosingNextLine"
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
||||
newline_style = "Unix"
|
|
@ -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}"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue