├── .gitignore ├── .vscode └── settings.json ├── README.md ├── api ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ └── lib.rs ├── app ├── htmx.min.js ├── index.html ├── json-enc.js └── style.css ├── migration.sql └── spin.toml /.gitignore: -------------------------------------------------------------------------------- 1 | main.wasm 2 | .spin/ 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spin & HTMX sample application 2 | 3 | This is a sample application to demonstrate how to use [HTMX](https://htmx.org) with [Fermyon Spin](https://developer.fermyon.com/spin). The Spin app consists of two components: 4 | 5 | - `app`: A static fileserver that servces the frontend (`./app`) 6 | - `api`: A simple API implemented with Rust (`./api`) 7 | 8 | For persistence, the app uses SQLite. The database schema is defined in `./migration.sql`. 9 | 10 | ## Running locally 11 | 12 | To run the app locally, you must have `spin` CLI installed. See [https://developer.fermyon.com/spin/v2/install](https://developer.fermyon.com/spin/v2/install) for installation instructions. 13 | 14 | Once you have `spin` installed, you can run the app locally by running the following command: 15 | 16 | ```bash 17 | # Compile the sample 18 | spin build 19 | 20 | # Run the sample 21 | spin up --sqlite @migration.sql 22 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .spin/ 3 | -------------------------------------------------------------------------------- /api/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.75" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 10 | 11 | [[package]] 12 | name = "api" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "anyhow", 16 | "build_html", 17 | "serde", 18 | "serde_json", 19 | "spin-sdk", 20 | ] 21 | 22 | [[package]] 23 | name = "async-trait" 24 | version = "0.1.74" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" 27 | dependencies = [ 28 | "proc-macro2", 29 | "quote", 30 | "syn 2.0.41", 31 | ] 32 | 33 | [[package]] 34 | name = "autocfg" 35 | version = "1.1.0" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 38 | 39 | [[package]] 40 | name = "bitflags" 41 | version = "2.4.1" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 44 | 45 | [[package]] 46 | name = "build_html" 47 | version = "2.4.0" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "3108fe6fe7ac796fb7625bdde8fa2b67b5a7731496251ca57c7b8cadd78a16a1" 50 | 51 | [[package]] 52 | name = "bytes" 53 | version = "1.5.0" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 56 | 57 | [[package]] 58 | name = "equivalent" 59 | version = "1.0.1" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 62 | 63 | [[package]] 64 | name = "fnv" 65 | version = "1.0.7" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 68 | 69 | [[package]] 70 | name = "form_urlencoded" 71 | version = "1.2.1" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 74 | dependencies = [ 75 | "percent-encoding", 76 | ] 77 | 78 | [[package]] 79 | name = "futures" 80 | version = "0.3.29" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" 83 | dependencies = [ 84 | "futures-channel", 85 | "futures-core", 86 | "futures-executor", 87 | "futures-io", 88 | "futures-sink", 89 | "futures-task", 90 | "futures-util", 91 | ] 92 | 93 | [[package]] 94 | name = "futures-channel" 95 | version = "0.3.29" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" 98 | dependencies = [ 99 | "futures-core", 100 | "futures-sink", 101 | ] 102 | 103 | [[package]] 104 | name = "futures-core" 105 | version = "0.3.29" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" 108 | 109 | [[package]] 110 | name = "futures-executor" 111 | version = "0.3.29" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" 114 | dependencies = [ 115 | "futures-core", 116 | "futures-task", 117 | "futures-util", 118 | ] 119 | 120 | [[package]] 121 | name = "futures-io" 122 | version = "0.3.29" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" 125 | 126 | [[package]] 127 | name = "futures-macro" 128 | version = "0.3.29" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" 131 | dependencies = [ 132 | "proc-macro2", 133 | "quote", 134 | "syn 2.0.41", 135 | ] 136 | 137 | [[package]] 138 | name = "futures-sink" 139 | version = "0.3.29" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" 142 | 143 | [[package]] 144 | name = "futures-task" 145 | version = "0.3.29" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" 148 | 149 | [[package]] 150 | name = "futures-util" 151 | version = "0.3.29" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" 154 | dependencies = [ 155 | "futures-channel", 156 | "futures-core", 157 | "futures-io", 158 | "futures-macro", 159 | "futures-sink", 160 | "futures-task", 161 | "memchr", 162 | "pin-project-lite", 163 | "pin-utils", 164 | "slab", 165 | ] 166 | 167 | [[package]] 168 | name = "hashbrown" 169 | version = "0.14.3" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 172 | 173 | [[package]] 174 | name = "heck" 175 | version = "0.4.1" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 178 | dependencies = [ 179 | "unicode-segmentation", 180 | ] 181 | 182 | [[package]] 183 | name = "http" 184 | version = "1.1.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 187 | dependencies = [ 188 | "bytes", 189 | "fnv", 190 | "itoa", 191 | ] 192 | 193 | [[package]] 194 | name = "id-arena" 195 | version = "2.2.1" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" 198 | 199 | [[package]] 200 | name = "indexmap" 201 | version = "2.1.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" 204 | dependencies = [ 205 | "equivalent", 206 | "hashbrown", 207 | "serde", 208 | ] 209 | 210 | [[package]] 211 | name = "itoa" 212 | version = "1.0.10" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 215 | 216 | [[package]] 217 | name = "leb128" 218 | version = "0.2.5" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" 221 | 222 | [[package]] 223 | name = "log" 224 | version = "0.4.20" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 227 | 228 | [[package]] 229 | name = "memchr" 230 | version = "2.6.4" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 233 | 234 | [[package]] 235 | name = "once_cell" 236 | version = "1.19.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 239 | 240 | [[package]] 241 | name = "percent-encoding" 242 | version = "2.3.1" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 245 | 246 | [[package]] 247 | name = "pin-project-lite" 248 | version = "0.2.13" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 251 | 252 | [[package]] 253 | name = "pin-utils" 254 | version = "0.1.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 257 | 258 | [[package]] 259 | name = "proc-macro2" 260 | version = "1.0.70" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" 263 | dependencies = [ 264 | "unicode-ident", 265 | ] 266 | 267 | [[package]] 268 | name = "quote" 269 | version = "1.0.33" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 272 | dependencies = [ 273 | "proc-macro2", 274 | ] 275 | 276 | [[package]] 277 | name = "routefinder" 278 | version = "0.5.3" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "94f8f99b10dedd317514253dda1fa7c14e344aac96e1f78149a64879ce282aca" 281 | dependencies = [ 282 | "smartcow", 283 | "smartstring", 284 | ] 285 | 286 | [[package]] 287 | name = "ryu" 288 | version = "1.0.16" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 291 | 292 | [[package]] 293 | name = "semver" 294 | version = "1.0.20" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" 297 | 298 | [[package]] 299 | name = "serde" 300 | version = "1.0.193" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 303 | dependencies = [ 304 | "serde_derive", 305 | ] 306 | 307 | [[package]] 308 | name = "serde_derive" 309 | version = "1.0.193" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 312 | dependencies = [ 313 | "proc-macro2", 314 | "quote", 315 | "syn 2.0.41", 316 | ] 317 | 318 | [[package]] 319 | name = "serde_json" 320 | version = "1.0.108" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 323 | dependencies = [ 324 | "itoa", 325 | "ryu", 326 | "serde", 327 | ] 328 | 329 | [[package]] 330 | name = "slab" 331 | version = "0.4.9" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 334 | dependencies = [ 335 | "autocfg", 336 | ] 337 | 338 | [[package]] 339 | name = "smallvec" 340 | version = "1.11.2" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 343 | 344 | [[package]] 345 | name = "smartcow" 346 | version = "0.2.1" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "656fcb1c1fca8c4655372134ce87d8afdf5ec5949ebabe8d314be0141d8b5da2" 349 | dependencies = [ 350 | "smartstring", 351 | ] 352 | 353 | [[package]] 354 | name = "smartstring" 355 | version = "1.0.1" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" 358 | dependencies = [ 359 | "autocfg", 360 | "static_assertions", 361 | "version_check", 362 | ] 363 | 364 | [[package]] 365 | name = "spdx" 366 | version = "0.10.2" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "b19b32ed6d899ab23174302ff105c1577e45a06b08d4fe0a9dd13ce804bbbf71" 369 | dependencies = [ 370 | "smallvec", 371 | ] 372 | 373 | [[package]] 374 | name = "spin-executor" 375 | version = "3.0.1" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "2df1a5e2cc70a628c9ea6914770c234cc4a292218091e6707ae8be68b4a5de76" 378 | dependencies = [ 379 | "futures", 380 | "once_cell", 381 | "wit-bindgen", 382 | ] 383 | 384 | [[package]] 385 | name = "spin-macro" 386 | version = "3.0.1" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "ef3d03e5a205a641d85ace3af1604b39dba63d3ffe3865a71bda02fb482ae60a" 389 | dependencies = [ 390 | "anyhow", 391 | "bytes", 392 | "proc-macro2", 393 | "quote", 394 | "syn 1.0.109", 395 | ] 396 | 397 | [[package]] 398 | name = "spin-sdk" 399 | version = "3.0.1" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "ed97f54a15f2d8b1fa15e436d88bacb95a5b379a3e0f8fbd8042eb8696ca048a" 402 | dependencies = [ 403 | "anyhow", 404 | "async-trait", 405 | "bytes", 406 | "form_urlencoded", 407 | "futures", 408 | "http", 409 | "once_cell", 410 | "routefinder", 411 | "serde", 412 | "serde_json", 413 | "spin-executor", 414 | "spin-macro", 415 | "thiserror", 416 | "wit-bindgen", 417 | ] 418 | 419 | [[package]] 420 | name = "static_assertions" 421 | version = "1.1.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 424 | 425 | [[package]] 426 | name = "syn" 427 | version = "1.0.109" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 430 | dependencies = [ 431 | "proc-macro2", 432 | "quote", 433 | "unicode-ident", 434 | ] 435 | 436 | [[package]] 437 | name = "syn" 438 | version = "2.0.41" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" 441 | dependencies = [ 442 | "proc-macro2", 443 | "quote", 444 | "unicode-ident", 445 | ] 446 | 447 | [[package]] 448 | name = "thiserror" 449 | version = "1.0.51" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" 452 | dependencies = [ 453 | "thiserror-impl", 454 | ] 455 | 456 | [[package]] 457 | name = "thiserror-impl" 458 | version = "1.0.51" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" 461 | dependencies = [ 462 | "proc-macro2", 463 | "quote", 464 | "syn 2.0.41", 465 | ] 466 | 467 | [[package]] 468 | name = "unicode-ident" 469 | version = "1.0.12" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 472 | 473 | [[package]] 474 | name = "unicode-segmentation" 475 | version = "1.10.1" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 478 | 479 | [[package]] 480 | name = "unicode-xid" 481 | version = "0.2.4" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" 484 | 485 | [[package]] 486 | name = "version_check" 487 | version = "0.9.4" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 490 | 491 | [[package]] 492 | name = "wasm-encoder" 493 | version = "0.38.1" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "0ad2b51884de9c7f4fe2fd1043fccb8dcad4b1e29558146ee57a144d15779f3f" 496 | dependencies = [ 497 | "leb128", 498 | ] 499 | 500 | [[package]] 501 | name = "wasm-metadata" 502 | version = "0.10.14" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "d835d67708f6374937c550ad8dd1d17c616ae009e3f00d7a0ac9f7825e78c36a" 505 | dependencies = [ 506 | "anyhow", 507 | "indexmap", 508 | "serde", 509 | "serde_derive", 510 | "serde_json", 511 | "spdx", 512 | "wasm-encoder", 513 | "wasmparser", 514 | ] 515 | 516 | [[package]] 517 | name = "wasmparser" 518 | version = "0.118.1" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "95ee9723b928e735d53000dec9eae7b07a60e490c85ab54abb66659fc61bfcd9" 521 | dependencies = [ 522 | "indexmap", 523 | "semver", 524 | ] 525 | 526 | [[package]] 527 | name = "wit-bindgen" 528 | version = "0.16.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "b76f1d099678b4f69402a421e888bbe71bf20320c2f3f3565d0e7484dbe5bc20" 531 | dependencies = [ 532 | "bitflags", 533 | "wit-bindgen-rust-macro", 534 | ] 535 | 536 | [[package]] 537 | name = "wit-bindgen-core" 538 | version = "0.16.0" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "75d55e1a488af2981fb0edac80d8d20a51ac36897a1bdef4abde33c29c1b6d0d" 541 | dependencies = [ 542 | "anyhow", 543 | "wit-component", 544 | "wit-parser", 545 | ] 546 | 547 | [[package]] 548 | name = "wit-bindgen-rust" 549 | version = "0.16.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "a01ff9cae7bf5736750d94d91eb8a49f5e3a04aff1d1a3218287d9b2964510f8" 552 | dependencies = [ 553 | "anyhow", 554 | "heck", 555 | "wasm-metadata", 556 | "wit-bindgen-core", 557 | "wit-component", 558 | ] 559 | 560 | [[package]] 561 | name = "wit-bindgen-rust-macro" 562 | version = "0.16.0" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "804a98e2538393d47aa7da65a7348116d6ff403b426665152b70a168c0146d49" 565 | dependencies = [ 566 | "anyhow", 567 | "proc-macro2", 568 | "quote", 569 | "syn 2.0.41", 570 | "wit-bindgen-core", 571 | "wit-bindgen-rust", 572 | "wit-component", 573 | ] 574 | 575 | [[package]] 576 | name = "wit-component" 577 | version = "0.18.2" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "5b8a35a2a9992898c9d27f1664001860595a4bc99d32dd3599d547412e17d7e2" 580 | dependencies = [ 581 | "anyhow", 582 | "bitflags", 583 | "indexmap", 584 | "log", 585 | "serde", 586 | "serde_derive", 587 | "serde_json", 588 | "wasm-encoder", 589 | "wasm-metadata", 590 | "wasmparser", 591 | "wit-parser", 592 | ] 593 | 594 | [[package]] 595 | name = "wit-parser" 596 | version = "0.13.2" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "316b36a9f0005f5aa4b03c39bc3728d045df136f8c13a73b7db4510dec725e08" 599 | dependencies = [ 600 | "anyhow", 601 | "id-arena", 602 | "indexmap", 603 | "log", 604 | "semver", 605 | "serde", 606 | "serde_derive", 607 | "serde_json", 608 | "unicode-xid", 609 | ] 610 | -------------------------------------------------------------------------------- /api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "api" 3 | authors = ["Thorsten Hans "] 4 | description = "" 5 | version = "0.1.0" 6 | edition = "2021" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | anyhow = "1" 13 | build_html = "2.4.0" 14 | serde = { version = "1.0.193", features = ["derive"] } 15 | serde_json = "1.0.108" 16 | spin-sdk = "3.0.1" 17 | 18 | [workspace] 19 | -------------------------------------------------------------------------------- /api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use build_html::{Container, ContainerType, Html, HtmlContainer}; 3 | use serde::{Deserialize, Serialize}; 4 | use spin_sdk::http::{IntoResponse, Params, Request, Response, Router}; 5 | use spin_sdk::http_component; 6 | use spin_sdk::sqlite::{Connection, Value}; 7 | 8 | #[http_component] 9 | fn handle_api(req: Request) -> Result { 10 | // lets use the Router to handle requests based on method and path 11 | let mut r = Router::default(); 12 | r.post("/api/items", add_new); 13 | r.get("/api/items", get_all); 14 | r.delete("/api/items/:id", delete_one); 15 | Ok(r.handle(req)) 16 | } 17 | 18 | #[derive(Debug, Deserialize, Serialize)] 19 | struct Item { 20 | #[serde(skip_deserializing)] 21 | id: i64, 22 | value: String, 23 | } 24 | 25 | impl Html for Item { 26 | fn to_html_string(&self) -> String { 27 | Container::new(ContainerType::Div) 28 | .with_attributes(vec![ 29 | ("class", "item"), 30 | ("id", format!("item-{}", &self.id).as_str()), 31 | ]) 32 | .with_container( 33 | Container::new(ContainerType::Div) 34 | .with_attributes(vec![("class", "value")]) 35 | .with_raw(&self.value), 36 | ) 37 | .with_container( 38 | Container::new(ContainerType::Div) 39 | .with_attributes(vec![ 40 | ("class", "delete-item"), 41 | ("hx-delete", format!("/api/items/{}", &self.id).as_str()), 42 | ]) 43 | .with_raw("❌"), 44 | ) 45 | .to_html_string() 46 | } 47 | } 48 | 49 | fn get_all(_r: Request, _p: Params) -> Result { 50 | let connection = Connection::open_default()?; 51 | 52 | let row_set = connection.execute("SELECT ID, VALUE FROM ITEMS ORDER BY ID DESC", &[])?; 53 | let items = row_set 54 | .rows() 55 | .map(|row| Item { 56 | id: row.get::("ID").unwrap(), 57 | value: row.get::<&str>("VALUE").unwrap().to_owned(), 58 | }) 59 | .map(|item| item.to_html_string()) 60 | .reduce(|acc, e| format!("{} {}", acc, e)) 61 | .unwrap_or(String::from("")); 62 | 63 | Ok(Response::builder() 64 | .status(200) 65 | .header("Content-Type", "text/html") 66 | .body(items) 67 | .build()) 68 | } 69 | 70 | fn add_new(req: Request, _params: Params) -> Result { 71 | let Ok(item): Result = 72 | serde_json::from_reader(req.body()).with_context(|| "Error while deserializing payload") 73 | else { 74 | return Ok(Response::new(400, "Invalid payload received")); 75 | }; 76 | let connection = Connection::open_default()?; 77 | let parameters = &[Value::Text(item.value)]; 78 | connection.execute("INSERT INTO ITEMS (VALUE) VALUES (?)", parameters)?; 79 | Ok(Response::builder() 80 | .status(200) 81 | .header("HX-Trigger", "newItem") 82 | .body(()) 83 | .build()) 84 | } 85 | 86 | fn delete_one(_req: Request, params: Params) -> Result { 87 | let Some(id) = params.get("id") else { 88 | return Ok(Response::new(404, "Missing identifier")); 89 | }; 90 | let Ok(id) = id.parse::() else { 91 | return Ok(Response::new(400, "Unexpected identifier format")); 92 | }; 93 | 94 | let connection = Connection::open_default()?; 95 | let parameters = &[Value::Integer(id)]; 96 | 97 | Ok( 98 | match connection.execute("DELETE FROM ITEMS WHERE ID = ?", parameters) { 99 | // HTMX requires status 200 instead of 204 100 | Ok(_) => Response::new(200, ()), 101 | Err(e) => { 102 | println!("Error while deleting item: {}", e); 103 | Response::builder() 104 | .status(500) 105 | .body("Error while deleting item") 106 | .build() 107 | } 108 | }, 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /app/htmx.min.js: -------------------------------------------------------------------------------- 1 | (function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:F,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Br,logAll:V,logNone:j,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null},parseInterval:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.10"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t=false){return new RegExp(`<${e}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${e}>`,t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function a(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/",0);return i.querySelector("template").content}switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return a(""+n+"
",1);case"col":return a(""+n+"
",2);case"tr":return a(""+n+"
",2);case"td":case"th":return a(""+n+"
",3);case"script":case"style":return a("
"+n+"
",1);default:return a(n,0)}}function ie(e){if(e){e()}}function I(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return I(e,"Function")}function P(e){return I(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function M(e){var t=[];if(e){for(var r=0;r=0}function se(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return re().body.contains(e.getRootNode().host)}else{return re().body.contains(e)}}function D(e){return e.trim().split(/\s+/)}function le(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function E(e){try{return JSON.parse(e)}catch(e){b(e);return null}}function U(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function B(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function t(e){return Tr(re().body,function(){return eval(e)})}function F(t){var e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function j(){Q.logger=null}function C(e,t){if(t){return e.querySelector(t)}else{return C(re(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(re(),e)}}function _(e,t){e=g(e);if(t){setTimeout(function(){_(e);e=null},t)}else{e.parentElement.removeChild(e)}}function z(e,t,r){e=g(e);if(r){setTimeout(function(){z(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=g(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function $(e,t){e=g(e);e.classList.toggle(t)}function W(e,t){e=g(e);oe(e.parentElement.children,function(e){n(e,t)});z(e,t)}function v(e,t){e=g(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function s(e,t){return e.substring(0,t.length)===t}function G(e,t){return e.substring(e.length-t.length)===t}function J(e){var t=e.trim();if(s(t,"<")&&G(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function Z(e,t){if(t.indexOf("closest ")===0){return[v(e,J(t.substr(8)))]}else if(t.indexOf("find ")===0){return[C(e,J(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[K(e,J(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[Y(e,J(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return re().querySelectorAll(J(t))}}var K=function(e,t){var r=re().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ue(e,t){if(t){return Z(e,t)[0]}else{return Z(re().body,e)[0]}}function g(e){if(I(e,"String")){return C(e)}else{return e}}function ve(e,t,r){if(k(t)){return{target:re().body,event:e,listener:t}}else{return{target:g(e),event:t,listener:r}}}function de(t,r,n){jr(function(){var e=ve(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=k(r);return e?r:n}function ge(t,r,n){jr(function(){var e=ve(t,r,n);e.target.removeEventListener(e.event,e.listener)});return k(r)?r:n}var me=re().createElement("output");function pe(e,t){var r=ne(e,t);if(r){if(r==="this"){return[xe(e,t)]}else{var n=Z(e,r);if(n.length===0){b('The selector "'+r+'" on '+t+" returned no matches!");return[me]}else{return n}}}}function xe(e,t){return c(e,function(e){return te(e,t)!=null})}function ye(e){var t=ne(e,"hx-target");if(t){if(t==="this"){return xe(e,"hx-target")}else{return ue(e,t)}}else{var r=ae(e);if(r.boosted){return re().body}else{return e}}}function be(e){var t=Q.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=re().querySelectorAll(t);if(r){oe(r,function(e){var t;var r=i.cloneNode(true);t=re().createDocumentFragment();t.appendChild(r);if(!Se(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ce(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Fe(o,e,e,t,a)}oe(a.elts,function(e){ce(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);fe(re().body,"htmx:oobErrorNoTarget",{content:i})}return e}function Ce(e,t,r){var n=ne(e,"hx-select-oob");if(n){var i=n.split(",");for(var a=0;a0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();we(e,i);s.tasks.push(function(){we(e,a)})}}})}function Oe(e){return function(){n(e,Q.config.addedClass);zt(e);Nt(e);qe(e);ce(e,"htmx:load")}}function qe(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function m(e,t,r,n){Te(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;z(i,Q.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Oe(i))}}}function He(e,t){var r=0;while(r-1){var t=e.replace(H,"");var r=t.match(q);if(r){return r[2]}}}function je(e,t,r,n,i,a){i.title=Ve(n);var o=l(n);if(o){Ce(r,o,i);o=Be(r,o,a);Re(o);return Fe(e,r,t,o,i)}}function _e(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=E(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!P(o)){o={value:o}}ce(r,a,o)}}}else{var s=n.split(",");for(var l=0;l0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=Tr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){fe(re().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Qe(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function y(e,t){var r="";while(e.length>0&&!t.test(e[0])){r+=e.shift()}return r}function tt(e){var t;if(e.length>0&&Ze.test(e[0])){e.shift();t=y(e,Ke).trim();e.shift()}else{t=y(e,x)}return t}var rt="input, textarea, select";function nt(e,t,r){var n=[];var i=Ye(t);do{y(i,Je);var a=i.length;var o=y(i,/[,\[\s]/);if(o!==""){if(o==="every"){var s={trigger:"every"};y(i,Je);s.pollInterval=d(y(i,/[,\[\s]/));y(i,Je);var l=et(e,i,"event");if(l){s.eventFilter=l}n.push(s)}else if(o.indexOf("sse:")===0){n.push({trigger:"sse",sseEvent:o.substr(4)})}else{var u={trigger:o};var l=et(e,i,"event");if(l){u.eventFilter=l}while(i.length>0&&i[0]!==","){y(i,Je);var f=i.shift();if(f==="changed"){u.changed=true}else if(f==="once"){u.once=true}else if(f==="consume"){u.consume=true}else if(f==="delay"&&i[0]===":"){i.shift();u.delay=d(y(i,x))}else if(f==="from"&&i[0]===":"){i.shift();if(Ze.test(i[0])){var c=tt(i)}else{var c=y(i,x);if(c==="closest"||c==="find"||c==="next"||c==="previous"){i.shift();var h=tt(i);if(h.length>0){c+=" "+h}}}u.from=c}else if(f==="target"&&i[0]===":"){i.shift();u.target=tt(i)}else if(f==="throttle"&&i[0]===":"){i.shift();u.throttle=d(y(i,x))}else if(f==="queue"&&i[0]===":"){i.shift();u.queue=y(i,x)}else if(f==="root"&&i[0]===":"){i.shift();u[f]=tt(i)}else if(f==="threshold"&&i[0]===":"){i.shift();u[f]=y(i,x)}else{fe(e,"htmx:syntax:error",{token:i.shift()})}}n.push(u)}}if(i.length===a){fe(e,"htmx:syntax:error",{token:i.shift()})}y(i,Je)}while(i[0]===","&&i.shift());if(r){r[t]=n}return n}function it(e){var t=te(e,"hx-trigger");var r=[];if(t){var n=Q.config.triggerSpecsCache;r=n&&n[t]||nt(e,t,n)}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,rt)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function at(e){ae(e).cancelled=true}function ot(e,t,r){var n=ae(e);n.timeout=setTimeout(function(){if(se(e)&&n.cancelled!==true){if(!ct(r,e,Wt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}ot(e,t,r)}},r.pollInterval)}function st(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function lt(t,r,e){if(t.tagName==="A"&&st(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=ee(t,"href")}else{var a=ee(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=ee(t,"action")}e.forEach(function(e){ht(t,function(e,t){if(v(e,Q.config.disableSelector)){p(e);return}he(n,i,e,t)},r,e,true)})}}function ut(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&v(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ft(e,t){return ae(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function ct(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){fe(re().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function ht(a,o,e,s,l){var u=ae(a);var t;if(s.from){t=Z(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ae(e);t.lastValue=e.value})}oe(t,function(n){var i=function(e){if(!se(a)){n.removeEventListener(s.trigger,i);return}if(ft(a,e)){return}if(l||ut(e,a)){e.preventDefault()}if(ct(s,a,e)){return}var t=ae(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ae(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle>0){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay>0){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{ce(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var vt=false;var dt=null;function gt(){if(!dt){dt=function(){vt=true};window.addEventListener("scroll",dt);setInterval(function(){if(vt){vt=false;oe(re().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){mt(e)})}},200)}}function mt(t){if(!o(t,"data-hx-revealed")&&X(t)){t.setAttribute("data-hx-revealed","true");var e=ae(t);if(e.initHash){ce(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){ce(t,"revealed")},{once:true})}}}function pt(e,t,r){var n=D(r);for(var i=0;i=0){var t=wt(n);setTimeout(function(){xt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ae(s).webSocket=t;t.addEventListener("message",function(e){if(yt(s)){return}var t=e.data;R(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=M(n.children);for(var a=0;a0){ce(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(ut(e,u)){e.preventDefault()}})}else{fe(u,"htmx:noWebSocketSourceError")}}function wt(e){var t=Q.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}b('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function St(e,t,r){var n=D(r);for(var i=0;i0){setTimeout(i,n)}else{i()}}function Ht(t,i,e){var a=false;oe(w,function(r){if(o(t,"hx-"+r)){var n=te(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){Lt(t,e,i,function(e,t){if(v(e,Q.config.disableSelector)){p(e);return}he(r,n,e,t)})})}});return a}function Lt(n,e,t,r){if(e.sseEvent){Rt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){gt();ht(n,r,t,e);mt(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=ue(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t0){t.polling=true;ot(n,r,e)}else{ht(n,r,t,e)}}function At(e){if(Q.config.allowScriptTags&&(e.type==="text/javascript"||e.type==="module"||e.type==="")){var t=re().createElement("script");oe(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){b(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Nt(e){if(h(e,"script")){At(e)}oe(f(e,"script"),function(e){At(e)})}function It(e){var t=e.attributes;for(var r=0;r0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Bt(o)}for(var l in r){Ft(e,l,r[l])}}}function jt(e){Ae(e);for(var t=0;tQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(re().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Yt(e){if(!U()){return null}e=B(e);var t=E(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){ce(re().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Zt();var r=T(t);var n=Ve(this.response);if(n){var i=C("title");if(i){i.innerHTML=n}else{window.document.title=n}}Ue(t,e,r);nr(r.tasks);Jt=a;ce(re().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{fe(re().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function ar(e){er();e=e||location.pathname+location.search;var t=Yt(e);if(t){var r=l(t.content);var n=Zt();var i=T(n);Ue(n,r,i);nr(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Jt=e;ce(re().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{ir(e)}}}function or(e){var t=pe(e,"hx-indicator");if(t==null){t=[e]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Q.config.requestClass)});return t}function sr(e){var t=pe(e,"hx-disabled-elt");if(t==null){t=[]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function lr(e,t){oe(e,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Q.config.requestClass)}});oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function ur(e,t){for(var r=0;r=0}function wr(e,t){var r=t?t:ne(e,"hx-swap");var n={swapStyle:ae(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ae(e).boosted&&!br(e)){n["show"]="top"}if(r){var i=D(r);if(i.length>0){for(var a=0;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var v=o.substr("focus-scroll:".length);n["focusScroll"]=v=="true"}else if(a==0){n["swapStyle"]=o}else{b("Unknown modifier in hx-swap: "+o)}}}}return n}function Sr(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function Er(t,r,n){var i=null;R(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(Sr(r)){return pr(n)}else{return mr(n)}}}function T(e){return{tasks:[],elts:[e]}}function Cr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ue(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ue(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Rr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=te(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=Tr(e,function(){return Function("return ("+a+")")()},{})}else{s=E(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return Rr(u(e),t,r,n)}function Tr(e,t,r){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return r}}function Or(e,t){return Rr(e,"hx-vars",true,t)}function qr(e,t){return Rr(e,"hx-vals",false,t)}function Hr(e){return le(Or(e),qr(e))}function Lr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Ar(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(re().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Nr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||I(r,"String")){return he(e,t,null,null,{targetOverride:g(r),returnPromise:true})}else{return he(e,t,g(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:g(r.target),swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function Ir(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function kr(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=s(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!n){return false}}return ce(e,"htmx:validateUrl",le({url:i,sameHost:n},r))}function he(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=re().body}var M=a.handler||Mr;var X=a.select||null;if(!se(n)){ie(o);return l}var u=a.targetOverride||ye(n);if(u==null||u==me){fe(n,"htmx:targetError",{target:te(n,"hx-target")});ie(s);return l}var f=ae(n);var c=f.lastButtonClicked;if(c){var h=ee(c,"formaction");if(h!=null){r=h}var v=ee(c,"formmethod");if(v!=null){if(v.toLowerCase()!=="dialog"){t=v}}}var d=ne(n,"hx-confirm");if(e===undefined){var D=function(e){return he(t,r,n,i,a,!!e)};var U={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:D,question:d};if(ce(n,"htmx:confirm",U)===false){ie(o);return l}}var g=n;var m=ne(n,"hx-sync");var p=null;var x=false;if(m){var B=m.split(":");var F=B[0].trim();if(F==="this"){g=xe(n,"hx-sync")}else{g=ue(n,F)}m=(B[1]||"drop").trim();f=ae(g);if(m==="drop"&&f.xhr&&f.abortable!==true){ie(o);return l}else if(m==="abort"){if(f.xhr){ie(o);return l}else{x=true}}else if(m==="replace"){ce(g,"htmx:abort")}else if(m.indexOf("queue")===0){var V=m.split(" ");p=(V[1]||"last").trim()}}if(f.xhr){if(f.abortable){ce(g,"htmx:abort")}else{if(p==null){if(i){var y=ae(i);if(y&&y.triggerSpec&&y.triggerSpec.queue){p=y.triggerSpec.queue}}if(p==null){p="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(p==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(p==="all"){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(p==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){he(t,r,n,i,a)})}ie(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=x;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var j=ne(n,"hx-prompt");if(j){var S=prompt(j);if(S===null||!ce(n,"htmx:prompt",{prompt:S,target:u})){ie(o);w();return l}}if(d&&!e){if(!confirm(d)){ie(o);w();return l}}var E=xr(n,u,S);if(t!=="get"&&!Sr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(a.headers){E=le(E,a.headers)}var _=dr(n,t);var C=_.errors;var R=_.values;if(a.values){R=le(R,a.values)}var z=Hr(n);var $=le(R,z);var T=yr($,n);if(Q.config.getCacheBusterParam&&t==="get"){T["org.htmx.cache-buster"]=ee(u,"id")||"true"}if(r==null||r===""){r=re().location.href}var O=Rr(n,"hx-request");var W=ae(n).boosted;var q=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:W,useUrlParams:q,parameters:T,unfilteredParameters:$,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||O.credentials||Q.config.withCredentials,timeout:a.timeout||O.timeout||Q.config.timeout,path:r,triggeringEvent:i};if(!ce(n,"htmx:configRequest",H)){ie(o);w();return l}r=H.path;t=H.verb;E=H.headers;T=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){ce(n,"htmx:validation:halted",H);ie(o);w();return l}var G=r.split("#");var J=G[0];var L=G[1];var A=r;if(q){A=J;var Z=Object.keys(T).length!==0;if(Z){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=mr(T);if(L){A+="#"+L}}}if(!kr(n,A,H)){fe(n,"htmx:invalidPath",H);ie(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(O.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var K=E[N];Lr(b,N,K)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:W,select:X,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Ir(n);I.pathInfo.responsePath=Ar(b);M(n,I);lr(k,P);ce(n,"htmx:afterRequest",I);ce(n,"htmx:afterOnLoad",I);if(!se(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(se(r)){t=r}}if(t){ce(t,"htmx:afterRequest",I);ce(t,"htmx:afterOnLoad",I)}}ie(o);w()}catch(e){fe(n,"htmx:onLoadError",le({error:e},I));throw e}};b.onerror=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendError",I);ie(s);w()};b.onabort=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendAbort",I);ie(s);w()};b.ontimeout=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:timeout",I);ie(s);w()};if(!ce(n,"htmx:beforeRequest",I)){ie(o);w();return l}var k=or(n);var P=sr(n);oe(["loadstart","loadend","progress","abort"],function(t){oe([b,b.upload],function(e){e.addEventListener(t,function(e){ce(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ce(n,"htmx:beforeSend",I);var Y=q?null:Er(b,n,T);b.send(Y);return l}function Pr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=ne(e,"hx-push-url");var l=ne(e,"hx-replace-url");var u=ae(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function Mr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;var h=u.select;if(!ce(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){_e(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){er();var r=f.getResponseHeader("HX-Location");var v;if(r.indexOf("{")===0){v=E(r);r=v["path"];delete v["path"]}Nr("GET",r,v).then(function(){tr(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){if(f.getResponseHeader("HX-Retarget")==="this"){u.target=l}else{u.target=ue(l,f.getResponseHeader("HX-Retarget"))}}var d=Pr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var g=f.response;var a=f.status>=400;var m=Q.config.ignoreTitle;var o=le({shouldSwap:i,serverResponse:g,isError:a,ignoreTitle:m},u);if(!ce(c,"htmx:beforeSwap",o))return;c=o.target;g=o.serverResponse;a=o.isError;m=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){at(l)}R(l,function(e){g=e.transformResponse(g,f,l)});if(d.type){er()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var v=wr(l,s);if(v.hasOwnProperty("ignoreTitle")){m=v.ignoreTitle}c.classList.add(Q.config.swappingClass);var p=null;var x=null;var y=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(h){r=h}if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}if(d.type){ce(re().body,"htmx:beforeHistoryUpdate",le({history:d},u));if(d.type==="push"){tr(d.path);ce(re().body,"htmx:pushedIntoHistory",{path:d.path})}else{rr(d.path);ce(re().body,"htmx:replacedInHistory",{path:d.path})}}var n=T(c);je(v.swapStyle,c,l,g,n,r);if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){var i=document.getElementById(ee(t.elt,"id"));var a={preventScroll:v.focusScroll!==undefined?!v.focusScroll:!Q.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Q.config.swappingClass);oe(n.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ce(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!se(l)){o=re().body}_e(f,"HX-Trigger-After-Swap",o)}var s=function(){oe(n.tasks,function(e){e.call()});oe(n.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ce(e,"htmx:afterSettle",u)});if(u.pathInfo.anchor){var e=re().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!m){var t=C("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}Cr(n.elts,v);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!se(l)){r=re().body}_e(f,"HX-Trigger-After-Settle",r)}ie(p)};if(v.settleDelay>0){setTimeout(s,v.settleDelay)}else{s()}}catch(e){fe(l,"htmx:swapError",u);ie(x);throw e}};var b=Q.config.globalViewTransitions;if(v.hasOwnProperty("transition")){b=v.transition}if(b&&ce(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var w=new Promise(function(e,t){p=e;x=t});var S=y;y=function(){document.startViewTransition(function(){S();return w})}}if(v.swapDelay>0){setTimeout(y,v.swapDelay)}else{y()}}if(a){fe(l,"htmx:responseError",le({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Xr={};function Dr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Ur(e,t){if(t.init){t.init(r)}Xr[e]=le(Dr(),t)}function Br(e){delete Xr[e]}function Fr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=te(e,"hx-ext");if(t){oe(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Xr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Fr(u(e),r,n)}var Vr=false;re().addEventListener("DOMContentLoaded",function(){Vr=true});function jr(e){if(Vr||re().readyState==="complete"){e()}else{re().addEventListener("DOMContentLoaded",e)}}function _r(){if(Q.config.includeIndicatorStyles!==false){re().head.insertAdjacentHTML("beforeend","")}}function zr(){var e=re().querySelector('meta[name="htmx-config"]');if(e){return E(e.content)}else{return null}}function $r(){var e=zr();if(e){Q.config=le(Q.config,e)}}jr(function(){$r();_r();var e=re().body;zt(e);var t=re().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ae(t);if(r&&r.xhr){r.xhr.abort()}});const r=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){ar();oe(t,function(e){ce(e,"htmx:restored",{document:re(),triggerEvent:ce})})}else{if(r){r(e)}}};setTimeout(function(){ce(e,"htmx:load",{});e=null},0)});return Q}()}); -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shopping List 7 | 8 | 9 | 10 |
11 |

