diff --git a/Cargo.lock b/Cargo.lock index 535920f..ce36e35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,41 +26,12 @@ dependencies = [ "libc", ] -[[package]] -name = "any_spawner" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d" -dependencies = [ - "futures", - "thiserror 2.0.18", - "tokio", - "wasm-bindgen-futures", -] - [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-once-cell" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" - [[package]] name = "async-trait" version = "0.1.89" @@ -87,36 +58,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "attribute-derive" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" -dependencies = [ - "attribute-derive-macro", - "derive-where", - "manyhow", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "attribute-derive-macro" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" -dependencies = [ - "collection_literals", - "interpolator", - "manyhow", - "proc-macro-utils", - "proc-macro2", - "quote", - "quote-use", - "syn", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -152,7 +93,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", - "base64", "bytes", "form_urlencoded", "futures-util", @@ -172,10 +112,8 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite", "tower", "tower-layer", "tower-service", @@ -201,12 +139,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "base16" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" - [[package]] name = "base64" version = "0.22.1" @@ -263,10 +195,6 @@ dependencies = [ "dotenvy", "futures", "hyper", - "leptos", - "leptos_axum", - "leptos_meta", - "leptos_router", "mime_guess", "minijinja", "reqwest", @@ -311,12 +239,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" - [[package]] name = "cc" version = "1.2.56" @@ -380,23 +302,6 @@ dependencies = [ "cc", ] -[[package]] -name = "codee" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5" -dependencies = [ - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "collection_literals" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" - [[package]] name = "combine" version = "4.6.7" @@ -416,84 +321,12 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "config" -version = "0.15.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" -dependencies = [ - "convert_case 0.6.0", - "pathdiff", - "serde_core", - "toml", - "winnow", -] - [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const-str" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" - -[[package]] -name = "const_format" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "const_str_slice_concat" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b" - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "convert_case" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "convert_case_extras" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589c70f0faf8aa9d17787557d5eae854d7755cac50f5c3d12c81d3d57661cebb" -dependencies = [ - "convert_case 0.11.0", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -578,12 +411,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - [[package]] name = "der" version = "0.7.10" @@ -604,17 +431,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive-where" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "digest" version = "0.10.7" @@ -644,12 +460,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "drain_filter_polyfill" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" - [[package]] name = "dunce" version = "1.0.5" @@ -665,16 +475,6 @@ dependencies = [ "serde", ] -[[package]] -name = "either_of" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f7f86eef3a7e4b9c2107583dbbbe3d9535c4b800796faf1774b82ba22033da" -dependencies = [ - "paste", - "pin-project-lite", -] - [[package]] name = "encoding_rs" version = "0.8.35" @@ -690,12 +490,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "erased" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472" - [[package]] name = "errno" version = "0.3.14" @@ -728,16 +522,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - [[package]] name = "fastrand" version = "2.3.0" @@ -952,46 +736,6 @@ dependencies = [ "wasip3", ] -[[package]] -name = "gloo-net" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" -dependencies = [ - "futures-channel", - "futures-core", - "futures-sink", - "gloo-utils", - "http", - "js-sys", - "pin-project", - "serde", - "serde_json", - "thiserror 1.0.69", - "wasm-bindgen", - "wasm-bindgen-futures", - "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 = "guardian" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" - [[package]] name = "h2" version = "0.4.13" @@ -1076,15 +820,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "html-escape" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" -dependencies = [ - "utf8-width", -] - [[package]] name = "http" version = "1.4.0" @@ -1136,22 +871,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hydration_context" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8714ae4adeaa846d838f380fbd72f049197de629948f91bf045329e0cf0a283" -dependencies = [ - "futures", - "js-sys", - "once_cell", - "or_poisoned", - "pin-project-lite", - "serde", - "throw_error", - "wasm-bindgen", -] - [[package]] name = "hyper" version = "1.8.1" @@ -1369,21 +1088,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "interpolator" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" - -[[package]] -name = "inventory" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" -dependencies = [ - "rustversion", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -1400,15 +1104,6 @@ dependencies = [ "serde", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.17" @@ -1472,228 +1167,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "leptos" -version = "0.8.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b540ac2868724738f0f5d00f00ec4640e587223774219c1baddc46bad46fb8e" -dependencies = [ - "any_spawner", - "base64", - "cfg-if", - "either_of", - "futures", - "getrandom 0.4.2", - "hydration_context", - "leptos_config", - "leptos_dom", - "leptos_hot_reload", - "leptos_macro", - "leptos_server", - "oco_ref", - "or_poisoned", - "paste", - "rand 0.9.2", - "reactive_graph", - "rustc-hash", - "rustc_version", - "send_wrapper", - "serde", - "serde_json", - "serde_qs", - "server_fn", - "slotmap", - "tachys", - "thiserror 2.0.18", - "throw_error", - "typed-builder", - "typed-builder-macro", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm_split_helpers", - "web-sys", -] - -[[package]] -name = "leptos_axum" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196de3f5cde6a4c4cd254bb16dc6abd2efbf46cc3ae1b6c7da0731f77b4bdf61" -dependencies = [ - "any_spawner", - "axum", - "futures", - "hydration_context", - "leptos", - "leptos_integration_utils", - "leptos_macro", - "leptos_meta", - "leptos_router", - "or_poisoned", - "server_fn", - "tachys", - "tokio", - "tower", - "tower-http", -] - -[[package]] -name = "leptos_config" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a2ac32008dda0d657f2147cc33336f4e743e091597db10f7a99d668e92a46d" -dependencies = [ - "config", - "regex", - "serde", - "thiserror 2.0.18", - "typed-builder", -] - -[[package]] -name = "leptos_dom" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35742e9ed8f8aaf9e549b454c68a7ac0992536e06856365639b111f72ab07884" -dependencies = [ - "js-sys", - "or_poisoned", - "reactive_graph", - "send_wrapper", - "tachys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "leptos_hot_reload" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2a0f220c8a5ef3c51199dfb9cdd702bc0eb80d52fbe70c7890adfaaae8a4b1" -dependencies = [ - "anyhow", - "camino", - "indexmap", - "or_poisoned", - "proc-macro2", - "quote", - "rstml", - "serde", - "syn", - "walkdir", -] - -[[package]] -name = "leptos_integration_utils" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c097f89cd9aa606297672f56fa5bdda09f01609a9f4eefaccdbb5ab5afea4279" -dependencies = [ - "futures", - "hydration_context", - "leptos", - "leptos_config", - "leptos_meta", - "leptos_router", - "reactive_graph", -] - -[[package]] -name = "leptos_macro" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712325a77f1d050bf2897061ccaf2b075930aab36954980d658f04452686c474" -dependencies = [ - "attribute-derive", - "cfg-if", - "convert_case 0.11.0", - "convert_case_extras", - "html-escape", - "itertools", - "leptos_hot_reload", - "prettyplease", - "proc-macro-error2", - "proc-macro2", - "quote", - "rstml", - "rustc_version", - "server_fn_macro", - "syn", - "uuid", -] - -[[package]] -name = "leptos_meta" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c3efe657b4c55ed2e078922786ffe20acfb71767c3dd913767b09a35c75c890" -dependencies = [ - "futures", - "indexmap", - "leptos", - "or_poisoned", - "send_wrapper", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "leptos_router" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35058d4096407b8369843b5b5d227588dfd57ecc9e9bda0567523f084dce69e8" -dependencies = [ - "any_spawner", - "either_of", - "futures", - "gloo-net", - "js-sys", - "leptos", - "leptos_router_macro", - "or_poisoned", - "percent-encoding", - "reactive_graph", - "rustc_version", - "send_wrapper", - "tachys", - "thiserror 2.0.18", - "url", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "leptos_router_macro" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "409c0bd99f986c3cfa1a4db2443c835bc602ded1a12784e22ecb28c3ed5a2ae2" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "leptos_server" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da974775c5ccbb6bd64be7f53f75e8321542e28f21563a416574dbe4d5447eae" -dependencies = [ - "any_spawner", - "base64", - "codee", - "futures", - "hydration_context", - "or_poisoned", - "reactive_graph", - "send_wrapper", - "serde", - "serde_json", - "server_fn", - "tachys", -] - [[package]] name = "libc" version = "0.2.183" @@ -1761,29 +1234,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "manyhow" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" -dependencies = [ - "manyhow-macros", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "manyhow-macros" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" -dependencies = [ - "proc-macro-utils", - "proc-macro2", - "quote", -] - [[package]] name = "matchers" version = "0.2.0" @@ -1885,12 +1335,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "next_tuple" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1952,16 +1396,6 @@ dependencies = [ "libm", ] -[[package]] -name = "oco_ref" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d" -dependencies = [ - "serde", - "thiserror 2.0.18", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -2012,12 +1446,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "or_poisoned" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" - [[package]] name = "parking" version = "2.2.1" @@ -2047,18 +1475,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2074,26 +1490,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "pin-project" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -2173,39 +1569,6 @@ dependencies = [ "syn", ] -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "proc-macro-utils" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" -dependencies = [ - "proc-macro2", - "quote", - "smallvec", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -2215,19 +1578,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "version_check", - "yansi", -] - [[package]] name = "quinn" version = "0.11.9" @@ -2286,33 +1636,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "quote-use" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" -dependencies = [ - "quote", - "quote-use-macros", -] - -[[package]] -name = "quote-use-macros" -version = "0.8.4" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ - "proc-macro-utils", "proc-macro2", - "quote", - "syn", ] [[package]] @@ -2386,60 +1714,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "reactive_graph" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35774620b3da884a07341e9e36612e1509b1eb0553ef3bb76f1547dd1b797417" -dependencies = [ - "any_spawner", - "async-lock", - "futures", - "guardian", - "hydration_context", - "indexmap", - "or_poisoned", - "paste", - "pin-project-lite", - "rustc-hash", - "rustc_version", - "send_wrapper", - "serde", - "slotmap", - "thiserror 2.0.18", - "web-sys", -] - -[[package]] -name = "reactive_stores" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e114642d342893571ff40b4e1da8ccdea907be44c649041eb7d8413b3fd95e8" -dependencies = [ - "guardian", - "indexmap", - "itertools", - "or_poisoned", - "paste", - "reactive_graph", - "reactive_stores_macro", - "rustc-hash", - "send_wrapper", -] - -[[package]] -name = "reactive_stores_macro" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b024812c47a6867b6cb32767a46182203f94e59eb88c69b032fd9caffa304ce" -dependencies = [ - "convert_case 0.11.0", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -2458,18 +1732,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - [[package]] name = "regex-automata" version = "0.4.14" @@ -2561,36 +1823,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rstml" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56" -dependencies = [ - "derive-where", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn", - "syn_derive", - "thiserror 2.0.18", -] - [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "1.1.4" @@ -2744,15 +1982,6 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" -dependencies = [ - "futures-core", -] - [[package]] name = "serde" version = "1.0.228" @@ -2807,26 +2036,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_qs" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" -dependencies = [ - "percent-encoding", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "serde_spanned" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2839,71 +2048,6 @@ dependencies = [ "serde", ] -[[package]] -name = "server_fn" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c799cec4e8e210dfb2f203aa97f0e82232c619e385ef4d011b17a58d6397c7b" -dependencies = [ - "axum", - "base64", - "bytes", - "const-str", - "const_format", - "futures", - "gloo-net", - "http", - "http-body-util", - "hyper", - "inventory", - "js-sys", - "or_poisoned", - "pin-project-lite", - "rustc_version", - "rustversion", - "send_wrapper", - "serde", - "serde_json", - "serde_qs", - "server_fn_macro_default", - "thiserror 2.0.18", - "throw_error", - "tokio", - "tower", - "tower-layer", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "xxhash-rust", -] - -[[package]] -name = "server_fn_macro" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1295b54815397d30d986b63f93cfd515fa86d5e528e0bb589ce9d530502f9e0f" -dependencies = [ - "const_format", - "convert_case 0.11.0", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "xxhash-rust", -] - -[[package]] -name = "server_fn_macro_default" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63eb08f80db903d3c42f64e60ebb3875e0305be502bdc064ec0a0eab42207f00" -dependencies = [ - "server_fn_macro", - "syn", -] - [[package]] name = "sha1" version = "0.10.6" @@ -2967,15 +2111,6 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - [[package]] name = "smallvec" version = "1.15.1" @@ -3245,18 +2380,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3298,38 +2421,6 @@ dependencies = [ "libc", ] -[[package]] -name = "tachys" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f768750b0d5514f487772187d4b20c66f56faff4541b1faa5aad4975f5aee085" -dependencies = [ - "any_spawner", - "async-trait", - "const_str_slice_concat", - "drain_filter_polyfill", - "either_of", - "erased", - "futures", - "html-escape", - "indexmap", - "itertools", - "js-sys", - "next_tuple", - "oco_ref", - "or_poisoned", - "paste", - "reactive_graph", - "reactive_stores", - "rustc-hash", - "rustc_version", - "send_wrapper", - "slotmap", - "throw_error", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "tempfile" version = "3.27.0" @@ -3392,15 +2483,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "throw_error" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0ed6038fcbc0795aca7c92963ddda636573b956679204e044492d2b13c8f64" -dependencies = [ - "pin-project-lite", -] - [[package]] name = "time" version = "0.3.47" @@ -3506,18 +2588,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -3531,37 +2601,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.0.9+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" -dependencies = [ - "winnow", -] - [[package]] name = "tower" version = "0.5.3" @@ -3698,43 +2737,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.2", - "sha1", - "thiserror 2.0.18", - "utf-8", -] - -[[package]] -name = "typed-builder" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda" -dependencies = [ - "typed-builder-macro", -] - -[[package]] -name = "typed-builder-macro" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "typenum" version = "1.19.0" @@ -3774,12 +2776,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -3804,18 +2800,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -3982,41 +2966,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasm_split_helpers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a114b3073258dd5de3d812cdd048cca6842342755e828a14dbf15f843f2d1b84" -dependencies = [ - "async-once-cell", - "wasm_split_macros", -] - -[[package]] -name = "wasm_split_macros" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56481f8ed1a9f9ae97ea7b08a5e2b12e8adf9a7818a6ba952b918e09c7be8bf0" -dependencies = [ - "base16", - "quote", - "sha2", - "syn", -] - [[package]] name = "wasmparser" version = "0.244.0" @@ -4361,15 +3310,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -4464,18 +3404,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" -[[package]] -name = "xxhash-rust" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 8f73b98..f5ef608 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,13 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -axum = "0.8" +axum = { version = "0.8", features = ["multipart"] } tower = "0.5" tower-http = { version = "0.6", features = ["cors", "fs", "trace", "validate-request"] } -leptos = "0.8" -leptos_axum = "0.8" -leptos_meta = "0.8" -leptos_router = "0.8" sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-native-tls", "postgres", "uuid", "json", "chrono"] } bcrypt = "0.19" minijinja = "2" diff --git a/admin.html b/admin.html index 835b0dd..e55fea4 100644 --- a/admin.html +++ b/admin.html @@ -47,6 +47,64 @@ setTimeout(() => toast.remove(), 3000); } + // WYSIWYG Editor Functions + function getWysiwygToolbar(blockId) { + return ` +
+ + + + + + + + + + + + + + +
+ `; + } + + function formatText(command, blockId, value = null) { + const editor = document.getElementById(`editor-${blockId}`); + if (!editor) return; + + editor.focus(); + + if (command === 'formatBlock' && value) { + document.execCommand(command, false, value); + } else if (command === 'createLink') { + const url = prompt('Enter URL:'); + if (url) { + document.execCommand(command, false, url); + } + } else { + document.execCommand(command, false, value); + } + + // Update the block content + updateBlockFromEditor(blockId); + } + + function promptLink(blockId) { + const url = prompt('Enter URL:'); + if (url) { + formatText('createLink', blockId, url); + } + } + + function updateBlockFromEditor(blockId) { + const editor = document.getElementById(`editor-${blockId}`); + if (editor) { + const content = editor.innerHTML; + updateBlock(blockId, content); + } + } + // Escape HTML to prevent XSS function escapeHtml(str) { if (!str) return ''; @@ -359,6 +417,29 @@

${escapeHtml(site.name)}

} catch (err) { showToast('Error: ' + err.message, 'error'); } } + async function deployPages() { + if (!currentSite) return; + try { + showToast('Deploying to Cloudflare Pages...'); + const res = await fetch(API_BASE + '/api/sites/' + currentSite + '/deploy', { + method: 'POST', + headers: { 'Authorization': 'Bearer ' + authToken } + }); + const data = await res.json(); + if (res.ok) { + const match = data.message.match(/https:\/\/[^\s]+/); + const url = match ? match[0] : null; + if (url) { + showToast(`Deployed! ${url}`); + } else { + showToast(data.message || 'Deployed successfully!'); + } + } else { + showToast(data.message || 'Deploy failed', 'error'); + } + } catch (err) { showToast('Error: ' + err.message, 'error'); } + } + async function createSite(e) { e.preventDefault(); const name = document.getElementById('site-name').value; @@ -412,23 +493,6 @@

${escapeHtml(site.name)}

} } catch (err) { showToast('Error: ' + err.message, 'error'); } } - - async function deployPages() { - if (!currentSite) return; - try { - showToast('Deploying to Cloudflare Pages...'); - const res = await fetch(API_BASE + '/api/sites/' + currentSite + '/deploy', { - method: 'POST', - headers: { 'Authorization': 'Bearer ' + authToken } - }); - const data = await res.json(); - if (res.ok) { - showToast(data.message || 'Deployed successfully!'); - } else { - showToast(data.message || 'Failed to deploy', 'error'); - } - } catch (err) { showToast('Error: ' + err.message, 'error'); } - } function logout() { currentUser = null; @@ -453,7 +517,7 @@

${escapeHtml(site.name)}

`; } else if (block.type === 'heading') { @@ -944,7 +1012,7 @@

${escapeHtml(post.title)}

// Parse blocks from content if (post.content && post.content.length > 0) { blocks = post.content.map((b, i) => { - const blockType = b.block_type || 'paragraph'; + const blockType = b.type || b.block_type || 'paragraph'; let content = ''; if (blockType === 'hero') { content = b.content || { title: '', subtitle: '', backgroundImage: '', ctaText: '', ctaLink: '' }; @@ -952,6 +1020,8 @@

${escapeHtml(post.title)}

content = b.content || { url: '', caption: '' }; } else if (blockType === 'columns') { content = b.content || { left: '', right: '', leftImage: '', rightImage: '' }; + } else if (typeof b.content === 'string') { + content = b.content; } else { content = b.content?.text || b.content?.url || b.content?.href || ''; } @@ -976,20 +1046,23 @@

${escapeHtml(post.title)}

function buildContentFromBlocks() { return blocks.map(b => { if (b.type === 'paragraph') { - return { block_type: 'paragraph', content: { text: b.content } }; + const content = typeof b.content === 'string' ? b.content : (b.content?.text || ''); + return { type: 'paragraph', content }; } else if (b.type === 'heading') { - return { block_type: 'heading', content: { text: b.content, level: 2 } }; + const content = typeof b.content === 'string' ? b.content : (b.content?.text || ''); + return { type: 'heading', content: { text: content, level: 2 } }; } else if (b.type === 'image') { - return { block_type: 'image', content: { url: b.content, alt: '' } }; + return { type: 'image', content: b.content }; } else if (b.type === 'link') { - return { block_type: 'link', content: { href: b.content, text: b.content } }; + return { type: 'link', content: b.content }; } else if (b.type === 'hero') { - return { block_type: 'hero', content: b.content || { title: '', subtitle: '', backgroundImage: '', ctaText: '', ctaLink: '' } }; + return { type: 'hero', content: b.content || { title: '', subtitle: '', backgroundImage: '', ctaText: '', ctaLink: '' } }; } else if (b.type === 'video') { - return { block_type: 'video', content: b.content || { url: '', caption: '' } }; + return { type: 'video', content: b.content || { url: '', caption: '' } }; } else if (b.type === 'columns') { - return { block_type: 'columns', content: b.content || { left: '', right: '', leftImage: '', rightImage: '' } }; + return { type: 'columns', content: b.content || { left: '', right: '', leftImage: '', rightImage: '' } }; } + return { type: 'paragraph', content: b.content || '' }; }); } @@ -1045,6 +1118,8 @@

${escapeHtml(post.title)}

try { let res; + let postId = editingPostId; + if (editingPostId) { res = await fetch(API_BASE + '/api/sites/' + currentSite + '/posts/' + editingPostId, { method: 'PUT', @@ -1056,7 +1131,7 @@

${escapeHtml(post.title)}

}); if (res.ok) { const post = await res.json(); - editingPostId = post.id; + postId = post.id; } } else { res = await fetch(API_BASE + '/api/sites/' + currentSite + '/posts', { @@ -1069,13 +1144,23 @@

${escapeHtml(post.title)}

}); if (res.ok) { const post = await res.json(); - editingPostId = post.id; + postId = post.id; + editingPostId = postId; } } - if (!editingPostId) { showToast('Failed to save', 'error'); return; } + if (!res.ok) { + const err = await res.json().catch(() => ({ error: 'Unknown error' })); + showToast('Failed to save: ' + (err.error || res.status), 'error'); + return; + } + + if (!postId) { + showToast('Failed to save: No post ID returned', 'error'); + return; + } - const publishRes = await fetch(API_BASE + '/api/sites/' + currentSite + '/posts/' + editingPostId + '/publish', { + const publishRes = await fetch(API_BASE + '/api/sites/' + currentSite + '/posts/' + postId + '/publish', { method: 'POST', headers: { 'Authorization': 'Bearer ' + authToken } }); @@ -1250,11 +1335,19 @@

New Page

document.querySelectorAll('#page-blocks-container .editor-block').forEach(b => b.classList.remove('drag-over')); } + function updatePageBlockFromEditor(blockId) { + const editor = document.getElementById(`page-editor-${blockId}`); + if (editor) { + const content = editor.innerHTML; + updatePageBlock(blockId, content); + } + } + function renderPageBlocks() { const container = document.getElementById('page-blocks-container'); container.innerHTML = pageBlocks.map(block => { if (block.type === 'paragraph') { - return `
⋮⋮
`; + return `
⋮⋮
${getWysiwygToolbar(block.id)}
${block.content || ''}
`; } else if (block.type === 'heading') { return `
⋮⋮
`; } else if (block.type === 'image') { @@ -1286,145 +1379,191 @@

New Page

}) ]); - if (pagesRes.ok && siteRes.ok) { - let pages = await pagesRes.json(); - const site = await siteRes.json(); + if (pagesRes.ok) { + const pages = await pagesRes.json(); + const site = siteRes.ok ? await siteRes.json() : {}; + console.log('loadPages - site:', site); + const homepageType = site.homepage_type || 'both'; + const blogSortOrder = site.blog_sort_order || 1; + const blogPath = site.blog_path || '/blog'; + + const list = document.getElementById('pages-list'); - // Add blog as a reorderable page when homepage_type includes blog - const blogSortOrder = site.blog_sort_order ?? 1; - if (site.homepage_type === 'blog' || site.homepage_type === 'both') { - pages.unshift({ - id: '__blog__', + // Build items array with blog as a special item + let items = pages.map(page => ({ + id: page.id, + title: page.title, + slug: page.slug, + is_homepage: page.is_homepage, + show_in_nav: page.show_in_nav, + sort_order: page.sort_order || 0, + isBlog: false + })); + + // Add blog first if enabled + if (homepageType === 'blog' || homepageType === 'both') { + items.push({ + id: 'blog', title: 'Blog', - slug: 'blog', + slug: blogPath, is_homepage: false, show_in_nav: true, sort_order: blogSortOrder, - is_blog: true + isBlog: true }); } - const list = document.getElementById('pages-list'); - if (pages.length === 0) { + // Sort by the stored sort_order + items.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); + + // Display using index + 1 for position numbers (not overwriting stored values) + const displayItems = items.map((item, idx) => ({...item, displayOrder: idx + 1})); + + if (displayItems.length === 0) { list.innerHTML = '

No pages yet.

'; } else { - // Sort by sort_order - pages.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); - - list.innerHTML = pages.map((page, index) => ` -
+ list.innerHTML = displayItems.map((page, index) => ` +
${index + 1}
-

${escapeHtml(page.title)}${page.is_blog ? ' (Blog)' : ''}

-

/${escapeHtml(page.slug)}${page.is_homepage ? ' (Homepage)' : ''}

+

${page.isBlog ? '📰 ' : ''}${escapeHtml(page.title)}

+

${escapeHtml(page.slug)}${page.is_homepage ? ' (Homepage)' : ''}

- ${page.is_blog ? 'Always on' : ` - - `} - ${page.is_blog ? '' : ` - - - `} + ${!page.isBlog ? ` + ` : ''}
`).join(''); - - // Attach event handlers after rendering (skip for blog) - pages.filter(p => !p.is_blog).forEach((page, index) => { - const btn = document.getElementById('navbtn-' + page.id); - if (btn) { - btn.onclick = function() { - var newVal = page.show_in_nav !== false ? false : true; - var url = API_BASE + '/api/sites/' + currentSite + '/pages/' + page.id; - fetch(url, { - method: 'PUT', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken }, - body: JSON.stringify({ show_in_nav: newVal }) - }).then(function(res) { - if (res.ok) loadPages(); - }); - }; - } - }); } } } catch (err) { console.error(err); } } async function doMove(pageId, direction) { + console.log('doMove called:', pageId, direction); if (!currentSite) { alert('No site selected'); return; } - try { - // Fetch both site and pages data - const [siteRes, pagesRes] = await Promise.all([ - fetch(API_BASE + '/api/sites/' + currentSite, { - headers: { 'Authorization': 'Bearer ' + authToken } - }), - fetch(API_BASE + '/api/sites/' + currentSite + '/pages', { - headers: { 'Authorization': 'Bearer ' + authToken } - }) - ]); - - const site = await siteRes.json(); - const pages = await pagesRes.json(); - - // Build list of all items including blog - let allItems = pages.map(p => ({ id: p.id, title: p.title, sort_order: p.sort_order || 0, is_blog: false })); - - // Add blog if enabled - if (site.homepage_type === 'blog' || site.homepage_type === 'both') { - allItems.push({ - id: '__blog__', - title: 'Blog', - sort_order: site.blog_sort_order ?? 1, - is_blog: true - }); + + const isBlog = pageId === 'blog'; + console.log('isBlog:', isBlog, 'pageId:', pageId); + + if (isBlog) { + try { + // Get site and pages + const [siteRes, pagesRes] = await Promise.all([ + fetch(API_BASE + '/api/sites/' + currentSite, { + headers: { 'Authorization': 'Bearer ' + authToken } + }), + fetch(API_BASE + '/api/sites/' + currentSite + '/pages', { + headers: { 'Authorization': 'Bearer ' + authToken } + }) + ]); + + const site = await siteRes.json(); + const pages = await pagesRes.json(); + const blogSortOrder = site.blog_sort_order || 1; + + // Build combined list + let items = pages.map(p => ({ + id: p.id, + sort_order: p.sort_order || 0, + isBlog: false + })); + + const homepageType = site.homepage_type || 'both'; + console.log('homepageType:', homepageType, 'blogSortOrder:', blogSortOrder); + if (homepageType === 'blog' || homepageType === 'both') { + items.push({ + id: 'blog', + sort_order: blogSortOrder, + isBlog: true + }); + } + + items.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); + console.log('Items after sort:', JSON.stringify(items.map(i => ({id: i.id, sort_order: i.sort_order, isBlog: i.isBlog})))); + + // Find current blog position + const currentIndex = items.findIndex(i => i.isBlog); + console.log('currentIndex:', currentIndex, 'direction:', direction); + const newIndex = currentIndex + direction; + console.log('newIndex:', newIndex, 'items.length:', items.length); + + if (newIndex < 0 || newIndex >= items.length) { + console.log('Bailing out - bounds check'); + return; + } + + // Move blog to new position + const blogIndex = items.findIndex(i => i.isBlog); + const [blogItem] = items.splice(blogIndex, 1); // Remove blog from current position + items.splice(newIndex, 0, blogItem); // Insert at new position + console.log('Items after splice:', JSON.stringify(items.map(i => ({id: i.id, sort_order: i.sort_order, isBlog: i.isBlog})))); + + // Assign new sequential sort_order values + for (let i = 0; i < items.length; i++) { + console.log(`Updating index ${i}:`, items[i].id, 'isBlog:', items[i].isBlog); + if (items[i].isBlog) { + await fetch(API_BASE + '/api/sites/' + currentSite, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken }, + body: JSON.stringify({ blog_sort_order: i + 1 }) + }).then(r => console.log('Blog update res:', r.status)).catch(e => console.log('Blog update err:', e)); + } else { + await fetch(API_BASE + '/api/sites/' + currentSite + '/pages/' + items[i].id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken }, + body: JSON.stringify({ sort_order: i + 1 }) + }).then(r => console.log('Page', items[i].id, 'update res:', r.status)).catch(e => console.log('Page update err:', e)); + } + } + + loadPages(); + } catch (err) { + console.error(err); + showToast('Error moving blog: ' + err.message, 'error'); } + return; + } + + // Handle regular page reordering + try { + const res = await fetch(API_BASE + '/api/sites/' + currentSite + '/pages', { + headers: { 'Authorization': 'Bearer ' + authToken } + }); + if (!res.ok) { alert('Failed to load pages'); return; } + const pages = await res.json(); - // Sort by sort_order - allItems.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); + // Sort by stored sort_order + pages.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); - // Find current index of the moved item - const currentIndex = allItems.findIndex(p => p.id === pageId); - if (currentIndex === -1) return; + // Find current page + const currentIndex = pages.findIndex(p => p.id === pageId); + if (currentIndex === -1) { alert('Page not found'); return; } const newIndex = currentIndex + direction; - if (newIndex < 0 || newIndex >= allItems.length) return; + if (newIndex < 0 || newIndex >= pages.length) { return; } - // Remove item from current position and insert at new position - const [movedItem] = allItems.splice(currentIndex, 1); - allItems.splice(newIndex, 0, movedItem); + const page1 = pages[currentIndex]; + const page2 = pages[newIndex]; - // Update all sort_orders to be sequential - const updatePromises = []; - for (let i = 0; i < allItems.length; i++) { - const item = allItems[i]; - if (item.is_blog) { - updatePromises.push( - fetch(API_BASE + '/api/sites/' + currentSite, { - method: 'PUT', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken }, - body: JSON.stringify({ blog_sort_order: i }) - }) - ); - } else { - updatePromises.push( - fetch(API_BASE + '/api/sites/' + currentSite + '/pages/' + item.id, { - method: 'PUT', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken }, - body: JSON.stringify({ sort_order: i }) - }) - ); - } - } + // Swap sort_order values (use array position + 1) + await fetch(API_BASE + '/api/sites/' + currentSite + '/pages/' + page1.id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken }, + body: JSON.stringify({ sort_order: newIndex + 1 }) + }); + + await fetch(API_BASE + '/api/sites/' + currentSite + '/pages/' + page2.id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken }, + body: JSON.stringify({ sort_order: currentIndex + 1 }) + }); - await Promise.all(updatePromises); loadPages(); - } catch (err) { console.error(err); alert('Error: ' + err.message); } + } catch (err) { console.error(err); } } async function editPage(id) { @@ -1444,7 +1583,7 @@

${escapeHtml(page.title)}${page.is_blog ? ' < if (page.content && page.content.length > 0) { pageBlocks = page.content.map((b, i) => { - const blockType = b.block_type || 'paragraph'; + const blockType = b.type || b.block_type || 'paragraph'; let content = ''; if (blockType === 'hero') { content = b.content || { title: '', subtitle: '', backgroundImage: '', ctaText: '', ctaLink: '' }; @@ -1452,6 +1591,8 @@

${escapeHtml(page.title)}${page.is_blog ? ' < content = b.content || { url: '', caption: '' }; } else if (blockType === 'columns') { content = b.content || { left: '', right: '', leftImage: '', rightImage: '' }; + } else if (typeof b.content === 'string') { + content = b.content; } else { content = b.content?.text || b.content?.url || b.content?.href || ''; } @@ -1475,13 +1616,24 @@

${escapeHtml(page.title)}${page.is_blog ? ' < function buildPageContentFromBlocks() { return pageBlocks.map(b => { - if (b.type === 'paragraph') return { block_type: 'paragraph', content: { text: b.content } }; - else if (b.type === 'heading') return { block_type: 'heading', content: { text: b.content, level: 2 } }; - else if (b.type === 'image') return { block_type: 'image', content: { url: b.content, alt: '' } }; - else if (b.type === 'link') return { block_type: 'link', content: { href: b.content, text: b.content } }; - else if (b.type === 'hero') return { block_type: 'hero', content: b.content || { title: '', subtitle: '', backgroundImage: '', ctaText: '', ctaLink: '' } }; - else if (b.type === 'video') return { block_type: 'video', content: b.content || { url: '', caption: '' } }; - else if (b.type === 'columns') return { block_type: 'columns', content: b.content || { left: '', right: '', leftImage: '', rightImage: '' } }; + if (b.type === 'paragraph') { + const content = typeof b.content === 'string' ? b.content : (b.content?.text || ''); + return { type: 'paragraph', content }; + } else if (b.type === 'heading') { + const content = typeof b.content === 'string' ? b.content : (b.content?.text || ''); + return { type: 'heading', content: { text: content, level: 2 } }; + } else if (b.type === 'image') { + return { type: 'image', content: b.content }; + } else if (b.type === 'link') { + return { type: 'link', content: b.content }; + } else if (b.type === 'hero') { + return { type: 'hero', content: b.content || { title: '', subtitle: '', backgroundImage: '', ctaText: '', ctaLink: '' } }; + } else if (b.type === 'video') { + return { type: 'video', content: b.content || { url: '', caption: '' } }; + } else if (b.type === 'columns') { + return { type: 'columns', content: b.content || { left: '', right: '', leftImage: '', rightImage: '' } }; + } + return { type: 'paragraph', content: b.content || '' }; }); } @@ -1762,6 +1914,12 @@

Homepage

+
+ +
+
diff --git a/admin/Cargo.toml b/admin/Cargo.toml deleted file mode 100644 index de10670..0000000 --- a/admin/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "admin" -version = "0.1.0" -edition = "2021" - -[dependencies] -leptos = { workspace = true, features = ["serde"] } -leptos_axum = { workspace = true } -leptos_meta = { workspace = true } -leptos_router = { workspace = true } -axum = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } -reqwest = { workspace = true } -uuid = { workspace = true } - -[lib] -crate-type = ["cdylib", "rlib"] diff --git a/admin/index.html b/admin/index.html deleted file mode 100644 index 6e70517..0000000 --- a/admin/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - Blog Platform Admin - - - - -
- - - diff --git a/admin/src/lib.rs b/admin/src/lib.rs deleted file mode 100644 index 0013ca9..0000000 --- a/admin/src/lib.rs +++ /dev/null @@ -1,275 +0,0 @@ -use leptos::*; -use leptos_meta::*; -use leptos_router::*; - -#[component] -pub fn App() -> impl IntoView { - provide_meta_context(); - - view! { - - - - - - - - - - - - - - - - - - - - - - - } -} - -#[component] -fn HomePage() -> impl IntoView { - view! { -
-
-

Blog Platform

- Go to Admin -
-
- } -} - -#[component] -fn AdminLayout() -> impl IntoView { - view! { -
- -
- -
-
- } -} - -#[component] -fn LoginPage() -> impl IntoView { - view! { -
-
-

Sign In

-
-
- - -
-
- - -
- -
-
-
- } -} - -#[component] -fn SitesPage() -> impl IntoView { - view! { -
-
-

Sites

- -
-
-

No sites yet. Create your first site!

-
-
- } -} - -#[component] -fn SiteDetailPage() -> impl IntoView { - view! { -
-

Site Dashboard

- -
- } -} - -#[component] -fn PostsPage() -> impl IntoView { - view! { -
-
-

Posts

- - New Post - -
-
-

No posts yet. Create your first post!

-
-
- } -} - -#[component] -fn PostEditorPage() -> impl IntoView { - view! { -
-
-

New Post

-
-
-
- - -
-
- - -
-
-
- - - - -
- -
-
-
- } -} - -#[component] -fn PagesPage() -> impl IntoView { - view! { -
-
-

Pages

- - New Page - -
-
-

No pages yet.

-
-
- } -} - -#[component] -fn PageEditorPage() -> impl IntoView { - view! { -
-
-

