Compare commits

...
Sign in to create a new pull request.

36 commits

Author SHA1 Message Date
443c7a7e0c build(files): remove unused item_form.rs and wikidata_lookup.rs files 2025-02-06 22:19:42 +03:00
4bfd47d8c4 feat(item_list): normalize property labels to display the right labels on the item list 2025-02-04 14:29:11 +03:00
94ed4c46b9 feat(item_list): succesfully fetch property labels using SPARQL 2025-02-03 19:48:50 +03:00
25b3128181 feat(item_list): use sparql to fetch properties 2025-02-03 14:58:56 +03:00
23cd674e31 feat(item_list): implement querying using GRAPHQL 2025-02-01 05:28:36 +03:00
af921088f9 feat(wikidata): enhance property value parsing and improve time handling
- Refactor `fetch_item_properties` to handle nested JSON types efficiently
- Introduce `parse_property_value` for structured processing of various value types
  - Handle time values with varying precision (year, month, day)
  - Parse and format dates from RFC 3339 format
  - Support fetching and displaying labels for Wikidata entity references
- Replace raw JSON object handling with cleaner structured parsing
- Update property label fetching and signal updates for better UI data synchronization
2025-01-30 14:59:55 +03:00
a40e9c98c4 feat(Item_list): auto-add property on dropdown selection
-No need for an "Add Property" button
2025-01-29 19:42:53 +03:00
2d072f3303 feat(item_list): move delete buttons next to item name 2025-01-29 16:07:10 +03:00
792b4daf04 feat(item_list): put item names into the header 2025-01-29 15:51:11 +03:00
9eb930da19 build(gitignore): update .gitignore to ignore compareware.db 2025-01-29 15:34:59 +03:00
e0c49ffa86 build(merge): merge branch 'db' into main 2025-01-29 15:30:35 +03:00
1318319ad1 fix(properties): reflect added properties in real time in the ui 2025-01-28 23:45:27 +03:00
ac8eb8118d fix(item_list):initialize with one empty item if the database is empty 2025-01-28 20:44:52 +03:00
4ff9928a94 buid(toolchain): update rust-toolchain.toml to use Rust 1.82.0 instead of 1.83.0 2025-01-28 15:19:31 +03:00
68b458df5e feat(db): enable user to delete items and properties from the database. 2025-01-28 14:36:17 +03:00
afa3bd3ece feat(Item_list): update ItemsList component to include delete button for property input fields 2025-01-28 02:43:07 +03:00
c38f19d76c feat(labels): persist property labels on refresh. 2025-01-27 16:48:28 +03:00
49315128f8 build(git): add compareware.db file to .gitignore 2025-01-27 16:38:23 +03:00
2455619735 build(git): add compareware.db file to .gitignore 2025-01-27 16:37:32 +03:00
3fa56abc83 feat(db): persist custom properties from db 2025-01-27 16:34:25 +03:00
e0e5fc49c2 feat(db): load items from database on startup.
-successfully loading names and description
2025-01-24 15:10:25 +03:00
c1207f613d feat(db): add selected properties state and update save_item_to_db function to include selected properties 2025-01-24 02:23:24 +03:00
fc13b0dae6 feat(db): enable db to update items keeping track of the item's id 2025-01-24 01:54:25 +03:00
3ed12c80a6 feat(db): integrate the database with the frontend. 2025-01-23 21:36:30 +03:00
0ac35c3ca5 fix(db): register custom API routes before the Leptos server function handler 2025-01-23 14:29:42 +03:00
e46b693e56 build(toolchain): update rust-toolchain.toml to use Rust 1.82.0 instead of 1.83.0 2025-01-23 00:02:48 +03:00
291cb05847 feat(db): run db on backend using actix web 2025-01-22 20:16:43 +03:00
af3f89c561 feat(db): add API endpoint for updating items to db and implement server-side functionality 2025-01-22 14:14:18 +03:00
5bd19803fe feat(ssr): add SSR feature to ItemsList component 2025-01-22 02:50:00 +03:00
29434dc37c feat(db): revert to previous working commit. 2025-01-20 19:14:34 +03:00
dc70316bae fix(db): db debugging (in progress) 2025-01-20 18:49:54 +03:00
4760364491 feat(reviews): remove reviews from Item struct 2025-01-17 18:52:20 +03:00
c8f32d027f feat(db): add a database using rusqlite. 2025-01-17 18:51:59 +03:00
a99b5164d8 feat(item_list): add property labels fetching and displaying in ItemsList component 2025-01-16 15:06:16 +03:00
1f81eae135 feat(items_list): populate new item cells with related property values based on their wikidata_id 2025-01-16 02:29:27 +03:00
765227e7aa fix(items_list): modify add_property to fetch and populate relevant values per item 2025-01-16 01:43:51 +03:00
13 changed files with 1013 additions and 253 deletions

5
.gitignore vendored
View file

@ -15,4 +15,7 @@ playwright/.cache/
# Sass cache dir
.sass-cache/
.idea/
.idea/
# Ignore database file
compareware.db

283
Cargo.lock generated
View file

