logicaffeine_web/ui/components/
recall_block.rs1use crate::guide_review::record_recall_mark;
10use crate::progress::UserProgress;
11use dioxus::prelude::*;
12
13#[derive(Props, Clone, PartialEq)]
14pub struct RecallCardProps {
15 pub id: String,
16 pub question: String,
17 pub answer: String,
18}
19
20#[component]
21pub fn RecallCard(props: RecallCardProps) -> Element {
22 let mut revealed = use_signal(|| false);
23 let mut marked = use_signal(|| false);
24
25 let progress_ctx = try_consume_context::<Signal<UserProgress>>();
26 let recall_id = props.id.clone();
27 let self_mark = move |quality: u8| {
28 let today = crate::ui::pages::guide::today_string();
29 match progress_ctx {
30 Some(mut progress) => {
31 record_recall_mark(&mut progress.write(), &recall_id, quality, &today);
32 }
33 None => {
34 let mut progress = UserProgress::load();
35 record_recall_mark(&mut progress, &recall_id, quality, &today);
36 }
37 }
38 marked.set(true);
39 };
40
41 rsx! {
42 div {
43 class: "guide-recall-card",
44 id: "{props.id}",
45 style: "border: 1px solid rgba(255,255,255,0.10); border-radius: 10px; padding: 12px 16px; background: rgba(255,255,255,0.03);",
46
47 div {
48 style: "display: flex; align-items: center; justify-content: space-between; gap: 12px;",
49 span {
50 style: "font-size: 14px; color: rgba(229,231,235,0.9); line-height: 1.5;",
51 "{props.question}"
52 }
53 if !*revealed.read() {
54 button {
55 class: "guide-recall-reveal",
56 style: "flex: none; padding: 5px 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.05); color: rgba(229,231,235,0.75); font-size: 12px; cursor: pointer;",
57 onclick: move |_| revealed.set(true),
58 "Reveal"
59 }
60 }
61 }
62
63 if *revealed.read() {
64 div {
65 style: "margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.08); font-size: 13px; color: #a78bfa; line-height: 1.6;",
66 "{props.answer}"
67 }
68 if !*marked.read() {
69 div {
70 class: "guide-recall-marks",
71 style: "display: flex; gap: 8px; margin-top: 10px; align-items: center;",
72 span {
73 style: "font-size: 11px; color: rgba(229,231,235,0.45);",
74 "Did you have it?"
75 }
76 button {
77 class: "guide-recall-mark",
78 style: "padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(248,113,113,0.4); background: none; color: #f87171; font-size: 11px; cursor: pointer;",
79 onclick: { let mut m = self_mark.clone(); move |_| m(1) },
80 "Again"
81 }
82 button {
83 class: "guide-recall-mark",
84 style: "padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(251,191,36,0.4); background: none; color: #fbbf24; font-size: 11px; cursor: pointer;",
85 onclick: { let mut m = self_mark.clone(); move |_| m(3) },
86 "Hard"
87 }
88 button {
89 class: "guide-recall-mark",
90 style: "padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(96,165,250,0.4); background: none; color: #60a5fa; font-size: 11px; cursor: pointer;",
91 onclick: { let mut m = self_mark.clone(); move |_| m(4) },
92 "Good"
93 }
94 button {
95 class: "guide-recall-mark",
96 style: "padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(34,197,94,0.4); background: none; color: #22c55e; font-size: 11px; cursor: pointer;",
97 onclick: { let mut m = self_mark.clone(); move |_| m(5) },
98 "Easy"
99 }
100 }
101 } else {
102 div {
103 style: "margin-top: 10px; font-size: 11px; color: rgba(229,231,235,0.45);",
104 "Scheduled — it will resurface on the Review page when it's about to fade."
105 }
106 }
107 }
108 }
109 }
110}
111
112#[component]
114pub fn RecallBlock(cards: Vec<(String, String, String)>) -> Element {
115 rsx! {
116 div {
117 class: "guide-recall",
118 style: "margin-top: 24px; display: flex; flex-direction: column; gap: 10px;",
119 div {
120 style: "font-size: 13px; font-weight: 700; color: rgba(229,231,235,0.85); text-transform: uppercase; letter-spacing: 0.5px;",
121 "Check yourself"
122 }
123 for (id, question, answer) in cards {
124 RecallCard { id, question, answer }
125 }
126 }
127 }
128}