Added a nice animation for when correclty answering a question

master
Wynd 2025-06-15 13:00:47 +03:00
parent c8b87b88b5
commit a65a9591af
6 changed files with 210 additions and 118 deletions

11
.editorconfig 100644
View File

@ -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

2
Cargo.lock generated
View File

@ -1405,8 +1405,10 @@ name = "flashcards"
version = "0.1.0"
dependencies = [
"dioxus",
"futures",
"rand 0.9.1",
"serde",
"tokio",
"toml",
]

View File

@ -11,6 +11,8 @@ dioxus = { version = "0.6.3", features = [] }
toml = { version = "0.8" }
serde = { version = "1.0", features = ["derive"] }
rand = { version = "0.9" }
futures = { version = "0.3" }
tokio = { version = "1.45" }
[features]
default = ["desktop"]

View File

@ -1,72 +1,118 @@
/* App-wide styling */
:root {
--correct: limegreen;
--correct-shadow: green;
--hover: gray;
--selection: goldenrod;
}
html {
}
body {
background-color: #0f1116;
color: #ffffff;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
background-color: #0f1116;
color: #ffffff;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
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 {
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
font-size: 24px;
user-select: none;
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;
h1 {
&.success {
color: var(--correct);
}
}
&.selected {
border: solid 2px goldenrod;
}
> div {
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) {
border: solid 1px gray;
}
&.selected {
border: solid 2px var(--selection);
}
> input[type="checkbox"] {
display: none;
scale: 1.5;
margin-right: 10px;
}
&:hover:not(.selected) {
border: solid 1px var(--hoveer);
}
> label {
flex-grow: 1;
}
> input[type="checkbox"] {
display: none;
scale: 1.5;
margin-right: 10px;
}
&:hover,
> *:hover {
cursor: pointer;
}
}
> label {
flex-grow: 1;
}
> input[type="submit"] {
margin-top: 40px;
height: 70px;
background-color: transparent;
border: none;
color: white;
font-size: 42px;
&:hover,
> *:hover {
cursor: pointer;
}
}
&:hover {
font-size: 48px;
color: goldenrod;
cursor: pointer;
}
> input[type="submit"] {
margin-top: 40px;
height: 70px;
background-color: transparent;
border: none;
color: white;
font-size: 42px;
&:focus {
outline: none;
}
}
&:hover {
font-size: 48px;
color: var(--selection);
cursor: pointer;
}
&:focus {
outline: none;
}
}
}

View File

@ -1,59 +1,16 @@
use std::{
ops::{Deref, DerefMut},
sync::LazyLock,
};
use std::{sync::LazyLock, time::Duration};
use dioxus::{
desktop::{Config, WindowBuilder},
desktop::{Config, LogicalSize, WindowBuilder},
prelude::*,
};
use futures::StreamExt;
use models::{Question, Questions};
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 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();
@ -81,6 +38,7 @@ fn main() {
.with_cfg(
Config::default().with_menu(None).with_window(
WindowBuilder::new()
.with_min_inner_size(LogicalSize::new(640, 540))
.with_maximized(true)
.with_title("Flashcards"),
),
@ -91,14 +49,13 @@ fn main() {
#[component]
fn App() -> Element {
rsx! {
// document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS }
QuestionPrompt {}
QuestionForm {}
}
}
#[component]
pub fn QuestionPrompt() -> Element {
pub fn QuestionForm() -> Element {
let mut questions = use_signal(get_questions);
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 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()
.answers
.into_iter()
@ -134,23 +110,26 @@ pub fn QuestionPrompt() -> Element {
});
rsx! {
div { "{left_questions}/{total_questions}" },
ResultPopup { is_visible: success_animation },
div {
id: "counter",
"{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));
}
success_animation.set(true);
}
else {
}
},
h1 { "{current().message}" }
h1 {
class: if success_animation() { "success" },
"{current().message}"
}
{ answer_buttons }
input {
type: "submit",
@ -181,7 +160,7 @@ pub fn AnswerCheckbox(current: Signal<Question>, id: usize) -> Element {
rsx! {
div {
class: if checked() { "selected" } else { "" },
class: if checked() { "selected" },
onclick: move |_| {
let mut write_lock = current.write();
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" },
""
}
}
}

39
src/models.rs 100644
View File

@ -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,
}