Added a nice animation for when correclty answering a question
parent
c8b87b88b5
commit
a65a9591af
|
@ -0,0 +1,11 @@
|
||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
|
@ -1405,8 +1405,10 @@ name = "flashcards"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dioxus",
|
"dioxus",
|
||||||
|
"futures",
|
||||||
"rand 0.9.1",
|
"rand 0.9.1",
|
||||||
"serde",
|
"serde",
|
||||||
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ dioxus = { version = "0.6.3", features = [] }
|
||||||
toml = { version = "0.8" }
|
toml = { version = "0.8" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
rand = { version = "0.9" }
|
rand = { version = "0.9" }
|
||||||
|
futures = { version = "0.3" }
|
||||||
|
tokio = { version = "1.45" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["desktop"]
|
default = ["desktop"]
|
||||||
|
|
156
assets/main.css
156
assets/main.css
|
@ -1,72 +1,118 @@
|
||||||
/* App-wide styling */
|
:root {
|
||||||
|
--correct: limegreen;
|
||||||
|
--correct-shadow: green;
|
||||||
|
--hover: gray;
|
||||||
|
--selection: goldenrod;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #0f1116;
|
background-color: #0f1116;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
margin: 20px;
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#counter {
|
||||||
|
margin: 10px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#result {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 40vw;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--correct);
|
||||||
|
text-shadow: var(--correct-shadow) 1px 1px 30px;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
display: block;
|
||||||
|
animation: 200ms fade-in forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
> div {
|
h1 {
|
||||||
width: 100%;
|
&.success {
|
||||||
height: 80px;
|
color: var(--correct);
|
||||||
display: flex;
|
}
|
||||||
margin-top: 10px;
|
}
|
||||||
align-items: center;
|
|
||||||
align-self: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: solid 2px unset;
|
|
||||||
|
|
||||||
&.selected {
|
> div {
|
||||||
border: solid 2px goldenrod;
|
width: 100%;
|
||||||
}
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
margin-top: 10px;
|
||||||
|
align-items: center;
|
||||||
|
align-self: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: solid 2px unset;
|
||||||
|
|
||||||
&:hover:not(.selected) {
|
&.selected {
|
||||||
border: solid 1px gray;
|
border: solid 2px var(--selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
> input[type="checkbox"] {
|
&:hover:not(.selected) {
|
||||||
display: none;
|
border: solid 1px var(--hoveer);
|
||||||
scale: 1.5;
|
}
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> label {
|
> input[type="checkbox"] {
|
||||||
flex-grow: 1;
|
display: none;
|
||||||
}
|
scale: 1.5;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover,
|
> label {
|
||||||
> *:hover {
|
flex-grow: 1;
|
||||||
cursor: pointer;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> input[type="submit"] {
|
&:hover,
|
||||||
margin-top: 40px;
|
> *:hover {
|
||||||
height: 70px;
|
cursor: pointer;
|
||||||
background-color: transparent;
|
}
|
||||||
border: none;
|
}
|
||||||
color: white;
|
|
||||||
font-size: 42px;
|
|
||||||
|
|
||||||
&:hover {
|
> input[type="submit"] {
|
||||||
font-size: 48px;
|
margin-top: 40px;
|
||||||
color: goldenrod;
|
height: 70px;
|
||||||
cursor: pointer;
|
background-color: transparent;
|
||||||
}
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 42px;
|
||||||
|
|
||||||
&:focus {
|
&:hover {
|
||||||
outline: none;
|
font-size: 48px;
|
||||||
}
|
color: var(--selection);
|
||||||
}
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
118
src/main.rs
118
src/main.rs
|
@ -1,59 +1,16 @@
|
||||||
use std::{
|
use std::{sync::LazyLock, time::Duration};
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
sync::LazyLock,
|
|
||||||
};
|
|
||||||
|
|
||||||
use dioxus::{
|
use dioxus::{
|
||||||
desktop::{Config, WindowBuilder},
|
desktop::{Config, LogicalSize, WindowBuilder},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use models::{Question, Questions};
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
// const FAVICON: Asset = asset!("/assets/favicon.ico");
|
mod models;
|
||||||
|
|
||||||
const MAIN_CSS: Asset = asset!("/assets/main.css");
|
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(|| {
|
pub static QUESTIONS: LazyLock<Questions> = LazyLock::new(|| {
|
||||||
let questions_str = std::fs::read_to_string("questions.toml").unwrap();
|
let questions_str = std::fs::read_to_string("questions.toml").unwrap();
|
||||||
|
@ -81,6 +38,7 @@ fn main() {
|
||||||
.with_cfg(
|
.with_cfg(
|
||||||
Config::default().with_menu(None).with_window(
|
Config::default().with_menu(None).with_window(
|
||||||
WindowBuilder::new()
|
WindowBuilder::new()
|
||||||
|
.with_min_inner_size(LogicalSize::new(640, 540))
|
||||||
.with_maximized(true)
|
.with_maximized(true)
|
||||||
.with_title("Flashcards"),
|
.with_title("Flashcards"),
|
||||||
),
|
),
|
||||||
|
@ -91,14 +49,13 @@ fn main() {
|
||||||
#[component]
|
#[component]
|
||||||
fn App() -> Element {
|
fn App() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
// document::Link { rel: "icon", href: FAVICON }
|
|
||||||
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||||
QuestionPrompt {}
|
QuestionForm {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn QuestionPrompt() -> Element {
|
pub fn QuestionForm() -> Element {
|
||||||
let mut questions = use_signal(get_questions);
|
let mut questions = use_signal(get_questions);
|
||||||
let mut current = use_signal(move || questions.remove(0));
|
let mut current = use_signal(move || questions.remove(0));
|
||||||
|
|
||||||
|
@ -123,6 +80,25 @@ pub fn QuestionPrompt() -> Element {
|
||||||
let current_question = use_memo(move || questions().len());
|
let current_question = use_memo(move || questions().len());
|
||||||
let left_questions = use_memo(move || total_questions - current_question());
|
let left_questions = use_memo(move || total_questions - current_question());
|
||||||
|
|
||||||
|
let mut success_animation = use_signal(|| false);
|
||||||
|
|
||||||
|
use_future(move || async move {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(Duration::from_millis(400)).await;
|
||||||
|
if success_animation() {
|
||||||
|
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||||
|
success_animation.set(false);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let answer_buttons = current()
|
let answer_buttons = current()
|
||||||
.answers
|
.answers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -134,23 +110,26 @@ pub fn QuestionPrompt() -> Element {
|
||||||
});
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { "{left_questions}/{total_questions}" },
|
ResultPopup { is_visible: success_animation },
|
||||||
|
div {
|
||||||
|
id: "counter",
|
||||||
|
"{left_questions}/{total_questions}"
|
||||||
|
},
|
||||||
form {
|
form {
|
||||||
id: "form",
|
id: "form",
|
||||||
onsubmit: move |_| {
|
onsubmit: move |_| {
|
||||||
if actual_correct() == total_correct() {
|
if actual_correct() == total_correct() {
|
||||||
current().answers.iter_mut().for_each(|a| a.checked = false);
|
success_animation.set(true);
|
||||||
if !questions().is_empty() {
|
}
|
||||||
current.set(questions.remove(0));
|
else {
|
||||||
}
|
|
||||||
else {
|
|
||||||
questions.set(get_questions());
|
|
||||||
current.set(questions.remove(0));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
h1 { "{current().message}" }
|
h1 {
|
||||||
|
class: if success_animation() { "success" },
|
||||||
|
"{current().message}"
|
||||||
|
}
|
||||||
{ answer_buttons }
|
{ answer_buttons }
|
||||||
input {
|
input {
|
||||||
type: "submit",
|
type: "submit",
|
||||||
|
@ -181,7 +160,7 @@ pub fn AnswerCheckbox(current: Signal<Question>, id: usize) -> Element {
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: if checked() { "selected" } else { "" },
|
class: if checked() { "selected" },
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let mut write_lock = current.write();
|
let mut write_lock = current.write();
|
||||||
if let Some(answer) = write_lock.answers.get_mut(id) {
|
if let Some(answer) = write_lock.answers.get_mut(id) {
|
||||||
|
@ -205,3 +184,16 @@ pub fn AnswerCheckbox(current: Signal<Question>, id: usize) -> Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ResultPopup(is_visible: Signal<bool>) -> Element {
|
||||||
|
let is_visible = use_memo(move || is_visible());
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
id: "result",
|
||||||
|
class: if is_visible() { "visible" },
|
||||||
|
"✓"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Questions {
|
||||||
|
pub 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 {
|
||||||
|
pub message: String,
|
||||||
|
pub answers: Vec<Answer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub struct Answer {
|
||||||
|
pub message: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_correct: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default, skip)]
|
||||||
|
pub checked: bool,
|
||||||
|
}
|
Loading…
Reference in New Issue