Shopping List | powered by Spin & HTMX

12 |

This is a simple shopping list for demonstrating the combination of HTMX and Fermyon Spin.

14 |
15 |
16 |
18 |
19 |
20 | 27 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/json-enc.js: -------------------------------------------------------------------------------- 1 | htmx.defineExtension('json-enc', { 2 | onEvent: function (name, evt) { 3 | if (name === "htmx:configRequest") { 4 | evt.detail.headers['Content-Type'] = "application/json"; 5 | } 6 | }, 7 | 8 | encodeParameters : function(xhr, parameters, elt) { 9 | xhr.overrideMimeType('text/json'); 10 | return (JSON.stringify(parameters)); 11 | } 12 | }); -------------------------------------------------------------------------------- /app/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Verdana, Geneva, Tahoma, sans-serif; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | overflow: hidden; 8 | height: 100vh; 9 | } 10 | 11 | body, 12 | header, 13 | main, 14 | aside { 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | header { 20 | align-items: center; 21 | } 22 | 23 | main { 24 | flex: 6; 25 | padding: 1em; 26 | justify-content: start; 27 | align-items: center; 28 | overflow-y: scroll; 29 | overflow-x: hidden; 30 | } 31 | 32 | aside { 33 | flex: 2; 34 | align-items: center; 35 | } 36 | 37 | footer { 38 | display: flex; 39 | justify-content: center; 40 | font-size: 12px; 41 | font-style: italic; 42 | padding: 6px; 43 | color: #666; 44 | } 45 | 46 | h1 { 47 | font-size: 150%; 48 | margin: 12px; 49 | } 50 | 51 | h2 { 52 | font-size: 130%; 53 | margin: 8px; 54 | } 55 | 56 | a, 57 | a:hover, 58 | a:visited { 59 | color: #7c059c; 60 | text-decoration: none; 61 | } 62 | 63 | .slug { 64 | margin: 10px 20px; 65 | text-align: center; 66 | line-height: 1.4; 67 | color: #666; 68 | } 69 | 70 | #all-items { 71 | display: flex; 72 | flex-direction: column; 73 | width: 80%vw; 74 | margin: 30px; 75 | } 76 | 77 | .item { 78 | font-size: 20px; 79 | color: #666; 80 | font-style: italic; 81 | display: flex; 82 | flex-direction: row; 83 | justify-content: space-between; 84 | padding-bottom: 8px; 85 | margin-bottom: 16px; 86 | border-bottom: 1px solid #e5e4e4; 87 | } 88 | 89 | 90 | .item:hover { 91 | background-color: #f5f5f5; 92 | } 93 | 94 | 95 | .item>.value { 96 | width: 500px; 97 | margin-right: 10px; 98 | } 99 | 100 | @media only screen and (max-width: 768px) { 101 | .item { 102 | font-size: 16px; 103 | } 104 | .item > .value { 105 | 106 | overflow: hidden; 107 | width: 290px; 108 | } 109 | 110 | input[type='text'] { 111 | min-width: 100px; 112 | } 113 | } 114 | @media only screen and (min-width: 768px) and (max-width: 900px) { 115 | .item { 116 | font-size: 18px; 117 | word-wrap: break-word; 118 | } 119 | 120 | .item>.value { 121 | width: 450px; 122 | } 123 | h2 { 124 | font-size: 18px; 125 | } 126 | .slug { 127 | display: none; 128 | } 129 | input[type='text'] { 130 | min-width: 300px; 131 | } 132 | } 133 | 134 | @media only screen and (min-width:900px){ 135 | input[type='text'] { 136 | min-width: 300px; 137 | } 138 | } 139 | 140 | footer>* { 141 | margin: 0 3px; 142 | } 143 | 144 | /* Forms */ 145 | button { 146 | background-color: #7c059c; 147 | margin: 10px 0; 148 | padding: 12px 30px; 149 | border-radius: 5px; 150 | border: 1px solid #ccc; 151 | font-size: 16px; 152 | color: #fff; 153 | cursor: pointer; 154 | min-width: 130px; 155 | } 156 | 157 | input[type='text'] { 158 | margin: 10px 0; 159 | padding: 10px; 160 | border-radius: 5px; 161 | border: 1px solid #ccc; 162 | font-size: 16px; 163 | color: #666; 164 | } 165 | 166 | /* Misc */ 167 | .delete-item { 168 | cursor: pointer; 169 | } 170 | 171 | .item.htmx-swapping div { 172 | opacity: 0; 173 | transition: opacity 1s ease-out; 174 | } 175 | -------------------------------------------------------------------------------- /migration.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS ITEMS ( 2 | ID INTEGER PRIMARY KEY AUTOINCREMENT, 3 | VALUE TEXT NOT NULL 4 | ); 5 | 6 | DELETE FROM ITEMS; 7 | INSERT INTO ITEMS (VALUE) VALUES ('Milk'); 8 | INSERT INTO ITEMS (VALUE) VALUES ('Coffee'); 9 | -------------------------------------------------------------------------------- /spin.toml: -------------------------------------------------------------------------------- 1 | spin_manifest_version = 2 2 | 3 | [application] 4 | name = "spin-htmx" 5 | version = "0.1.0" 6 | authors = ["Thorsten Hans "] 7 | description = "A simple shopping list built with HTMX, Fermyon Spin & Rust" 8 | 9 | [[trigger.http]] 10 | route = "/api/..." 11 | component = "api" 12 | 13 | [[trigger.http]] 14 | route = "/..." 15 | component = "app" 16 | 17 | [component.app] 18 | source = { url = "https://github.com/fermyon/spin-fileserver/releases/download/v0.1.0/spin_static_fs.wasm", digest = "sha256:96c76d9af86420b39eb6cd7be5550e3cb5d4cc4de572ce0fd1f6a29471536cb4" } 19 | files = [{ source = "app", destination = "/" }] 20 | 21 | [component.api] 22 | source = "api/target/wasm32-wasi/release/api.wasm" 23 | allowed_outbound_hosts = [] 24 | sqlite_databases = ["default"] 25 | 26 | [component.api.build] 27 | command = "cargo build --target wasm32-wasi --release" 28 | workdir = "api" 29 | watch = ["src/**/*.rs", "Cargo.toml"] 30 | --------------------------------------------------------------------------------