diff --git a/Cargo.lock b/Cargo.lock index c4aea3e8..664f7fe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -606,8 +606,10 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets", ] @@ -661,7 +663,7 @@ dependencies = [ "nom", "pathdiff", "serde", - "toml", + "toml 0.5.11", ] [[package]] @@ -730,6 +732,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -904,6 +916,33 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "flate2" version = "1.0.27" @@ -1077,7 +1116,28 @@ dependencies = [ "futures-channel", "futures-core", "futures-sink", - "gloo-utils", + "gloo-utils 0.1.7", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http", "js-sys", "pin-project", "serde", @@ -1101,6 +1161,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "h2" version = "0.3.21" @@ -1236,6 +1309,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.57" @@ -1261,20 +1347,19 @@ dependencies = [ [[package]] name = "icondata" -version = "0.0.7" +version = "0.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffb16cf8cbf74c05967c0c12b492a10ad23d3511abdc77bc43c142004e3d435" +checksum = "f41f2deec9249d16ef6b1a8442fbe16013f67053797052aa0b7d2f5ebd0f0098" dependencies = [ "icondata_ch", "icondata_core", - "icondata_macros", ] [[package]] name = "icondata_ch" -version = "0.0.7" +version = "0.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b4861ce5433667d58467d23805e427692882d260a65f42ab4be0ff0701e043c" +checksum = "80ec6aec272bdbadfd83f0f3b61ae4bd4ba5a4aa4cea1b6759923fe3bfb26ff5" dependencies = [ "icondata_core", ] @@ -1285,12 +1370,6 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1640a4c1d5ddd08ab1d9854ffa7a2fa3dc52339492676b6d3031e77ca579f434" -[[package]] -name = "icondata_macros" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec327fa14753d8bb89a56159d617b2eee49a7d1df6575bf9e8ef524009298395" - [[package]] name = "ident_case" version = "1.0.1" @@ -1429,27 +1508,30 @@ dependencies = [ "console_error_panic_hook", "console_log", "getrandom", - "gloo-net", + "gloo-net 0.4.0", "lemmy_api_common", "leptos", "leptos_actix", + "leptos_i18n", "leptos_icons", "leptos_meta", "leptos_router", "log", + "reqwest", "serde", "serde_json", "serde_urlencoded", "thiserror", "tracing", "wasm-bindgen", + "wasm-cookies", "web-sys", ] [[package]] name = "lemmy_api_common" version = "0.19.0-rc.1" -source = "git+https://github.com/LemmyNet/lemmy.git#d45a2a644185171dafb57ed4ade876f3cad191af" +source = "git+https://github.com/LemmyNet/lemmy.git?tag=0.19.0-rc.1#9275041f4251c72e8387a6a180256252df001b89" dependencies = [ "anyhow", "getrandom", @@ -1466,7 +1548,7 @@ dependencies = [ [[package]] name = "lemmy_db_schema" version = "0.19.0-rc.1" -source = "git+https://github.com/LemmyNet/lemmy.git#d45a2a644185171dafb57ed4ade876f3cad191af" +source = "git+https://github.com/LemmyNet/lemmy.git?tag=0.19.0-rc.1#9275041f4251c72e8387a6a180256252df001b89" dependencies = [ "async-trait", "chrono", @@ -1484,7 +1566,7 @@ dependencies = [ [[package]] name = "lemmy_db_views" version = "0.19.0-rc.1" -source = "git+https://github.com/LemmyNet/lemmy.git#d45a2a644185171dafb57ed4ade876f3cad191af" +source = "git+https://github.com/LemmyNet/lemmy.git?tag=0.19.0-rc.1#9275041f4251c72e8387a6a180256252df001b89" dependencies = [ "lemmy_db_schema", "serde", @@ -1494,7 +1576,7 @@ dependencies = [ [[package]] name = "lemmy_db_views_actor" version = "0.19.0-rc.1" -source = "git+https://github.com/LemmyNet/lemmy.git#d45a2a644185171dafb57ed4ade876f3cad191af" +source = "git+https://github.com/LemmyNet/lemmy.git?tag=0.19.0-rc.1#9275041f4251c72e8387a6a180256252df001b89" dependencies = [ "chrono", "lemmy_db_schema", @@ -1505,7 +1587,7 @@ dependencies = [ [[package]] name = "lemmy_db_views_moderator" version = "0.19.0-rc.1" -source = "git+https://github.com/LemmyNet/lemmy.git#d45a2a644185171dafb57ed4ade876f3cad191af" +source = "git+https://github.com/LemmyNet/lemmy.git?tag=0.19.0-rc.1#9275041f4251c72e8387a6a180256252df001b89" dependencies = [ "lemmy_db_schema", "serde", @@ -1613,10 +1695,38 @@ dependencies = [ "walkdir", ] +[[package]] +name = "leptos_i18n" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1f5938c4bd20bb46de24440313112c199e6fec01938d80e7c214d21f1d0d4d" +dependencies = [ + "actix-web", + "leptos", + "leptos_i18n_macro", + "leptos_meta", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_i18n_macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb8a9510418389133f50411219df4e72e0ff25e5e743acb2f35e2e98a2b67aad" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.37", + "toml 0.7.8", +] + [[package]] name = "leptos_icons" -version = "0.0.16-rc3" -source = "git+https://github.com/Carlosted/leptos-icons.git#120961e8863dbc4e4faaa3e9697f6bb8274c1528" +version = "0.1.0" +source = "git+https://github.com/Carlosted/leptos-icons.git#2fc9423f7239c6b1333dadfb4a3dd98f5fb2dbf2" dependencies = [ "icondata", "leptos", @@ -1710,7 +1820,7 @@ dependencies = [ "cached", "cfg-if", "common_macros", - "gloo-net", + "gloo-net 0.2.6", "js-sys", "lazy_static", "leptos", @@ -1764,6 +1874,12 @@ dependencies = [ "serde_test", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" + [[package]] name = "local-channel" version = "0.1.4" @@ -1831,9 +1947,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.3" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "mime" @@ -1878,6 +1994,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -1938,6 +2072,12 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "openssl-sys" version = "0.9.93" @@ -2244,9 +2384,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.20" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "base64", "bytes", @@ -2257,17 +2397,21 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-tls", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", + "tokio-native-tls", "tower-service", "url", "wasm-bindgen", @@ -2354,6 +2498,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustls" version = "0.21.7" @@ -2397,6 +2554,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2419,6 +2585,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.0.1" @@ -2484,6 +2673,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + [[package]] name = "serde_test" version = "1.0.176" @@ -2542,7 +2740,7 @@ checksum = "29eefae61211e81059a092a3428612c475a3a28e0ea4fb3fd49b0a940d837f84" dependencies = [ "ciborium", "const_format", - "gloo-net", + "gloo-net 0.2.6", "inventory", "js-sys", "lazy_static", @@ -2720,12 +2918,46 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.49" @@ -2806,6 +3038,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-openssl" version = "0.6.3" @@ -2841,6 +3083,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.0.2", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -2992,6 +3268,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a1f0175e03a0973cf4afd476bef05c26e228520400eb1fd473ad417b1c00ffb" + [[package]] name = "utf8-width" version = "0.1.6" @@ -3111,6 +3393,19 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +[[package]] +name = "wasm-cookies" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "792b7c4fd62cde421f0e757620053efa7b0c23441c05c6ec02baf96cd74973be" +dependencies = [ + "chrono", + "js-sys", + "urlencoding", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.64" @@ -3227,6 +3522,15 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "winnow" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 54d845ea..c87afda7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,40 +6,53 @@ edition = "2021" [lib] crate-type = ["cdylib", "rlib"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -leptos = { version = "0.5.0", features = ["nightly"] } -leptos_actix = { version = "0.5.0", optional = true } -leptos_meta = { version = "0.5.0", features = ["nightly"] } -leptos_router = { version = "0.5.0", features = ["nightly"] } +leptos = { version = "0.5", features = ["nightly"] } +leptos_actix = { version = "0.5", optional = true } +leptos_meta = { version = "0.5", features = ["nightly"] } +leptos_router = { version = "0.5", features = ["nightly"] } leptos_icons = { git = "https://github.com/Carlosted/leptos-icons.git", features = [ - "macros", + # "macros", "ChBell", "ChSearch", "ChMenuHamburger", + "ChHeart", "ChEye", "ChEyeSlash", ] } +leptos_i18n = { version = "0.2.0-rc" } + serde = { version = "1", features = ["derive"] } -serde_json = "1.0.107" +serde_json = "1" web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] } -gloo-net = { version = "0.2", features = ["http", "json"] } +gloo-net = { version = "0.4", features = ["http"] } log = "0.4" cfg-if = "1.0" tracing = "0.1" -getrandom = { version = "0.2.10", features = ["js"] } -lemmy_api_common = { git = "https://github.com/LemmyNet/lemmy.git", default-features = false } +reqwest = { version = "0.11", features = ["json"] } +getrandom = { version = "0.2", features = ["js"] } +lemmy_api_common = { tag = "0.19.0-rc.1", git = "https://github.com/LemmyNet/lemmy.git", default-features = false } + serde_urlencoded = "0.7" thiserror = "1.0" -wasm-bindgen = "0.2" -console_log = "1" -console_error_panic_hook = "0.1" + +# dependecies for client (enable when csr or hydrate set) +wasm-bindgen = { version = "0.2", optional = true } +console_log = { version = "1", optional = true } +console_error_panic_hook = { version = "0.1", optional = true } +wasm-cookies = "0.2" + +# dependecies for server (enable when ssr set) actix-files = { version = "0.6", optional = true } actix-web = { version = "4", features = ["macros"], optional = true } -actix-proxy = { version = "0.2.0", optional = true } -awc = { version = "3.2.0", optional = true } -async-trait = "0.1.73" +actix-proxy = { version = "0.2", optional = true } +awc = { version = "3.2", optional = true } +async-trait = "0.1" + + +[package.metadata.leptos-i18n] +default = "en" +locales = ["en", "fr"] [features] default = ["csr"] @@ -47,6 +60,7 @@ hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] ssr = [ "leptos/ssr", + "leptos_i18n/actix", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_actix", diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 00000000..63e1889e --- /dev/null +++ b/locales/en.json @@ -0,0 +1,20 @@ +{ + "nav_communities": "Communities", + "nav_create_post": "Create post", + "nav_create_community": "Create community", + "nav_donate": "Donate", + + "nav_search": "Search", + "nav_login": "Login", + "nav_signup": "Sign Up", + "nav_unread_messages": "unread messages", + + "nav_profile": "Profile", + "nav_settings": "Settings", + "nav_logout": "Logout", + + "nav_modlog": "Modlog", + "nav_instances": "Instances", + "nav_docs": "Docs", + "nav_code": "Code" +} diff --git a/locales/fr.json b/locales/fr.json new file mode 100644 index 00000000..63e1889e --- /dev/null +++ b/locales/fr.json @@ -0,0 +1,20 @@ +{ + "nav_communities": "Communities", + "nav_create_post": "Create post", + "nav_create_community": "Create community", + "nav_donate": "Donate", + + "nav_search": "Search", + "nav_login": "Login", + "nav_signup": "Sign Up", + "nav_unread_messages": "unread messages", + + "nav_profile": "Profile", + "nav_settings": "Settings", + "nav_logout": "Logout", + + "nav_modlog": "Modlog", + "nav_instances": "Instances", + "nav_docs": "Docs", + "nav_code": "Code" +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 00000000..52f15a01 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,282 @@ +use crate::errors::LemmyAppError; +use cfg_if::cfg_if; +use leptos::Serializable; +use serde::Serialize; +use serde_json::Value; + +const ENDPOINT: &str = "https://voyager.lemmy.ml/api/v3"; + +pub enum HttpType { + Get, + Post, + Put, +} + +fn json_deser_err(json: &str) -> String { + serde_json::from_str(json) + .map(|v: Value| v["error"].as_str().unwrap_or("Unknown").to_string()) + .unwrap_or("Unknown".to_string()) +} + +pub async fn api_wrapper( + type_: HttpType, + path: &str, + form: &Form, +) -> Result +where + Response: Serializable, + Form: Serialize + std::fmt::Debug, +{ + let route = &build_route(path); + #[allow(clippy::needless_late_init)] + let json; + + cfg_if! { + if #[cfg(feature = "ssr")] { + let client = reqwest::Client::new(); + + let mut request_builder = match type_ { + HttpType::Get => client.get(&build_fetch_query(route, form)), + HttpType::Post => client.post(route), + HttpType::Put => client.put(route), + }; + + match get_cookie_wrapper("jwt").await { + Ok(Some(jwt)) => { + request_builder = request_builder.header("Authorization", &format!("Bearer {}", jwt)[..]); + }, + _ => { + }, + }; + + json = match type_ { + HttpType::Get => request_builder.send().await?.text().await?, + HttpType::Post => request_builder.json(form).send().await?.text().await?, + HttpType::Put => request_builder.json(form).send().await?.text().await?, + }; + } else { + + + let abort_controller = web_sys::AbortController::new().ok(); + let abort_signal = abort_controller.as_ref().map(|a| a.signal()); + + let mut request_builder = match type_ { + HttpType::Get => gloo_net::http::Request::get(&build_fetch_query(route, form)), + HttpType::Post => gloo_net::http::Request::post(route), + HttpType::Put => gloo_net::http::Request::put(route), + }; + + match get_cookie_wrapper("jwt").await { + Ok(Some(jwt)) => { + request_builder = request_builder.header("Authorization", &format!("Bearer {}", jwt)[..]); + }, + _ => { + }, + }; + + json = match type_ { + HttpType::Get => { + request_builder + .abort_signal(abort_signal.as_ref()) + .send() + .await? + .text() + .await? + } + HttpType::Post => { + request_builder + .abort_signal(abort_signal.as_ref()) + .json(form)? + .send() + .await? + .text() + .await? + } + HttpType::Put => { + request_builder + .abort_signal(abort_signal.as_ref()) + .json(form)? + .send() + .await? + .text() + .await? + } + }; + + leptos::on_cleanup( move || { + if let Some(abort_controller) = abort_controller { + abort_controller.abort() + } + }); + } + } + + // Return the error response json as an error + Response::de(&json).map_err(|_| LemmyAppError::APIError { + error: json_deser_err(&json), + }) +} + +fn build_route(route: &str) -> String { + format!("{ENDPOINT}/{route}") +} + +fn build_fetch_query(path: &str, form: T) -> String { + let form_str = serde_urlencoded::to_string(&form).unwrap_or(path.to_string()); + format!("{path}?{form_str}") +} + +#[cfg(not(feature = "ssr"))] +pub async fn get_cookie_wrapper(name: &str) -> Result, LemmyAppError> { + use crate::wasm_bindgen::JsCast; + use leptos::window; + + let cookie_string = window() + .document() + .ok_or(LemmyAppError::APIError { + error: String::from("DOM document is None"), + })? + .dyn_into::() + .map_err(|_| LemmyAppError::APIError { + error: String::from("DOM document could not be cast"), + })? + .cookie() + .map_err(|_| LemmyAppError::APIError { + error: String::from("Could not get cookie string"), + })?; + + if let Ok(value) = wasm_cookies::cookies::get(&cookie_string, name) + .ok_or(LemmyAppError::APIError { + error: String::from("DOM cookie is None"), + })? + .map_err(|e| LemmyAppError::APIError { + error: e.to_string(), + }) + { + Ok(Some(value)) + } else { + Ok(None) + } +} + +#[cfg(not(feature = "ssr"))] +pub async fn set_cookie_wrapper(path: &str, value: &str) -> Result<(), LemmyAppError> { + use crate::wasm_bindgen::JsCast; + use leptos::window; + use wasm_cookies::CookieOptions; + + let r = window() + .document() + .ok_or(LemmyAppError::APIError { + error: String::from("DOM document is None"), + })? + .dyn_into::() + .map_err(|_| LemmyAppError::APIError { + error: String::from("DOM document could not be cast"), + })? + .set_cookie(&wasm_cookies::cookies::set(path, value, &CookieOptions::default())[..]) + .map_err(|_| LemmyAppError::APIError { + error: String::from("Cookie could not be set"), + }); + + r +} + +#[cfg(not(feature = "ssr"))] +pub async fn remove_cookie_wrapper(path: &str) -> Result<(), LemmyAppError> { + use crate::wasm_bindgen::JsCast; + use leptos::window; + + let r = window() + .document() + .ok_or(LemmyAppError::APIError { + error: String::from("DOM document is None"), + })? + .dyn_into::() + .map_err(|_| LemmyAppError::APIError { + error: String::from("DOM document could not be cast"), + })? + .set_cookie(&wasm_cookies::cookies::delete(path)) + .map_err(|_| LemmyAppError::APIError { + error: String::from("Cookie could not be set"), + }); + + r +} + +#[cfg(feature = "ssr")] +pub async fn set_cookie_wrapper(path: &str, value: &str) -> Result<(), LemmyAppError> { + use actix_web::{ + cookie::{ + time::{Duration, OffsetDateTime}, + Cookie, + }, + http::{header, header::HeaderValue}, + }; + use leptos::expect_context; + use leptos_actix::ResponseOptions; + + let response = expect_context::(); + + let mut cookie = Cookie::build(path, value).finish(); + let mut now = OffsetDateTime::now_utc(); + now += Duration::weeks(1); + cookie.set_expires(now); + cookie.set_path("/"); + + if let Ok(cookie) = HeaderValue::from_str(&cookie.to_string()) { + response.insert_header(header::SET_COOKIE, cookie); + } + + Ok(()) +} + +#[cfg(feature = "ssr")] +pub async fn remove_cookie_wrapper(path: &str) -> Result<(), LemmyAppError> { + use actix_web::{ + cookie::{ + time::{Duration, OffsetDateTime}, + Cookie, + }, + http::{header, header::HeaderValue}, + }; + use leptos::expect_context; + use leptos_actix::ResponseOptions; + + let response = expect_context::(); + + let mut cookie = Cookie::build(path, "").finish(); + let mut now = OffsetDateTime::now_utc(); + now += Duration::weeks(-1); + cookie.set_expires(now); + cookie.set_path("/"); + + if let Ok(cookie) = HeaderValue::from_str(&cookie.to_string()) { + response.insert_header(header::SET_COOKIE, cookie); + } + + Ok(()) +} + +#[cfg(feature = "ssr")] +pub async fn get_cookie_wrapper(path: &str) -> Result, LemmyAppError> { + use actix_web::HttpRequest; + use leptos_actix::extract; + + let path_string = path.to_string().clone(); + + let cookie_value = extract(|req: HttpRequest| async move { + if let Some(c) = req.cookie(&path_string) { + let s = c.clone(); + Some(s.value().to_string()) + } else { + None + } + }) + .await + .map_err(|e| LemmyAppError::APIError { + error: e.to_string(), + })?; + + Ok(cookie_value) +} diff --git a/src/config.rs b/src/config.rs index b1785342..9550d182 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1 +1 @@ -pub const TEST_HOST: &str = "0.0.0.0:8536"; +pub const TEST_HOST: &str = "voyager.lemmy.ml"; diff --git a/src/errors.rs b/src/errors.rs index b2cc992c..d4a25d42 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -33,6 +33,14 @@ impl From for LemmyAppError { } } +impl From for LemmyAppError { + fn from(value: reqwest::Error) -> Self { + Self::APIError { + error: value.to_string(), + } + } +} + impl From for LemmyAppError { fn from(_value: ParseIntError) -> Self { Self::ParamsError diff --git a/src/lemmy_client.rs b/src/lemmy_client.rs index a781d8eb..11259cc6 100644 --- a/src/lemmy_client.rs +++ b/src/lemmy_client.rs @@ -65,30 +65,57 @@ cfg_if! { path: &str, form: &Form, ) -> Result { + use crate::api::get_cookie_wrapper; + let route = &build_route(path); + + let mut request_builder = match method { + HttpType::Get => self.get(route).query(form)?, + HttpType::Post => self.post(route), + HttpType::Put => self.put(route), + }; + + match get_cookie_wrapper("jwt").await { + Ok(Some(jwt)) => { + request_builder = request_builder.insert_header(("Authorization", &format!("Bearer {}", jwt)[..])); + }, + _ => { + }, + }; + + leptos::logging::log!("{:#?}", route); + match method { - HttpType::Get => { - self - .get(route) - .query(form)? - .send() - } - HttpType::Post => self.post(route).send_json(form), - HttpType::Put => self.put(route).send_json(form) + HttpType::Get => request_builder.send(), + HttpType::Post => request_builder.send_json(form), + HttpType::Put => request_builder.send_json(form) }.await?.json::().await.map_err(Into::into) - } + + // match method { + // HttpType::Get => { + // self + // .get(route) + // .query(form)? + // .send() + // } + // HttpType::Post => self.post(route).send_json(form), + // HttpType::Put => self.put(route).send_json(form) + // }.await?.json::().await.map_err(Into::into) + + } } impl LemmyClient for awc::Client {} } else { - use wasm_bindgen::UnwrapThrowExt; + use crate::wasm_bindgen::UnwrapThrowExt; use web_sys::AbortController; use gloo_net::http::Request; + use crate::api::get_cookie_wrapper; pub struct Fetch; #[async_trait(?Send)] - impl private_trait::LemmyClient for Fetch { + impl private_trait::LemmyClient for Fetch { async fn make_request Deserialize<'de>, Form: Serialize>( &self, method: HttpType, @@ -99,32 +126,70 @@ cfg_if! { let abort_controller = AbortController::new().ok(); let abort_signal = abort_controller.as_ref().map(AbortController::signal); - // abort in-flight requests if the Scope is disposed - // i.e., if we've navigated away from this page leptos::on_cleanup( move || { if let Some(abort_controller) = abort_controller { abort_controller.abort() } }); - match method { - HttpType::Get => { - Request::get(&build_fetch_query(path, form)) - .abort_signal(abort_signal.as_ref()) - } - HttpType::Post => { - Request::post(route) - .json(form) - .expect_throw("Could not parse json body") - .abort_signal(abort_signal.as_ref()) - } - HttpType::Put => { - Request::put(route) - .json(form) - .expect_throw("Could not parse json body") - .abort_signal(abort_signal.as_ref()) - } - }.send().await?.json::().await.map_err(Into::into) + let mut request_builder = match method { + HttpType::Get => { + Request::get(&build_fetch_query(path, form)) + .abort_signal(abort_signal.as_ref()) + } + HttpType::Post => { + Request::post(route) + .abort_signal(abort_signal.as_ref()) + } + HttpType::Put => { + Request::put(route) + .abort_signal(abort_signal.as_ref()) + } + }; + + match get_cookie_wrapper("jwt").await { + Ok(Some(jwt)) => { + request_builder = request_builder.header("Authorization", &format!("Bearer {}", jwt)[..]); + }, + _ => { + }, + }; + + match method { + HttpType::Get => { + request_builder.send().await + } + HttpType::Post => { + request_builder.json(form) + .expect_throw("Could not parse json body").send().await + } + HttpType::Put => { + request_builder.json(form) + .expect_throw("Could not parse json body").send().await + } + }?.json::().await.map_err(Into::into) + + // match method { + // HttpType::Get => { + // Request::get(&build_fetch_query(path, form)) + // .abort_signal(abort_signal.as_ref()) + // .send().await + // } + // HttpType::Post => { + // Request::post(route) + // .abort_signal(abort_signal.as_ref()) + // .json(form) + // .expect_throw("Could not parse json body") + // .send().await + // } + // HttpType::Put => { + // Request::put(route) + // .abort_signal(abort_signal.as_ref()) + // .json(form) + // .expect_throw("Could not parse json body") + // .send().await + // } + // }?.json::().await.map_err(Into::into) } } diff --git a/src/lib.rs b/src/lib.rs index d71d956c..ed5bde63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,17 @@ -use crate::ui::components::{ - common::nav::{BottomNav, TopNav}, - home::home_activity::HomeActivity, - login::login_activity::LoginActivity, - post::post_activity::PostActivity, +use crate::{ + i18n::*, + ui::components::{ + common::nav::{BottomNav, TopNav}, + home::home_activity::HomeActivity, + login::login_activity::LoginActivity, + post::post_activity::PostActivity, + }, }; use cfg_if::cfg_if; use leptos::*; use leptos_meta::*; use leptos_router::*; +mod api; pub mod api_service; mod config; mod errors; @@ -15,9 +19,16 @@ mod host; mod lemmy_client; mod ui; +leptos_i18n::load_locales!(); + #[component] pub fn App() -> impl IntoView { provide_meta_context(); + provide_i18n_context(); + + let authenticated = create_rw_signal::(false); + provide_context(authenticated); + let (is_routing, set_is_routing) = create_signal(false); view! { @@ -27,21 +38,39 @@ pub fn App() -> impl IntoView { + // adding `set_is_routing` causes the router to wait for async data to load on new pages <Router set_is_routing> - // shows a progress bar while async data are loading - <RoutingProgress is_routing max_time=std::time::Duration::from_millis(250)/> - <TopNav/> - <Routes> - <Route path="home" view=HomeActivity/> - <Route path="" view=HomeActivity/> - <Route path="login" view=LoginActivity/> - <Route path="post/:id" view=PostActivity/> - // <Route path="stories/:id" view=Story/> - // <Route path=":stories?" view=Stories/> - </Routes> - <BottomNav/> + <div class="flex flex-col h-screen"> + <RoutingProgress is_routing max_time=std::time::Duration::from_millis(250)/> + <TopNav/> + <main class="container mx-auto"> + <Routes> + <Route path="" view=HomeActivity ssr=SsrMode::Async/> + <Route path="home" view=HomeActivity ssr=SsrMode::Async/> + + <Route path="communities" view=HomeActivity ssr=SsrMode::Async/> + <Route path="create_post" view=HomeActivity ssr=SsrMode::Async/> + <Route path="create_community" view=HomeActivity ssr=SsrMode::Async/> + + <Route path="search" view=HomeActivity ssr=SsrMode::Async/> + <Route path="login" view=LoginActivity ssr=SsrMode::Async/> + <Route path="signup" view=LoginActivity ssr=SsrMode::Async/> + + <Route path="inbox" view=HomeActivity ssr=SsrMode::Async/> + <Route path="u/:id" view=HomeActivity ssr=SsrMode::Async/> + <Route path="settings" view=HomeActivity ssr=SsrMode::Async/> + <Route path="logout" view=HomeActivity ssr=SsrMode::Async/> + + <Route path="modlog" view=HomeActivity ssr=SsrMode::Async/> + <Route path="instances" view=HomeActivity ssr=SsrMode::Async/> + + <Route path="post/:id" view=PostActivity ssr=SsrMode::Async/> + </Routes> + </main> + <BottomNav/> + </div> </Router> } } @@ -53,8 +82,6 @@ cfg_if! { #[wasm_bindgen] pub fn hydrate() { - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); leptos::mount_to_body(App); } } diff --git a/src/main.rs b/src/main.rs index da3ff78d..3dfbeee3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,9 +63,6 @@ cfg_if! { fn main() { use lemmy_ui_leptos::App; use leptos::*; - - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); mount_to_body(App) } } diff --git a/src/ui/components/common/nav.rs b/src/ui/components/common/nav.rs index 45cde413..d9b44524 100644 --- a/src/ui/components/common/nav.rs +++ b/src/ui/components/common/nav.rs @@ -1,61 +1,231 @@ -use leptos::{component, view, IntoView}; +use crate::{api::get_cookie_wrapper, i18n::*}; +#[cfg(feature = "ssr")] +use leptos::IntoAttribute; +use leptos::{ + component, + create_effect, + create_resource, + create_rw_signal, + create_server_action, + server, + use_context, + view, + ErrorBoundary, + IntoView, + RwSignal, + ServerFnError, + SignalGet, + SignalSet, + Suspense, +}; use leptos_icons::*; use leptos_router::*; +#[server(LogoutFormFn, "/serverfn")] +pub async fn logout_form_fn() -> Result<(), ServerFnError> { + use crate::api::remove_cookie_wrapper; + use leptos_actix::redirect; + + // redirect("/"); + + match remove_cookie_wrapper("jwt").await { + Ok(o) => Ok(o), + Err(e) => Err(ServerFnError::ServerError(e.to_string())), + } +} + #[component] pub fn TopNav() -> impl IntoView { + let i18n = use_i18n(); + + let authenticated = use_context::<RwSignal<bool>>().unwrap_or(create_rw_signal(false)); + + let auth_resource = create_resource( + || (), + move |()| async move { + match get_cookie_wrapper("jwt").await { + Ok(Some(_jwt)) => { + authenticated.set(true); + true + } + Ok(None) => { + authenticated.set(false); + false + } + Err(_e) => { + authenticated.set(false); + false + } + } + }, + ); + + let logout_form_action = create_server_action::<LogoutFormFn>(); + + create_effect(move |_| match logout_form_action.value().get() { + None => {} + Some(Ok(_o)) => { + authenticated.set(false); + let navigate = leptos_router::use_navigate(); + navigate("/", Default::default()); + } + Some(Err(_e)) => {} + }); + view! { - <div class="navbar bg-base-300"> + <nav class="container navbar mx-auto"> <div class="navbar-start"> - <div class="dropdown"> - <label tabindex="0" class="btn btn-ghost btn-circle"> - <Icon icon=Icon::from(ChIcon::ChMenuHamburger) width="1.25rem" height="1.25rem"/> - </label> - <ul - tabindex="0" - class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52" - > - <li> - <a>"Homepage"</a> - </li> - </ul> - </div> - </div> - <div class="navbar-center"> - <a class="btn btn-ghost normal-case text-xl">"Lemmy"</a> + <ul class="menu menu-horizontal flex-nowrap"> + <li> + <A href="/" class="text-xl whitespace-nowrap"> + "Brand from env" + </A> + </li> + <li> + <A href="/communities" class="text-md"> + {t!(i18n, nav_communities)} + </A> + </li> + <li> + <A href="/create_post" class="text-md"> + {t!(i18n, nav_create_post)} + </A> + </li> + <li> + <A href="/create_community" class="text-md"> + {t!(i18n, nav_create_community)} + </A> + </li> + <li> + <a href="//join-lemmy.org/donate"> + <span title=t!(i18n, nav_donate)> + <Icon icon=Icon::from(ChIcon::ChHeart) class="h-6 w-6"/> + </span> + </a> + </li> + </ul> </div> - <div class="navbar-end gap-3"> - <button class="btn btn-ghost btn-circle"> - <Icon icon=Icon::from(ChIcon::ChSearch) width="1.25rem" height="1.25rem"/> - </button> - <button class="btn btn-ghost btn-circle"> - <div class="indicator"> - <Icon icon=Icon::from(ChIcon::ChBell) width="1.25rem" height="1.25rem"/> - <span class="badge badge-xs badge-primary indicator-item"></span> - </div> - </button> - <A href="/login">"Login"</A> + <div class="navbar-end"> + <ul class="menu menu-horizontal flex-nowrap"> + <li> + <A href="/search"> + <span title=t!(i18n, nav_search)> + <Icon icon=Icon::from(ChIcon::ChSearch) class="h-6 w-6"/> + </span> + </A> + </li> + <Suspense fallback=move || { + view! { + <li></li> + <li></li> + } + }> + <ErrorBoundary fallback=|_| { + view! { <p>"Something went wrong"</p> } + }> + {move || { + auth_resource + .get() + .map(move |_| { + if !authenticated.get() { + view! { + <li> + <A href="/login">{t!(i18n, nav_login)}</A> + </li> + <li> + <A href="/signup">{t!(i18n, nav_signup)}</A> + </li> + } + } else { + view! { + <li> + <A href="/inbox"> + <span title=t!(i18n, nav_unread_messages)> + <Icon icon=Icon::from(ChIcon::ChBell) class="h-6 w-6"/> + </span> + </A> + </li> + <li> + <details> + <summary>"User name"</summary> + <ul> + <li> + <A href="/u/jimmy90">{t!(i18n, nav_profile)}</A> + </li> + <li> + <A href="/settings">{t!(i18n, nav_settings)}</A> + </li> + <li> + <hr/> + </li> + <li> + <ActionForm action=logout_form_action> + <button type="submit">{t!(i18n, nav_logout)}</button> + </ActionForm> + </li> + </ul> + </details> + </li> + } + } + }) + }} + + </ErrorBoundary> + </Suspense> + + </ul> </div> - </div> + </nav> } } #[component] pub fn BottomNav() -> impl IntoView { + let i18n = use_i18n(); + view! { - <footer class="sticky bottom-0"> - <div class="btm-nav btm-nav-lg"> - <A href="/" class="active"> - // TODO put svg's here - <span class="btm-nav-label">"Home"</span> - </A> - <button> - <span class="btm-nav-label">"TODO 1"</span> - </button> - <button> - <span class="btm-nav-label">"TODO 2"</span> - </button> + <nav class="container navbar mx-auto"> + <div class="navbar-start"></div> + <div class="navbar-end "> + <ul class="menu menu-horizontal flex-nowrap"> + <li> + <a href="//github.com/LemmyNet/lemmy-ui-leptos/releases" class="text-md"> + "f/e from env" + </a> + </li> + <li> + <a href="//github.com/LemmyNet/lemmy/releases" class="text-md"> + "b/e from env" + </a> + </li> + <li> + <A href="/modlog" class="text-md"> + {t!(i18n, nav_modlog)} + </A> + </li> + <li> + <A href="/instances" class="text-md"> + {t!(i18n, nav_instances)} + </A> + </li> + <li> + <a href="join-lemmy.org/docs/en/index.html" class="text-md"> + {t!(i18n, nav_docs)} + </a> + </li> + <li> + <a href="//github.com/LemmyNet" class="text-md"> + {t!(i18n, nav_code)} + </a> + </li> + <li> + <a href="//join-lemmy.org" class="text-md"> + "join-lemmy.org" + </a> + </li> + </ul> </div> - </footer> + </nav> } } diff --git a/src/ui/components/home/home_activity.rs b/src/ui/components/home/home_activity.rs index d67fdbfb..78740ffe 100644 --- a/src/ui/components/home/home_activity.rs +++ b/src/ui/components/home/home_activity.rs @@ -1,59 +1,90 @@ +// use actix_web::web; +use crate::{ + api::{api_wrapper, HttpType}, + errors::LemmyAppError, + ui::components::post::post_listings::PostListings, +}; +use lemmy_api_common::post::{GetPosts, GetPostsResponse}; use leptos::*; +use leptos_router::use_query_map; -// This is helpful: -// https://github.com/leptos-rs/leptos/blob/main/examples/hackernews/src/routes/stories.rs +pub async fn list_posts(form: &GetPosts) -> Result<GetPostsResponse, LemmyAppError> { + api_wrapper::<GetPostsResponse, GetPosts>(HttpType::Get, "post/list", form).await +} #[component] pub fn HomeActivity() -> impl IntoView { - // let query = use_query_map(); - // let page = move || { - // query - // .with(|q| q.get("page").and_then(|page| page.parse::<i64>().ok())) - // .unwrap_or(1) - // }; - - // let posts = create_resource(page, move |page| async move { - // let form = GetPosts { - // type_: None, - // sort: None, - // community_name: None, - // community_id: None, - // page: Some(page), - // limit: None, - // saved_only: None, - // disliked_only: None, - // liked_only: None, - // // moderator_view: None, - // auth: None, - // }; - // list_posts(&form).await.ok() - // }); - - // let err_msg = " Error loading this post."; + let query = use_query_map(); + let page = move || { + query + .with(|q| q.get("page").and_then(|page| page.parse::<i64>().ok())) + .unwrap_or(1) + }; + + let authenticated = use_context::<RwSignal<bool>>().unwrap_or(create_rw_signal(false)); + + let posts = create_resource( + move || (page(), authenticated()), + move |(page, _authenticated)| async move { + let form = GetPosts { + type_: None, + sort: None, + community_name: None, + community_id: None, + page: Some(page), + limit: None, + saved_only: None, + disliked_only: None, + liked_only: None, + // moderator_view: None, + // auth: None, + page_cursor: None, + }; + + list_posts(&form).await.ok() + // cfg_if! { + // if #[cfg(feature = "ssr")] { + // use crate::{api::set_cookie_wrapper, lemmy_client::LemmyClient}; + // use awc::Client; + // awc::Client::new().list_posts(&form).await.ok() + // } else { + // use crate::lemmy_client::Fetch; + // use crate::lemmy_client::LemmyClient; + // // let c = Fetch::new(); + // // c.list_posts(&form).await.ok() + // let v: Vec<PostView> = vec![]; + // Some(GetPostsResponse { next_page: None, posts: v } ) + // } + // } + }, + ); + + let err_msg = " Error loading this post."; view! { <main class="mx-auto"> <h2 class="p-6 text-4xl">"Home activity"</h2> - // <Suspense fallback=|| { - // view! { "Loading..." } - // }> - // {move || { - // posts() - // .map(|res| match res { - // None => { - // view! { <div>{err_msg}</div> } - // } - // Some(res) => { - // view! { - // <div> - // <PostListings posts=res.posts.into()/> - // </div> - // } - // } - // }) - // }} - - // </Suspense> + <Suspense fallback=|| { + view! { "Loading..." } + }> + {move || { + posts + .get() + .map(|res| match res { + None => { + view! { <div>{err_msg}</div> } + } + Some(res) => { + view! { + <div> + <PostListings posts=res.posts.into()/> + </div> + } + } + }) + }} + + </Suspense> </main> } } diff --git a/src/ui/components/login/login_form.rs b/src/ui/components/login/login_form.rs index 706a0e71..9d37637c 100644 --- a/src/ui/components/login/login_form.rs +++ b/src/ui/components/login/login_form.rs @@ -1,76 +1,119 @@ -use crate::ui::components::common::password_input::PasswordInput; -use leptos::*; +use crate::{ + api::{api_wrapper, HttpType}, + errors::LemmyAppError, +}; +use lemmy_api_common::person::{Login, LoginResponse}; +use leptos::{ev, logging::*, *}; use leptos_router::ActionForm; -#[server(LoginAction, "/serverfn")] -pub async fn login(username_or_email: String, password: String) -> Result<(), ServerFnError> { - use crate::lemmy_client::LemmyClient; +pub async fn login(form: &Login) -> Result<LoginResponse, LemmyAppError> { + api_wrapper::<LoginResponse, Login>(HttpType::Post, "user/login", form).await +} + +#[server(LoginFormFn, "/serverfn")] +pub async fn login_form_fn( + username: String, + password: String, +) -> Result<LoginResponse, ServerFnError> { + use crate::{api::set_cookie_wrapper, lemmy_client::LemmyClient}; use actix_web::web; use awc::Client; use lemmy_api_common::person::Login; - use leptos::logging::log; - use leptos_actix::extract; - - log!("Try to login with {username_or_email}"); + use leptos_actix::{extract, redirect}; let form = Login { - username_or_email: username_or_email.into(), + username_or_email: username.into(), password: password.into(), totp_2fa_token: None, }; + // let result = + // extract(|client: web::Data<Client>| async move { client.login(&form).await }).await?; + let result = login(&form).await; + // redirect("/"); - let res = extract(|client: web::Data<Client>| async move { client.login(&form).await }).await??; - - // TODO figure out how to handle errors - log!("Login res: {:?}", res); - // JWT can be extracted using into_inner() - - log!("jwt: {:?}", res.jwt.unwrap().into_inner()); - - Ok(()) + match result { + Ok(res) => match set_cookie_wrapper("jwt", &res.jwt.clone().unwrap().into_inner()[..]).await { + Ok(_) => Ok(res), + Err(e) => Err(ServerFnError::ServerError(e.to_string())), + }, + Err(err) => Err(ServerFnError::ServerError(err.to_string())), + } } #[component] pub fn LoginForm() -> impl IntoView { let (password, set_password) = create_signal(String::new()); let (name, set_name) = create_signal(String::new()); + let error = create_rw_signal::<Option<String>>(None); + let (disabled, _set_disabled) = create_signal(false); + + let _button_is_disabled = + Signal::derive(move || disabled.get() || password.get().is_empty() || name.get().is_empty()); + + let login_form_action = create_server_action::<LoginFormFn>(); - let button_is_disabled = - Signal::derive(move || password.with(|p| p.is_empty()) || name.with(|n| n.is_empty())); + let authenticated = use_context::<RwSignal<bool>>().unwrap_or(create_rw_signal(false)); - let login = create_server_action::<LoginAction>(); + create_effect(move |_| match login_form_action.value().get() { + None => {} + Some(Ok(_o)) => { + authenticated.set(true); + let navigate = leptos_router::use_navigate(); + navigate("/", Default::default()); + } + Some(Err(e)) => { + error.set(Some(e.to_string())); + } + }); view! { - <ActionForm class="space-y-3" action=login> - // {move || { - // error - // .get() - // .map(|err| { - // view! { <p style="color:red;">{err}</p> } - // }) - // }} - <div class="form-control w-full"> - <label class="label" for="username"> - <span class="label-text">Username</span> - </label> - <input - id="username" - type="text" - required - name="username_or_email" - class="input input-bordered" - placeholder="Username" - on:input=move |ev| set_name.update(|v| *v = event_target_value(&ev)) - /> - </div> + <ActionForm action=login_form_action> + {move || { + error + .get() + .map(|err| { + view! { + <div class="alert shadow-lg"> + <span>{err}</span> + </div> + } + }) + }} + <input + name="username" + type="text" + required + placeholder="Username" + prop:disabled=move || disabled.get() + on:keyup=move |ev: ev::KeyboardEvent| { + let val = event_target_value(&ev); + set_name.update(|v| *v = val); + } - <PasswordInput - id="password" - name="password" - on_input=move |s| set_password.update(|p| *p = s) + on:change=move |ev| { + let val = event_target_value(&ev); + set_name.update(|v| *v = val); + } + + class="input input-bordered" /> + <input + name="password" + type="password" + required + placeholder="Password" + prop:disabled=move || disabled.get() + on:keyup=move |ev: ev::KeyboardEvent| { + match &*ev.key() { + _ => { + let val = event_target_value(&ev); + set_password.update(|p| *p = val); + } + } + } - <button class="btn btn-lg" type="submit" disabled=button_is_disabled> + class="input input-bordered" + /> <button type="submit" class="btn"> "Login" </button> </ActionForm>