diff --git a/.env b/.env new file mode 100644 index 0000000..cac2217 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +SUPABASE_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV5YXFndHVueXhvZHh5YWN4cG5rIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODkwNzg5MzYsImV4cCI6MjAwNDY1NDkzNn0.4gXuwv8BfQ0-MYuj4J_MbE_h5WyJKxRY7YlIGJYMSC8 +SUPABASE_URL=https://eyaqgtunyxodxyacxpnk.supabase.co \ No newline at end of file diff --git a/index.html b/index.html index 0f4554a..b7412c0 100644 --- a/index.html +++ b/index.html @@ -33,6 +33,7 @@ } if (counter === 1) { + e.preventDefault(); counter = 0 new_e = new e.constructor(e.type, e); gamearea.dispatchEvent(new_e); diff --git a/packages/client/Cargo.toml b/packages/client/Cargo.toml index 48252ef..0cb0298 100644 --- a/packages/client/Cargo.toml +++ b/packages/client/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] rand = "0.8.5" getrandom = { version = "0.2", features = ["js"] } +uuid = { version = "1.2.2", features = ["v4", "serde"] } futures = "0.3" reqwest = { version = "0.11", features = ["json"] } serde = { version = "1.0", features = ["derive"] } @@ -16,6 +17,7 @@ log = "0.4" shared = { path = "../shared" } dioxus-html-macro = "0.3.0" fermi = "0.3.0" +gloo-storage = "0.2.2" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dioxus = "0.3.0" diff --git a/packages/client/src/components/header.rs b/packages/client/src/components/header.rs index 8189200..9b83485 100644 --- a/packages/client/src/components/header.rs +++ b/packages/client/src/components/header.rs @@ -2,7 +2,7 @@ use dioxus::prelude::*; use dioxus::html::MouseEvent; use fermi::*; use dioxus_html_macro::html; -use shared::translate::{TRANSLATION}; +use shared::translate::TRANSLATION; #[cfg(not(target_arch = "wasm32"))] use dioxus_desktop::use_eval; @@ -71,9 +71,7 @@ pub fn LanguageButton<'a> (cx: Scope<'a, LanguageButtonProps<'a>>) -> Element<'a cx.render(rsx!( li { onclick: move |evt| cx.props.onclick.call(evt), - a { - cx.props.language.clone() - } + a { cx.props.language.clone() } } )) } diff --git a/packages/client/src/main.rs b/packages/client/src/main.rs index ca2129e..f513880 100644 --- a/packages/client/src/main.rs +++ b/packages/client/src/main.rs @@ -81,6 +81,7 @@ fn main() { ); #[cfg(target_arch = "wasm32")] + wasm_logger::init(wasm_logger::Config::default()); dioxus_web::launch(app); } diff --git a/packages/client/src/pages/game.rs b/packages/client/src/pages/game.rs index bf68684..897b451 100644 --- a/packages/client/src/pages/game.rs +++ b/packages/client/src/pages/game.rs @@ -5,9 +5,10 @@ use dioxus::html::KeyboardEvent; use shared::types::{Board, GameStatus, ProgressReqeust}; use reqwest; use shared::logic::{get_initial_board_data, add_random, move_up, move_down, move_left, move_right, check_and_do_next}; -use log; +use uuid::Uuid; use crate::components::row::Row; use shared::translate::TRANSLATION; +use gloo_storage::{LocalStorage, Storage, errors::StorageError}; pub fn Game(cx: Scope) -> Element { let game_status = use_state(cx, || GameStatus::Playing); @@ -15,40 +16,65 @@ pub fn Game(cx: Scope) -> Element { let is_first_load = use_state(cx, || true); let translator = use_read(cx, TRANSLATION); + use_effect(cx, (is_first_load), |_| async move { + let a: Result = LocalStorage::get("uuid"); + match a { + Ok(uuid) => { + log::info!("Found uuid: {}", uuid); + }, + StorageError => { + log::info!("No uuid found, creating one..."); + let uuid = Uuid::new_v4(); + LocalStorage::set("uuid", uuid.to_string()).unwrap(); + } + }; + }); + use_effect(cx, (is_first_load, board_data, game_status), |(is_first_load, board_data, game_status)| async move { if !is_first_load.get() { return; } - - let client = reqwest::Client::new(); - let res = client.get("http://localhost:3000/progress").send().await; - match res { - Ok(response) => { - let payload = response.json::().await; - match payload { - Ok(data) => { - is_first_load.set(false); - board_data.set(data.board); - match check_and_do_next(&data.board) { - GameStatus::Win => { - game_status.set(GameStatus::Win); - }, - GameStatus::Fail => { - game_status.set(GameStatus::Fail); + let a: Result = LocalStorage::get("uuid"); + match a { + Ok(uuid) => { + let client = reqwest::Client::new(); + let url = format!("http://localhost:3000/progress?uuid={uuid}", uuid=uuid); + let res = client.get(url).send().await; + match res { + Ok(response) => { + let payload = response.json::().await; + match payload { + Ok(data) => { + is_first_load.set(false); + board_data.set(data.board); + + match check_and_do_next(&data.board) { + GameStatus::Win => { + game_status.set(GameStatus::Win); + }, + GameStatus::Fail => { + game_status.set(GameStatus::Fail); + }, + GameStatus::Playing => { game_status.set(GameStatus::Playing); }, + } }, - GameStatus::Playing => { game_status.set(GameStatus::Playing); }, + Err(err) => { + log::error!("Failed to parse JSON: {}", err); + } } }, Err(err) => { - log::error!("Failed to parse JSON: {}", err); + log::error!("Failed to send request: {}", err); } } }, - Err(err) => { - log::error!("Failed to send request: {}", err); + StorageError => { + log::info!("No uuid found, creating one..."); + let uuid = Uuid::new_v4(); + LocalStorage::set("uuid", uuid.to_string()).unwrap(); } - } + }; }); let handle_key_down_event = move |evt: KeyboardEvent| -> () { @@ -82,18 +108,27 @@ pub fn Game(cx: Scope) -> Element { cx.spawn({ async move { - let client = reqwest::Client::new(); - let res = client.post("http://localhost:3000/progress") - .json(&ProgressReqeust { - board: new_data - }) - .send() - .await; - - match res { - Ok(_) => {}, - Err(err) => { - log::error!("Failed to record progress: {}", err); + let a: Result = LocalStorage::get("uuid"); + match a { + Ok(uuid) => { + let client = reqwest::Client::new(); + let res = client.post("http://localhost:3000/progress") + .json(&ProgressReqeust { + board: new_data, + uuid: Some(uuid), + }) + .send() + .await; + + match res { + Ok(_) => {}, + Err(err) => { + log::error!("Failed to record progress: {}", err); + } + } + }, + StorageError => { + log::info!("No uuid found, skip..."); } } } diff --git a/packages/client/src/pages/homepage.rs b/packages/client/src/pages/homepage.rs index 6e1abd8..92fba89 100644 --- a/packages/client/src/pages/homepage.rs +++ b/packages/client/src/pages/homepage.rs @@ -1,6 +1,6 @@ use fermi::*; use dioxus::prelude::*; -use dioxus_router::{Link}; +use dioxus_router::Link; use dioxus_html_macro::html; use shared::translate::TRANSLATION; diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index eaef21d..3775c91 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -10,4 +10,6 @@ tokio = { version = "1.0", features = ["full"] } tower-http = { version = "0.3.0", features = ["cors"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -shared = { path = "../shared" } \ No newline at end of file +shared = { path = "../shared" } +postgrest = "1.0" +dotenv = "0.15.0" \ No newline at end of file diff --git a/packages/server/src/main.rs b/packages/server/src/main.rs index f856622..a2eedf3 100644 --- a/packages/server/src/main.rs +++ b/packages/server/src/main.rs @@ -2,20 +2,54 @@ use axum::{ response::Json, response::IntoResponse, routing::get, - extract::{State}, Router, + extract::{State, Query}, http::StatusCode }; use std::net::SocketAddr; -use std::{collections::HashMap, sync::{Arc, RwLock}}; -use tower_http::cors::{Any}; -use shared::types::{Board, ProgressResponse, ProgressReqeust}; +use std::sync::Arc; +use tower_http::cors::Any; +use shared::types::{ + UuidQuery, + GetProgressResponse, + SaveProgressResponse, + ProgressReqeust, + DbBoard}; +use shared::logic::get_initial_board_data; +use postgrest::Postgrest; +use dotenv::dotenv; +use serde_json; -type Db = Arc>>; +struct AppState { + supabase_client: Postgrest, +} + +impl Clone for AppState { + fn clone(&self) -> Self { + let supabase_url: String = dotenv::var("SUPABASE_URL") + .expect("Need to set environment variable SUPABASE_URL") + "/rest/v1/"; + let client: Postgrest = Postgrest::new(supabase_url) + .insert_header("apikey", dotenv::var("SUPABASE_API_KEY").unwrap()); + Self { + supabase_client: client, + } + } +} + +impl AppState { + fn create() -> Arc { + let supabase_url: String = dotenv::var("SUPABASE_URL") + .expect("Need to set environment variable SUPABASE_URL") + "/rest/v1/"; + let client: Postgrest = Postgrest::new(supabase_url) + .insert_header("apikey", dotenv::var("SUPABASE_API_KEY").unwrap()); //2 + Arc::new(AppState { supabase_client: client }) + } +} #[tokio::main] async fn main() { - let shared_state = Db::default(); + dotenv().ok(); + let app_state = AppState::create(); let app = Router::new() .route("/", get(ping)) @@ -26,7 +60,7 @@ async fn main() { .allow_headers(Any) .allow_methods(Any), ) - .with_state(Arc::clone(&shared_state)); + .with_state(Arc::clone(&app_state)); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); @@ -41,49 +75,81 @@ async fn ping() -> &'static str { } async fn save_user_progress( - State(db): State, - Json(input): Json + State(app_state): State>, + Json(input): Json ) -> impl IntoResponse { - let board: Board = input.board; - match db.write() { - Ok(mut db_instance) => { - db_instance.insert("test".to_string(), board); - (StatusCode::CREATED, Json(ProgressResponse{ - success: true, - board: Some(board) - })) - }, - Err(err) => { - println!("Error: {}", err); - (StatusCode::INTERNAL_SERVER_ERROR, Json(ProgressResponse{ - success: false, - board: None - })) + match input.uuid { + Some(uuid) => { + let client = &app_state.supabase_client; + let board_str = serde_json::to_string(&input.board).unwrap(); + let s = format!("[{{ \"uuid\": \"{}\", \"progress\": {} }}]", + uuid, + board_str); + match client + .from("user_progress") + .upsert(s) + .on_conflict("uuid") + .execute() + .await { + Ok(_) => { + (StatusCode::OK, Json(SaveProgressResponse { + success: true, + })) + }, + Err(_) => { + get_default_save_response() + }, } + } + None => { + get_default_save_response() + }, } } -async fn get_user_progress(State(db): State) -> impl IntoResponse { - match db.read() { - Ok(db_instance) => { - let board: Option = db_instance.get("test").cloned(); - match board { - Some(b) => (StatusCode::OK, Json(ProgressResponse { - success: true, - board: Some(b) - })), - None => (StatusCode::NOT_FOUND, Json(ProgressResponse{ - success: false, - board: None - })) - } - }, - Err(err) => { - println!("Error: {}", err); - (StatusCode::INTERNAL_SERVER_ERROR, Json(ProgressResponse{ - success: false, - board: None - })) +fn get_default_save_response() -> (StatusCode, Json) { + (StatusCode::OK, Json(SaveProgressResponse { + success: false, + })) +} + +async fn get_user_progress( + Query(q): Query, + State(app_state): State>, +) -> impl IntoResponse { + match q.uuid { + Some(uuid) => { + let client = &app_state.supabase_client; + match client + .from("user_progress") + .select("*") + .eq("uuid", uuid) + .execute() + .await { + Ok(res) => match res.text().await { + Ok(resp) => { + + let board_data: DbBoard = serde_json::from_str(&resp).unwrap(); + if board_data.len() == 0 { + return get_default_progress(); + } + (StatusCode::OK, Json(GetProgressResponse { + success: true, + board: Some(board_data[0].progress), + })) + }, + Err(_) => get_default_progress(), + }, + Err(_) => get_default_progress(), + } } - } -} \ No newline at end of file + None => get_default_progress(), + } +} + +fn get_default_progress() -> (StatusCode, Json) { + (StatusCode::OK, Json(GetProgressResponse { + success: true, + board: Some(get_initial_board_data()), + })) +} diff --git a/packages/server/src/sql/schema.sql b/packages/server/src/sql/schema.sql new file mode 100644 index 0000000..9bdf018 --- /dev/null +++ b/packages/server/src/sql/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE `user_progress` ( + `uuid` VARCHAR(36) NOT NULL, + `progress` JSON, + PRIMARY KEY (`uuid`) +); + +CREATE TABLE `user_score` ( + `name` VARCHAR(36) NOT NULL, + `score` INT NOT NULL +); \ No newline at end of file diff --git a/packages/shared/Cargo.toml b/packages/shared/Cargo.toml index 81e7610..f3a6af7 100644 --- a/packages/shared/Cargo.toml +++ b/packages/shared/Cargo.toml @@ -11,4 +11,5 @@ getrandom = { version = "0.2", features = ["js"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" fermi = "0.3.0" -log = "0.4" \ No newline at end of file +log = "0.4" +uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] } diff --git a/packages/shared/src/logic.rs b/packages/shared/src/logic.rs index 19d8b77..1a610cc 100644 --- a/packages/shared/src/logic.rs +++ b/packages/shared/src/logic.rs @@ -1,5 +1,6 @@ use rand::seq::SliceRandom; use rand::Rng; +use uuid::Uuid; use crate::types::{Board, GameStatus}; pub fn get_initial_board_data () -> [[i32; 4]; 4] { @@ -173,3 +174,8 @@ pub fn check_and_do_next (board_status: &Board) -> GameStatus { } GameStatus::Playing } + +pub fn get_uuid () -> String { + let uuid = Uuid::new_v4(); + uuid.to_string() +} \ No newline at end of file diff --git a/packages/shared/src/translate.rs b/packages/shared/src/translate.rs index b546239..2e4a65e 100644 --- a/packages/shared/src/translate.rs +++ b/packages/shared/src/translate.rs @@ -1,5 +1,6 @@ -use std::{collections::HashMap}; +use std::collections::HashMap; use fermi::*; + pub struct Translator { default_language: String, translations: HashMap<(String, String), String>, @@ -20,14 +21,6 @@ impl Translator { } } - pub fn set_language(&mut self, language: String) { - self.default_language = language; - } - - pub fn get_language (&self) -> String { - self.default_language.clone() - } - fn insert(&mut self, key: (String, String), value: String) { self.translations.insert(key, value); } diff --git a/packages/shared/src/types.rs b/packages/shared/src/types.rs index 4716dce..ad677a1 100644 --- a/packages/shared/src/types.rs +++ b/packages/shared/src/types.rs @@ -1,4 +1,5 @@ -use serde::{Serialize, Deserialize}; +use serde::{Serialize, de, Deserialize, Deserializer}; +use std::{fmt, str::FromStr}; pub type Row = [i32; 4]; pub type Board = [Row; 4]; @@ -11,13 +12,48 @@ pub enum GameStatus { } #[derive(Debug, Serialize, Deserialize)] -pub struct ProgressResponse { +pub struct GetProgressResponse { pub success: bool, pub board: Option, } +#[derive(Debug, Serialize, Deserialize)] +pub struct SaveProgressResponse { + pub success: bool, +} + #[derive(Debug, Serialize, Deserialize)] pub struct ProgressReqeust { + pub uuid: Option, pub board: Board, } + +#[derive(Debug, Serialize, Deserialize)] +pub struct UuidQuery { + #[serde(default, deserialize_with = "empty_string_as_none")] + pub uuid: Option +} + +fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: FromStr, + T::Err: fmt::Display, +{ + let opt = Option::::deserialize(de)?; + match opt.as_deref() { + None | Some("") => Ok(None), + Some(s) => FromStr::from_str(s).map_err(de::Error::custom).map(Some), + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DbBoardRow { + pub id: i32, + pub uuid: String, + pub progress: Board, + } + +pub type DbBoard = Vec; + \ No newline at end of file