@ -8,7 +8,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"bytes",
"futures-core",
"futures-sink",
@ -29,7 +29,7 @@ dependencies = [
"actix-service",
"actix-utils",
"actix-web",
"bitflags",
"bitflags 2.6.0",
"bytes",
"derive_more",
"futures-core",
@ -52,9 +52,9 @@ dependencies = [
"actix-rt",
"actix-service",
"actix-utils",
"ahash",
"ahash 0.8.11",
"base64",
"bitflags",
"bitflags 2.6.0",
"brotli",
"bytes",
"bytestring",
@ -127,7 +127,7 @@ dependencies = [
"actix-utils",
"futures-core",
"futures-util",
"mio",
"mio 1.0.3",
"socket2",
"tokio",
"tracing",
@ -169,7 +169,7 @@ dependencies = [
"actix-service",
"actix-utils",
"actix-web-codegen",
"ahash",
"ahash 0.8.11",
"bytes",
"bytestring",
"cfg-if",
@ -233,6 +233,17 @@ dependencies = [
"generic-array",
]
[[package]]
name = "ahash"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "ahash"
version = "0.8.11"
@ -276,6 +287,21 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.94"
@ -398,7 +424,7 @@ dependencies = [
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@ -510,6 +536,12 @@ dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
@ -670,6 +702,20 @@ dependencies = [
"zeroize",
]
[[package]]
name = "chrono"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.52.6",
]
[[package]]
name = "ciborium"
version = "0.2.2"
@ -720,6 +766,7 @@ version = "0.1.3"
dependencies = [
"actix-files",
"actix-web",
"chrono",
"console_error_panic_hook",
"futures",
"gloo-net 0.5.0",
@ -729,12 +776,16 @@ dependencies = [
"leptos_actix",
"leptos_meta",
"leptos_router",
"mio 0.8.11",
"nostr-sdk",
"paste",
"rusqlite",
"secp256k1 0.30.0",
"serde",
"serde_json",
"thiserror 2.0.9",
"tokio",
"urlencoding",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
@ -811,6 +862,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.16"
@ -982,6 +1039,18 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "flate2"
version = "1.0.35"
@ -1227,6 +1296,15 @@ dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash 0.7.8",
]
[[package]]
name = "hashbrown"
version = "0.13.2"
@ -1239,7 +1317,7 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"ahash 0.8.11",
"allocator-api2",
]
@ -1254,6 +1332,15 @@ dependencies = [
"foldhash",
]
[[package]]
name = "hashlink"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
dependencies = [
"hashbrown 0.11.2",
]
[[package]]
name = "hex-conservative"
version = "0.1.2"
@ -1333,6 +1420,29 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
@ -1810,6 +1920,16 @@ version = "0.2.168"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
[[package]]
name = "libsqlite3-sys"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
name = "linear-map"
version = "1.2.0"
@ -1937,6 +2057,18 @@ dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.3"
@ -2052,6 +2184,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.36.5"
@ -2109,7 +2250,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@ -2380,7 +2521,7 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags",
"bitflags 2.6.0",
]
[[package]]
@ -2447,6 +2588,21 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "rusqlite"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a"
dependencies = [
"bitflags 1.3.2",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"memchr",
"smallvec",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -2990,7 +3146,7 @@ dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"mio 1.0.3",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
@ -3245,6 +3401,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf-8"
version = "0.7.6"
@ -3284,6 +3446,12 @@ version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
@ -3414,13 +3582,31 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@ -3429,7 +3615,22 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
@ -3438,28 +3639,46 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@ -3472,24 +3691,48 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"

View file

@ -15,7 +15,9 @@ leptos = { version = "0.6" }
leptos_meta = { version = "0.6" }
leptos_actix = { version = "0.6", optional = true }
leptos_router = { version = "0.6" }
paste = "1.0"
wasm-bindgen = "=0.2.99"
rusqlite = { version = "0.27.0", optional = true}
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.0", features = ["v4"] }
web-sys = { version = "0.3", features = ["Event"] }
@ -28,8 +30,12 @@ wasm-bindgen-futures = "0.4"
serde_json="1.0.133"
thiserror = "2.0.9"
zerofrom = "0.1"
mio = "0.8"
chrono = "0.4"
urlencoding = "2.1.2"
[features]
default = ["ssr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
@ -39,6 +45,7 @@ ssr = [
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:rusqlite"
]
# Override secp256k1's default features

View file

@ -1,3 +1,3 @@
[toolchain]
channel = "1.83.0"
channel = "1.82.0"
targets = [ "wasm32-unknown-unknown" ]

65
src/api.rs Normal file
View file

@ -0,0 +1,65 @@
#[cfg(feature = "ssr")]
use actix_web::{web, HttpResponse};
#[cfg(feature = "ssr")]
use crate::db::{Database, DbItem};
#[cfg(feature = "ssr")]
use std::sync::Arc;
#[cfg(feature = "ssr")]
use tokio::sync::Mutex;
#[cfg(feature = "ssr")]
pub async fn get_items(db: web::Data<Arc<Mutex<Database>>>) -> HttpResponse {
let db = db.lock().await;
match db.get_items().await {
Ok(items) => HttpResponse::Ok().json(items),
Err(err) => {
leptos::logging::error!("Failed to fetch items: {:?}", err);
HttpResponse::InternalServerError().body("Failed to fetch items")
}
}
}
#[cfg(feature = "ssr")]
pub async fn create_item(
db: web::Data<Arc<Mutex<Database>>>,
item: web::Json<DbItem>,
) -> HttpResponse {
let db = db.lock().await;
match db.insert_item(&item.into_inner()).await {
Ok(_) => HttpResponse::Ok().body("Item inserted"),
Err(err) => {
leptos::logging::error!("Failed to insert item: {:?}", err);
HttpResponse::InternalServerError().body("Failed to insert item")
}
}
}
#[cfg(feature = "ssr")]
pub async fn delete_item(
db: web::Data<Arc<Mutex<Database>>>,
item_id: web::Path<String>,
) -> HttpResponse {
let db = db.lock().await;
match db.delete_item(&item_id).await {
Ok(_) => HttpResponse::Ok().body("Item deleted"),
Err(err) => {
leptos::logging::error!("Failed to delete item: {:?}", err);
HttpResponse::InternalServerError().body("Failed to delete item")
}
}
}
#[cfg(feature = "ssr")]
pub async fn delete_property(
db: web::Data<Arc<Mutex<Database>>>,
property: web::Path<String>,
) -> HttpResponse {
let db = db.lock().await;
match db.delete_property(&property).await {
Ok(_) => HttpResponse::Ok().body("Property deleted"),
Err(err) => {
leptos::logging::error!("Failed to delete property: {:?}", err);
HttpResponse::InternalServerError().body("Failed to delete property")
}
}
}

View file

@ -1,74 +0,0 @@
use leptos::*;
use leptos_dom::ev::SubmitEvent;
use leptos::logging::log;
#[component]
pub fn ItemForm(on_submit: Box<dyn Fn(String, String, Vec<(String, String)>, String, u8)>) -> impl IntoView {
let (name, set_name) = create_signal(String::new());
let (description, set_description) = create_signal(String::new());
let (tags, set_tags) = create_signal(Vec::<(String, String)>::new());
let (tag_key, set_tag_key) = create_signal(String::new());
let (tag_value, set_tag_value) = create_signal(String::new());
let (review, set_review) = create_signal(String::new());
let (rating, set_rating) = create_signal(5u8); // Default rating to 5
let add_tag = move |_| {
if !tag_key.get().is_empty() && !tag_value.get().is_empty() {
set_tags.update(|t| t.push((tag_key.get(), tag_value.get())));
set_tag_key.set(String::new());
set_tag_value.set(String::new());
}
};
let handle_submit = move |ev: SubmitEvent| {
ev.prevent_default();
// Validation
if name.get().is_empty() || description.get().is_empty() || rating.get() < 1 || rating.get() > 5 {
log!("Validation failed: Check required fields.");
return;
}
on_submit(
name.get(),
description.get(),
tags.get().clone(),
review.get(),
rating.get(),
);
// Reset values
set_name.set(String::new());
set_description.set(String::new());
set_tags.set(vec![]);
set_review.set(String::new());
set_rating.set(5);
};
view! {
<form on:submit=handle_submit>
<input type="text" placeholder="Name" on:input=move |e| set_name.set(event_target_value(&e)) />
<textarea placeholder="Description" on:input=move |e| set_description.set(event_target_value(&e)) />
<h3>{ "Add Tags" }</h3>
<input type="text" placeholder="Key" on:input=move |e| set_tag_key.set(event_target_value(&e)) />
<input type="text" placeholder="Value" on:input=move |e| set_tag_value.set(event_target_value(&e)) />
<button type="button" on:click=add_tag>{ "Add Tag" }</button>
<ul>
{tags.get().iter().map(|(key, value)| view! {
<li>{ format!("{}: {}", key, value) }</li>
}).collect::<Vec<_>>() }
</ul>
<h3>{ "Write a Review" }</h3>
<textarea placeholder="Review" on:input=move |e| set_review.set(event_target_value(&e)) />
<h3>{ "Rating (1-5)" }</h3>
<input
type="number"
min="1"
max="5"
value={rating.get()}
on:input=move |e| set_rating.set(event_target_value(&e).parse::<u8>().unwrap_or(5))
/>
<button type="submit">{ "Add Item" }</button>
</form>
}
}

View file

@ -1,13 +1,16 @@
use crate::components::editable_cell::EditableCell;
use crate::components::editable_cell::InputType;
use leptos::*;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use leptos::logging::log;
use crate::models::item::Item;
use std::collections::HashMap;
use std::sync::Arc;
use wasm_bindgen::JsCast;
use urlencoding::encode;
use gloo_net::http::Request;
use serde_json::Value;
#[derive(Deserialize, Clone, Debug)]
struct WikidataSuggestion {
@ -16,39 +19,262 @@ struct WikidataSuggestion {
description: Option<String>,
}
#[derive(Deserialize, Debug)]
struct DbItem {
id: String,
name: String,
description: String,
wikidata_id: Option<String>,
custom_properties: String,
}
#[component]
pub fn ItemsList(
items: ReadSignal<Vec<Item>>,
set_items: WriteSignal<Vec<Item>>,
) -> impl IntoView {
// State to track selected properties
let (selected_properties, set_selected_properties) = create_signal(HashMap::<String, bool>::new());
// State to track the currently focused cell
let (focused_cell, set_focused_cell) = create_signal(None::<String>);
// State to manage dynamic property names
let (custom_properties, set_custom_properties) = create_signal(Vec::<String>::new());
// state to manage suggestions visibility
// State to manage suggestions visibility
let (show_suggestions, set_show_suggestions) = create_signal(HashMap::<String, bool>::new());
// cache to store fetched properties
let (fetched_properties, set_fetched_properties) = create_signal(HashMap::<String, String>::new());
let (fetched_properties, set_fetched_properties) = create_signal(HashMap::<String, HashMap<String, String>>::new());
// Signal to store the fetched property labels
let (property_labels, set_property_labels) = create_signal(HashMap::<String, String>::new());
spawn_local(async move {
match load_items_from_db().await {
Ok(loaded_items) => {
// Set the loaded items
if loaded_items.is_empty() {
// Initialize with one empty item if the database is empty
set_items.set(vec![Item {
id: Uuid::new_v4().to_string(),
name: String::new(),
description: String::new(),
wikidata_id: None,
custom_properties: HashMap::new(),
}]);
} else {
set_items.set(loaded_items.clone());
}
// Derive selected properties from the loaded items
let mut selected_props = HashMap::new();
let loaded_items_clone = loaded_items.clone();
for item in loaded_items {
for (property, _) in item.custom_properties {
selected_props.insert(property, true);
}
}
set_selected_properties.set(selected_props);
// Update the custom_properties signal
let mut custom_props = Vec::new();
for item in loaded_items_clone {
for (property, _) in &item.custom_properties {
if !custom_props.iter().any(|p| p == property) {
custom_props.push(property.clone());
}
}
}
let custom_props_clone = custom_props.clone();
set_custom_properties.set(custom_props);
// Fetch labels for the custom properties
let property_ids = custom_props_clone;
let labels = fetch_property_labels(property_ids).await;
set_property_labels.update(|labels_map| {
for (key, value) in labels {
labels_map.insert(key, value);
}
});
// log!("Items after loading: {:?}", items.get());
}
Err(err) => {
log!("Error loading items: {}", err);
}
}
});
// Ensure there's an initial empty row
if items.get().is_empty() {
set_items.set(vec![Item {
id: Uuid::new_v4().to_string(),
name: String::new(),
description: String::new(),
reviews: vec![],
// reviews: vec![],
wikidata_id: None,
custom_properties: HashMap::new(),
}]);
}
// Function to send an item to the backend API
async fn save_item_to_db(item: Item, selected_properties: ReadSignal<HashMap<String, bool>>) {
// Use a reactive closure to access `selected_properties`
let custom_properties: HashMap<String, String> = (move || {
let selected_props = selected_properties.get(); // Access the signal inside a reactive closure
item.custom_properties
.into_iter()
.filter(|(key, _)| selected_props.contains_key(key)) // Use the extracted value
.collect()
})();
// Serialize `custom_properties` to a JSON string
let custom_properties = serde_json::to_string(&custom_properties).unwrap();
// Create a new struct to send to the backend
#[derive(Serialize, Debug)]
struct ItemToSend {
id: String,
name: String,
description: String,
wikidata_id: Option<String>,
custom_properties: String, // JSON-encoded string
}
let item_to_send = ItemToSend {
id: item.id,
name: item.name,
description: item.description,
wikidata_id: item.wikidata_id,
custom_properties, // Use the serialized string
};
let response = gloo_net::http::Request::post("/api/items")
.json(&item_to_send)
.unwrap()
.send()
.await;
match response {
Ok(resp) => {
if resp.status() == 200 {
// log!("Item saved to database: {:?}", item_to_send);
} else {
log!("Failed to save item: {}", resp.status_text());
}
}
Err(err) => log!("Failed to save item: {:?}", err),
}
}
//function to load items from database
async fn load_items_from_db() -> Result<Vec<Item>, String> {
let response = gloo_net::http::Request::get("/api/items")
.send()
.await
.map_err(|err| format!("Failed to fetch items: {:?}", err))?;
if response.status() == 200 {
// Deserialize into Vec<DbItem>
log!("Loading items from DB...");
let db_items = response
.json::<Vec<DbItem>>()
.await
.map_err(|err| format!("Failed to parse items: {:?}", err))?;
// log!("Deserialized DB items: {:?}", db_items);
// Convert DbItem to Item
let items = db_items
.into_iter()
.map(|db_item| {
// Deserialize `custom_properties` from a JSON string to a HashMap
let custom_properties: HashMap<String, String> =
serde_json::from_str(&db_item.custom_properties)
.unwrap_or_default(); // Fallback to an empty HashMap if deserialization fails
log!("Loaded item: {:?}", db_item.id);
log!("Custom properties: {:?}", custom_properties);
Item {
id: db_item.id,
name: db_item.name,
description: db_item.description,
wikidata_id: db_item.wikidata_id,
custom_properties, // Deserialized HashMap
}
})
.collect();
// log!("Converted items: {:?}", items);
Ok(items)
} else {
Err(format!("Failed to fetch items: {}", response.status_text()))
}
}
// Function to remove an item
let remove_item = move |index: usize| {
let item_id = items.get()[index].id.clone();
spawn_local(async move {
let response = gloo_net::http::Request::delete(&format!("/api/items/{}", item_id))
.send()
.await;
match response {
Ok(resp) => {
if resp.status() == 200 {
set_items.update(|items| {
items.remove(index);
});
log!("Item deleted: {}", item_id);
} else {
log!("Failed to delete item: {}", resp.status_text());
}
}
Err(err) => log!("Failed to delete item: {:?}", err),
}
});
};
// Function to remove a property
let remove_property = move |property: String| {
spawn_local(async move {
let response = gloo_net::http::Request::delete(&format!("/api/properties/{}", property))
.send()
.await;
match response {
Ok(resp) => {
if resp.status() == 200 {
set_custom_properties.update(|props| {
props.retain(|p| p != &property);
});
set_selected_properties.update(|selected| {
selected.remove(&property);
});
set_items.update(|items| {
for item in items {
item.custom_properties.remove(&property);
}
});
log!("Property deleted: {}", property);
} else {
log!("Failed to delete property: {}", resp.status_text());
}
}
Err(err) => log!("Failed to delete property: {:?}", err),
}
});
};
// State to store Wikidata suggestions
let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(HashMap::<String, Vec<WikidataSuggestion>>::new());
// Fetch Wikidata suggestions
let fetch_wikidata_suggestions = move |key:String, query: String| {
// Function to fetch Wikidata suggestions
let fetch_wikidata_suggestions = move |key: String, query: String| {
log!("Fetching suggestions for key: {}, query: {}", key, query);
spawn_local(async move {
if query.is_empty() {
@ -66,9 +292,7 @@ pub fn ItemsList(
match gloo_net::http::Request::get(&url).send().await {
Ok(response) => {
if let Ok(data) = response.json::<WikidataResponse>().await {
log!("Fetching suggestions for key: {}, query: {}", key, query);
set_wikidata_suggestions.update(|suggestions| {
log!("Updated suggestions: {:?}", suggestions);
suggestions.insert(key, data.search);
});
}
@ -79,66 +303,204 @@ pub fn ItemsList(
};
//function to fetch properties
async fn fetch_item_properties(wikidata_id: &str, set_fetched_properties: WriteSignal<HashMap<String, String>>) -> HashMap<String, String> {
let url = format!(
"https://www.wikidata.org/wiki/Special:EntityData/{}.json",
async fn fetch_item_properties(wikidata_id: &str) -> HashMap<String, String> {
let sparql_query = format!(
r#"
SELECT ?propLabel ?value ?valueLabel WHERE {{
wd:{} ?prop ?statement.
?statement ?ps ?value.
?property wikibase:claim ?prop.
?property wikibase:statementProperty ?ps.
SERVICE wikibase:label {{ bd:serviceParam wikibase:language "en". }}
}}
"#,
wikidata_id
);
match gloo_net::http::Request::get(&url).send().await {
let url = format!(
"https://query.wikidata.org/sparql?query={}&format=json",
urlencoding::encode(&sparql_query)
);
match gloo_net::http::Request::get(&url)
.header("Accept", "application/json")
.send()
.await
{
Ok(response) => {
if let Ok(data) = response.json::<serde_json::Value>().await {
if let Some(entities) = data["entities"].as_object() {
if let Some(entity) = entities.get(wikidata_id) {
if let Some(claims) = entity["claims"].as_object() {
let mut result = HashMap::new();
if let Some(bindings) = data["results"]["bindings"].as_array() {
for binding in bindings {
let prop_label = binding["propLabel"]["value"].as_str().unwrap_or("").to_string();
let prop_label = prop_label.replace("http://www.wikidata.org/prop/", "");
let value_label = binding["valueLabel"]["value"].as_str().unwrap_or("").to_string();
result.insert(prop_label, value_label);
log!("result: {:?}", result);
}
}
result
} else {
HashMap::new()
}
}
Err(_) => HashMap::new(),
}
}
async fn fetch_property_labels(property_ids: Vec<String>) -> HashMap<String, String> {
log!("Fetching property labels for properties: {:?}", property_ids);
// Remove the "http://www.wikidata.org/prop/" prefix from property IDs
let property_ids: Vec<String> = property_ids
.into_iter()
.map(|id| id.replace("http://www.wikidata.org/prop/", ""))
.collect();
let property_ids_str = property_ids.join(" wd:");
let sparql_query = format!(
r#"
SELECT ?prop ?propLabel WHERE {{
VALUES ?prop {{ wd:{} }}
SERVICE wikibase:label {{ bd:serviceParam wikibase:language "en". }}
}}
"#,
property_ids_str
);
let url = format!(
"https://query.wikidata.org/sparql?query={}&format=json",
urlencoding::encode(&sparql_query)
);
log!("Sending request to URL: {}", url);
match gloo_net::http::Request::get(&url)
.header("Accept", "application/json")
.send()
.await
{
Ok(response) => {
log!("Received response from Wikidata. Status: {}", response.status());
if response.status() != 200 {
log!("Error: Unexpected status code {}", response.status());
return HashMap::new();
}
match response.text().await {
Ok(text) => {
log!("Response body: {}", text);
match serde_json::from_str::<serde_json::Value>(&text) {
Ok(data) => {
log!("Successfully parsed response from Wikidata");
let mut result = HashMap::new();
for (property, values) in claims {
if let Some(value) = values[0]["mainsnak"]["datavalue"]["value"].as_str() {
result.insert(property.clone(), value.to_string());
} else if let Some(value) = values[0]["mainsnak"]["datavalue"]["value"].as_object() {
result.insert(property.clone(), serde_json::to_string(value).unwrap());
} else if let Some(value) = values[0]["mainsnak"]["datavalue"]["value"].as_f64() {
result.insert(property.clone(), value.to_string());
} else {
result.insert(property.clone(), "Unsupported data type".to_string());
if let Some(bindings) = data["results"]["bindings"].as_array() {
log!("Found {} bindings in response", bindings.len());
for (i, binding) in bindings.iter().enumerate() {
if let (Some(prop), Some(label)) = (
binding["prop"]["value"].as_str(),
binding["propLabel"]["value"].as_str()
) {
let prop_id = prop.split('/').last().unwrap_or("").to_string();
result.insert(prop_id.clone(), label.to_string());
log!("Processed binding {}: prop_id = {}, label = {}", i, prop_id, label);
} else {
log!("Warning: Binding {} is missing prop or propLabel", i);
}
}
} else {
log!("Warning: No bindings found in the response");
}
set_fetched_properties.update(|properties| {
for (key, val) in result.clone() {
properties.insert(key.clone(), val.clone());
}
});
return result;
log!("Fetched {} property labels", result.len());
result
}
Err(e) => {
log!("Error parsing response from Wikidata: {:?}", e);
HashMap::new()
}
}
}
Err(e) => {
log!("Error reading response body: {:?}", e);
HashMap::new()
}
}
}
Err(err) => log!("Error fetching item properties: {:?}", err),
Err(e) => {
log!("Error fetching property labels from Wikidata: {:?}", e);
HashMap::new()
}
}
HashMap::new()
}
// Add a new custom property
let add_property = move |property: String| {
// Normalize the property ID
let normalized_property = property.replace("http://www.wikidata.org/prop/", "");
set_custom_properties.update(|props| {
if !props.contains(&property) && !property.is_empty() {
props.push(property.clone());
if !props.contains(&normalized_property) && !normalized_property.is_empty() {
props.push(normalized_property.clone());
//update the selected_properties state when a new property is added
set_selected_properties.update(|selected| {
selected.insert(normalized_property.clone(), true);
});
// Ensure the grid updates reactively
set_items.update(|items| {
for item in items {
item.custom_properties.entry(property.clone()).or_insert_with(|| "".to_string());
item.custom_properties.entry(normalized_property.clone()).or_insert_with(|| "".to_string());
// Save the updated item to the database
let item_clone = item.clone();
spawn_local(async move {
save_item_to_db(item_clone, selected_properties).await;
});
}
});
// Fetch the property label
let property_id = normalized_property.clone();
spawn_local(async move {
let labels = fetch_property_labels(vec![property_id.clone()]).await;
log!("Fetched labels: {:?}", labels);
set_property_labels.update(|labels_map| {
for (key, value) in labels {
log!("Inserting label: {} -> {}", key, value);
labels_map.insert(key, value);
}
});
});
}
});
// Populate the value of the property in the corresponding cell
// Fetch the relevant value for each item and populate the corresponding cells
set_items.update(|items| {
for item in items {
if let Some(value) = fetched_properties.get().get(&property) {
item.custom_properties.insert(property.clone(), value.clone());
if let Some(wikidata_id) = &item.wikidata_id {
let wikidata_id = wikidata_id.clone();
let set_fetched_properties = set_fetched_properties.clone();
let set_property_labels = set_property_labels.clone();
let property_clone = property.clone();
spawn_local(async move {
let properties = fetch_item_properties(&wikidata_id).await;
// Update fetched properties and property labels
set_fetched_properties.update(|fp| {
fp.insert(wikidata_id.clone(), properties.clone());
});
set_property_labels.update(|pl| {
for (key, value) in properties.iter() {
pl.entry(key.clone()).or_insert_with(|| value.clone());
}
});
if let Some(value) = properties.get(&property_clone) {
set_items.update(|items| {
if let Some(item) = items.iter_mut().find(|item| item.wikidata_id.as_ref().unwrap() == &wikidata_id) {
item.custom_properties.insert(property_clone.clone(), value.clone());
}
});
}
});
}
}
});
@ -157,9 +519,10 @@ pub fn ItemsList(
if !value.is_empty() {
if let Some(wikidata_id) = &item.wikidata_id {
let wikidata_id = wikidata_id.clone();
let set_fetched_properties = set_fetched_properties.clone(); // Clone the set_fetched_properties signal
let set_fetched_properties = set_fetched_properties.clone();
let set_property_labels = set_property_labels.clone();
spawn_local(async move {
let properties = fetch_item_properties(&wikidata_id, set_fetched_properties).await;
let properties = fetch_item_properties(&wikidata_id).await;
log!("Fetched properties for index {}: {:?}", index, properties);
});
}
@ -173,33 +536,36 @@ pub fn ItemsList(
item.custom_properties.insert(field.to_string(), value.clone());
}
}
}
// Save the updated item to the database
let item_clone = item.clone();
spawn_local(async move {
save_item_to_db(item_clone, selected_properties).await;
});
}
// Automatically add a new row when editing the last row
if index == items.len() - 1 && !value.is_empty() {
items.push(Item {
let new_item = Item {
id: Uuid::new_v4().to_string(),
name: String::new(),
description: String::new(),
reviews: vec![],
// reviews: vec![],
wikidata_id: None,
custom_properties: HashMap::new(),
};
items.push(new_item.clone());
// Save the new item to the database
spawn_local(async move {
save_item_to_db(new_item, selected_properties).await;
});
}
log!("Items updated: {:?}", items);
});
};
// Remove an item
let remove_item = move |index: usize| {
set_items.update(|items| {
items.remove(index);
});
};
// List of properties to display as rows
let properties = vec!["Name", "Description", "Actions"];
let properties = vec!["Name", "Description"];
view! {
<div>
@ -208,9 +574,12 @@ pub fn ItemsList(
<thead>
<tr>
<th>{ "Property" }</th>
{move || items.get().iter().enumerate().map(|(index, _)| {
{move || items.get().iter().enumerate().map(|(index, item)| {
view! {
<th>{ format!("Item {}", index + 1) }</th>
<th>
{ item.name.clone() }
<button on:click=move |_| remove_item(index)>{ "Delete" }</button>
</th>
}
}).collect::<Vec<_>>()}
</tr>
@ -292,9 +661,20 @@ pub fn ItemsList(
// Fetch additional properties from Wikidata
let wikidata_id = id.clone();
let set_fetched_properties = set_fetched_properties.clone();
let set_property_labels = set_property_labels.clone();
spawn_local(async move {
let properties = fetch_item_properties(&wikidata_id, set_fetched_properties.clone()).await;
log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties);
let properties = fetch_item_properties(&wikidata_id).await;
// log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties);
// Populate the custom properties for the new item
set_items.update(|items| {
if let Some(item) = items.iter_mut().find(|item| item.wikidata_id.as_ref() == Some(&wikidata_id)) {
for (property, value) in properties {
item.custom_properties.insert(property, value);
}
}
});
});
// Hide the suggestion list
@ -335,9 +715,6 @@ pub fn ItemsList(
input_type=InputType::TextArea
/>
}.into_view(),
"Actions" => view! {
<button on:click=move |_| remove_item(index)>{ "Delete" }</button>
}.into_view(),
_ => view! {
{ "" }
}.into_view(),
@ -352,34 +729,52 @@ pub fn ItemsList(
{move || {
let custom_props = custom_properties.get().clone();
custom_props.into_iter().map(move |property| {
let property_clone = property.clone();
let normalized_property = property.replace("http://www.wikidata.org/prop/", "");
let property_label = property_labels.get().get(&normalized_property).cloned().unwrap_or_else(|| normalized_property.clone());
log!("Rendering property: {} -> {}", normalized_property, property_label);
let property_clone_for_button = normalized_property.clone();
view! {
<tr>
<td>{ property }</td>
<td>
{ property_label }
<button class="delete-property" on:click=move |_| {
log!("Deleting property: {}", property_clone_for_button);
remove_property(property_clone_for_button.clone());
set_custom_properties.update(|props| {
props.retain(|p| p != &property_clone_for_button);
});
set_selected_properties.update(|selected| {
selected.remove(&property_clone_for_button);
});
set_items.update(|items| {
for item in items {
item.custom_properties.remove(&property_clone_for_button);
}
});
}>{ "Delete" }</button>
</td>
{move || {
let property_clone = property_clone.clone(); // Clone `property_clone` again for the inner closure
let property_clone_for_cells = normalized_property.clone();
items.get().iter().enumerate().map(move |(index, item)| {
let property_clone_for_closure = property_clone.clone();
view! {
<td>
<EditableCell
value=item.custom_properties.get(&property_clone).cloned().unwrap_or_default()
on_input=move |value| update_item(index, &property_clone_for_closure, value)
key=Arc::new(format!("custom-{}-{}", property_clone, index))
focused_cell=focused_cell
set_focused_cell=set_focused_cell.clone()
on_focus=Some(Callback::new(move |_| {
log!("Custom property input focused");
}))
on_blur=Some(Callback::new(move |_| {
log!("Custom property input blurred");
}))
input_type=InputType::TextArea
/>
</td>
}
}).collect::<Vec<_>>()
}}
let property_clone_for_closure = property_clone_for_cells.clone();
view! {
<td>
<EditableCell
value=item.custom_properties.get(&property_clone_for_closure).cloned().unwrap_or_default()
on_input=move |value| update_item(index, &property_clone_for_closure, value)
key=Arc::new(format!("custom-{}-{}", property_clone_for_cells, index))
focused_cell=focused_cell
set_focused_cell=set_focused_cell.clone()
on_focus=Some(Callback::new(move |_| {
}))
on_blur=Some(Callback::new(move |_| {
}))
input_type=InputType::TextArea
/>
</td>
}
}).collect::<Vec<_>>()}
}
</tr>
}
}).collect::<Vec<_>>()
@ -387,30 +782,33 @@ pub fn ItemsList(
</tbody>
</table>
<div style="margin-bottom: 20px;">
<input type="text" id="new-property" placeholder="Add New Property" list="properties"/>
<input type="text" id="new-property" placeholder="Add New Property" list="properties" on:keydown=move |event| {
if event.key() == "Enter"{
let input_element = event.target().unwrap().dyn_into::<web_sys::HtmlInputElement>().unwrap();
let property = input_element.value();
if !property.is_empty() {
// Extract the coded name from the selected value
let coded_name = property.split(" - ").next().unwrap_or(&property).to_string();
// Add the property using the coded name
add_property(coded_name);
// Clear the input field
input_element.set_value("");
}
}
} />
<datalist id="properties">
{move || {
let properties = fetched_properties.get().clone();
properties.into_iter().map(|(key, _)| {
let key_clone = key.clone();
let property_labels = property_labels.get().clone();
property_labels.into_iter().map(|(property, label)| {
let property_clone = property.clone();
view! {
<option value={key}>{key_clone}</option>
<option value={property}>{ format!("{} - {}", property_clone, label) }</option>
}
}).collect::<Vec<_>>()
}}
</datalist>
<button on:click=move |_| {
let property = web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id("new-property")
.unwrap()
.dyn_into::<web_sys::HtmlInputElement>()
.unwrap()
.value();
add_property(property);
}>{ "Add Property" }</button>
</div>
</div>
}

View file

@ -1,3 +1,2 @@
pub mod item_form;
pub mod items_list;
pub mod editable_cell;

View file

@ -1,48 +0,0 @@
use leptos::*;
use serde::Deserialize;
#[derive(Deserialize, Clone, Debug)]
struct WikidataResult {
id: String,
label: String,
description: Option<String>,
}
#[component]
pub fn WikidataLookup(
query: String,
on_select: impl Fn(WikidataResult) + 'static,
) -> impl IntoView {
let (suggestions, set_suggestions) = create_signal(Vec::new());
let fetch_suggestions = move |query: String| {
spawn_local(async move {
if query.is_empty() {
set_suggestions(Vec::new());
return;
}
let url = format!("https://www.wikidata.org/w/api.php?action=wbsearchentities&search={}&language=en&limit=5&format=json&origin=*", query);
if let Ok(response) = reqwest::get(&url).await {
if let Ok(data) = response.json::<WikidataResponse>().await {
set_suggestions(data.search);
}
}
});
};
create_effect(move || {
fetch_suggestions(query.clone());
});
view! {
<ul>
{suggestions.get().iter().map(|suggestion| {
view! {
<li on:click=move |_| on_select(suggestion.clone())>
{format!("{} - {}", suggestion.label, suggestion.description.clone().unwrap_or_default())}
</li>
}
}).collect::<Vec<_>>()}
</ul>
}
}

136
src/db.rs Normal file
View file

@ -0,0 +1,136 @@
#[cfg(feature = "ssr")]
mod db_impl {
use rusqlite::{Connection, Error};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
use leptos::logging;
// Define a struct to represent a database connection
#[derive(Debug)]
pub struct Database {
conn: Arc<Mutex<Connection>>,
}
impl Database {
// Create a new database connection
pub fn new(db_path: &str) -> Result<Self, Error> {
let conn = Connection::open(db_path)?;
logging::log!("Database connection established at: {}", db_path);
Ok(Database {
conn: Arc::new(Mutex::new(conn)),
})
}
// Create the database schema
pub async fn create_schema(&self) -> Result<(), Error> {
let conn = self.conn.lock().await;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
wikidata_id TEXT,
custom_properties TEXT
);",
)?;
logging::log!("Database schema created or verified");
Ok(())
}
// Insert a new item into the database
pub async fn insert_item(&self, item: &DbItem) -> Result<(), Error> {
let conn = self.conn.lock().await;
let wikidata_id = item.wikidata_id.as_ref().map(|s| s.as_str()).unwrap_or("");
conn.execute(
"INSERT INTO items (id, name, description, wikidata_id, custom_properties)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
description = excluded.description,
wikidata_id = excluded.wikidata_id,
custom_properties = excluded.custom_properties;",
&[
&item.id,
&item.name,
&item.description,
&wikidata_id.to_string(),
&item.custom_properties,
],
)?;
logging::log!("Item inserted: {}", item.id);
Ok(())
}
pub async fn delete_item(&self, item_id: &str) -> Result<(), Error> {
let conn = self.conn.lock().await;
conn.execute("DELETE FROM items WHERE id = ?", &[item_id])?;
logging::log!("Item deleted: {}", item_id);
Ok(())
}
pub async fn delete_property(&self, property: &str) -> Result<(), Error> {
let conn = self.conn.lock().await;
let query = format!("UPDATE items SET custom_properties = json_remove(custom_properties, '$.{}')", property);
conn.execute(&query, []).map_err(|e| Error::from(e))?;
logging::log!("Property deleted: {}", property);
Ok(())
}
// Retrieve all items from the database
pub async fn get_items(&self) -> Result<Vec<DbItem>, Error> {
let conn = self.conn.lock().await;
let mut stmt = conn.prepare("SELECT * FROM items;")?;
let items = stmt.query_map([], |row| {
Ok(DbItem {
id: row.get(0)?,
name: row.get(1)?,
description: row.get(2)?,
wikidata_id: row.get(3)?,
custom_properties: row.get(4)?,
})
})?;
let mut result = Vec::new();
for item in items {
result.push(item?);
}
logging::log!("Fetched {} items from the database", result.len()); // Log with Leptos
Ok(result)
}
}
// Define a struct to represent an item in the database
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct DbItem {
pub id: String,
pub name: String,
pub description: String,
pub wikidata_id: Option<String>,
pub custom_properties: String,
}
// Implement conversion from DbItem to a JSON-friendly format
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ItemResponse {
pub id: String,
pub name: String,
pub description: String,
pub wikidata_id: Option<String>,
pub custom_properties: String,
}
impl From<DbItem> for ItemResponse {
fn from(item: DbItem) -> Self {
ItemResponse {
id: item.id,
name: item.name,
description: item.description,
wikidata_id: item.wikidata_id,
custom_properties: item.custom_properties,
}
}
}
}
#[cfg(feature = "ssr")]
pub use db_impl::{Database, DbItem, ItemResponse};

View file

@ -2,6 +2,9 @@ pub mod app;
pub mod components;
pub mod models;
pub mod nostr;
pub mod api;
#[cfg(feature = "ssr")]
pub mod db;
#[cfg(feature = "hydrate")]

View file

@ -6,27 +6,55 @@ async fn main() -> std::io::Result<()> {
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
use compareware::app::*;
use compareware::db::{Database, DbItem};
use compareware::api::{get_items, create_item, delete_item, delete_property}; // Import API handlers
use std::sync::Arc;
use tokio::sync::Mutex;
// Load configuration
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Initialize the database
let db = Database::new("compareware.db").unwrap();
db.create_schema().await.unwrap(); // Ensure the schema is created
let db = Arc::new(Mutex::new(db)); // Wrap the database in an Arc<Mutex<T>> for shared state
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
println!("listening on http://{}", &addr);
// Start the Actix Web server
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
let db = db.clone(); // Clone the Arc for each worker
App::new()
// serve JS/WASM/CSS from `pkg`
// Register custom API routes BEFORE Leptos server functions
.service(
web::scope("/api")
.route("/items", web::get().to(get_items)) // GET /api/items
.route("/items", web::post().to(create_item)) // POST /api/items
.route("/items/{id}", web::delete().to(delete_item)) // DELETE /api/items/{id}
.route("/properties/{property}", web::delete().to(delete_property)), // DELETE /api/properties/{property}
)
// Register server functions
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
// Serve JS/WASM/CSS from `pkg`
.service(Files::new("/pkg", format!("{site_root}/pkg")))
// serve other assets from the `assets` directory
// Serve other assets from the `assets` directory
.service(Files::new("/assets", site_root))
// serve the favicon from /favicon.ico
// Serve the favicon from /favicon.ico
.service(favicon)
// Register Leptos routes
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
// Pass Leptos options to the app
.app_data(web::Data::new(leptos_options.to_owned()))
//.wrap(middleware::Compress::default())
//.wrap(middleware::Compress::default())
// Pass the database as shared state
.app_data(web::Data::new(db))
})
.bind(&addr)?
.run()
@ -63,4 +91,4 @@ pub fn main() {
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
}

View file

@ -7,7 +7,7 @@ pub struct Item {
pub id: String,
pub name: String,
pub description: String,
pub reviews: Vec<ReviewWithRating>,
// pub reviews: Vec<ReviewWithRating>,
pub wikidata_id: Option<String>,
pub custom_properties: HashMap<String, String>,
}