New Page

-
-
-
- - -
-
- - -
-
-
- } -} - -#[component] -fn MediaPage() -> impl IntoView { - view! { -
-

Media Library

-
- -
-
- } -} - -#[component] -fn ContactSubmissionsPage() -> impl IntoView { - view! { -
-

Contact Form Submissions

-
-

No submissions yet.

-
-
- } -} diff --git a/admin/src/main.rs b/admin/src/main.rs deleted file mode 100644 index cf2518f..0000000 --- a/admin/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -use admin::App; - -fn main() { - leptos::mount_to_body(App); -} diff --git a/src/Cargo.toml b/src/Cargo.toml deleted file mode 100644 index 2e81239..0000000 --- a/src/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "blog-platform" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "blog-platform" -path = "main.rs" - -[dependencies] -axum = { workspace = true } -tower = { workspace = true } -tower-http = { workspace = true } -leptos = { workspace = true } -leptos_axum = { workspace = true } -leptos_meta = { workspace = true } -leptos_router = { workspace = true } -sqlx = { workspace = true } -bcrypt = { workspace = true } -minijinja = { workspace = true } -tokio = { workspace = true } -uuid = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -chrono = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } -tracing-appender = { workspace = true } -async-trait = { workspace = true } -futures = { workspace = true } -mime_guess = { workspace = true } -reqwest = { workspace = true } diff --git a/src/api/posts.rs b/src/api/posts.rs index 607ff96..73d6673 100644 --- a/src/api/posts.rs +++ b/src/api/posts.rs @@ -170,6 +170,7 @@ pub async fn update( } let title = payload.title.clone(); + let slug = payload.slug.clone(); let content = payload.content.clone(); let excerpt = payload.excerpt.clone(); let featured_image = payload.featured_image.clone(); @@ -179,11 +180,12 @@ pub async fn update( let result = sqlx::query_as::<_, PostRow>( "UPDATE posts SET title = COALESCE($3, title), - content = COALESCE($4, content), - excerpt = COALESCE($5, excerpt), - featured_image = COALESCE($6, featured_image), - status = COALESCE($7, status), - seo = COALESCE($8, seo), + slug = COALESCE($4, slug), + content = COALESCE($5, content), + excerpt = COALESCE($6, excerpt), + featured_image = COALESCE($7, featured_image), + status = COALESCE($8, status), + seo = COALESCE($9, seo), updated_at = NOW() WHERE site_id = $1 AND id = $2 RETURNING id, site_id, author_id, title, slug, content, excerpt, featured_image, status, published_at, created_at, updated_at, seo" @@ -191,6 +193,7 @@ pub async fn update( .bind(site_id) .bind(id) .bind(title) + .bind(slug) .bind(content) .bind(excerpt) .bind(featured_image) diff --git a/src/api/sites.rs b/src/api/sites.rs index f843204..1303787 100644 --- a/src/api/sites.rs +++ b/src/api/sites.rs @@ -19,7 +19,7 @@ pub async fn list( let current_user = require_auth(State(state.clone()), headers).await?; let rows = sqlx::query( - "SELECT id, subdomain, custom_domain, name, description, logo_url, favicon_url, theme, nav_links, footer_text, social_links, contact_phone, contact_email, contact_address, homepage_type, blog_path, blog_sort_order, landing_blocks, settings, created_at FROM sites WHERE id IN (SELECT site_id FROM site_members WHERE user_id = $1) ORDER BY created_at DESC" + "SELECT id, subdomain, custom_domain, name, description, logo_url, favicon_url, theme, nav_links, footer_text, social_links, contact_phone, contact_email, contact_address, homepage_type, blog_path, COALESCE(blog_sort_order, 1) as blog_sort_order, landing_blocks, settings, created_at FROM sites WHERE id IN (SELECT site_id FROM site_members WHERE user_id = $1) ORDER BY created_at DESC" ) .bind(current_user.user_id) .fetch_all(&state.db) @@ -64,7 +64,7 @@ pub async fn get( require_site_member(&state, id, current_user.user_id).await?; let row = sqlx::query( - "SELECT id, subdomain, custom_domain, name, description, logo_url, favicon_url, theme, nav_links, footer_text, social_links, contact_phone, contact_email, contact_address, homepage_type, blog_path, blog_sort_order, landing_blocks, settings, created_at FROM sites WHERE id = $1" + "SELECT id, subdomain, custom_domain, name, description, logo_url, favicon_url, theme, nav_links, footer_text, social_links, contact_phone, contact_email, contact_address, homepage_type, blog_path, COALESCE(blog_sort_order, 1) as blog_sort_order, landing_blocks, settings, created_at FROM sites WHERE id = $1" ) .bind(id) .fetch_one(&state.db) @@ -109,14 +109,14 @@ pub async fn create( } // Validate URLs to prevent XSS - if let Some(url) = &payload.logo_url { + if let Some(ref url) = payload.logo_url { if !util::is_valid_url(url) { return Err(ApiError::new( "Invalid logo URL: javascript: and data: URLs are not allowed", )); } } - if let Some(url) = &payload.favicon_url { + if let Some(ref url) = payload.favicon_url { if !util::is_valid_url(url) { return Err(ApiError::new( "Invalid favicon URL: javascript: and data: URLs are not allowed", @@ -128,7 +128,7 @@ pub async fn create( let custom_domain = payload.custom_domain.filter(|s| !s.is_empty()); let row = sqlx::query( - "INSERT INTO sites (subdomain, custom_domain, name, description, logo_url, favicon_url, homepage_type, nav_links, blog_path) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, subdomain, custom_domain, name, description, logo_url, favicon_url, theme, nav_links, footer_text, social_links, contact_phone, contact_email, contact_address, homepage_type, landing_blocks, settings, created_at, blog_path, blog_sort_order" + "INSERT INTO sites (subdomain, custom_domain, name, description, logo_url, favicon_url, homepage_type, nav_links, blog_path, blog_sort_order) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 2) RETURNING id, subdomain, custom_domain, name, description, logo_url, favicon_url, theme, nav_links, footer_text, social_links, contact_phone, contact_email, contact_address, homepage_type, blog_path, COALESCE(blog_sort_order, 2) as blog_sort_order, landing_blocks, settings, created_at" ) .bind(subdomain) .bind(custom_domain) @@ -161,9 +161,9 @@ pub async fn create( {"block_type": "paragraph", "content": {"text": "Get in touch with us!"}} ]); - // Insert homepage page + // Insert homepage page with sort_order sqlx::query( - "INSERT INTO pages (site_id, title, slug, content, is_homepage) VALUES ($1, $2, $3, $4, $5)" + "INSERT INTO pages (site_id, title, slug, content, is_homepage, sort_order) VALUES ($1, $2, $3, $4, $5, 1)" ) .bind(site_id) .bind("Home") @@ -174,9 +174,9 @@ pub async fn create( .await .map_err(|e| ApiError::new(format!("Failed to create homepage: {}", e)))?; - // Insert About page + // Insert About page with sort_order sqlx::query( - "INSERT INTO pages (site_id, title, slug, content, is_homepage) VALUES ($1, $2, $3, $4, $5)" + "INSERT INTO pages (site_id, title, slug, content, is_homepage, sort_order) VALUES ($1, $2, $3, $4, $5, 2)" ) .bind(site_id) .bind("About") @@ -187,9 +187,9 @@ pub async fn create( .await .map_err(|e| ApiError::new(format!("Failed to create about page: {}", e)))?; - // Insert Contact page + // Insert Contact page with sort_order sqlx::query( - "INSERT INTO pages (site_id, title, slug, content, is_homepage) VALUES ($1, $2, $3, $4, $5)" + "INSERT INTO pages (site_id, title, slug, content, is_homepage, sort_order) VALUES ($1, $2, $3, $4, $5, 3)" ) .bind(site_id) .bind("Contact") @@ -301,11 +301,11 @@ pub async fn update( contact_address = COALESCE($14, contact_address), homepage_type = COALESCE($15, homepage_type), blog_path = $16, - landing_blocks = COALESCE($17, landing_blocks), - settings = COALESCE($18, settings), - blog_sort_order = $19 + blog_sort_order = COALESCE($17, blog_sort_order), + landing_blocks = COALESCE($18, landing_blocks), + settings = COALESCE($19, settings) WHERE id = $1 - RETURNING id, subdomain, custom_domain, name, description, logo_url, favicon_url, theme, nav_links, footer_text, social_links, contact_phone, contact_email, contact_address, homepage_type, blog_path, landing_blocks, settings, created_at, blog_sort_order" + RETURNING id, subdomain, custom_domain, name, description, logo_url, favicon_url, theme, nav_links, footer_text, social_links, contact_phone, contact_email, contact_address, homepage_type, blog_path, COALESCE(blog_sort_order, 1) as blog_sort_order, landing_blocks, settings, created_at" ) .bind(id) .bind(name) @@ -323,9 +323,9 @@ pub async fn update( .bind(contact_address) .bind(homepage_type) .bind(blog_path) + .bind(blog_sort_order) .bind(landing_blocks) .bind(settings) - .bind(blog_sort_order) .fetch_one(&state.db) .await .map_err(|_| ApiError::new("Site not found"))?; diff --git a/src/main.rs b/src/main.rs index d66f5a4..b938a2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -227,6 +227,7 @@ async fn run_migrations(db: &sqlx::PgPool) { homepage_type VARCHAR(20) DEFAULT 'both', landing_blocks JSONB DEFAULT '[]', blog_path VARCHAR(100) DEFAULT '/blog', + blog_sort_order INTEGER DEFAULT 1, created_at TIMESTAMPTZ DEFAULT NOW(), settings JSONB DEFAULT '{}' )", @@ -239,11 +240,30 @@ async fn run_migrations(db: &sqlx::PgPool) { .execute(db) .await .ok(); - sqlx::query("ALTER TABLE sites ADD COLUMN IF NOT EXISTS blog_sort_order INTEGER DEFAULT 1") .execute(db) .await .ok(); + sqlx::query( + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS homepage_type VARCHAR(20) DEFAULT 'both'", + ) + .execute(db) + .await + .ok(); + sqlx::query( + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS blog_path VARCHAR(100) DEFAULT '/blog'", + ) + .execute(db) + .await + .ok(); + sqlx::query("ALTER TABLE sites ADD COLUMN IF NOT EXISTS landing_blocks JSONB DEFAULT '[]'") + .execute(db) + .await + .ok(); + sqlx::query("ALTER TABLE sites ADD COLUMN IF NOT EXISTS settings JSONB DEFAULT '{}'") + .execute(db) + .await + .ok(); sqlx::query( "CREATE TABLE IF NOT EXISTS users ( diff --git a/src/models.rs b/src/models.rs index e590882..6b1553a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -20,7 +20,7 @@ pub struct Site { pub contact_address: Option, pub homepage_type: String, pub blog_path: Option, - pub blog_sort_order: Option, + pub blog_sort_order: i32, pub landing_blocks: serde_json::Value, pub settings: serde_json::Value, pub created_at: DateTime, diff --git a/src/ssg/mod.rs b/src/ssg/mod.rs index 6d4d966..9f6ff5c 100644 --- a/src/ssg/mod.rs +++ b/src/ssg/mod.rs @@ -12,41 +12,6 @@ fn escape_html>(s: S) -> String { .replace('\'', "'") } -/// Validate and sanitize URL to prevent XSS attacks via javascript:, data:, etc. -/// Returns None if URL is unsafe, otherwise returns the sanitized URL -fn sanitize_url(url: &str) -> Option { - let url = url.trim(); - if url.is_empty() { - return None; - } - - // Check for dangerous URL schemes - let lower = url.to_lowercase(); - if lower.starts_with("javascript:") - || lower.starts_with("data:") - || lower.starts_with("vbscript:") - || lower.starts_with("file:") - { - return None; - } - - // Only allow http, https, and relative URLs - if !lower.starts_with("http://") - && !lower.starts_with("https://") - && !lower.starts_with('/') - && !lower.starts_with("data:") - { - // Allow common relative paths - if !url.starts_with("..") && !url.contains("..") { - Some(url.to_string()) - } else { - None - } - } else { - Some(url.to_string()) - } -} - fn extract_first_image(content: &serde_json::Value) -> Option { if let Some(blocks) = content.as_array() { for block in blocks { @@ -83,10 +48,6 @@ fn make_context( ) -> Context { let mut ctx = Context::new(); ctx.insert("site_name".into(), minijinja::Value::from(site_name)); - ctx.insert( - "build_timestamp".into(), - minijinja::Value::from(chrono::Utc::now().to_rfc3339()), - ); ctx.insert( "site_description".into(), minijinja::Value::from_serialize(site_description), @@ -131,57 +92,12 @@ pub async fn build_site( site_id: Uuid, ) -> Result<(), Box> { let site_row = sqlx::query( - "SELECT id, name, description, logo_url, favicon_url, footer_text, social_links, contact_phone, contact_email, contact_address, custom_domain, homepage_type, blog_path, blog_sort_order FROM sites WHERE id = $1" + "SELECT id, name, description, logo_url, favicon_url, footer_text, social_links, contact_phone, contact_email, contact_address, custom_domain, COALESCE(homepage_type, 'both') as homepage_type, COALESCE(blog_path, '/blog') as blog_path, COALESCE(blog_sort_order, 1) as blog_sort_order FROM sites WHERE id = $1" ) .bind(site_id) .fetch_one(db) .await?; - // Get current pages from DB to know what to keep - let pages: Vec<(String,)> = sqlx::query_as("SELECT slug FROM pages WHERE site_id = $1") - .bind(site_id) - .fetch_all(db) - .await?; - - let page_slugs: Vec = pages.iter().map(|p| p.0.clone()).collect(); - - // Get homepage_type to determine if blog should exist - let homepage_type: Option = site_row.get("homepage_type"); - let homepage_type = homepage_type.unwrap_or_else(|| "both".to_string()); - let blog_enabled = homepage_type == "blog" || homepage_type == "both"; - - // Clean up old output files - remove HTML files for deleted pages - let output_dir = std::path::Path::new("output"); - if output_dir.exists() { - // Keep these special files (blog.html only if blog is enabled) - let mut keep_files = vec!["index.html", "feed.xml", "sitemap.xml"]; - if blog_enabled { - keep_files.push("blog.html"); - } - let keep_files: Vec<&str> = keep_files; - - if let Ok(entries) = std::fs::read_dir(output_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_file() { - if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { - // Skip special files - if keep_files.contains(&filename) || filename.starts_with("blog/") { - continue; - } - // Remove .html files not in the page list - if filename.ends_with(".html") { - let slug = filename.trim_end_matches(".html"); - if !page_slugs.contains(&slug.to_string()) { - let _ = std::fs::remove_file(&path); - } - } - } - } - } - } - } - let site_id: Uuid = site_row.get("id"); let site_name: String = site_row.get("name"); let site_description: Option = site_row.get("description"); @@ -197,7 +113,9 @@ pub async fn build_site( let domain: Option = site_row.get("custom_domain"); let homepage_type: Option = site_row.get("homepage_type"); let blog_path: Option = site_row.get("blog_path"); - let blog_sort_order: Option = site_row.get("blog_sort_order"); + let blog_sort_order: i32 = site_row.get("blog_sort_order"); + let homepage_type = homepage_type.unwrap_or_else(|| "both".to_string()); + let blog_path = blog_path.unwrap_or_else(|| "/blog".to_string()); // Build site URL - use custom_domain as the primary domain let site_url = if let Some(d) = domain { @@ -226,56 +144,46 @@ pub async fn build_site( .fetch_all(db) .await?; - // Build nav_links from pages with show_in_nav = true (excluding homepage which is always at /) - let nav_pages: Vec<_> = pages.iter().filter(|p| p.4 && !p.3).collect(); - let mut nav_links: Vec = nav_pages - .iter() - .map(|p| { - serde_json::json!({ - "label": p.0, - "url": format!("/{}", p.1), - "sort_order": p.5 - }) - }) - .collect(); + // Build nav_links from pages with show_in_nav = true + // Include homepage at position 0, then other pages sorted by sort_order + let mut nav_links: Vec = Vec::new(); + + // Add homepage first if show_in_nav is true + if let Some(homepage) = pages.iter().find(|p| p.3) { + if homepage.4 { + nav_links.push(serde_json::json!({ + "label": homepage.0, + "url": "/" + })); + } + } - // Add Blog link if homepage_type is 'blog' or 'both' - let homepage_type = homepage_type.unwrap_or_else(|| "both".to_string()); - let blog_order = blog_sort_order.unwrap_or(1); - if homepage_type == "blog" || homepage_type == "both" { - let blog_url = blog_path - .filter(|p| !p.is_empty()) - .map(|p| { - if p.starts_with('/') { - p - } else { - format!("/{}", p) - } - }) - .unwrap_or_else(|| "/blog".to_string()); - nav_links - .push(serde_json::json!({"label": "Blog", "url": blog_url, "sort_order": blog_order})); + // Add other pages sorted by sort_order + let other_pages: Vec<_> = pages.iter().filter(|p| !p.3).collect(); + for page in other_pages { + if page.4 { + nav_links.push(serde_json::json!({ + "label": page.0, + "url": format!("/{}", page.1) + })); + } } - // Sort all nav links by sort_order (default to large number for pages without explicit order) - nav_links.sort_by(|a, b| { - let order_a = a.get("sort_order").and_then(|v| v.as_i64()).unwrap_or(100) as i32; - let order_b = b.get("sort_order").and_then(|v| v.as_i64()).unwrap_or(100) as i32; - order_a.cmp(&order_b) - }); + // Add blog link at the correct position based on blog_sort_order if homepage_type is "blog" or "both" + if homepage_type == "blog" || homepage_type == "both" { + let blog_link = serde_json::json!({ + "label": "Blog", + "url": blog_path + }); + // Insert at blog_sort_order position (convert from 1-based to 0-based) + let insert_pos = (blog_sort_order as usize) + .saturating_sub(1) + .min(nav_links.len()); + nav_links.insert(insert_pos, blog_link); + } let mut env = Environment::new(); - // Add custom filter for JSON-LD escaping (prevents XSS in script tags) - env.add_filter("json_escape", |s: String| { - s.replace('\\', "\\\\") - .replace('"', "\\\"") - .replace('\n', "\\n") - .replace('\r', "\\r") - .replace('\t', "\\t") - .replace(" String { if let Some(blocks) = content.as_array() { blocks.iter() .map(|block| { - let block_type = block.get("block_type").and_then(|b| b.as_str()).unwrap_or("text"); + let block_type = block.get("type").or_else(|| block.get("block_type")).and_then(|b| b.as_str()).unwrap_or("text"); let block_content = block.get("content"); match block_type { "heading" => { let level = block.get("level").and_then(|l| l.as_i64()).unwrap_or(2); - let text = escape_html(block_content.and_then(|c| c.get("text")).and_then(|t| t.as_str()).unwrap_or("")); + let text = block_content + .and_then(|c| c.get("text")) + .and_then(|t| t.as_str()) + .unwrap_or(""); + let text = escape_html(text); format!("{}", level, text, level) } "paragraph" => { - let text = escape_html(block_content.and_then(|c| c.get("text")).and_then(|t| t.as_str()).unwrap_or("")); - format!("

{}

", text) + let text = if let Some(s) = block_content.and_then(|c| c.as_str()) { + s.to_string() + } else { + block_content + .and_then(|c| c.get("text")) + .and_then(|t| t.as_str()) + .map(escape_html) + .unwrap_or_default() + }; + if text.is_empty() { + String::new() + } else if text.contains('<') && text.contains('>') { + text.clone() + } else { + format!("

{}

", text) + } } "image" => { - let url = sanitize_url(block_content.and_then(|c| c.get("url")).and_then(|u| u.as_str()).unwrap_or_default()).unwrap_or_default(); - let alt = escape_html(block_content.and_then(|c| c.get("alt")).and_then(|a| a.as_str()).unwrap_or("")); - format!("
\"{}\"
{}
", url, alt, alt) + let url = if let Some(s) = block_content.and_then(|c| c.as_str()) { + escape_html(s) + } else { + escape_html(block_content.and_then(|c| c.get("url")).and_then(|u| u.as_str()).unwrap_or("")) + }; + let alt = block_content + .and_then(|c| c.get("alt")) + .and_then(|a| a.as_str()) + .map(escape_html) + .unwrap_or_default(); + if url.is_empty() { + String::new() + } else { + format!("
\"{}\"
{}
", url, alt, alt) + } } "code" => { let code = escape_html(block_content.and_then(|c| c.get("code")).and_then(|c| c.as_str()).unwrap_or("")); @@ -621,16 +518,24 @@ fn render_blocks(content: &serde_json::Value) -> String { format!("
{}
", lang, code) } "quote" => { - let text = escape_html(block_content.and_then(|c| c.get("text")).and_then(|t| t.as_str()).unwrap_or("")); - let citation = escape_html(block_content.and_then(|c| c.get("citation")).and_then(|c| c.as_str()).unwrap_or("")); + let text = block_content + .and_then(|c| c.get("text")) + .and_then(|t| t.as_str()) + .map(escape_html) + .unwrap_or_default(); + let citation = block_content + .and_then(|c| c.get("citation")) + .and_then(|c| c.as_str()) + .map(escape_html) + .unwrap_or_default(); format!("
{}{}
", text, citation) } "hero" => { let title = escape_html(block_content.and_then(|c| c.get("title")).and_then(|t| t.as_str()).unwrap_or("")); let subtitle = escape_html(block_content.and_then(|c| c.get("subtitle")).and_then(|t| t.as_str()).unwrap_or("")); - let bg = sanitize_url(block_content.and_then(|c| c.get("backgroundImage")).and_then(|t| t.as_str()).unwrap_or_default()).unwrap_or_default(); - let cta_text = escape_html(block_content.and_then(|c| c.get("ctaText")).and_then(|t| t.as_str()).unwrap_or("")); - let cta_link = sanitize_url(block_content.and_then(|c| c.get("ctaLink")).and_then(|t| t.as_str()).unwrap_or_default()).unwrap_or_else(|| "#".to_string()); + let bg = block_content.and_then(|c| c.get("backgroundImage")).and_then(|t| t.as_str()).unwrap_or(""); + let cta_text = block_content.and_then(|c| c.get("ctaText")).and_then(|t| t.as_str()).unwrap_or(""); + let cta_link = block_content.and_then(|c| c.get("ctaLink")).and_then(|t| t.as_str()).unwrap_or("#"); let bg_style = if !bg.is_empty() { format!("background-image: linear-gradient(rgba(0,0,0,0.6), rgba(0,0,0,0.6)), url('{}'); background-size: cover; background-position: center;", bg) } else { @@ -643,7 +548,7 @@ fn render_blocks(content: &serde_json::Value) -> String { "#, bg_style, title, subtitle, if !cta_text.is_empty() { format!("{}", cta_link, cta_text) } else { String::new() }) } "video" => { - let url = escape_html(block_content.and_then(|c| c.get("url")).and_then(|t| t.as_str()).unwrap_or("")); + let url = block_content.and_then(|c| c.get("url")).and_then(|t| t.as_str()).unwrap_or(""); let caption = escape_html(block_content.and_then(|c| c.get("caption")).and_then(|t| t.as_str()).unwrap_or("")); let embed_html = if url.contains("youtube.com") || url.contains("youtu.be") { let video_id = if url.contains("v=") { @@ -665,10 +570,18 @@ fn render_blocks(content: &serde_json::Value) -> String { } else { String::new() } } "columns" => { - let left = escape_html(block_content.and_then(|c| c.get("left")).and_then(|t| t.as_str()).unwrap_or("")); - let right = escape_html(block_content.and_then(|c| c.get("right")).and_then(|t| t.as_str()).unwrap_or("")); - let left_img = sanitize_url(block_content.and_then(|c| c.get("leftImage")).and_then(|t| t.as_str()).unwrap_or("")).unwrap_or_default(); - let right_img = sanitize_url(block_content.and_then(|c| c.get("rightImage")).and_then(|t| t.as_str()).unwrap_or("")).unwrap_or_default(); + let left = block_content + .and_then(|c| c.get("left")) + .and_then(|t| t.as_str()) + .map(escape_html) + .unwrap_or_default(); + let right = block_content + .and_then(|c| c.get("right")) + .and_then(|t| t.as_str()) + .map(escape_html) + .unwrap_or_default(); + let left_img = block_content.and_then(|c| c.get("leftImage")).and_then(|t| t.as_str()).unwrap_or(""); + let right_img = block_content.and_then(|c| c.get("rightImage")).and_then(|t| t.as_str()).unwrap_or(""); format!(r#"
{} {} @@ -684,8 +597,20 @@ fn render_blocks(content: &serde_json::Value) -> String { ) } _ => { - let text = escape_html(block_content.and_then(|c| c.get("text")).and_then(|t| t.as_str()).unwrap_or("")); - format!("

{}

", text) + let text = if let Some(s) = block_content.and_then(|c| c.as_str()) { + s.to_string() + } else { + block_content + .and_then(|c| c.get("text")) + .and_then(|t| t.as_str()) + .map(escape_html) + .unwrap_or_default() + }; + if text.is_empty() { + String::new() + } else { + format!("

{}

", text) + } } } }) @@ -696,80 +621,6 @@ fn render_blocks(content: &serde_json::Value) -> String { } } -fn extract_plain_text(content: &serde_json::Value) -> String { - if let Some(blocks) = content.as_array() { - let mut text = String::new(); - - for block in blocks.iter() { - let block_type = block - .get("block_type") - .and_then(|b| b.as_str()) - .unwrap_or("text"); - let block_content = block.get("content"); - - let block_text: String = match block_type { - "heading" => block_content - .and_then(|c| c.get("text")) - .and_then(|t| t.as_str()) - .unwrap_or("") - .to_string(), - "paragraph" => block_content - .and_then(|c| c.get("text")) - .and_then(|t| t.as_str()) - .unwrap_or("") - .to_string(), - "quote" => block_content - .and_then(|c| c.get("text")) - .and_then(|t| t.as_str()) - .unwrap_or("") - .to_string(), - "hero" => { - let title = block_content - .and_then(|c| c.get("title")) - .and_then(|t| t.as_str()) - .unwrap_or(""); - let subtitle = block_content - .and_then(|c| c.get("subtitle")) - .and_then(|t| t.as_str()) - .unwrap_or(""); - format!("{} {}", title, subtitle) - } - "columns" => { - let left = block_content - .and_then(|c| c.get("left")) - .and_then(|t| t.as_str()) - .unwrap_or(""); - let right = block_content - .and_then(|c| c.get("right")) - .and_then(|t| t.as_str()) - .unwrap_or(""); - format!("{} {}", left, right) - } - _ => block_content - .and_then(|c| c.get("text")) - .and_then(|t| t.as_str()) - .unwrap_or("") - .to_string(), - }; - - if !block_text.trim().is_empty() { - if !text.is_empty() { - text.push(' '); - } - text.push_str(block_text.trim()); - } - } - - if text.len() > 160 { - format!("{}...", &text[..157]) - } else { - text - } - } else { - String::new() - } -} - pub async fn deploy_to_cloudflare() -> Result> { let project_name = std::env::var("CLOUDFLARE_PAGES_PROJECT") .map_err(|_| "CLOUDFLARE_PAGES_PROJECT not set")?; @@ -786,7 +637,6 @@ pub async fn deploy_to_cloudflare() -> Result Result {% if title and title != site_name %}{{ title }} - {% endif %}{{ site_name }} - {% if meta_description %}{% elif site_description %}{% endif %} + {% if site_description %}{% endif %} {% if favicon_url %}{% endif %} {% if logo_url %}{% endif %} - + + - {% if meta_description %}{% elif site_description %}{% endif %} + {% if site_description %}{% endif %} {% if featured_image %} @@ -26,51 +27,28 @@ - {% if meta_description %}{% elif site_description %}{% endif %} + {% if site_description %}{% endif %} {% if featured_image %} {% elif logo_url %} {% endif %} - {% if is_blog_post %} - - - {% else %} - + - {% endif %}