#![warn(clippy::pedantic)]
use crate::model::{AppState, AppStatusUpdate, MakeTeaError, Status};
use dioxus::prelude::*;
use dioxus_tea::TeaModel;
fn main() {
launch(rsx_components::App);
}
mod model {
use std::fmt::Display;
#[derive(Default, Clone, PartialEq, Debug)]
pub enum Status {
#[default]
FetchingCup,
EmptyCup,
TeaBag(TeaType),
Water(u8),
TeaReady,
Error(MakeTeaError),
}
impl Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let status_message = match self {
Status::FetchingCup => "Fetching a cup...",
Status::EmptyCup => "Empty cup. Add a tea bag.",
Status::TeaBag(tea_type) => &format!("Tea bag added: {tea_type}"),
Status::Water(temperature) => &format!("Water added at {temperature}°C. Waiting for tea to brew..."),
Status::TeaReady => "Tea is ready!",
Status::Error(error) => &format!("Error: {error}"),
};
write!(f, "{status_message}")
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum TeaType {
Black,
Green,
White,
Oolong,
}
impl TeaType {
pub fn temp_range(&self) -> (u8, u8) {
match self {
TeaType::Black => (100, 100),
TeaType::Green => (70, 79),
TeaType::White => (70, 82),
TeaType::Oolong => (85, 93),
}
}
}
impl Display for TeaType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let tea_name = match self {
TeaType::Black => "Black",
TeaType::Green => "Green",
TeaType::White => "White",
TeaType::Oolong => "Oolong",
};
write!(f, "{tea_name}")
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum MakeTeaError {
MissingTeaBag,
WaterTooHot,
WaterTooCold,
MissingWater,
CupNotEmpty,
NoCup,
}
impl Display for MakeTeaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let error_message = match self {
MakeTeaError::MissingTeaBag => "No tea bag added",
MakeTeaError::WaterTooHot => "Water is too hot",
MakeTeaError::WaterTooCold => "Water is too cold",
MakeTeaError::MissingWater => "No water added",
MakeTeaError::CupNotEmpty => "The cup is not empty",
MakeTeaError::NoCup => "No cup fetched",
};
write!(f, "{error_message}")
}
}
#[derive(Default, Clone, PartialEq)]
pub struct AppState {
pub status: Status,
}
pub enum AppStatusUpdate {
CupFetched,
AddWater(u8),
AddTeaBag(TeaType),
Done,
}
}
impl TeaModel for AppState {
type Action = AppStatusUpdate;
fn update(&mut self, action: Self::Action) {
match action {
AppStatusUpdate::CupFetched => {
self.status = Status::EmptyCup;
}
AppStatusUpdate::AddWater(temperature) => {
let Status::TeaBag(tea_type) = &self.status else {
self.status = Status::Error(MakeTeaError::MissingTeaBag);
return;
};
let (low, high) = tea_type.temp_range();
if temperature < low {
self.status = Status::Error(MakeTeaError::WaterTooCold);
} else if temperature > high {
self.status = Status::Error(MakeTeaError::WaterTooHot);
} else {
self.status = Status::Water(temperature);
}
}
AppStatusUpdate::AddTeaBag(tea_type) => {
match self.status {
Status::EmptyCup => {
self.status = Status::TeaBag(tea_type);
}
Status::FetchingCup => {
self.status = Status::Error(MakeTeaError::NoCup);
}
_ => {
self.status = Status::Error(MakeTeaError::CupNotEmpty);
}
}
}
AppStatusUpdate::Done => {
if let Status::Water(_) = &self.status {
self.status = Status::TeaReady;
} else {
self.status = Status::Error(MakeTeaError::MissingWater);
}
}
}
}
}
mod rsx_components {
use super::model::{AppState, AppStatusUpdate, Status, TeaType};
use dioxus::prelude::*;
use dioxus_sdk::time::sleep;
use dioxus_tea::{use_tea_model, TeaModelSignal};
use std::time::Duration;
const MAIN_CSS: Asset = asset!("/examples/tea-time/assets/main.css");
const FAVICON: Asset = asset!("/examples/tea-time/assets/favicon.ico");
#[component]
pub(super) fn App() -> Element {
let app_state = use_tea_model::<AppState>();
use_context_provider(|| app_state);
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS }
DemoApp {}
}
}
#[component]
fn DemoApp() -> Element {
let app_state = use_context::<TeaModelSignal<AppState>>();
use_hook(|| {
spawn(async move {
sleep(Duration::from_secs(1)).await;
app_state.send(AppStatusUpdate::CupFetched);
});
});
use_effect(move || {
let current_status = &app_state.read().status;
if matches!(current_status, Status::Water(_)) {
spawn(async move {
sleep(Duration::from_secs(2)).await;
app_state.send(AppStatusUpdate::Done);
});
}
});
let app_state_r = app_state.read();
let message = app_state_r.status.to_string();
rsx! {
div {
id: "title",
div { class: "heading", "Tea Time 🫖" }
div { "{message}" }
}
TeaOptions {
app_state
}
}
}
#[component]
fn TeaOptions(app_state: TeaModelSignal<AppState>) -> Element {
let app_state_r = app_state.read();
rsx! {
if matches!(app_state_r.status, Status::EmptyCup) {
div {
class: "tea-options",
button {
onclick: move |_| app_state.send(AppStatusUpdate::AddTeaBag(TeaType::Black)),
"Add Black Tea Bag"
}
button {
onclick: move |_| app_state.send(AppStatusUpdate::AddTeaBag(TeaType::Green)),
"Add Green Tea Bag"
}
button {
onclick: move |_| app_state.send(AppStatusUpdate::AddTeaBag(TeaType::White)),
"Add White Tea Bag"
}
button {
onclick: move |_| app_state.send(AppStatusUpdate::AddTeaBag(TeaType::Oolong)),
"Add Oolong Tea Bag"
}
}
} else if matches!(app_state_r.status, Status::TeaBag(_)) {
div {
class: "tea-options",
button {
onclick: move |_| app_state.send(AppStatusUpdate::AddWater(100)),
"Add Water (100°C)"
}
button {
onclick: move |_| app_state.send(AppStatusUpdate::AddWater(90)),
"Add Water (90°C)"
}
button {
onclick: move |_| app_state.send(AppStatusUpdate::AddWater(80)),
"Add Water (80°C)"
}
button {
onclick: move |_| app_state.send(AppStatusUpdate::AddWater(70)),
"Add Water (70°C)"
}
}
} else if matches!(app_state_r.status, Status::Error(_) | Status::TeaReady) {
div {
class: "tea-options",
button {
onclick: move |_| app_state.send(AppStatusUpdate::CupFetched),
"Try again"
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::TeaType;
#[test]
fn confirm_that_we_start_without_a_cup() {
let app_state = AppState::default();
assert_eq!(app_state.status, Status::FetchingCup);
}
#[test]
fn confirm_that_we_can_only_add_a_tea_bag_when_we_have_a_cup() {
let mut app_state = AppState::default();
app_state.update(AppStatusUpdate::AddTeaBag(TeaType::Black));
assert_eq!(
app_state.status,
Status::Error(MakeTeaError::NoCup),
"Cannot add a tea bag without a cup"
);
}
#[test]
fn should_be_able_to_add_a_tea_bag_when_we_have_a_cup() {
let mut app_state = AppState::default();
app_state.update(AppStatusUpdate::CupFetched);
app_state.update(AppStatusUpdate::AddTeaBag(TeaType::Black));
assert_eq!(
app_state.status,
Status::TeaBag(TeaType::Black),
"Should be able to add a tea bag after fetching a cup"
);
}
}