Repository: IgnisDa/ryot (language: rust)

Issue:
[FEATURE REQUEST] - "Up Next" Section for TV shows
To mark an episode as seen, I would go into the show and update my progress. Would be nice if there was a quicker way to do that. I have Trakt's "Up Next" section in mind here where on the "dashboard" I can view a list of where I currently am in each show, and with the tap of a button mark the episode that is up next as seen. It would then automatically show the next episode for that show.


Discussion / hints:
> I have Trakt's "Up Next" section in mind here where on the "dashboard" I can view a list of where I currently am in each show, and with the tap of a button mark the episode that is up next as seen. 

Please attach a screenshot. I don't have a trakt account.
Here is a screenshot from the Trakt iOS app:

![IMG_6490](https://github.com/IgnisDa/ryot/assets/116678368/22766d6a-f8df-4ca2-9364-0d04baed6498)

Can you attach a screenshot of the entire page too. I want to see what all information they present on the dashboard.
Sorry for the delay. Here is all what I see on the iOS app's home screen: quick access to calendar, stats, recent activity, and recommendations.

![IMG_6492](https://github.com/IgnisDa/ryot/assets/116678368/1164a2d5-5587-4b74-afd0-39ff4793c33f)
![IMG_6493](https://github.com/IgnisDa/ryot/assets/116678368/922dfc8a-dab2-4671-a88c-1c4d7b40dbe1)
![IMG_6494](https://github.com/IgnisDa/ryot/assets/116678368/7e83debd-5d3a-4f4e-84bf-2369364ff1cb)

THanks
Hello, I would like to provide additional screenshots for this but from the desktop interface which is ultimately the main UI

![image](https://github.com/IgnisDa/ryot/assets/27901016/2134900d-2835-4472-9119-711fb2d39213)

Also, when it comes to marking shows as watched, trakt has a very easy and quick way of marking the next episode as seen, from the dashboard for convenience:
![image](https://github.com/IgnisDa/ryot/assets/27901016/254733f9-4c10-47d5-bc4e-de1d16807826)

And finally, it's not really part of this issue but an updated calendar view with previews would be nice, here is trakt's one for reference:
![image](https://github.com/IgnisDa/ryot/assets/27901016/8acfbdea-9b5d-47f6-b677-efda018bc6c9)


Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index b6e920659f..e0950b3f01 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -332,7 +332,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "strum",
- "syn 2.0.66",
+ "syn 2.0.68",
  "thiserror",
 ]
 
@@ -388,7 +388,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -399,7 +399,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -455,9 +455,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-s3"
-version = "1.37.0"
+version = "1.38.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "955365fd032cc94608c7e0f4aea36a9825399d6a1bf7b96f56cf157414b04c40"
+checksum = "2ecec5514e9d9c76c5e9489ca65ab9055ae0b3c49da3a1afd452c57cb68734fb"
 dependencies = [
  "ahash 0.8.11",
  "aws-credential-types",
@@ -671,15 +671,14 @@ dependencies = [
 
 [[package]]
 name = "aws-types"
-version = "1.3.1"
+version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f734808d43702a67e57d478a12e227d4d038d0b90c9005a78c87890d3805922"
+checksum = "2009a9733865d0ebf428a314440bbe357cc10d0c16d86a8e15d32e9b47c1e80e"
 dependencies = [
  "aws-credential-types",
  "aws-smithy-async",
  "aws-smithy-runtime-api",
  "aws-smithy-types",
- "http 0.2.12",
  "rustc_version",
  "tracing",
 ]
@@ -753,14 +752,14 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
 name = "backtrace"
-version = "0.3.72"
+version = "0.3.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
+checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
 dependencies = [
  "addr2line",
  "cc",
@@ -825,9 +824,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.5.0"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
 dependencies = [
  "serde",
 ]
@@ -902,7 +901,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
  "syn_derive",
 ]
 
@@ -987,7 +986,7 @@ dependencies = [
  "darling",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -1007,9 +1006,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.0.98"
+version = "1.0.102"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
+checksum = "779e6b7d17797c0b42023d417228c02889300190e700cb074c3438d9c541d332"
 
 [[package]]
 name = "cfg-if"
@@ -1072,9 +1071,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.4"
+version = "4.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -1082,9 +1081,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.2"
+version = "4.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708"
 dependencies = [
  "anstream",
  "anstyle",
@@ -1094,21 +1093,21 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.4"
+version = "4.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
+checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
 dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
 name = "clap_lex"
-version = "0.7.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
 
 [[package]]
 name = "colorchoice"
@@ -1343,7 +1342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
 dependencies = [
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -1369,16 +1368,15 @@ dependencies = [
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.1.2"
+version = "4.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
 dependencies = [
  "cfg-if",
  "cpufeatures",
  "curve25519-dalek-derive",
  "digest 0.10.7",
  "fiat-crypto",
- "platforms",
  "rustc_version",
  "subtle",
  "zeroize",
@@ -1392,7 +1390,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -1416,7 +1414,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "strsim",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -1427,7 +1425,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
 dependencies = [
  "darling_core",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -1485,13 +1483,13 @@ dependencies = [
 
 [[package]]
 name = "derive_more"
-version = "0.99.17"
+version = "0.99.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -1511,7 +1509,7 @@ checksum = "2bba3e9872d7c58ce7ef0fcf1844fcc3e23ef2a58377b50df35dd98e42a5726e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -1620,9 +1618,9 @@ checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
 
 [[package]]
 name = "dtoa-short"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
 dependencies = [
  "dtoa",
 ]
@@ -1702,7 +1700,19 @@ dependencies = [
  "enum-ordinalize",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "educe"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417"
+dependencies = [
+ "enum-ordinalize",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -1713,9 +1723,9 @@ checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591"
 
 [[package]]
 name = "either"
-version = "1.12.0"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
 dependencies = [
  "serde",
 ]
@@ -1809,7 +1819,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -1910,7 +1920,7 @@ checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
 dependencies = [
  "futures-core",
  "futures-sink",
- "spin 0.9.8",
+ "spin",
 ]
 
 [[package]]
@@ -2034,7 +2044,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -2113,7 +2123,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "regex",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -2421,12 +2431,12 @@ dependencies = [
 
 [[package]]
 name = "http-body-util"
-version = "0.1.1"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
 dependencies = [
  "bytes",
- "futures-core",
+ "futures-util",
  "http 1.1.0",
  "http-body 1.0.0",
  "pin-project-lite",
@@ -2446,9 +2456,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
 
 [[package]]
 name = "hybrid-array"
-version = "0.2.0-rc.8"
+version = "0.2.0-rc.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53668f5da5a41d9eaf4bf7064be46d1ebe6a4e1ceed817f387587b18f2b51047"
+checksum = "4d306b679262030ad8813a82d4915fc04efff97776e4db7f8eb5137039d56400"
 dependencies = [
  "typenum",
 ]
@@ -2608,7 +2618,7 @@ checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -2667,15 +2677,6 @@ dependencies = [
  "either",
 ]
 
-[[package]]
-name = "itertools"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
-dependencies = [
- "either",
-]
-
 [[package]]
 name = "itertools"
 version = "0.13.0"
@@ -2731,16 +2732,16 @@ dependencies = [
  "convert_case",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
 name = "lazy_static"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 dependencies = [
- "spin 0.5.2",
+ "spin",
 ]
 
 [[package]]
@@ -2760,12 +2761,12 @@ dependencies = [
  "nom",
  "percent-encoding",
  "quoted_printable",
- "rustls 0.23.9",
+ "rustls 0.23.10",
  "rustls-pemfile 2.1.2",
  "socket2",
  "tokio",
  "url",
- "webpki-roots 0.26.2",
+ "webpki-roots 0.26.3",
 ]
 
 [[package]]
@@ -2786,7 +2787,7 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "libc",
 ]
 
@@ -2819,9 +2820,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.21"
+version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
 
 [[package]]
 name = "logs-wheel"
@@ -2898,9 +2899,9 @@ dependencies = [
 
 [[package]]
 name = "memchr"
-version = "2.7.2"
+version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
 [[package]]
 name = "miette"
@@ -2922,7 +2923,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -2939,9 +2940,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
 [[package]]
 name = "miniz_oxide"
-version = "0.7.3"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
+checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
 dependencies = [
  "adler",
 ]
@@ -2970,7 +2971,7 @@ dependencies = [
  "httparse",
  "memchr",
  "mime",
- "spin 0.9.8",
+ "spin",
  "version_check",
 ]
 
@@ -3094,9 +3095,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.35.0"
+version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
+checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
 dependencies = [
  "memchr",
 ]
@@ -3190,7 +3191,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -3301,7 +3302,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
 dependencies = [
  "cfg-if",
  "libc",
- "redox_syscall 0.5.1",
+ "redox_syscall 0.5.2",
  "smallvec",
  "windows-targets 0.52.5",
 ]
@@ -3378,7 +3379,7 @@ dependencies = [
  "pest_meta",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -3461,7 +3462,7 @@ dependencies = [
  "phf_shared 0.11.2",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -3499,7 +3500,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -3551,12 +3552,6 @@ version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
 
-[[package]]
-name = "platforms"
-version = "3.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7"
-
 [[package]]
 name = "powerfmt"
 version = "0.2.0"
@@ -3633,9 +3628,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.85"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
 dependencies = [
  "unicode-ident",
 ]
@@ -3794,11 +3789,11 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
+checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
 ]
 
 [[package]]
@@ -3820,8 +3815,8 @@ checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-automata 0.4.6",
- "regex-syntax 0.8.3",
+ "regex-automata 0.4.7",
+ "regex-syntax 0.8.4",
 ]
 
 [[package]]
@@ -3835,20 +3830,20 @@ dependencies = [
 
 [[package]]
 name = "regex-automata"
-version = "0.4.6"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax 0.8.3",
+ "regex-syntax 0.8.4",
 ]
 
 [[package]]
 name = "regex-lite"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e"
+checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
 
 [[package]]
 name = "regex-syntax"
@@ -3858,9 +3853,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
 
 [[package]]
 name = "regex-syntax"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
 
 [[package]]
 name = "rend"
@@ -3956,9 +3951,9 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.12.4"
+version = "0.12.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
+checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
 dependencies = [
  "base64 0.22.1",
  "bytes",
@@ -3980,7 +3975,7 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_urlencoded",
- "sync_wrapper 0.1.2",
+ "sync_wrapper 1.0.1",
  "tokio",
  "tower-service",
  "url",
@@ -4021,7 +4016,7 @@ dependencies = [
  "cfg-if",
  "getrandom",
  "libc",
- "spin 0.9.8",
+ "spin",
  "untrusted",
  "windows-sys 0.52.0",
 ]
@@ -4163,7 +4158,7 @@ version = "0.38.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -4184,9 +4179,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.23.9"
+version = "0.23.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a218f0f6d05669de4eabfb24f31ce802035c952429d037507b4a4a39f0e60c5b"
+checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402"
 dependencies = [
  "log",
  "once_cell",
@@ -4286,6 +4281,7 @@ dependencies = [
  "derive_more 1.0.0-beta.6",
  "dotenvy",
  "dotenvy_macro",
+ "educe 0.6.0",
  "enum_meta",
  "flate2",
  "futures",
@@ -4353,7 +4349,7 @@ dependencies = [
  "garde",
  "indexmap 2.2.6",
  "miette",
- "reqwest 0.12.4",
+ "reqwest 0.12.5",
  "schematic_macros",
  "schematic_types",
  "serde",
@@ -4368,22 +4364,22 @@ dependencies = [
 
 [[package]]
 name = "schematic_macros"
-version = "0.16.4"
+version = "0.16.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "906eff11b3963e20f5d33fb6dfc67d84351769917970c0d7ebf86e1782d869b5"
+checksum = "fbba5ab97b490c225145d5beba9d565c49049a4c3798227d9ef1dba23f6fcb0f"
 dependencies = [
  "convert_case",
  "darling",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
 name = "schematic_types"
-version = "0.8.2"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "788660c0972a2aab386e2b4b8bd304e0b21c21751e2722cead28d059597d3dad"
+checksum = "e23da8bf72241430f0a710266256139667125e51d11283e60147df0015da4758"
 dependencies = [
  "chrono",
  "indexmap 2.2.6",
@@ -4432,7 +4428,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -4464,9 +4460,9 @@ dependencies = [
 
 [[package]]
 name = "sea-orm-cli"
-version = "1.0.0-rc.5"
+version = "1.0.0-rc.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e1d7c669920a0734f9a269bd210f2e2ac9aca603a531854bc81f03100e92d3a"
+checksum = "76cf2bfe983fad4c67500ef7c9da9b65c94efbbe40293b5cc21936c66aa7f3fe"
 dependencies = [
  "chrono",
  "clap",
@@ -4481,15 +4477,15 @@ dependencies = [
 
 [[package]]
 name = "sea-orm-macros"
-version = "1.0.0-rc.5"
+version = "1.0.0-rc.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "902d060e075c4eed1c31a316c60202982aa7f89e2c23542d841c78ea33755b42"
+checksum = "2e35a73940be69d0b1574773fef989e15bb3e8e5bfd2ac54c5e31b0650775d21"
 dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
  "sea-bae",
- "syn 2.0.66",
+ "syn 2.0.68",
  "unicode-ident",
 ]
 
@@ -4517,7 +4513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f62d5f33113186ad8b0f47c3522e40f9c31cdbc3056a8d7ad402e7498fc29789"
 dependencies = [
  "chrono",
- "educe",
+ "educe 0.5.11",
  "inherent",
  "ordered-float 3.9.2",
  "rust_decimal",
@@ -4527,9 +4523,9 @@ dependencies = [
 
 [[package]]
 name = "sea-query-binder"
-version = "0.6.0-rc.2"
+version = "0.6.0-rc.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c368d1a4050b6b45b47498c544d31a9c35b96aea926599f2a9db82088cc674aa"
+checksum = "9891be9fd1ba58a710274f0387ca1ed2bd0c883a98458cdf706fc35009dae171"
 dependencies = [
  "chrono",
  "rust_decimal",
@@ -4547,15 +4543,15 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
  "thiserror",
 ]
 
 [[package]]
 name = "sea-schema"
-version = "0.15.0-rc.6"
+version = "0.15.0-rc.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87445dedfebeb9eac9ffe825ac8f867eaeb092633431bc262f8e7cf77eb4ba72"
+checksum = "3d26463bcf02ab6fe334ca814d8ec06bc2a8c69e190890a25b109fa5c50c7018"
 dependencies = [
  "futures",
  "sea-query",
@@ -4614,7 +4610,7 @@ version = "2.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "core-foundation",
  "core-foundation-sys",
  "libc",
@@ -4637,9 +4633,9 @@ version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "cssparser",
- "derive_more 0.99.17",
+ "derive_more 0.99.18",
  "fxhash",
  "log",
  "new_debug_unreachable",
@@ -4695,7 +4691,7 @@ checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -4777,7 +4773,7 @@ dependencies = [
  "darling",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -4931,12 +4927,6 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
-[[package]]
-name = "spin"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
-
 [[package]]
 name = "spin"
 version = "0.9.8"
@@ -4968,11 +4958,10 @@ dependencies = [
 
 [[package]]
 name = "sqlformat"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
+checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f"
 dependencies = [
- "itertools 0.12.1",
  "nom",
  "unicode_categories",
 ]
@@ -5081,7 +5070,7 @@ checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
 dependencies = [
  "atoi",
  "base64 0.21.7",
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "byteorder",
  "bytes",
  "chrono",
@@ -5125,7 +5114,7 @@ checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
 dependencies = [
  "atoi",
  "base64 0.21.7",
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "byteorder",
  "chrono",
  "crc",
@@ -5202,9 +5191,9 @@ dependencies = [
 
 [[package]]
 name = "starbase_styles"
-version = "0.4.0"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e483937f25c81ee4a5b70b608436025d23bffa656d62793ea5bbe6e9b94206d"
+checksum = "dcdc25102288ba49a8ae037a3de74dde8bcd519dfe00dc00dae4ed7344475983"
 dependencies = [
  "dirs",
  "owo-colors",
@@ -5285,7 +5274,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -5302,9 +5291,9 @@ dependencies = [
 
 [[package]]
 name = "subtle"
-version = "2.5.0"
+version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
 
 [[package]]
 name = "supports-color"
@@ -5328,9 +5317,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.66"
+version = "2.0.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
+checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5346,7 +5335,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -5439,7 +5428,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -5485,9 +5474,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "1.6.0"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -5525,7 +5514,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -5628,7 +5617,7 @@ dependencies = [
  "serde",
  "serde_spanned",
  "toml_datetime",
- "winnow 0.6.11",
+ "winnow 0.6.13",
 ]
 
 [[package]]
@@ -5654,7 +5643,7 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "bytes",
  "futures-util",
  "http 1.1.0",
@@ -5698,7 +5687,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -5874,9 +5863,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
 
 [[package]]
 name = "url"
-version = "2.5.0"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
 dependencies = [
  "form_urlencoded",
  "idna",
@@ -5898,15 +5887,15 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
 [[package]]
 name = "utf8parse"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "uuid"
-version = "1.8.0"
+version = "1.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
+checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
 dependencies = [
  "serde",
 ]
@@ -5983,7 +5972,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
  "wasm-bindgen-shared",
 ]
 
@@ -6017,7 +6006,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -6069,9 +6058,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
 
 [[package]]
 name = "webpki-roots"
-version = "0.26.2"
+version = "0.26.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c452ad30530b54a4d8e71952716a212b08efd0f3562baa66c29a618b07da7c3"
+checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd"
 dependencies = [
  "rustls-pki-types",
 ]
@@ -6267,9 +6256,9 @@ dependencies = [
 
 [[package]]
 name = "winnow"
-version = "0.6.11"
+version = "0.6.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56c52728401e1dc672a56e81e593e912aa54c78f40246869f78359a2bf24d29d"
+checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
 dependencies = [
  "memchr",
 ]
@@ -6341,7 +6330,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
@@ -6352,7 +6341,7 @@ checksum = "201585ea96d37ee69f2ac769925ca57160cef31acb137c16f38b02b76f4c1e62"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.66",
+ "syn 2.0.68",
 ]
 
 [[package]]
diff --git a/apps/backend/Cargo.toml b/apps/backend/Cargo.toml
index 7457e64fa0..21dce3b158 100644
--- a/apps/backend/Cargo.toml
+++ b/apps/backend/Cargo.toml
@@ -12,7 +12,7 @@ argon2 = "=0.6.0-pre.0"
 async-graphql = { workspace = true }
 async-graphql-axum = "=7.0.6"
 async-trait = "=0.1.80"
-aws-sdk-s3 = { version = "=1.37.0", features = ["behavior-version-latest"] }
+aws-sdk-s3 = { version = "=1.38.0", features = ["behavior-version-latest"] }
 axum = { version = "=0.7.5", features = ["macros", "multipart"] }
 boilermates = "=0.3.0"
 cached = { version = "=0.51.4", features = ["disk_store"] }
@@ -31,6 +31,7 @@ derive_more = { version = "=1.0.0-beta.6", features = [
 ], default-features = false }
 dotenvy = "=0.15.7"
 dotenvy_macro = "=0.15.7"
+educe = { version = "=0.6.0", features = ["Debug"], default-features = false }
 enum_meta = "=0.7.0"
 flate2 = "=1.0.30"
 futures = "=0.3.30"
diff --git a/apps/backend/src/entities/seen.rs b/apps/backend/src/entities/seen.rs
index 124fb8b66c..56b3149054 100644
--- a/apps/backend/src/entities/seen.rs
+++ b/apps/backend/src/entities/seen.rs
@@ -4,6 +4,7 @@ use async_graphql::SimpleObject;
 use async_trait::async_trait;
 use chrono::NaiveDate;
 use database::SeenState;
+use educe::Educe;
 use nanoid::nanoid;
 use rust_decimal::Decimal;
 use rust_decimal_macros::dec;
@@ -21,9 +22,10 @@ use crate::{
 // When updating a media item's progress, here are the things that should happen:
 // - remove from watchlist if it was in there
 // - add to in progress
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, SimpleObject)]
+#[derive(Clone, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, SimpleObject, Educe)]
 #[graphql(name = "Seen")]
 #[sea_orm(table_name = "seen")]
+#[educe(Debug)]
 pub struct Model {
     #[sea_orm(primary_key, auto_increment = false)]
     pub id: String,
@@ -36,6 +38,7 @@ pub struct Model {
     pub provider_watched_on: Option<String>,
     #[graphql(skip)]
     #[serde(skip)]
+    #[educe(Debug(ignore))]
     pub updated_at: Vec<DateTimeUtc>,
     pub show_extra_information: Option<SeenShowExtraInformation>,
     pub podcast_extra_information: Option<SeenPodcastExtraInformation>,
diff --git a/apps/backend/src/fitness/logic.rs b/apps/backend/src/fitness/logic.rs
index 86dc417cd2..fdd031e9d5 100644
--- a/apps/backend/src/fitness/logic.rs
+++ b/apps/backend/src/fitness/logic.rs
@@ -332,7 +332,7 @@ impl UserWorkoutInput {
 
 impl workout::Model {
     // DEV: For exercises, reduce count, remove from history if present. We will not
-    // recalculate exercise associations totals or change personal bests.
+    // re-calculate exercise associations totals or change personal bests.
     pub async fn delete_existing(self, db: &DatabaseConnection, user_id: String) -> Result<()> {
         for (idx, ex) in self.information.exercises.iter().enumerate() {
             let association = match UserToEntity::find()
diff --git a/apps/backend/src/importer/strong_app.rs b/apps/backend/src/importer/strong_app.rs
index 180b3d813a..7a464bce59 100644
--- a/apps/backend/src/importer/strong_app.rs
+++ b/apps/backend/src/importer/strong_app.rs
@@ -60,7 +60,7 @@ pub async fn import(
         .deserialize::<Entry>()
         .map(|r| r.unwrap())
         .collect_vec();
-    // DEV: without this, the last workout does not get appended
+    // DEV: Without this, the last workout does not get appended
     entries_reader.push(Entry {
         date: "invalid".to_string(),
         set_order: 0,
diff --git a/apps/backend/src/main.rs b/apps/backend/src/main.rs
index e2084cb5d1..62255b8414 100644
--- a/apps/backend/src/main.rs
+++ b/apps/backend/src/main.rs
@@ -179,7 +179,7 @@ async fn main() -> Result<()> {
     if cfg!(debug_assertions) {
         use schematic::schema::{SchemaGenerator, TypeScriptRenderer, YamlTemplateRenderer};
 
-        // FIXME: Once https://github.com/rust-lang/cargo/issues/3946 is resolved
+        // TODO: Once https://github.com/rust-lang/cargo/issues/3946 is resolved
         let base_dir = PathBuf::from(BASE_DIR)
             .parent()
             .unwrap()
diff --git a/apps/backend/src/miscellaneous/resolver.rs b/apps/backend/src/miscellaneous/resolver.rs
index e90ff3fbe2..24e77a93e0 100644
--- a/apps/backend/src/miscellaneous/resolver.rs
+++ b/apps/backend/src/miscellaneous/resolver.rs
@@ -1481,6 +1481,7 @@ impl MiscellaneousService {
         };
         let genres = meta
             .find_related(Genre)
+            .order_by_asc(genre::Column::Name)
             .into_model::<GenreListItem>()
             .all(&self.db)
             .await
@@ -1734,7 +1735,7 @@ impl MiscellaneousService {
             .iter()
             .find(|h| h.state == SeenState::InProgress || h.state == SeenState::OnAHold)
             .cloned();
-        let next_episode = history.first().and_then(|h| {
+        let next_entry = history.first().and_then(|h| {
             if let Some(s) = &media_details.show_specifics {
                 let all_episodes = s
                     .seasons
@@ -1898,7 +1899,7 @@ impl MiscellaneousService {
             reviews,
             history,
             in_progress,
-            next_entry: next_episode,
+            next_entry,
             seen_by_all_count: seen_by,
             seen_by_user_count,
             average_rating,
@@ -4039,8 +4040,10 @@ impl MiscellaneousService {
                 })
                 .apply_if(filter.entity_type, |query, v| {
                     let f = match v {
-                        EntityLot::Media => collection_to_entity::Column::MetadataId.is_not_null(),
-                        EntityLot::MediaGroup => {
+                        EntityLot::Metadata => {
+                            collection_to_entity::Column::MetadataId.is_not_null()
+                        }
+                        EntityLot::MetadataGroup => {
                             collection_to_entity::Column::MetadataGroupId.is_not_null()
                         }
                         EntityLot::Person => collection_to_entity::Column::PersonId.is_not_null(),
@@ -4088,7 +4091,7 @@ impl MiscellaneousService {
                             publish_year: m.publish_year,
                         },
                         metadata_lot: Some(m.lot),
-                        entity_lot: EntityLot::Media,
+                        entity_lot: EntityLot::Metadata,
                     }
                 } else if let Some(id) = cte.person_id {
                     let p = Person::find_by_id(id).one(&self.db).await?.unwrap();
@@ -4114,7 +4117,7 @@ impl MiscellaneousService {
                             publish_year: None,
                         },
                         metadata_lot: None,
-                        entity_lot: EntityLot::MediaGroup,
+                        entity_lot: EntityLot::MetadataGroup,
                     }
                 } else if let Some(id) = cte.exercise_id {
                     let e = Exercise::find_by_id(id).one(&self.db).await?.unwrap();
@@ -4251,13 +4254,13 @@ impl MiscellaneousService {
                 (
                     mi.to_string(),
                     self.generic_metadata(&mi).await?.model.title,
-                    EntityLot::Media,
+                    EntityLot::Metadata,
                 )
             } else if let Some(mgi) = insert.metadata_group_id.unwrap() {
                 (
                     mgi.to_string(),
                     self.metadata_group_details(mgi).await?.details.title,
-                    EntityLot::MediaGroup,
+                    EntityLot::MetadataGroup,
                 )
             } else if let Some(pi) = insert.person_id.unwrap() {
                 (
@@ -6356,7 +6359,7 @@ impl MiscellaneousService {
                     identifier: m.id.to_string(),
                 },
                 metadata_lot: Some(m.lot),
-                entity_lot: EntityLot::Media,
+                entity_lot: EntityLot::Metadata,
             };
             contents.push(metadata);
         }
@@ -6870,7 +6873,7 @@ impl MiscellaneousService {
                 let meta = meta.unwrap();
                 let url = self.get_entity_details_frontend_url(
                     meta.id.to_string(),
-                    EntityLot::Media,
+                    EntityLot::Metadata,
                     None,
                 );
                 let notification = if let Some(show) = cal_event.metadata_show_extra_information {
@@ -7056,8 +7059,8 @@ GROUP BY m.id;
     pub async fn handle_review_posted_event(&self, event: ReviewPostedEvent) -> Result<()> {
         let (meta_map, meta_group_map, person_map) = self.get_entities_monitored_by().await?;
         let monitored_by = match event.entity_lot {
-            EntityLot::Media => meta_map.get(&event.obj_id).cloned().unwrap_or_default(),
-            EntityLot::MediaGroup => meta_group_map
+            EntityLot::Metadata => meta_map.get(&event.obj_id).cloned().unwrap_or_default(),
+            EntityLot::MetadataGroup => meta_group_map
                 .get(&event.obj_id)
                 .cloned()
                 .unwrap_or_default(),
@@ -7100,9 +7103,9 @@ GROUP BY m.id;
         default_tab: Option<&str>,
     ) -> String {
         let mut url = match entity_lot {
-            EntityLot::Media => format!("media/item/{}", id),
+            EntityLot::Metadata => format!("media/item/{}", id),
             EntityLot::Person => format!("media/people/item/{}", id),
-            EntityLot::MediaGroup => format!("media/groups/item/{}", id),
+            EntityLot::MetadataGroup => format!("media/groups/item/{}", id),
             EntityLot::Exercise => format!("fitness/exercises/{}", id),
             EntityLot::Collection => format!("collections/{}", id),
         };
diff --git a/apps/backend/src/models.rs b/apps/backend/src/models.rs
index 7bcfb59957..156c48686a 100644
--- a/apps/backend/src/models.rs
+++ b/apps/backend/src/models.rs
@@ -47,9 +47,9 @@ pub enum BackgroundJob {
 #[derive(Enum, Clone, Debug, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display)]
 pub enum EntityLot {
     #[default]
-    Media,
+    Metadata,
     Person,
-    MediaGroup,
+    MetadataGroup,
     Exercise,
     Collection,
 }
@@ -1491,7 +1491,7 @@ pub mod fitness {
         pub basal_metabolic_rate: Option<Decimal>,
         pub total_daily_energy_expenditure: Option<Decimal>,
         pub calories: Option<Decimal>,
-        // DEV: the only custom data type we allow is decimal
+        // DEV: The only custom data type we allow is decimal
         pub custom: Option<HashMap<String, Decimal>>,
     }
 
diff --git a/apps/frontend/app/components/common.tsx b/apps/frontend/app/components/common.tsx
index 633e3208ad..f9b4f22200 100644
--- a/apps/frontend/app/components/common.tsx
+++ b/apps/frontend/app/components/common.tsx
@@ -5,36 +5,24 @@ import {
 	Anchor,
 	Badge,
 	Box,
-	Button,
 	Flex,
 	Image,
-	Modal,
-	NumberInput,
-	Select,
 	SimpleGrid,
 	Stack,
 	Text,
 	TextInput,
-	Title,
 	useComputedColorScheme,
 } from "@mantine/core";
-import { DateInput, DateTimePicker } from "@mantine/dates";
 import { useDebouncedState, useDidUpdate } from "@mantine/hooks";
-import { Form, useNavigation } from "@remix-run/react";
-import {
-	CollectionExtraInformationLot,
-	type EntityLot,
-	type MediaLot,
-	type MediaSource,
-	type UserCollectionsListQuery,
+import type {
+	MediaLot,
+	MediaSource,
 } from "@ryot/generated/graphql/backend/graphql";
-import { formatDateToNaiveDate, groupBy, snakeCase } from "@ryot/ts-utils";
+import { snakeCase } from "@ryot/ts-utils";
 import { IconExternalLink, IconSearch, IconX } from "@tabler/icons-react";
-import { Fragment, type ReactNode, useEffect, useRef } from "react";
+import { type ReactNode, useRef } from "react";
 import { useState } from "react";
-import { match } from "ts-pattern";
-import { withoutHost } from "ufo";
-import events from "~/lib/events";
+import { withFragment, withoutHost } from "ufo";
 import { getFallbackImageUrl, redirectToQueryParam } from "~/lib/generals";
 import { useSearchParam } from "~/lib/hooks";
 import classes from "~/styles/common.module.css";
@@ -140,162 +128,18 @@ export const MediaDetailsLayout = (props: {
 
 export const MEDIA_DETAILS_HEIGHT = { base: "45vh", "2xl": "55vh" };
 
-type Collection = UserCollectionsListQuery["userCollectionsList"][number];
-
-export const AddEntityToCollectionModal = (props: {
-	userId: string;
-	opened: boolean;
-	onClose: () => void;
-	entityId: string;
-	entityLot: EntityLot;
-	collections: Array<Collection>;
-}) => {
-	const transition = useNavigation();
-	const selectData = Object.entries(
-		groupBy(props.collections, (c) =>
-			c.creator.id === props.userId ? "You" : c.creator.name,
-		),
-	).map(([g, items]) => ({
-		group: g,
-		items: items.map((c) => ({
-			label: c.name,
-			value: c.id.toString(),
-		})),
-	}));
-	const [selectedCollection, setSelectedCollection] =
-		useState<Collection | null>(null);
-	const [ownedOn, setOwnedOn] = useState<Date | null>();
-	useEffect(() => {
-		if (transition.state !== "submitting") {
-			props.onClose();
-		}
-	}, [transition.state]);
-
-	return (
-		<Modal
-			opened={props.opened}
-			onClose={props.onClose}
-			withCloseButton={false}
-			centered
-		>
-			<Form action="/actions?intent=addEntityToCollection" method="post">
-				<input readOnly hidden name="entityId" value={props.entityId} />
-				<input readOnly hidden name="entityLot" value={props.entityLot} />
-				<HiddenLocationInput />
-				<Stack>
-					<Title order={3}>Select collection</Title>
-					<Select
-						searchable
-						data={selectData}
-						nothingFoundMessage="Nothing found..."
-						value={selectedCollection?.id.toString()}
-						onChange={(v) => {
-							if (v) {
-								const collection = props.collections.find((c) => c.id === v);
-								if (collection) setSelectedCollection(collection);
-							}
-						}}
-					/>
-					{selectedCollection ? (
-						<>
-							<input
-								readOnly
-								hidden
-								name="collectionName"
-								value={selectedCollection.name}
-							/>
-							<input
-								readOnly
-								hidden
-								name="creatorUserId"
-								value={selectedCollection.creator.id}
-							/>
-							{selectedCollection.informationTemplate?.map((template) => (
-								<Fragment key={template.name}>
-									{match(template.lot)
-										.with(CollectionExtraInformationLot.String, () => (
-											<TextInput
-												name={`information.${template.name}`}
-												label={template.name}
-												description={template.description}
-												required={!!template.required}
-												defaultValue={template.defaultValue || undefined}
-											/>
-										))
-										.with(CollectionExtraInformationLot.Number, () => (
-											<NumberInput
-												name={`information.${template.name}`}
-												label={template.name}
-												description={template.description}
-												required={!!template.required}
-												defaultValue={
-													template.defaultValue
-														? Number(template.defaultValue)
-														: undefined
-												}
-											/>
-										))
-										.with(CollectionExtraInformationLot.Date, () => (
-											<>
-												<DateInput
-													label={template.name}
-													description={template.description}
-													required={!!template.required}
-													onChange={setOwnedOn}
-													value={ownedOn}
-													defaultValue={
-														template.defaultValue
-															? new Date(template.defaultValue)
-															: undefined
-													}
-												/>
-												<input
-													readOnly
-													hidden
-													name={`information.${template.name}`}
-													value={
-														ownedOn ? formatDateToNaiveDate(ownedOn) : undefined
-													}
-												/>
-											</>
-										))
-										.with(CollectionExtraInformationLot.DateTime, () => (
-											<DateTimePicker
-												name={`information.${template.name}`}
-												label={template.name}
-												description={template.description}
-												required={!!template.required}
-											/>
-										))
-										.exhaustive()}
-								</Fragment>
-							))}
-						</>
-					) : null}
-					<Button
-						disabled={!selectedCollection}
-						variant="outline"
-						type="submit"
-						onClick={() => events.addToCollection(props.entityLot)}
-					>
-						Set
-					</Button>
-					<Button variant="outline" color="red" onClick={props.onClose}>
-						Cancel
-					</Button>
-				</Stack>
-			</Form>
-		</Modal>
-	);
-};
-
-export const HiddenLocationInput = () => {
+export const HiddenLocationInput = (props: { hash?: string }) => {
 	const value = withoutHost(
 		typeof window !== "undefined" ? window.location.href : "",
 	);
 
 	return (
-		<input type="hidden" name={redirectToQueryParam} value={value} readOnly />
+		<input
+			type="hidden"
+			name={redirectToQueryParam}
+			value={withFragment(value, props.hash || "")}
+			readOnly
+		/>
 	);
 };
 
diff --git a/apps/frontend/app/components/media.tsx b/apps/frontend/app/components/media.tsx
index 511e2da7c3..115a1d9084 100644
--- a/apps/frontend/app/components/media.tsx
+++ b/apps/frontend/app/components/media.tsx
@@ -7,27 +7,20 @@ import {
 	Badge,
 	Box,
 	Button,
-	Checkbox,
 	Collapse,
 	Divider,
 	Flex,
 	Group,
 	Image,
-	Input,
 	Loader,
 	type MantineStyleProp,
 	Menu,
-	Modal,
-	NumberInput,
 	Paper,
-	Rating,
 	ScrollArea,
-	SegmentedControl,
 	Stack,
 	type StyleProp,
 	Text,
 	TextInput,
-	Textarea,
 	ThemeIcon,
 	Tooltip,
 	useComputedColorScheme,
@@ -43,13 +36,12 @@ import {
 } from "@remix-run/react";
 import {
 	EntityLot,
-	MediaLot,
+	type MediaLot,
 	type MediaSource,
 	type PartialMetadata,
 	type ReviewItem,
 	UserReviewScale,
 	UserToMediaReason,
-	Visibility,
 } from "@ryot/generated/graphql/backend/graphql";
 import { changeCase, getInitials } from "@ryot/ts-utils";
 import {
@@ -60,26 +52,28 @@ import {
 	IconCheck,
 	IconCloudDownload,
 	IconEdit,
-	IconPercentage,
 	IconRosetteDiscountCheck,
 	IconStarFilled,
 	IconTrash,
 	IconX,
 } from "@tabler/icons-react";
-import { type ReactNode, useState } from "react";
+import type { ReactNode } from "react";
 import type { DeepPartial } from "ts-essentials";
 import { match } from "ts-pattern";
 import { withoutHost } from "ufo";
 import { HiddenLocationInput, MEDIA_DETAILS_HEIGHT } from "~/components/common";
 import { confirmWrapper } from "~/components/confirmation";
-import events from "~/lib/events";
 import {
 	dayjsLib,
 	getFallbackImageUrl,
 	redirectToQueryParam,
 } from "~/lib/generals";
-import { useGetMantineColor } from "~/lib/hooks";
-import type { ApplicationUser } from "~/lib/utilities.server";
+import {
+	useGetMantineColor,
+	useUserDetails,
+	useUserPreferences,
+} from "~/lib/hooks";
+import { useReviewEntity } from "~/lib/state/media";
 import type { action } from "~/routes/actions";
 import classes from "~/styles/common.module.css";
 
@@ -151,44 +145,24 @@ export const MediaScrollArea = (props: { children: ReactNode }) => {
 
 export const ReviewItemDisplay = (props: {
 	review: DeepPartial<ReviewItem>;
-	entityType: EntityType;
-	user: ApplicationUser;
-	reviewScale: UserReviewScale;
+	entityLot: EntityLot;
 	title: string;
-	metadataId?: string;
-	metadataGroupId?: string;
-	personId?: string;
-	collectionId?: string;
+	entityId: string;
 	lot?: MediaLot;
 }) => {
+	const userDetails = useUserDetails();
+	const userPreferences = useUserPreferences();
+	const reviewScale = userPreferences.general.reviewScale;
 	const [opened, { toggle }] = useDisclosure(false);
 	const [openedLeaveComment, { toggle: toggleLeaveComment }] =
 		useDisclosure(false);
-	const [postReviewModalData, setPostReviewModalData] = useState<
-		PostReview | undefined
-	>(undefined);
 	const deleteReviewFetcher = useFetcher<typeof action>();
+	const [_, setEntityToReview] = useReviewEntity();
 
 	const submit = useSubmit();
 
 	return (
 		<>
-			<PostReviewModal
-				onClose={() => setPostReviewModalData(undefined)}
-				opened={postReviewModalData !== undefined}
-				data={postReviewModalData}
-				entityType={props.entityType}
-				objectId={
-					props.metadataId?.toString() ||
-					props.metadataGroupId?.toString() ||
-					props.collectionId?.toString() ||
-					props.personId?.toString() ||
-					""
-				}
-				reviewScale={props.reviewScale}
-				title={props.title}
-				lot={props.lot}
-			/>
 			<Box key={props.review.id} data-review-id={props.review.id}>
 				<Flex align="center" gap="sm">
 					<Avatar color="cyan" radius="xl">
@@ -198,23 +172,16 @@ export const ReviewItemDisplay = (props: {
 						<Text>{props.review.postedBy?.name}</Text>
 						<Text>{dayjsLib(props.review.postedOn).format("L")}</Text>
 					</Box>
-					{props.user && props.user.id === props.review.postedBy?.id ? (
+					{userDetails.id === props.review.postedBy?.id ? (
 						<>
 							<ActionIcon
 								onClick={() => {
-									setPostReviewModalData({
+									setEntityToReview({
+										entityLot: props.entityLot,
+										entityId: props.entityId,
+										entityTitle: props.title,
+										metadataLot: props.lot,
 										existingReview: props.review,
-										showSeasonNumber: props.review.showExtraInformation?.season,
-										showEpisodeNumber:
-											props.review.showExtraInformation?.episode,
-										podcastEpisodeNumber:
-											props.review.podcastExtraInformation?.episode,
-										animeEpisodeNumber:
-											props.review.animeExtraInformation?.episode,
-										mangaChapterNumber:
-											props.review.mangaExtraInformation?.chapter,
-										mangaVolumeNumber:
-											props.review.mangaExtraInformation?.volume,
 									});
 								}}
 							>
@@ -283,9 +250,7 @@ export const ReviewItemDisplay = (props: {
 							<IconStarFilled size={16} style={{ color: "#EBE600FF" }} />
 							<Text className={classes.text} fw="bold">
 								{props.review.rating}
-								{props.reviewScale === UserReviewScale.OutOfFive
-									? undefined
-									: "%"}
+								{reviewScale === UserReviewScale.OutOfFive ? undefined : "%"}
 							</Text>
 						</Flex>
 					) : null}
@@ -363,7 +328,7 @@ export const ReviewItemDisplay = (props: {
 															<Text>{dayjsLib(c.createdOn).format("L")}</Text>
 														) : null}
 													</Box>
-													{props.user.id === c?.user?.id ? (
+													{userDetails.id === c?.user?.id ? (
 														<Form
 															action="/actions?intent=createReviewComment"
 															method="post"
@@ -420,7 +385,7 @@ export const ReviewItemDisplay = (props: {
 															hidden
 															name="incrementLikes"
 															value={String(
-																!c?.likedBy?.includes(props.user.id),
+																!c?.likedBy?.includes(userDetails.id),
 															)}
 															readOnly
 														/>
@@ -428,7 +393,7 @@ export const ReviewItemDisplay = (props: {
 															hidden
 															name="decrementLikes"
 															value={String(
-																c?.likedBy?.includes(props.user.id),
+																c?.likedBy?.includes(userDetails.id),
 															)}
 															readOnly
 														/>
@@ -610,15 +575,13 @@ export const MediaItemWithoutUpdateModal = (props: {
 	children?: ReactNode;
 	imageOverlayForLoadingIndicator?: boolean;
 	hasInteracted?: boolean;
-	averageRating?: string;
-	noRatingLink?: boolean;
+	topRight?: ReactNode;
 	noBottomRight?: boolean;
 	noHref?: boolean;
 	onClick?: (e: React.MouseEvent) => Promise<void>;
 	nameRight?: ReactNode;
 	mediaReason?: Array<UserToMediaReason> | null;
 }) => {
-	const navigate = useNavigate();
 	const id = props.item.identifier;
 
 	return (
@@ -629,10 +592,10 @@ export const MediaItemWithoutUpdateModal = (props: {
 					? props.href
 						? props.href
 						: match(props.entityLot)
-								.with(EntityLot.Media, undefined, null, () =>
+								.with(EntityLot.Metadata, undefined, null, () =>
 									$path("/media/item/:id", { id }),
 								)
-								.with(EntityLot.MediaGroup, () =>
+								.with(EntityLot.MetadataGroup, () =>
 									$path("/media/groups/item/:id", { id }),
 								)
 								.with(EntityLot.Person, () =>
@@ -656,41 +619,13 @@ export const MediaItemWithoutUpdateModal = (props: {
 			}
 			mediaReason={props.mediaReason}
 			topRight={
-				props.averageRating ? (
+				props.topRight ? (
 					<Box style={blackBgStyles}>
 						<Flex align="center" gap={4}>
-							<IconStarFilled size={12} style={{ color: "#EBE600FF" }} />
-							<Text c="white" size="xs" fw="bold" pr={4}>
-								{match(props.reviewScale)
-									.with(UserReviewScale.OutOfFive, () =>
-										// biome-ignore lint/style/noNonNullAssertion: it is validated above
-										Number.parseFloat(props.averageRating!.toString()).toFixed(
-											1,
-										),
-									)
-									.with(UserReviewScale.OutOfHundred, () => props.averageRating)
-									.exhaustive()}{" "}
-								{props.reviewScale === UserReviewScale.OutOfFive
-									? undefined
-									: "%"}
-							</Text>
-						</Flex>
-					</Box>
-				) : props.noRatingLink ? undefined : (
-					<Box
-						style={blackBgStyles}
-						onClick={(e) => {
-							e.preventDefault();
-							navigate(
-								$path("/media/item/:id", { id }, { openReviewModal: true }),
-							);
-						}}
-					>
-						<Flex align="center" gap={4}>
-							<IconStarFilled size={16} className={classes.starIcon} />
+							{props.topRight}
 						</Flex>
 					</Box>
-				)
+				) : undefined
 			}
 			bottomLeft={props.item.publishYear}
 			bottomRight={
@@ -701,7 +636,7 @@ export const MediaItemWithoutUpdateModal = (props: {
 						)
 			}
 			highlightRightText={
-				props.hasInteracted ? "This media exists in the database" : undefined
+				props.hasInteracted ? "You have interacted with this before" : undefined
 			}
 			name={props.item.title}
 			nameRight={props.nameRight}
@@ -712,7 +647,7 @@ export const MediaItemWithoutUpdateModal = (props: {
 };
 
 export const DisplayCollection = (props: {
-	userId: string;
+	creatorUserId: string;
 	col: { id: string; name: string };
 	entityId: string;
 	entityLot: EntityLot;
@@ -737,7 +672,12 @@ export const DisplayCollection = (props: {
 					<input readOnly hidden name="entityId" value={props.entityId} />
 					<input readOnly hidden name="entityLot" value={props.entityLot} />
 					<input readOnly hidden name="collectionName" value={props.col.name} />
-					<input readOnly hidden name="creatorUserId" value={props.userId} />
+					<input
+						readOnly
+						hidden
+						name="creatorUserId"
+						value={props.creatorUserId}
+					/>
 					<HiddenLocationInput />
 					<ActionIcon
 						size={16}
@@ -759,225 +699,6 @@ export const DisplayCollection = (props: {
 	);
 };
 
-export type PostReview = {
-	showSeasonNumber?: number | null;
-	showEpisodeNumber?: number | null;
-	animeEpisodeNumber?: number | null;
-	mangaChapterNumber?: number | null;
-	mangaVolumeNumber?: number | null;
-	podcastEpisodeNumber?: number | null;
-	existingReview?: DeepPartial<ReviewItem>;
-};
-
-type EntityType = "metadata" | "metadataGroup" | "collection" | "person";
-
-export const PostReviewModal = (props: {
-	opened: boolean;
-	onClose: () => void;
-	objectId: string;
-	entityType: EntityType;
-	title: string;
-	reviewScale: UserReviewScale;
-	data?: PostReview;
-	lot?: MediaLot;
-}) => {
-	if (!props.data) return <></>;
-	return (
-		<Modal
-			opened={props.opened}
-			onClose={props.onClose}
-			withCloseButton={false}
-			centered
-		>
-			<Form
-				method="post"
-				action="/actions?intent=performReviewAction"
-				replace
-				onSubmit={() => {
-					events.postReview(props.title);
-					props.onClose();
-				}}
-			>
-				<input
-					hidden
-					name={
-						props.entityType === "metadata"
-							? "metadataId"
-							: props.entityType === "metadataGroup"
-								? "metadataGroupId"
-								: props.entityType === "collection"
-									? "collectionId"
-									: props.entityType === "person"
-										? "personId"
-										: undefined
-					}
-					value={props.objectId}
-					readOnly
-				/>
-				<HiddenLocationInput />
-				{props.data.existingReview?.id ? (
-					<input hidden name="reviewId" value={props.data.existingReview.id} />
-				) : null}
-				<Stack>
-					<Flex align="center" gap="xl">
-						{match(props.reviewScale)
-							.with(UserReviewScale.OutOfFive, () => (
-								<Flex gap="sm" mt="lg">
-									<Input.Label>Rating:</Input.Label>
-									<Rating
-										name="rating"
-										defaultValue={
-											props.data?.existingReview?.rating
-												? Number(props.data.existingReview.rating)
-												: undefined
-										}
-										fractions={2}
-									/>
-								</Flex>
-							))
-							.with(UserReviewScale.OutOfHundred, () => (
-								<NumberInput
-									label="Rating"
-									name="rating"
-									min={0}
-									max={100}
-									step={1}
-									w="40%"
-									hideControls
-									rightSection={<IconPercentage size={16} />}
-									defaultValue={
-										props.data?.existingReview?.rating
-											? Number(props.data.existingReview.rating)
-											: undefined
-									}
-								/>
-							))
-							.exhaustive()}
-						<Checkbox
-							label="This review is a spoiler"
-							mt="lg"
-							name="isSpoiler"
-						/>
-					</Flex>
-					{props.lot === MediaLot.Show ? (
-						<Flex gap="md">
-							<NumberInput
-								label="Season"
-								name="showSeasonNumber"
-								hideControls
-								defaultValue={
-									typeof props.data?.existingReview?.showExtraInformation
-										?.season === "number"
-										? props.data.existingReview.showExtraInformation?.season
-										: typeof props.data.showSeasonNumber === "number"
-											? props.data.showSeasonNumber
-											: undefined
-								}
-							/>
-							<NumberInput
-								label="Episode"
-								name="showEpisodeNumber"
-								hideControls
-								defaultValue={
-									props.data?.existingReview?.showExtraInformation?.episode
-										? props.data.existingReview.showExtraInformation?.episode
-										: props.data.showEpisodeNumber || undefined
-								}
-							/>
-						</Flex>
-					) : null}
-					{props.lot === MediaLot.Podcast ? (
-						<NumberInput
-							label="Episode"
-							name="podcastEpisodeNumber"
-							hideControls
-							defaultValue={
-								props.data?.existingReview?.podcastExtraInformation?.episode
-									? props.data.existingReview.podcastExtraInformation?.episode
-									: props.data.podcastEpisodeNumber || undefined
-							}
-						/>
-					) : null}
-					{props.lot === MediaLot.Anime ? (
-						<NumberInput
-							label="Episode"
-							name="animeEpisodeNumber"
-							hideControls
-							defaultValue={
-								props.data?.existingReview?.animeExtraInformation?.episode
-									? props.data.existingReview.animeExtraInformation?.episode
-									: props.data.animeEpisodeNumber || undefined
-							}
-						/>
-					) : null}
-					{props.lot === MediaLot.Manga ? (
-						<>
-							<Group wrap="nowrap">
-								<NumberInput
-									label="Chapter"
-									name="mangaChapterNumber"
-									hideControls
-									defaultValue={
-										props.data?.existingReview?.mangaExtraInformation?.chapter
-											? props.data.existingReview.mangaExtraInformation?.chapter
-											: props.data.mangaChapterNumber || undefined
-									}
-								/>
-								<Text ta="center" fw="bold" mt="sm">
-									OR
-								</Text>
-								<NumberInput
-									label="Volume"
-									name="mangaVolumeNumber"
-									hideControls
-									defaultValue={
-										props.data?.existingReview?.mangaExtraInformation?.volume
-											? props.data.existingReview.mangaExtraInformation?.volume
-											: props.data.mangaVolumeNumber || undefined
-									}
-								/>
-							</Group>
-						</>
-					) : null}
-					<Textarea
-						label="Review"
-						name="text"
-						description="Markdown is supported"
-						autoFocus
-						minRows={10}
-						maxRows={20}
-						autosize
-						defaultValue={props.data.existingReview?.textOriginal ?? undefined}
-					/>
-					<Box>
-						<Input.Label>Visibility</Input.Label>
-						<SegmentedControl
-							fullWidth
-							data={[
-								{
-									label: Visibility.Public,
-									value: Visibility.Public,
-								},
-								{
-									label: Visibility.Private,
-									value: Visibility.Private,
-								},
-							]}
-							defaultValue={
-								props.data.existingReview?.visibility ?? Visibility.Public
-							}
-							name="visibility"
-						/>
-					</Box>
-					<Button mt="md" type="submit" w="100%">
-						{props.data.existingReview?.id ? "Update" : "Submit"}
-					</Button>
-				</Stack>
-			</Form>
-		</Modal>
-	);
-};
-
 export const NewUserGuideAlert = () => {
 	return (
 		<Alert icon={<IconArrowsRight />} variant="outline" color="teal">
@@ -1004,7 +725,6 @@ export const MediaIsPartial = (props: { mediaType: string }) => {
 };
 
 export const ToggleMediaMonitorMenuItem = (props: {
-	userId: string;
 	entityLot: EntityLot;
 	inCollections: Array<string>;
 	formValue: string;
@@ -1013,13 +733,14 @@ export const ToggleMediaMonitorMenuItem = (props: {
 	const action = isMonitored
 		? "removeEntityFromCollection"
 		: "addEntityToCollection";
+	const userDetails = useUserDetails();
 
 	return (
 		<Form action={`/actions?intent=${action}`} method="post" replace>
 			<HiddenLocationInput />
 			<input hidden name="collectionName" defaultValue="Monitoring" />
 			<input readOnly hidden name="entityLot" value={props.entityLot} />
-			<input readOnly hidden name="creatorUserId" value={props.userId} />
+			<input readOnly hidden name="creatorUserId" value={userDetails.id} />
 			<Menu.Item
 				type="submit"
 				color={isMonitored ? "red" : undefined}
diff --git a/apps/frontend/app/components/toaster.tsx b/apps/frontend/app/components/toaster.tsx
index 2e729f3d2d..0e827e727b 100644
--- a/apps/frontend/app/components/toaster.tsx
+++ b/apps/frontend/app/components/toaster.tsx
@@ -12,8 +12,8 @@ export function Toaster({ toast }: { toast?: Toast | null }) {
 	);
 }
 
-function ShowToast({ toast }: { toast: Toast }) {
-	const { id, type, title, message } = toast;
+const ShowToast = (props: { toast: Toast }) => {
+	const { id, type, title, message } = props.toast;
 	useEffect(() => {
 		setTimeout(() => {
 			const [defaultTitle, defaultColor] = match(type)
@@ -29,5 +29,5 @@ function ShowToast({ toast }: { toast: Toast }) {
 			});
 		}, 0);
 	}, [message, id, title, type]);
-	return undefined;
-}
+	return null;
+};
diff --git a/apps/frontend/app/lib/hooks.ts b/apps/frontend/app/lib/hooks.ts
index b36102fa51..285e032ad6 100644
--- a/apps/frontend/app/lib/hooks.ts
+++ b/apps/frontend/app/lib/hooks.ts
@@ -1,10 +1,28 @@
 import { $path } from "@ignisda/remix-routes";
 import { useMantineTheme } from "@mantine/core";
-import { useNavigate, useSearchParams } from "@remix-run/react";
+import {
+	useNavigate,
+	useRouteLoaderData,
+	useSearchParams,
+} from "@remix-run/react";
+import {
+	MetadataDetailsDocument,
+	UserMetadataDetailsDocument,
+} from "@ryot/generated/graphql/backend/graphql";
+import { skipToken, useQuery } from "@tanstack/react-query";
 import { useAtom } from "jotai";
 import Cookies from "js-cookie";
-import { CurrentWorkoutKey, getStringAsciiValue } from "~/lib/generals";
-import { type InProgressWorkout, currentWorkoutAtom } from "~/lib/workout";
+import {
+	CurrentWorkoutKey,
+	clientGqlService,
+	dayjsLib,
+	getStringAsciiValue,
+} from "~/lib/generals";
+import {
+	type InProgressWorkout,
+	currentWorkoutAtom,
+} from "~/lib/state/workout";
+import type { loader } from "~/routes/_dashboard";
 
 export function useGetMantineColor() {
 	const theme = useMantineTheme();
@@ -52,3 +70,45 @@ export function getWorkoutStarter() {
 	};
 	return fn;
 }
+
+export const getMetadataDetailsQuery = (metadataId?: string | null) =>
+	({
+		queryKey: ["metadataDetails", metadataId],
+		queryFn: metadataId
+			? () =>
+					clientGqlService
+						.request(MetadataDetailsDocument, { metadataId })
+						.then((data) => data.metadataDetails)
+			: skipToken,
+		staleTime: dayjsLib.duration(1, "day").asMilliseconds(),
+	}) as const;
+
+export const useMetadataDetails = (metadataId?: string | null) => {
+	return useQuery(getMetadataDetailsQuery(metadataId));
+};
+
+export const getUserMetadataDetailsQuery = (metadataId?: string | null) =>
+	({
+		queryKey: ["userMetadataDetails", metadataId],
+		queryFn: metadataId
+			? () =>
+					clientGqlService
+						.request(UserMetadataDetailsDocument, { metadataId })
+						.then((data) => data.userMetadataDetails)
+			: skipToken,
+		staleTime: Number.POSITIVE_INFINITY,
+	}) as const;
+
+export const useUserMetadataDetails = (metadataId?: string | null) => {
+	return useQuery(getUserMetadataDetailsQuery(metadataId));
+};
+
+const useDashboardData = () => {
+	const loaderData = useRouteLoaderData<typeof loader>("routes/_dashboard");
+	return loaderData;
+};
+
+export const useCoreDetails = () => useDashboardData().coreDetails;
+export const useUserPreferences = () => useDashboardData().userPreferences;
+export const useUserDetails = () => useDashboardData().userDetails;
+export const useUserCollections = () => useDashboardData().userCollections;
diff --git a/apps/frontend/app/lib/state/media.ts b/apps/frontend/app/lib/state/media.ts
new file mode 100644
index 0000000000..649fdc87ab
--- /dev/null
+++ b/apps/frontend/app/lib/state/media.ts
@@ -0,0 +1,99 @@
+import {
+	type EntityLot,
+	MediaLot,
+	type ReviewItem,
+} from "@ryot/generated/graphql/backend/graphql";
+import { atom, useAtom } from "jotai";
+import { useState } from "react";
+import type { DeepPartial } from "ts-essentials";
+import { match } from "ts-pattern";
+import { queryClient } from "~/lib/generals";
+import {
+	getMetadataDetailsQuery,
+	getUserMetadataDetailsQuery,
+} from "~/lib/hooks";
+
+export type UpdateProgressData = {
+	metadataId: string;
+	onlySeason?: boolean;
+	pageFragment?: string;
+	completeShow?: boolean;
+	completePodcast?: boolean;
+	showSeasonNumber?: number | null;
+	showEpisodeNumber?: number | null;
+	podcastEpisodeNumber?: number | null;
+	animeEpisodeNumber?: number | null;
+	mangaChapterNumber?: number | null;
+};
+
+const metadataProgressUpdateAtom = atom<UpdateProgressData | null>(null);
+
+export const useMetadataProgressUpdate = () => {
+	const [isLoading, setIsLoading] = useState(false);
+	const [metadataProgress, _setMetadataProgress] = useAtom(
+		metadataProgressUpdateAtom,
+	);
+	const setMetadataProgress = async (
+		draft: UpdateProgressData | null,
+		determineNext?: boolean,
+	) => {
+		setIsLoading(true);
+		if (draft && determineNext) {
+			const [metadataDetails, userMetadataDetails] = await Promise.all([
+				queryClient.ensureQueryData(getMetadataDetailsQuery(draft.metadataId)),
+				queryClient.ensureQueryData(
+					getUserMetadataDetailsQuery(draft.metadataId),
+				),
+			]);
+			const nextEntry = userMetadataDetails?.nextEntry;
+			if (nextEntry) {
+				match(metadataDetails.lot)
+					.with(MediaLot.Show, () => {
+						draft.showEpisodeNumber = nextEntry.episode;
+						draft.showSeasonNumber = nextEntry.season;
+					})
+					.with(MediaLot.Podcast, () => {
+						draft.podcastEpisodeNumber = nextEntry.episode;
+					})
+					.with(MediaLot.Anime, () => {
+						draft.animeEpisodeNumber = nextEntry.episode;
+					})
+					.with(MediaLot.Manga, () => {
+						draft.mangaChapterNumber = nextEntry.chapter;
+					})
+					.otherwise(() => undefined);
+			}
+		}
+		setIsLoading(false);
+		_setMetadataProgress(draft);
+	};
+	return [metadataProgress, setMetadataProgress, isLoading] as const;
+};
+
+export type ReviewEntityData = {
+	entityId: string;
+	entityLot: EntityLot;
+	entityTitle: string;
+	metadataLot?: MediaLot;
+	existingReview?: DeepPartial<ReviewItem>;
+};
+
+export const reviewEntityAtom = atom<ReviewEntityData | null>(null);
+
+export const useReviewEntity = () => {
+	return useAtom(reviewEntityAtom);
+};
+
+export type AddEntityToCollectionData = {
+	entityId: string;
+	entityLot: EntityLot;
+	alreadyInCollections?: Array<string>;
+};
+
+export const addEntityToCollectionAtom = atom<AddEntityToCollectionData | null>(
+	null,
+);
+
+export const useAddEntityToCollection = () => {
+	return useAtom(addEntityToCollectionAtom);
+};
diff --git a/apps/frontend/app/lib/workout.ts b/apps/frontend/app/lib/state/workout.ts
similarity index 100%
rename from apps/frontend/app/lib/workout.ts
rename to apps/frontend/app/lib/state/workout.ts
diff --git a/apps/frontend/app/lib/utilities.server.ts b/apps/frontend/app/lib/utilities.server.ts
index 821349c970..a1cdf920ce 100644
--- a/apps/frontend/app/lib/utilities.server.ts
+++ b/apps/frontend/app/lib/utilities.server.ts
@@ -8,13 +8,12 @@ import {
 	unstable_createMemoryUploadHandler,
 } from "@remix-run/node";
 import {
-	type CoreDetails,
 	CoreDetailsDocument,
 	CoreEnabledFeaturesDocument,
 	GetPresignedS3UrlDocument,
 	PresignedPutS3UrlDocument,
+	type User,
 	UserCollectionsListDocument,
-	type UserLot,
 	type UserPreferences,
 	UserPreferencesDocument,
 } from "@ryot/generated/graphql/backend/graphql";
@@ -29,6 +28,8 @@ import {
 	CORE_DETAILS_COOKIE_NAME,
 	USER_DETAILS_COOKIE_NAME,
 	USER_PREFERENCES_COOKIE_NAME,
+	dayjsLib,
+	queryClient,
 	redirectToQueryParam,
 } from "~/lib/generals";
 
@@ -42,40 +43,29 @@ export const getCookieValue = (request: Request, cookieName: string) => {
 	return parse(request.headers.get("cookie") || "")[cookieName];
 };
 
-const getAuthorizationCookie = async (request: Request) => {
+export const getAuthorizationCookie = (request: Request) => {
 	return getCookieValue(request, AUTH_COOKIE_NAME);
 };
 
-export const getAuthorizationHeader = async (
-	request?: Request,
-	token?: string,
-) => {
+export const getAuthorizationHeader = (request?: Request, token?: string) => {
 	let cookie: string;
-	if (request) cookie = await getAuthorizationCookie(request);
+	if (request) cookie = getAuthorizationCookie(request);
 	else if (token) cookie = token;
 	else cookie = "";
 	return { Authorization: `Bearer ${cookie}` };
 };
 
-export const getIsAuthenticated = async (request: Request) => {
-	const cookie = await getAuthorizationCookie(request);
+export const getIsAuthenticated = (request: Request) => {
+	const cookie = getAuthorizationCookie(request);
 	if (!cookie) return [false, null] as const;
-	try {
-		const { userDetails } = await serverGqlService.request(
-			UserDetailsDocument,
-			undefined,
-			await getAuthorizationHeader(request),
-		);
-		return [userDetails.__typename === "User", userDetails] as const;
-	} catch {
-		return [false, null] as const;
-	}
+	const value = getCookieValue(request, USER_DETAILS_COOKIE_NAME);
+	return [true, JSON.parse(value) as User] as const;
 };
 
 export const redirectIfNotAuthenticatedOrUpdated = async (request: Request) => {
-	const [isAuthenticated, userDetails] = await getIsAuthenticated(request);
-	const nextUrl = withoutHost(request.url);
-	if (!isAuthenticated || userDetails.__typename !== "User") {
+	const [isAuthenticated, userDetails] = getIsAuthenticated(request);
+	if (!isAuthenticated) {
+		const nextUrl = withoutHost(request.url);
 		throw redirect($path("/auth", { [redirectToQueryParam]: nextUrl }), {
 			status: 302,
 			headers: combineHeaders(
@@ -83,7 +73,7 @@ export const redirectIfNotAuthenticatedOrUpdated = async (request: Request) => {
 					type: "error",
 					message: "You must be logged in to view this page",
 				}),
-				await getLogoutCookies(),
+				getLogoutCookies(),
 			),
 		});
 	}
@@ -115,20 +105,13 @@ export function combineHeaders(
 	return combined;
 }
 
-export type ApplicationUser = {
-	__typename: "User";
-	id: string;
-	name: string;
-	lot: UserLot;
-	oidcIssuerId?: string;
-	isDemo: boolean;
-};
-
 const emptyNumberString = z
 	.any()
 	.transform((v) => (!v ? undefined : Number.parseInt(v)))
 	.nullable();
 
+export const MetadataIdSchema = z.object({ metadataId: z.string() });
+
 export const MetadataSpecificsSchema = z.object({
 	showSeasonNumber: emptyNumberString,
 	showEpisodeNumber: emptyNumberString,
@@ -152,13 +135,27 @@ export const processSubmission = <Schema extends ZodTypeAny>(
 	return submission.value;
 };
 
-export const getUserCollectionsList = async (request: Request) => {
-	const { userCollectionsList } = await serverGqlService.request(
-		UserCollectionsListDocument,
-		{},
-		await getAuthorizationHeader(request),
-	);
-	return userCollectionsList;
+export const getCachedUserCollectionsList = async (request: Request) => {
+	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
+	return queryClient.ensureQueryData({
+		queryKey: ["userCollectionsList", userDetails.id],
+		queryFn: () =>
+			serverGqlService
+				.request(
+					UserCollectionsListDocument,
+					{},
+					getAuthorizationHeader(request),
+				)
+				.then((data) => data.userCollectionsList),
+		gcTime: dayjsLib.duration(1, "hour").asMilliseconds(),
+	});
+};
+
+export const removeCachedUserCollectionsList = async (request: Request) => {
+	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
+	queryClient.removeQueries({
+		queryKey: ["userCollectionsList", userDetails.id],
+	});
 };
 
 export const uploadFileAndGetKey = async (
@@ -229,6 +226,12 @@ export const s3FileUploader = (prefix: string) =>
 		return undefined;
 	}, unstable_createMemoryUploadHandler());
 
+export const getUserPreferences = async (request: Request) => {
+	await redirectIfNotAuthenticatedOrUpdated(request);
+	const preferences = getCookieValue(request, USER_PREFERENCES_COOKIE_NAME);
+	return JSON.parse(preferences) as UserPreferences;
+};
+
 export const getCoreEnabledFeatures = async () => {
 	const { coreEnabledFeatures } = await serverGqlService.request(
 		CoreEnabledFeaturesDocument,
@@ -310,12 +313,12 @@ export const getCookiesForApplication = async (token: string) => {
 			serverGqlService.request(
 				UserPreferencesDocument,
 				undefined,
-				await getAuthorizationHeader(undefined, token),
+				getAuthorizationHeader(undefined, token),
 			),
 			serverGqlService.request(
 				UserDetailsDocument,
 				undefined,
-				await getAuthorizationHeader(undefined, token),
+				getAuthorizationHeader(undefined, token),
 			),
 		]);
 	const maxAge = coreDetails.tokenValidForDays * 24 * 60 * 60;
@@ -342,10 +345,11 @@ export const getCookiesForApplication = async (token: string) => {
 				options,
 			),
 		},
+		{ "set-cookie": serialize(AUTH_COOKIE_NAME, token, options) },
 	);
 };
 
-export const getLogoutCookies = async () => {
+export const getLogoutCookies = () => {
 	return combineHeaders(
 		{ "set-cookie": serialize(AUTH_COOKIE_NAME, "", { expires: new Date(0) }) },
 		{
@@ -366,23 +370,6 @@ export const getLogoutCookies = async () => {
 	);
 };
 
-export const getCoreDetails = async (request: Request) => {
-	const details = getCookieValue(request, CORE_DETAILS_COOKIE_NAME);
-	return JSON.parse(details) as CoreDetails;
-};
-
-export const getUserPreferences = async (request: Request) => {
-	await redirectIfNotAuthenticatedOrUpdated(request);
-	const preferences = getCookieValue(request, USER_PREFERENCES_COOKIE_NAME);
-	return JSON.parse(preferences) as UserPreferences;
-};
-
-export const getUserDetails = async (request: Request) => {
-	await redirectIfNotAuthenticatedOrUpdated(request);
-	const details = getCookieValue(request, USER_DETAILS_COOKIE_NAME);
-	return JSON.parse(details) as ApplicationUser;
-};
-
 export const extendResponseHeaders = (
 	responseHeaders: Headers,
 	headers: Headers,
diff --git a/apps/frontend/app/routes/_dashboard._index.tsx b/apps/frontend/app/routes/_dashboard._index.tsx
index 306913ce8a..c7bb1b074a 100644
--- a/apps/frontend/app/routes/_dashboard._index.tsx
+++ b/apps/frontend/app/routes/_dashboard._index.tsx
@@ -1,11 +1,13 @@
 import { $path } from "@ignisda/remix-routes";
 import {
+	ActionIcon,
 	Alert,
 	Anchor,
 	Box,
 	Center,
 	Container,
 	Flex,
+	Loader,
 	RingProgress,
 	SimpleGrid,
 	Stack,
@@ -19,6 +21,7 @@ import { Link, useLoaderData } from "@remix-run/react";
 import {
 	type CalendarEventPartFragment,
 	CollectionContentsDocument,
+	type CollectionContentsQuery,
 	DashboardElementLot,
 	GraphqlSortOrder,
 	LatestUserSummaryDocument,
@@ -32,6 +35,7 @@ import {
 	IconAlertCircle,
 	IconBarbell,
 	IconFriends,
+	IconPlayerPlay,
 	IconScaleOutline,
 	IconServer,
 } from "@tabler/icons-react";
@@ -50,10 +54,11 @@ import {
 	getLot,
 	getMetadataIcon,
 } from "~/lib/generals";
-import { useGetMantineColor } from "~/lib/hooks";
+import { useGetMantineColor, useUserPreferences } from "~/lib/hooks";
+import { useMetadataProgressUpdate } from "~/lib/state/media";
 import {
 	getAuthorizationHeader,
-	getUserCollectionsList,
+	getCachedUserCollectionsList,
 	getUserPreferences,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -72,7 +77,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 	const preferences = await getUserPreferences(request);
 	const takeUpcoming = getTake(preferences, DashboardElementLot.Upcoming);
 	const takeInProgress = getTake(preferences, DashboardElementLot.InProgress);
-	const userCollectionsList = await getUserCollectionsList(request);
+	const userCollectionsList = await getCachedUserCollectionsList(request);
 	const foundInProgressCollection = userCollectionsList.find(
 		(c) => c.name === "In Progress",
 	);
@@ -91,17 +96,17 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 					sort: { order: GraphqlSortOrder.Desc },
 				},
 			},
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
 		serverGqlService.request(
 			UserUpcomingCalendarEventsDocument,
 			{ input: { nextMedia: takeUpcoming } },
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
 		serverGqlService.request(
 			LatestUserSummaryDocument,
 			undefined,
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
 	]);
 	const cookies = request.headers.get("cookie");
@@ -127,6 +132,7 @@ export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
 	const theme = useMantineTheme();
 
 	return (
@@ -147,11 +153,14 @@ export default function Page() {
 				0 ? (
 					<NewUserGuideAlert />
 				) : null}
-				{loaderData.userPreferences.dashboard.map((de) =>
+				{userPreferences.general.dashboard.map((de) =>
 					match([de.section, de.hidden])
 						.with([DashboardElementLot.Upcoming, false], () =>
 							loaderData.userUpcomingCalendarEvents.length > 0 ? (
-								<Section key="upcoming">
+								<Section
+									key={DashboardElementLot.Upcoming}
+									lot={DashboardElementLot.Upcoming}
+								>
 									<Title>Upcoming</Title>
 									<ApplicationGrid>
 										{loaderData.userUpcomingCalendarEvents.map((um) => (
@@ -164,22 +173,15 @@ export default function Page() {
 						.with([DashboardElementLot.InProgress, false], () =>
 							loaderData.inProgressCollectionContents.results.items.length >
 							0 ? (
-								<Section key="inProgress">
+								<Section
+									key={DashboardElementLot.InProgress}
+									lot={DashboardElementLot.InProgress}
+								>
 									<Title>In Progress</Title>
 									<ApplicationGrid>
 										{loaderData.inProgressCollectionContents.results.items.map(
 											(lm) => (
-												<MediaItemWithoutUpdateModal
-													key={lm.details.identifier}
-													reviewScale={loaderData.userPreferences.reviewScale}
-													item={{
-														...lm.details,
-														publishYear: lm.details.publishYear?.toString(),
-													}}
-													lot={lm.metadataLot}
-													entityLot={lm.entityLot}
-													noRatingLink
-												/>
+												<InProgressItem key={lm.details.identifier} lm={lm} />
 											),
 										)}
 									</ApplicationGrid>
@@ -187,7 +189,10 @@ export default function Page() {
 							) : null,
 						)
 						.with([DashboardElementLot.Summary, false], () => (
-							<Section key="summary">
+							<Section
+								key={DashboardElementLot.Summary}
+								lot={DashboardElementLot.Summary}
+							>
 								<Title>Summary</Title>
 								<Text size="xs" mt={-15}>
 									Calculated{" "}
@@ -517,7 +522,7 @@ const UpComingMedia = ({ um }: { um: CalendarEventPartFragment }) => {
 		<MediaItemWithoutUpdateModal
 			reviewScale={loaderData.userPreferences.reviewScale}
 			item={{
-				identifier: um.metadataId.toString(),
+				identifier: um.metadataId,
 				title: um.metadataTitle,
 				image: um.metadataImage,
 				publishYear: `${match(um.metadataLot)
@@ -540,7 +545,6 @@ const UpComingMedia = ({ um }: { um: CalendarEventPartFragment }) => {
 			}}
 			lot={um.metadataLot}
 			noBottomRight
-			noRatingLink
 		/>
 	);
 };
@@ -583,10 +587,15 @@ const ActualDisplayStat = (props: {
 									{match(d.type)
 										.with("string", () => d.value)
 										.with("duration", () =>
-											humanizeDuration(Number(d.value) * 1000 * 60, {
-												round: true,
-												largest: 3,
-											}),
+											humanizeDuration(
+												dayjsLib
+													.duration(Number(d.value), "minutes")
+													.asMilliseconds(),
+												{
+													round: true,
+													largest: 3,
+												},
+											),
 										)
 										.with("number", () =>
 											new Intl.NumberFormat("en-US", {
@@ -640,8 +649,15 @@ const DisplayStatForMediaType = (props: {
 	) : null;
 };
 
-const Section = (props: { children: Array<ReactNode> }) => {
-	return <Stack gap="sm">{props.children}</Stack>;
+const Section = (props: {
+	children: Array<ReactNode>;
+	lot: DashboardElementLot;
+}) => {
+	return (
+		<Stack gap="sm" id={props.lot}>
+			{props.children}
+		</Stack>
+	);
 };
 
 const UnstyledLink = (props: { children: ReactNode; to: string }) => {
@@ -651,3 +667,47 @@ const UnstyledLink = (props: { children: ReactNode; to: string }) => {
 		</Link>
 	);
 };
+
+type InProgressItem =
+	CollectionContentsQuery["collectionContents"]["results"]["items"][number];
+
+const InProgressItem = ({ lm }: { lm: InProgressItem }) => {
+	const loaderData = useLoaderData<typeof loader>();
+	const [_, setMetadataToUpdate, isLoading] = useMetadataProgressUpdate();
+
+	return (
+		<MediaItemWithoutUpdateModal
+			key={lm.details.identifier}
+			reviewScale={loaderData.userPreferences.reviewScale}
+			item={{
+				...lm.details,
+				publishYear: lm.details.publishYear?.toString(),
+			}}
+			lot={lm.metadataLot}
+			entityLot={lm.entityLot}
+			topRight={
+				isLoading ? (
+					<Loader color="red" size="xs" m={2} />
+				) : (
+					<ActionIcon
+						variant="transparent"
+						color="blue"
+						size="compact-md"
+						onClick={async (e) => {
+							e.preventDefault();
+							await setMetadataToUpdate(
+								{
+									metadataId: lm.details.identifier,
+									pageFragment: DashboardElementLot.InProgress,
+								},
+								true,
+							);
+						}}
+					>
+						<IconPlayerPlay size={20} />
+					</ActionIcon>
+				)
+			}
+		/>
+	);
+};
diff --git a/apps/frontend/app/routes/_dashboard.calendar.tsx b/apps/frontend/app/routes/_dashboard.calendar.tsx
index 021eeed132..387174f668 100644
--- a/apps/frontend/app/routes/_dashboard.calendar.tsx
+++ b/apps/frontend/app/routes/_dashboard.calendar.tsx
@@ -29,7 +29,6 @@ import { dayjsLib } from "~/lib/generals";
 import { useSearchParam } from "~/lib/hooks";
 import {
 	getAuthorizationHeader,
-	getCoreDetails,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
@@ -42,19 +41,14 @@ export type SearchParams = z.infer<typeof searchParamsSchema>;
 export const loader = unstable_defineLoader(async ({ request }) => {
 	const query = zx.parseQuery(request, searchParamsSchema);
 	const date = dayjsLib(query.date);
-	const [coreDetails, { userCalendarEvents }] = await Promise.all([
-		getCoreDetails(request),
+	const [{ userCalendarEvents }] = await Promise.all([
 		serverGqlService.request(
 			UserCalendarEventsDocument,
 			{ input: { month: date.month() + 1, year: date.year() } },
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
 	]);
-	return {
-		query,
-		coreDetails,
-		calendarEvents: userCalendarEvents,
-	};
+	return { query, calendarEvents: userCalendarEvents };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
diff --git a/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx b/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
index de15b638ba..f5324a0432 100644
--- a/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
@@ -39,24 +39,23 @@ import {
 	IconSortDescending,
 	IconUser,
 } from "@tabler/icons-react";
-import { useState } from "react";
 import invariant from "tiny-invariant";
 import { z } from "zod";
 import { zx } from "zodix";
 import { ApplicationGrid, DebouncedSearchInput } from "~/components/common";
 import {
 	MediaItemWithoutUpdateModal,
-	type PostReview,
-	PostReviewModal,
 	ReviewItemDisplay,
 } from "~/components/media";
 import { dayjsLib } from "~/lib/generals";
-import { useSearchParam } from "~/lib/hooks";
+import {
+	useCoreDetails,
+	useSearchParam,
+	useUserPreferences,
+} from "~/lib/hooks";
+import { useReviewEntity } from "~/lib/state/media";
 import {
 	getAuthorizationHeader,
-	getCoreDetails,
-	getUserDetails,
-	getUserPreferences,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
@@ -86,17 +85,9 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 	const { collectionContents: info } = await serverGqlService.request(
 		CollectionContentsDocument,
 		{ input: { collectionId: id, take: 0 } },
-		await getAuthorizationHeader(request),
+		getAuthorizationHeader(request),
 	);
-	const [
-		coreDetails,
-		userPreferences,
-		userDetails,
-		{ collectionContents: contents },
-	] = await Promise.all([
-		getCoreDetails(request),
-		getUserPreferences(request),
-		getUserDetails(request),
+	const [{ collectionContents: contents }] = await Promise.all([
 		serverGqlService.request(
 			CollectionContentsDocument,
 			{
@@ -113,21 +104,10 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 					},
 				},
 			},
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
 	]);
-	return {
-		id,
-		query,
-		info,
-		contents: contents.results,
-		coreDetails: { pageLimit: coreDetails.pageLimit },
-		userPreferences: {
-			reviewScale: userPreferences.general.reviewScale,
-			disableReviews: userPreferences.general.disableReviews,
-		},
-		userDetails,
-	};
+	return { id, query, info, contents: contents.results };
 });
 
 export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -136,228 +116,215 @@ export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
+	const coreDetails = useCoreDetails();
 	const navigate = useNavigate();
 	const [_, { setP }] = useSearchParam();
+	const [_r, setEntityToReview] = useReviewEntity();
 	const [
 		filtersModalOpened,
 		{ open: openFiltersModal, close: closeFiltersModal },
 	] = useDisclosure(false);
-	const [postReviewModalData, setPostReviewModalData] = useState<
-		PostReview | undefined
-	>(undefined);
 
 	return (
-		<>
-			<PostReviewModal
-				onClose={() => setPostReviewModalData(undefined)}
-				opened={postReviewModalData !== undefined}
-				data={postReviewModalData}
-				entityType="collection"
-				objectId={loaderData.id}
-				reviewScale={loaderData.userPreferences.reviewScale}
-				title={loaderData.info.details.name}
-			/>
-			<Container>
-				<Stack>
-					<Box>
-						<Title>{loaderData.info.details.name}</Title>{" "}
-						<Text size="sm">
-							{loaderData.contents.details.total} items, created by{" "}
-							{loaderData.info.user.name}{" "}
-							{dayjsLib(loaderData.info.details.createdOn).fromNow()}
-						</Text>
-					</Box>
-					<Text>{loaderData.info.details.description}</Text>
-					<Tabs defaultValue={loaderData.query.defaultTab}>
-						<Tabs.List mb="xs">
+		<Container>
+			<Stack>
+				<Box>
+					<Title>{loaderData.info.details.name}</Title>{" "}
+					<Text size="sm">
+						{loaderData.contents.details.total} items, created by{" "}
+						{loaderData.info.user.name}{" "}
+						{dayjsLib(loaderData.info.details.createdOn).fromNow()}
+					</Text>
+				</Box>
+				<Text>{loaderData.info.details.description}</Text>
+				<Tabs defaultValue={loaderData.query.defaultTab}>
+					<Tabs.List mb="xs">
+						<Tabs.Tab
+							value="contents"
+							leftSection={<IconBucketDroplet size={16} />}
+						>
+							Contents
+						</Tabs.Tab>
+						<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
+							Actions
+						</Tabs.Tab>
+						{!userPreferences.general.disableReviews ? (
 							<Tabs.Tab
-								value="contents"
-								leftSection={<IconBucketDroplet size={16} />}
+								value="reviews"
+								leftSection={<IconMessageCircle2 size={16} />}
 							>
-								Contents
+								Reviews
 							</Tabs.Tab>
-							<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
-								Actions
-							</Tabs.Tab>
-							{!loaderData.userPreferences.disableReviews ? (
-								<Tabs.Tab
-									value="reviews"
-									leftSection={<IconMessageCircle2 size={16} />}
+						) : null}
+					</Tabs.List>
+					<Tabs.Panel value="contents">
+						<Stack>
+							<Group wrap="nowrap">
+								<DebouncedSearchInput
+									placeholder="Search in the collection"
+									initialValue={loaderData.query.query}
+								/>
+								<ActionIcon
+									onClick={openFiltersModal}
+									color={
+										loaderData.query.entityLot !== undefined ||
+										loaderData.query.metadataLot !== undefined ||
+										loaderData.query.sortBy !== defaultFiltersValue.sort ||
+										loaderData.query.orderBy !== defaultFiltersValue.order
+											? "blue"
+											: "gray"
+									}
 								>
-									Reviews
-								</Tabs.Tab>
-							) : null}
-						</Tabs.List>
-						<Tabs.Panel value="contents">
-							<Stack>
-								<Group wrap="nowrap">
-									<DebouncedSearchInput
-										placeholder="Search in the collection"
-										initialValue={loaderData.query.query}
-									/>
-									<ActionIcon
-										onClick={openFiltersModal}
-										color={
-											loaderData.query.entityLot !== undefined ||
-											loaderData.query.metadataLot !== undefined ||
-											loaderData.query.sortBy !== defaultFiltersValue.sort ||
-											loaderData.query.orderBy !== defaultFiltersValue.order
-												? "blue"
-												: "gray"
-										}
-									>
-										<IconFilter size={24} />
-									</ActionIcon>
-									<Modal
-										opened={filtersModalOpened}
-										onClose={closeFiltersModal}
-										centered
-										withCloseButton={false}
-									>
-										<Stack>
-											<Group>
-												<Title order={3}>Filters</Title>
-												<ActionIcon
-													onClick={() => {
-														navigate(".");
-														closeFiltersModal();
-													}}
-												>
-													<IconFilterOff size={24} />
-												</ActionIcon>
-											</Group>
-											<Flex gap="xs" align="center">
-												<Select
-													w="100%"
-													data={[
-														{
-															group: "Sort by",
-															items: Object.values(
-																CollectionContentsSortBy,
-															).map((o) => ({
+									<IconFilter size={24} />
+								</ActionIcon>
+								<Modal
+									opened={filtersModalOpened}
+									onClose={closeFiltersModal}
+									centered
+									withCloseButton={false}
+								>
+									<Stack>
+										<Group>
+											<Title order={3}>Filters</Title>
+											<ActionIcon
+												onClick={() => {
+													navigate(".");
+													closeFiltersModal();
+												}}
+											>
+												<IconFilterOff size={24} />
+											</ActionIcon>
+										</Group>
+										<Flex gap="xs" align="center">
+											<Select
+												w="100%"
+												data={[
+													{
+														group: "Sort by",
+														items: Object.values(CollectionContentsSortBy).map(
+															(o) => ({
 																value: o.toString(),
 																label: startCase(o.toLowerCase()),
-															})),
-														},
-													]}
-													defaultValue={loaderData.query.sortBy}
-													onChange={(v) => setP("sortBy", v)}
-												/>
-												<ActionIcon
-													onClick={() => {
-														if (
-															loaderData.query.orderBy === GraphqlSortOrder.Asc
-														)
-															setP("orderBy", GraphqlSortOrder.Desc);
-														else setP("orderBy", GraphqlSortOrder.Asc);
-													}}
-												>
-													{loaderData.query.orderBy === GraphqlSortOrder.Asc ? (
-														<IconSortAscending />
-													) : (
-														<IconSortDescending />
-													)}
-												</ActionIcon>
-											</Flex>
-											<Select
-												placeholder="Select an entity type"
-												defaultValue={loaderData.query.entityLot}
-												data={Object.values(EntityLot)
-													.filter((o) => o !== EntityLot.Collection)
-													.map((o) => ({
-														value: o.toString(),
-														label: startCase(o.toLowerCase()),
-													}))}
-												onChange={(v) => setP("entityLot", v)}
-												clearable
+															}),
+														),
+													},
+												]}
+												defaultValue={loaderData.query.sortBy}
+												onChange={(v) => setP("sortBy", v)}
 											/>
-											{loaderData.query.entityLot === EntityLot.Media ||
-											loaderData.query.entityLot === EntityLot.MediaGroup ? (
-												<Select
-													placeholder="Select a media type"
-													defaultValue={loaderData.query.metadataLot}
-													data={Object.values(MediaLot).map((o) => ({
-														value: o.toString(),
-														label: startCase(o.toLowerCase()),
-													}))}
-													onChange={(v) => setP("metadataLot", v)}
-													clearable
-												/>
-											) : null}
-										</Stack>
-									</Modal>
-								</Group>
-								{loaderData.contents.items.length > 0 ? (
-									<ApplicationGrid>
-										{loaderData.contents.items.map((lm) => (
-											<MediaItemWithoutUpdateModal
-												noRatingLink
-												key={lm.details.identifier}
-												item={{
-													...lm.details,
-													publishYear: lm.details.publishYear?.toString(),
+											<ActionIcon
+												onClick={() => {
+													if (loaderData.query.orderBy === GraphqlSortOrder.Asc)
+														setP("orderBy", GraphqlSortOrder.Desc);
+													else setP("orderBy", GraphqlSortOrder.Asc);
 												}}
-												lot={lm.metadataLot}
-												entityLot={lm.entityLot}
-												reviewScale={loaderData.userPreferences.reviewScale}
-											/>
-										))}
-									</ApplicationGrid>
-								) : (
-									<Text>You have not added anything this collection</Text>
-								)}
-								{loaderData.contents.details ? (
-									<Center>
-										<Pagination
-											size="sm"
-											value={loaderData.query.page}
-											onChange={(v) => setP("page", v.toString())}
-											total={Math.ceil(
-												loaderData.contents.details.total /
-													loaderData.coreDetails.pageLimit,
-											)}
+											>
+												{loaderData.query.orderBy === GraphqlSortOrder.Asc ? (
+													<IconSortAscending />
+												) : (
+													<IconSortDescending />
+												)}
+											</ActionIcon>
+										</Flex>
+										<Select
+											placeholder="Select an entity type"
+											defaultValue={loaderData.query.entityLot}
+											data={Object.values(EntityLot)
+												.filter((o) => o !== EntityLot.Collection)
+												.map((o) => ({
+													value: o.toString(),
+													label: startCase(o.toLowerCase()),
+												}))}
+											onChange={(v) => setP("entityLot", v)}
+											clearable
 										/>
-									</Center>
-								) : null}
-							</Stack>
-						</Tabs.Panel>
-						<Tabs.Panel value="actions">
-							<SimpleGrid cols={{ base: 2, md: 3, lg: 4 }} spacing="lg">
-								<Button
-									variant="outline"
-									w="100%"
-									onClick={() => {
-										setPostReviewModalData({});
-									}}
-								>
-									Post a review
-								</Button>
-							</SimpleGrid>
-						</Tabs.Panel>
-						{!loaderData.userPreferences.disableReviews ? (
-							<Tabs.Panel value="reviews">
-								{loaderData.info.reviews.length > 0 ? (
-									<Stack>
-										{loaderData.info.reviews.map((r) => (
-											<ReviewItemDisplay
-												title={loaderData.info.details.name}
-												review={r}
-												key={r.id}
-												collectionId={loaderData.id}
-												reviewScale={loaderData.userPreferences.reviewScale}
-												user={loaderData.userDetails}
-												entityType="collection"
+										{loaderData.query.entityLot === EntityLot.Metadata ||
+										loaderData.query.entityLot === EntityLot.MetadataGroup ? (
+											<Select
+												placeholder="Select a media type"
+												defaultValue={loaderData.query.metadataLot}
+												data={Object.values(MediaLot).map((o) => ({
+													value: o.toString(),
+													label: startCase(o.toLowerCase()),
+												}))}
+												onChange={(v) => setP("metadataLot", v)}
+												clearable
 											/>
-										))}
+										) : null}
 									</Stack>
-								) : (
-									<Text>No reviews</Text>
-								)}
-							</Tabs.Panel>
-						) : null}
-					</Tabs>
-				</Stack>
-			</Container>
-		</>
+								</Modal>
+							</Group>
+							{loaderData.contents.items.length > 0 ? (
+								<ApplicationGrid>
+									{loaderData.contents.items.map((lm) => (
+										<MediaItemWithoutUpdateModal
+											key={lm.details.identifier}
+											item={{
+												...lm.details,
+												publishYear: lm.details.publishYear?.toString(),
+											}}
+											lot={lm.metadataLot}
+											entityLot={lm.entityLot}
+											reviewScale={userPreferences.general.reviewScale}
+										/>
+									))}
+								</ApplicationGrid>
+							) : (
+								<Text>You have not added anything this collection</Text>
+							)}
+							{loaderData.contents.details ? (
+								<Center>
+									<Pagination
+										size="sm"
+										value={loaderData.query.page}
+										onChange={(v) => setP("page", v.toString())}
+										total={Math.ceil(
+											loaderData.contents.details.total / coreDetails.pageLimit,
+										)}
+									/>
+								</Center>
+							) : null}
+						</Stack>
+					</Tabs.Panel>
+					<Tabs.Panel value="actions">
+						<SimpleGrid cols={{ base: 2, md: 3, lg: 4 }} spacing="lg">
+							<Button
+								variant="outline"
+								w="100%"
+								onClick={() => {
+									setEntityToReview({
+										entityId: loaderData.id,
+										entityLot: EntityLot.Collection,
+										entityTitle: loaderData.info.details.name,
+									});
+								}}
+							>
+								Post a review
+							</Button>
+						</SimpleGrid>
+					</Tabs.Panel>
+					{!userPreferences.general.disableReviews ? (
+						<Tabs.Panel value="reviews">
+							{loaderData.info.reviews.length > 0 ? (
+								<Stack>
+									{loaderData.info.reviews.map((r) => (
+										<ReviewItemDisplay
+											title={loaderData.info.details.name}
+											review={r}
+											key={r.id}
+											entityId={loaderData.id}
+											entityLot={EntityLot.Collection}
+										/>
+									))}
+								</Stack>
+							) : (
+								<Text>No reviews</Text>
+							)}
+						</Tabs.Panel>
+					) : null}
+				</Tabs>
+			</Stack>
+		</Container>
 	);
 }
diff --git a/apps/frontend/app/routes/_dashboard.collections.list.tsx b/apps/frontend/app/routes/_dashboard.collections.list.tsx
index 6a77966d53..4ef6c1252b 100644
--- a/apps/frontend/app/routes/_dashboard.collections.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.collections.list.tsx
@@ -16,13 +16,12 @@ import {
 	Title,
 } from "@mantine/core";
 import { useDisclosure } from "@mantine/hooks";
-import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
+import { unstable_defineAction } from "@remix-run/node";
 import {
 	Form,
 	Link,
 	type MetaArgs_SingleFetch,
 	useFetcher,
-	useLoaderData,
 	useNavigation,
 } from "@remix-run/react";
 import {
@@ -42,29 +41,22 @@ import { useEffect, useRef, useState } from "react";
 import { namedAction } from "remix-utils/named-action";
 import { z } from "zod";
 import { confirmWrapper } from "~/components/confirmation";
+import { useUserCollections, useUserDetails } from "~/lib/hooks";
 import {
 	createToastHeaders,
 	getAuthorizationHeader,
-	getUserCollectionsList,
-	getUserDetails,
 	processSubmission,
+	removeCachedUserCollectionsList,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
-export const loader = unstable_defineLoader(async ({ request }) => {
-	const [userDetails, userCollectionsList] = await Promise.all([
-		getUserDetails(request),
-		getUserCollectionsList(request),
-	]);
-	return { collections: userCollectionsList, currentUserId: userDetails.id };
-});
-
-export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
+export const meta = (_args: MetaArgs_SingleFetch) => {
 	return [{ title: "Your collections | Ryot" }];
 };
 
 export const action = unstable_defineAction(async ({ request }) => {
 	const formData = await request.clone().formData();
+	await removeCachedUserCollectionsList(request);
 	return namedAction(request, {
 		createOrUpdate: async () => {
 			const submission = processSubmission(formData, createOrUpdateSchema);
@@ -72,7 +64,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 				await serverGqlService.request(
 					CreateOrUpdateCollectionDocument,
 					{ input: submission },
-					await getAuthorizationHeader(request),
+					getAuthorizationHeader(request),
 				);
 				return Response.json(
 					{},
@@ -110,7 +102,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 				await serverGqlService.request(
 					DeleteCollectionDocument,
 					submission,
-					await getAuthorizationHeader(request),
+					getAuthorizationHeader(request),
 				);
 			} catch {
 				wasSuccessful = false;
@@ -145,10 +137,8 @@ type UpdateCollectionInput = {
 
 export default function Page() {
 	const transition = useNavigation();
-	const loaderData = useLoaderData<typeof loader>();
-	const userCreatedCollections = loaderData.collections.filter(
-		(c) => !c.isDefault,
-	);
+	const collections = useUserCollections();
+	const userCreatedCollections = collections.filter((c) => !c.isDefault);
 
 	const [toUpdateCollection, setToUpdateCollection] =
 		useState<UpdateCollectionInput>();
@@ -164,80 +154,78 @@ export default function Page() {
 	}, [transition.state]);
 
 	return (
-		<>
-			<Container>
-				<Stack>
-					<Flex align="center" gap="md">
-						<Title>Your collections</Title>
-						<ActionIcon
-							color="green"
-							variant="outline"
-							onClick={() => {
-								setToUpdateCollection(undefined);
-								createOrUpdateModalOpen();
-							}}
+		<Container>
+			<Stack>
+				<Flex align="center" gap="md">
+					<Title>Your collections</Title>
+					<ActionIcon
+						color="green"
+						variant="outline"
+						onClick={() => {
+							setToUpdateCollection(undefined);
+							createOrUpdateModalOpen();
+						}}
+					>
+						<IconPlus size={20} />
+					</ActionIcon>
+					<Modal
+						opened={createOrUpdateModalOpened}
+						onClose={createOrUpdateModalClose}
+						withCloseButton={false}
+						centered
+						size="lg"
+					>
+						<CreateOrUpdateModal toUpdateCollection={toUpdateCollection} />
+					</Modal>
+				</Flex>
+				<Tabs defaultValue="userCreated" variant="outline">
+					<Tabs.List mb="xs">
+						<Tabs.Tab
+							value="userCreated"
+							leftSection={<IconUserCog size={16} />}
 						>
-							<IconPlus size={20} />
-						</ActionIcon>
-					</Flex>
-					<Tabs defaultValue="userCreated" variant="outline">
-						<Tabs.List mb="xs">
-							<Tabs.Tab
-								value="userCreated"
-								leftSection={<IconUserCog size={16} />}
-							>
-								User created
-							</Tabs.Tab>
-							<Tabs.Tab
-								value="systemCreated"
-								leftSection={<IconAssembly size={16} />}
-							>
-								System created
-							</Tabs.Tab>
-						</Tabs.List>
-						<Tabs.Panel value="userCreated">
-							{userCreatedCollections.length > 0 ? (
-								<SimpleGrid cols={{ base: 1, md: 2 }}>
-									{userCreatedCollections.map((c) => (
-										<DisplayCollection
-											key={c.id}
-											collection={c}
-											setToUpdateCollection={setToUpdateCollection}
-											openModal={createOrUpdateModalOpen}
-										/>
-									))}
-								</SimpleGrid>
-							) : (
-								<Text>You have not created any collections yet</Text>
-							)}
-						</Tabs.Panel>
-						<Tabs.Panel value="systemCreated">
+							User created
+						</Tabs.Tab>
+						<Tabs.Tab
+							value="systemCreated"
+							leftSection={<IconAssembly size={16} />}
+						>
+							System created
+						</Tabs.Tab>
+					</Tabs.List>
+					<Tabs.Panel value="userCreated">
+						{userCreatedCollections.length > 0 ? (
 							<SimpleGrid cols={{ base: 1, md: 2 }}>
-								{loaderData.collections
-									.filter((c) => c.isDefault)
-									.map((c) => (
-										<DisplayCollection
-											key={c.id}
-											collection={c}
-											setToUpdateCollection={setToUpdateCollection}
-											openModal={createOrUpdateModalOpen}
-										/>
-									))}
+								{userCreatedCollections.map((c) => (
+									<DisplayCollection
+										key={c.id}
+										collection={c}
+										setToUpdateCollection={setToUpdateCollection}
+										openModal={createOrUpdateModalOpen}
+									/>
+								))}
 							</SimpleGrid>
-						</Tabs.Panel>
-					</Tabs>
-				</Stack>
-			</Container>
-			<Modal
-				opened={createOrUpdateModalOpened}
-				onClose={createOrUpdateModalClose}
-				withCloseButton={false}
-				centered
-				size="lg"
-			>
-				<CreateOrUpdateModal toUpdateCollection={toUpdateCollection} />
-			</Modal>
-		</>
+						) : (
+							<Text>You have not created any collections yet</Text>
+						)}
+					</Tabs.Panel>
+					<Tabs.Panel value="systemCreated">
+						<SimpleGrid cols={{ base: 1, md: 2 }}>
+							{collections
+								.filter((c) => c.isDefault)
+								.map((c) => (
+									<DisplayCollection
+										key={c.id}
+										collection={c}
+										setToUpdateCollection={setToUpdateCollection}
+										openModal={createOrUpdateModalOpen}
+									/>
+								))}
+						</SimpleGrid>
+					</Tabs.Panel>
+				</Tabs>
+			</Stack>
+		</Container>
 	);
 }
 
@@ -248,12 +236,12 @@ const DisplayCollection = (props: {
 	setToUpdateCollection: (c: UpdateCollectionInput) => void;
 	openModal: () => void;
 }) => {
-	const loaderData = useLoaderData<typeof loader>();
+	const userDetails = useUserDetails();
 	const fetcher = useFetcher<typeof action>();
 	const deleteFormRef = useRef<HTMLFormElement>(null);
 	const additionalDisplay = [];
 
-	if (props.collection.creator.id !== loaderData.currentUserId)
+	if (props.collection.creator.id !== userDetails.id)
 		additionalDisplay.push(`By ${props.collection.creator.name}`);
 	if (props.collection.count > 0)
 		additionalDisplay.push(`${props.collection.count} items`);
@@ -279,14 +267,14 @@ const DisplayCollection = (props: {
 						<Text c="dimmed" size="xs">
 							({additionalDisplay.join(", ")})
 						</Text>
-					) : undefined}
+					) : null}
 				</Flex>
 				{props.collection.description ? (
 					<Text lineClamp={1}>{props.collection.description}</Text>
 				) : null}
 			</Box>
 			<Flex gap="sm" style={{ flex: 0 }}>
-				{loaderData.currentUserId === props.collection.creator.id ? (
+				{userDetails.id === props.collection.creator.id ? (
 					<ActionIcon
 						color="blue"
 						variant="outline"
@@ -302,7 +290,7 @@ const DisplayCollection = (props: {
 					>
 						<IconEdit size={18} />
 					</ActionIcon>
-				) : undefined}
+				) : null}
 				{!props.collection.isDefault ? (
 					<fetcher.Form
 						action="?intent=delete"
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.$action.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.$action.tsx
index 36a9e4fe91..2c35066650 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.$action.tsx
@@ -114,7 +114,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 				const { createCustomExercise } = await serverGqlService.request(
 					CreateCustomExerciseDocument,
 					{ input },
-					await getAuthorizationHeader(request),
+					getAuthorizationHeader(request),
 				);
 				return redirect(
 					$path("/fitness/exercises/item/:id", { id: createCustomExercise }),
@@ -125,7 +125,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 				await serverGqlService.request(
 					EditCustomExerciseDocument,
 					{ input: { ...input, oldName: submission.oldName } },
-					await getAuthorizationHeader(request),
+					getAuthorizationHeader(request),
 				);
 				return redirect($path("/fitness/exercises/list"));
 			},
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
index ad9bb205f3..4326297662 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
@@ -20,7 +20,6 @@ import {
 	Title,
 	rem,
 } from "@mantine/core";
-import { useDisclosure } from "@mantine/hooks";
 import { unstable_defineLoader } from "@remix-run/node";
 import type { MetaArgs_SingleFetch } from "@remix-run/react";
 import { Link, useLoaderData, useNavigate } from "@remix-run/react";
@@ -52,18 +51,16 @@ import { match } from "ts-pattern";
 import { withFragment } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
-import { AddEntityToCollectionModal } from "~/components/common";
 import { DisplayExerciseStats } from "~/components/fitness";
 import { DisplayCollection, MediaScrollArea } from "~/components/media";
 import { dayjsLib, getSetColor } from "~/lib/generals";
+import { useUserDetails, useUserPreferences } from "~/lib/hooks";
+import { useAddEntityToCollection } from "~/lib/state/media";
+import { addExerciseToWorkout, currentWorkoutAtom } from "~/lib/state/workout";
 import {
 	getAuthorizationHeader,
-	getUserCollectionsList,
-	getUserDetails,
-	getUserPreferences,
 	serverGqlService,
 } from "~/lib/utilities.server";
-import { addExerciseToWorkout, currentWorkoutAtom } from "~/lib/workout";
 
 const searchParamsSchema = z.object({
 	defaultTab: z.string().optional(),
@@ -76,36 +73,15 @@ export const loader = unstable_defineLoader(async ({ params, request }) => {
 	const exerciseId = params.id;
 	invariant(typeof exerciseId === "string", "id must be a string");
 	const query = zx.parseQuery(request, searchParamsSchema);
-	const [
-		userDetails,
-		userPreferences,
-		{ exerciseDetails },
-		{ userExerciseDetails },
-		collections,
-	] = await Promise.all([
-		getUserDetails(request),
-		getUserPreferences(request),
+	const [{ exerciseDetails }, { userExerciseDetails }] = await Promise.all([
 		serverGqlService.request(ExerciseDetailsDocument, { exerciseId }),
 		serverGqlService.request(
 			UserExerciseDetailsDocument,
 			{ input: { exerciseId } },
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
-		getUserCollectionsList(request),
 	]);
-	const canCurrentUserUpdate =
-		exerciseDetails.source === ExerciseSource.Custom &&
-		userDetails.id === exerciseDetails.createdByUserId;
-	return {
-		userId: userDetails.id,
-		query,
-		exerciseDetails,
-		userExerciseDetails,
-		unitSystem: userPreferences.fitness.exercises.unitSystem,
-		exerciseId,
-		collections,
-		canCurrentUserUpdate,
-	};
+	return { query, exerciseDetails, userExerciseDetails, exerciseId };
 });
 
 export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -114,377 +90,374 @@ export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
-	const [
-		collectionModalOpened,
-		{ open: collectionModalOpen, close: collectionModalClose },
-	] = useDisclosure(false);
+	const userPreferences = useUserPreferences();
+	const unitSystem = userPreferences.fitness.exercises.unitSystem;
+	const userDetails = useUserDetails();
+	const canCurrentUserUpdate =
+		loaderData.exerciseDetails.source === ExerciseSource.Custom &&
+		userDetails.id === loaderData.exerciseDetails.createdByUserId;
 	const [currentWorkout, setCurrentWorkout] = useAtom(currentWorkoutAtom);
 	const navigate = useNavigate();
+	const [_a, setAddEntityToCollectionData] = useAddEntityToCollection();
 
 	return (
-		<>
-			<AddEntityToCollectionModal
-				userId={loaderData.userId}
-				onClose={collectionModalClose}
-				opened={collectionModalOpened}
-				entityId={loaderData.exerciseDetails.id}
-				entityLot={EntityLot.Exercise}
-				collections={loaderData.collections}
-			/>
-			<Container size="xs" px="lg">
-				<Stack>
-					<Title id="exercise-title">{loaderData.exerciseDetails.id}</Title>
-					{loaderData.userExerciseDetails.collections.length > 0 ? (
-						<Group id="entity-collections">
-							{loaderData.userExerciseDetails.collections.map((col) => (
-								<DisplayCollection
-									key={col.id}
-									col={col}
-									userId={col.userId}
-									entityId={loaderData.exerciseDetails.id}
-									entityLot={EntityLot.Exercise}
-								/>
-							))}
-						</Group>
-					) : null}
-					<Tabs
-						variant="outline"
-						defaultValue={loaderData.query.defaultTab || "overview"}
-					>
-						<Tabs.List mb="xs">
+		<Container size="xs" px="lg">
+			<Stack>
+				<Title id="exercise-title">{loaderData.exerciseDetails.id}</Title>
+				{loaderData.userExerciseDetails.collections.length > 0 ? (
+					<Group id="entity-collections">
+						{loaderData.userExerciseDetails.collections.map((col) => (
+							<DisplayCollection
+								col={col}
+								key={col.id}
+								creatorUserId={col.userId}
+								entityLot={EntityLot.Exercise}
+								entityId={loaderData.exerciseDetails.id}
+							/>
+						))}
+					</Group>
+				) : null}
+				<Tabs
+					variant="outline"
+					defaultValue={loaderData.query.defaultTab || "overview"}
+				>
+					<Tabs.List mb="xs">
+						<Tabs.Tab
+							value="overview"
+							leftSection={<IconInfoCircle size={16} />}
+						>
+							Overview
+						</Tabs.Tab>
+						{loaderData.userExerciseDetails.history ? (
 							<Tabs.Tab
-								value="overview"
-								leftSection={<IconInfoCircle size={16} />}
+								value="history"
+								leftSection={<IconHistoryToggle size={16} />}
 							>
-								Overview
+								History
 							</Tabs.Tab>
-							{loaderData.userExerciseDetails.history ? (
-								<Tabs.Tab
-									value="history"
-									leftSection={<IconHistoryToggle size={16} />}
-								>
-									History
-								</Tabs.Tab>
+						) : null}
+						{loaderData.userExerciseDetails.details ? (
+							<Tabs.Tab value="records" leftSection={<IconTrophy size={16} />}>
+								Records
+							</Tabs.Tab>
+						) : null}
+						<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
+							Actions
+						</Tabs.Tab>
+					</Tabs.List>
+					<Tabs.Panel value="overview">
+						<Stack>
+							<ScrollArea>
+								<Flex gap={6}>
+									{loaderData.exerciseDetails.attributes.images.map((i) => (
+										<Image key={i} radius="md" src={i} h="200px" w="248px" />
+									))}
+								</Flex>
+							</ScrollArea>
+							<SimpleGrid py="xs" cols={4}>
+								{(["level", "force", "mechanic", "equipment"] as const).map(
+									(f) => (
+										<Fragment key={f}>
+											{loaderData.exerciseDetails[f] ? (
+												<DisplayData
+													name={f}
+													data={loaderData.exerciseDetails[f]}
+												/>
+											) : null}
+										</Fragment>
+									),
+								)}
+								{loaderData.exerciseDetails.lot ? (
+									<DisplayData
+										name="Type"
+										data={changeCase(loaderData.exerciseDetails.lot)}
+									/>
+								) : null}
+								{loaderData.userExerciseDetails.details
+									?.exerciseNumTimesInteracted ? (
+									<DisplayData
+										name="Times done"
+										data={`${loaderData.userExerciseDetails.details.exerciseNumTimesInteracted} times`}
+										noCasing
+									/>
+								) : null}
+								{loaderData.userExerciseDetails.details?.createdOn ? (
+									<DisplayData
+										name="First done on"
+										data={dayjsLib(
+											loaderData.userExerciseDetails.details.createdOn,
+										).format("ll")}
+										noCasing
+									/>
+								) : null}
+								{loaderData.userExerciseDetails.details?.lastUpdatedOn ? (
+									<DisplayData
+										name="Last done on"
+										data={dayjsLib(
+											loaderData.userExerciseDetails.details.lastUpdatedOn,
+										).format("ll")}
+										noCasing
+									/>
+								) : null}
+							</SimpleGrid>
+							{loaderData.exerciseDetails.muscles.length > 0 ? (
+								<>
+									<Divider />
+									<Group wrap="nowrap">
+										<Text c="dimmed" fz="sm">
+											Muscles
+										</Text>
+										<Text fz="sm">
+											{loaderData.exerciseDetails.muscles
+												.map((s) => startCase(s.toLowerCase()))
+												.join(", ")}
+										</Text>
+									</Group>
+								</>
 							) : null}
-							{loaderData.userExerciseDetails.details ? (
-								<Tabs.Tab
-									value="records"
-									leftSection={<IconTrophy size={16} />}
-								>
-									Records
-								</Tabs.Tab>
+							{loaderData.exerciseDetails.attributes.instructions.length > 0 ? (
+								<>
+									<Divider />
+									<Text size="xl" fw="bold">
+										Instructions
+									</Text>
+									<List type="ordered" spacing="xs">
+										{loaderData.exerciseDetails.attributes.instructions.map(
+											(d) => (
+												<List.Item key={d}>{d}</List.Item>
+											),
+										)}
+									</List>
+								</>
 							) : null}
-							<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
-								Actions
-							</Tabs.Tab>
-						</Tabs.List>
-						<Tabs.Panel value="overview">
+						</Stack>
+					</Tabs.Panel>
+					{loaderData.userExerciseDetails.history ? (
+						<Tabs.Panel value="history">
 							<Stack>
-								<ScrollArea>
-									<Flex gap={6}>
-										{loaderData.exerciseDetails.attributes.images.map((i) => (
-											<Image key={i} radius="md" src={i} h="200px" w="248px" />
+								{loaderData.userExerciseDetails.history.map((h) => (
+									<Paper key={h.workoutId} withBorder p="xs">
+										<Anchor
+											component={Link}
+											to={withFragment(
+												$path("/fitness/workouts/:id", { id: h.workoutId }),
+												`${loaderData.exerciseDetails.id}__${h.index}`,
+											)}
+											fw="bold"
+										>
+											{h.workoutName}
+										</Anchor>
+										<Text c="dimmed" fz="sm" mb="xs">
+											{dayjsLib(h.workoutTime).format("LLLL")}
+										</Text>
+										{h.sets.map((s, idx) => (
+											<Flex key={`${idx}-${s.lot}`} align="center">
+												<Text
+													fz="sm"
+													c={getSetColor(s.lot)}
+													mr="md"
+													fw="bold"
+													ff="monospace"
+												>
+													{match(s.lot)
+														.with(SetLot.Normal, () => idx + 1)
+														.otherwise(() => s.lot.at(0))}
+												</Text>
+												<DisplayExerciseStats
+													lot={loaderData.exerciseDetails.lot}
+													statistic={s.statistic}
+													unit={unitSystem}
+												/>
+											</Flex>
 										))}
-									</Flex>
-								</ScrollArea>
-								<SimpleGrid py="xs" cols={4}>
-									{(["level", "force", "mechanic", "equipment"] as const).map(
-										(f) => (
-											<Fragment key={f}>
-												{loaderData.exerciseDetails[f] ? (
-													<DisplayData
-														name={f}
-														data={loaderData.exerciseDetails[f]}
-													/>
-												) : null}
-											</Fragment>
-										),
-									)}
-									{loaderData.exerciseDetails.lot ? (
-										<DisplayData
-											name="Type"
-											data={changeCase(loaderData.exerciseDetails.lot)}
+									</Paper>
+								))}
+							</Stack>
+						</Tabs.Panel>
+					) : null}
+					{loaderData.userExerciseDetails.details?.exerciseExtraInformation ? (
+						<Tabs.Panel value="records">
+							<Stack gap="xl">
+								<Stack gap="xs">
+									<Text size="lg" td="underline">
+										Lifetime Stats
+									</Text>
+									<Box>
+										<DisplayLifetimeStatistic
+											stat="weight"
+											val={displayWeightWithUnit(
+												unitSystem,
+												loaderData.userExerciseDetails.details
+													.exerciseExtraInformation.lifetimeStats.weight,
+											)}
 										/>
-									) : null}
-									{loaderData.userExerciseDetails.details
-										?.exerciseNumTimesInteracted ? (
-										<DisplayData
-											name="Times done"
-											data={`${loaderData.userExerciseDetails.details.exerciseNumTimesInteracted} times`}
-											noCasing
+										<DisplayLifetimeStatistic
+											stat="distance"
+											val={displayDistanceWithUnit(
+												unitSystem,
+												loaderData.userExerciseDetails.details
+													.exerciseExtraInformation.lifetimeStats.distance,
+											)}
 										/>
-									) : null}
-									{loaderData.userExerciseDetails.details?.createdOn ? (
-										<DisplayData
-											name="First done on"
-											data={dayjsLib(
-												loaderData.userExerciseDetails.details.createdOn,
-											).format("ll")}
-											noCasing
+										<DisplayLifetimeStatistic
+											stat="duration"
+											val={`${loaderData.userExerciseDetails.details.exerciseExtraInformation.lifetimeStats.duration} MIN`}
 										/>
-									) : null}
-									{loaderData.userExerciseDetails.details?.lastUpdatedOn ? (
-										<DisplayData
-											name="Last done on"
-											data={dayjsLib(
-												loaderData.userExerciseDetails.details.lastUpdatedOn,
-											).format("ll")}
-											noCasing
+										<DisplayLifetimeStatistic
+											stat="reps"
+											val={
+												loaderData.userExerciseDetails.details
+													.exerciseExtraInformation.lifetimeStats.reps
+											}
 										/>
-									) : null}
-								</SimpleGrid>
-								{loaderData.exerciseDetails.muscles.length > 0 ? (
-									<>
-										<Divider />
-										<Group wrap="nowrap">
-											<Text c="dimmed" fz="sm">
-												Muscles
-											</Text>
-											<Text fz="sm">
-												{loaderData.exerciseDetails.muscles
-													.map((s) => startCase(s.toLowerCase()))
-													.join(", ")}
-											</Text>
-										</Group>
-									</>
-								) : null}
-								{loaderData.exerciseDetails.attributes.instructions.length >
-								0 ? (
-									<>
-										<Divider />
-										<Text size="xl" fw="bold">
-											Instructions
-										</Text>
-										<List type="ordered" spacing="xs">
-											{loaderData.exerciseDetails.attributes.instructions.map(
-												(d) => (
-													<List.Item key={d}>{d}</List.Item>
-												),
-											)}
-										</List>
-									</>
-								) : null}
-							</Stack>
-						</Tabs.Panel>
-						{loaderData.userExerciseDetails.history ? (
-							<Tabs.Panel value="history">
-								<Stack>
-									{loaderData.userExerciseDetails.history.map((h) => (
-										<Paper key={h.workoutId} withBorder p="xs">
-											<Anchor
-												component={Link}
-												to={withFragment(
-													$path("/fitness/workouts/:id", { id: h.workoutId }),
-													`${loaderData.exerciseDetails.id}__${h.index}`,
-												)}
-												fw="bold"
-											>
-												{h.workoutName}
-											</Anchor>
-											<Text c="dimmed" fz="sm" mb="xs">
-												{dayjsLib(h.workoutTime).format("LLLL")}
-											</Text>
-											{h.sets.map((s, idx) => (
-												<Flex key={`${idx}-${s.lot}`} align="center">
-													<Text
-														fz="sm"
-														c={getSetColor(s.lot)}
-														mr="md"
-														fw="bold"
-														ff="monospace"
-													>
-														{match(s.lot)
-															.with(SetLot.Normal, () => idx + 1)
-															.otherwise(() => s.lot.at(0))}
-													</Text>
-													<DisplayExerciseStats
-														lot={loaderData.exerciseDetails.lot}
-														statistic={s.statistic}
-														unit={loaderData.unitSystem}
-													/>
-												</Flex>
-											))}
-										</Paper>
-									))}
+										<DisplayLifetimeStatistic
+											stat="times done"
+											val={
+												loaderData.userExerciseDetails.details
+													.exerciseNumTimesInteracted || 0
+											}
+										/>
+									</Box>
 								</Stack>
-							</Tabs.Panel>
-						) : null}
-						{loaderData.userExerciseDetails.details
-							?.exerciseExtraInformation ? (
-							<Tabs.Panel value="records">
-								<Stack gap="xl">
-									<Stack gap="xs">
+								{loaderData.userExerciseDetails.details.exerciseExtraInformation
+									.personalBests.length > 0 ? (
+									<Stack gap="sm">
 										<Text size="lg" td="underline">
-											Lifetime Stats
+											Personal Bests
 										</Text>
-										<Box>
-											<DisplayLifetimeStatistic
-												stat="weight"
-												val={displayWeightWithUnit(
-													loaderData.unitSystem,
-													loaderData.userExerciseDetails.details
-														.exerciseExtraInformation.lifetimeStats.weight,
-												)}
-											/>
-											<DisplayLifetimeStatistic
-												stat="distance"
-												val={displayDistanceWithUnit(
-													loaderData.unitSystem,
-													loaderData.userExerciseDetails.details
-														.exerciseExtraInformation.lifetimeStats.distance,
-												)}
-											/>
-											<DisplayLifetimeStatistic
-												stat="duration"
-												val={`${loaderData.userExerciseDetails.details.exerciseExtraInformation.lifetimeStats.duration} MIN`}
-											/>
-											<DisplayLifetimeStatistic
-												stat="reps"
-												val={
-													loaderData.userExerciseDetails.details
-														.exerciseExtraInformation.lifetimeStats.reps
-												}
-											/>
-											<DisplayLifetimeStatistic
-												stat="times done"
-												val={
-													loaderData.userExerciseDetails.details
-														.exerciseNumTimesInteracted || 0
-												}
-											/>
-										</Box>
-									</Stack>
-									{loaderData.userExerciseDetails.details
-										.exerciseExtraInformation.personalBests.length > 0 ? (
-										<Stack gap="sm">
-											<Text size="lg" td="underline">
-												Personal Bests
-											</Text>
-											{loaderData.userExerciseDetails.details.exerciseExtraInformation.personalBests.map(
-												(pb) => (
-													<Box key={pb.lot}>
-														<Text size="sm" c="dimmed">
-															{changeCase(pb.lot)}
-														</Text>
-														<Stack gap={0}>
-															{pb.sets.map((s) => (
-																<Group
-																	justify="space-between"
-																	key={`${s.workoutId}-${s.setIdx}`}
-																>
+										{loaderData.userExerciseDetails.details.exerciseExtraInformation.personalBests.map(
+											(pb) => (
+												<Box key={pb.lot}>
+													<Text size="sm" c="dimmed">
+														{changeCase(pb.lot)}
+													</Text>
+													<Stack gap={0}>
+														{pb.sets.map((s) => (
+															<Group
+																justify="space-between"
+																key={`${s.workoutId}-${s.setIdx}`}
+															>
+																<Text size="sm">
+																	{match(pb.lot)
+																		.with(WorkoutSetPersonalBest.OneRm, () =>
+																			Number(s.data.statistic.oneRm).toFixed(2),
+																		)
+																		.with(
+																			WorkoutSetPersonalBest.Reps,
+																			() => s.data.statistic.reps,
+																		)
+																		.with(
+																			WorkoutSetPersonalBest.Time,
+																			() => `${s.data.statistic.duration} min`,
+																		)
+																		.with(WorkoutSetPersonalBest.Volume, () =>
+																			displayWeightWithUnit(
+																				unitSystem,
+																				s.data.statistic.volume,
+																			),
+																		)
+																		.with(WorkoutSetPersonalBest.Weight, () =>
+																			displayWeightWithUnit(
+																				unitSystem,
+																				s.data.statistic.weight,
+																			),
+																		)
+																		.with(
+																			WorkoutSetPersonalBest.Pace,
+																			() => `${s.data.statistic.pace}/min`,
+																		)
+																		.exhaustive()}
+																</Text>
+																<Group>
 																	<Text size="sm">
-																		{match(pb.lot)
-																			.with(WorkoutSetPersonalBest.OneRm, () =>
-																				Number(s.data.statistic.oneRm).toFixed(
-																					2,
-																				),
-																			)
-																			.with(
-																				WorkoutSetPersonalBest.Reps,
-																				() => s.data.statistic.reps,
-																			)
-																			.with(
-																				WorkoutSetPersonalBest.Time,
-																				() =>
-																					`${s.data.statistic.duration} min`,
-																			)
-																			.with(WorkoutSetPersonalBest.Volume, () =>
-																				displayWeightWithUnit(
-																					loaderData.unitSystem,
-																					s.data.statistic.volume,
-																				),
-																			)
-																			.with(WorkoutSetPersonalBest.Weight, () =>
-																				displayWeightWithUnit(
-																					loaderData.unitSystem,
-																					s.data.statistic.weight,
-																				),
-																			)
-																			.with(
-																				WorkoutSetPersonalBest.Pace,
-																				() => `${s.data.statistic.pace}/min`,
-																			)
-																			.exhaustive()}
+																		{dayjsLib(s.workoutDoneOn).format("ll")}
 																	</Text>
-																	<Group>
-																		<Text size="sm">
-																			{dayjsLib(s.workoutDoneOn).format("ll")}
-																		</Text>
-																		<Anchor
-																			component={Link}
-																			to={withFragment(
-																				$path("/fitness/workouts/:id", {
-																					id: s.workoutId,
-																				}),
-																				`${loaderData.exerciseDetails.id}__${s.exerciseIdx}`,
-																			)}
-																			fw="bold"
-																		>
-																			<IconExternalLink size={16} />
-																		</Anchor>
-																	</Group>
+																	<Anchor
+																		component={Link}
+																		to={withFragment(
+																			$path("/fitness/workouts/:id", {
+																				id: s.workoutId,
+																			}),
+																			`${loaderData.exerciseDetails.id}__${s.exerciseIdx}`,
+																		)}
+																		fw="bold"
+																	>
+																		<IconExternalLink size={16} />
+																	</Anchor>
 																</Group>
-															))}
-														</Stack>
-													</Box>
+															</Group>
+														))}
+													</Stack>
+												</Box>
+											),
+										)}
+									</Stack>
+								) : null}
+							</Stack>
+						</Tabs.Panel>
+					) : null}
+					<Tabs.Panel value="actions">
+						<MediaScrollArea>
+							<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
+								<Button
+									variant="outline"
+									onClick={() => {
+										setAddEntityToCollectionData({
+											entityId: loaderData.exerciseId,
+											entityLot: EntityLot.Exercise,
+											alreadyInCollections:
+												loaderData.userExerciseDetails.collections.map(
+													(c) => c.id,
 												),
-											)}
-										</Stack>
-									) : null}
-								</Stack>
-							</Tabs.Panel>
-						) : null}
-						<Tabs.Panel value="actions">
-							<MediaScrollArea>
-								<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
-									<Button variant="outline" onClick={collectionModalOpen}>
-										Add to collection
+										});
+									}}
+								>
+									Add to collection
+								</Button>
+								{canCurrentUserUpdate ? (
+									<Button
+										variant="outline"
+										component={Link}
+										to={$path(
+											"/fitness/exercises/:action",
+											{ action: "update" },
+											{ name: loaderData.exerciseDetails.id },
+										)}
+									>
+										Edit exercise
 									</Button>
-									{loaderData.canCurrentUserUpdate ? (
-										<Button
-											variant="outline"
-											component={Link}
-											to={$path(
-												"/fitness/exercises/:action",
-												{ action: "update" },
-												{ name: loaderData.exerciseDetails.id },
-											)}
-										>
-											Edit exercise
-										</Button>
-									) : null}
-								</SimpleGrid>
-							</MediaScrollArea>
-						</Tabs.Panel>
-					</Tabs>
-				</Stack>
-				{currentWorkout && loaderData.query.selectionEnabled ? (
-					<Affix position={{ bottom: rem(40), right: rem(30) }}>
-						<ActionIcon
-							color="blue"
-							variant="light"
-							radius="xl"
-							size="xl"
-							onClick={async () => {
-								await addExerciseToWorkout(
-									currentWorkout,
-									setCurrentWorkout,
-									[
-										{
-											name: loaderData.exerciseDetails.id,
-											lot: loaderData.exerciseDetails.lot,
-										},
-									],
-									navigate,
-								);
-							}}
-						>
-							<IconCheck size={32} />
-						</ActionIcon>
-					</Affix>
-				) : null}
-			</Container>
-		</>
+								) : null}
+							</SimpleGrid>
+						</MediaScrollArea>
+					</Tabs.Panel>
+				</Tabs>
+			</Stack>
+			{currentWorkout && loaderData.query.selectionEnabled ? (
+				<Affix position={{ bottom: rem(40), right: rem(30) }}>
+					<ActionIcon
+						color="blue"
+						variant="light"
+						radius="xl"
+						size="xl"
+						onClick={async () => {
+							await addExerciseToWorkout(
+								currentWorkout,
+								setCurrentWorkout,
+								[
+									{
+										name: loaderData.exerciseDetails.id,
+										lot: loaderData.exerciseDetails.lot,
+									},
+								],
+								navigate,
+							);
+						}}
+					>
+						<IconCheck size={32} />
+					</ActionIcon>
+				</Affix>
+			) : null}
+		</Container>
 	);
 }
 
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
index d64f2673a3..818412883c 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
@@ -54,16 +54,16 @@ import { z } from "zod";
 import { zx } from "zodix";
 import { DebouncedSearchInput } from "~/components/common";
 import { dayjsLib } from "~/lib/generals";
-import { useSearchParam } from "~/lib/hooks";
+import {
+	useCoreDetails,
+	useSearchParam,
+	useUserCollections,
+} from "~/lib/hooks";
+import { addExerciseToWorkout, currentWorkoutAtom } from "~/lib/state/workout";
 import {
 	getAuthorizationHeader,
-	getCoreDetails,
-	getUserCollectionsList,
-	getUserDetails,
-	getUserPreferences,
 	serverGqlService,
 } from "~/lib/utilities.server";
-import { addExerciseToWorkout, currentWorkoutAtom } from "~/lib/workout";
 
 const defaultFiltersValue = {
 	muscle: undefined,
@@ -97,17 +97,7 @@ export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = unstable_defineLoader(async ({ request }) => {
 	const query = zx.parseQuery(request, searchParamsSchema);
-	const [
-		coreDetails,
-		userPreferences,
-		userDetails,
-		{ exerciseParameters },
-		{ exercisesList },
-		userCollectionsList,
-	] = await Promise.all([
-		getCoreDetails(request),
-		getUserPreferences(request),
-		getUserDetails(request),
+	const [{ exerciseParameters }, { exercisesList }] = await Promise.all([
 		serverGqlService.request(ExerciseParametersDocument, {}),
 		serverGqlService.request(
 			ExercisesListDocument,
@@ -129,19 +119,10 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 					sortBy: query.sortBy,
 				},
 			},
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
-		getUserCollectionsList(request),
 	]);
-	return {
-		coreDetails: { pageLimit: coreDetails.pageLimit },
-		userCollectionsList,
-		userPreferences,
-		userDetails,
-		query,
-		exerciseParameters,
-		exercisesList,
-	};
+	return { query, exerciseParameters, exercisesList };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
@@ -150,6 +131,8 @@ export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
+	const collections = useUserCollections();
 	const navigate = useNavigate();
 	const [_, { setP }] = useSearchParam();
 	const [selectedExercises, setSelectedExercises] = useListState<{
@@ -268,14 +251,14 @@ export default function Page() {
 													onChange={(v) => setP(f, v)}
 												/>
 											))}
-										{loaderData.userCollectionsList.length > 0 ? (
+										{collections.length > 0 ? (
 											<Select
 												label="Collection"
 												defaultValue={loaderData.query.collection?.toString()}
 												data={[
 													{
 														group: "My collections",
-														items: loaderData.userCollectionsList.map((c) => ({
+														items: collections.map((c) => ({
 															value: c.id.toString(),
 															label: c.name,
 														})),
@@ -389,7 +372,7 @@ export default function Page() {
 									onChange={(v) => setP("page", v.toString())}
 									total={Math.ceil(
 										loaderData.exercisesList.details.total /
-											loaderData.coreDetails.pageLimit,
+											coreDetails.pageLimit,
 									)}
 								/>
 							</Center>
diff --git a/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx
index b04d7692ce..34f923291e 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx
@@ -50,11 +50,10 @@ import { z } from "zod";
 import { zx } from "zodix";
 import events from "~/lib/events";
 import { dayjsLib, redirectToQueryParam } from "~/lib/generals";
-import { useSearchParam } from "~/lib/hooks";
+import { useSearchParam, useUserPreferences } from "~/lib/hooks";
 import {
 	createToastHeaders,
 	getAuthorizationHeader,
-	getUserPreferences,
 	processSubmission,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -87,8 +86,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 		.with(TimeSpan.Last365Days, () => [now, now.subtract(365, "days")])
 		.with(TimeSpan.AllTime, () => [null, null])
 		.exhaustive();
-	const [userPreferences, { userMeasurementsList }] = await Promise.all([
-		getUserPreferences(request),
+	const [{ userMeasurementsList }] = await Promise.all([
 		serverGqlService.request(
 			UserMeasurementsListDocument,
 			{
@@ -97,14 +95,10 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 					endTime: endTime?.toISOString(),
 				},
 			},
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
 	]);
-	return {
-		query,
-		userPreferences: { fitness: userPreferences.fitness },
-		userMeasurementsList,
-	};
+	return { query, userMeasurementsList };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
@@ -124,7 +118,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				CreateUserMeasurementDocument,
 				{ input },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			const toastHeaders = {
 				headers: await createToastHeaders({
@@ -144,7 +138,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				DeleteUserMeasurementDocument,
 				submission,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json({ status: "success", submission } as const, {
 				headers: await createToastHeaders({
@@ -160,6 +154,7 @@ const deleteSchema = z.object({ timestamp: z.string() });
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
 	const formattedData = loaderData.userMeasurementsList.map((m) => {
 		const customStats = Object.fromEntries(
 			Object.entries(m.stats.custom || {})
@@ -214,15 +209,12 @@ export default function Page() {
 						/>
 						<TextInput label="Name" name="name" />
 						<SimpleGrid cols={2} style={{ alignItems: "end" }}>
-							{Object.keys(
-								loaderData.userPreferences.fitness.measurements.inbuilt,
-							)
+							{Object.keys(userPreferences.fitness.measurements.inbuilt)
 								.filter((n) => n !== "custom")
 								.filter(
 									(n) =>
 										// biome-ignore lint/suspicious/noExplicitAny: required
-										(loaderData.userPreferences as any).fitness.measurements
-											.inbuilt[n],
+										(userPreferences as any).fitness.measurements.inbuilt[n],
 								)
 								.map((v) => (
 									<NumberInput
@@ -232,15 +224,13 @@ export default function Page() {
 										name={`stats.${v}`}
 									/>
 								))}
-							{loaderData.userPreferences.fitness.measurements.custom.map(
-								({ name }) => (
-									<NumberInput
-										key={name}
-										label={changeCase(snakeCase(name))}
-										name={`stats.custom.${name}`}
-									/>
-								),
-							)}
+							{userPreferences.fitness.measurements.custom.map(({ name }) => (
+								<NumberInput
+									key={name}
+									label={changeCase(snakeCase(name))}
+									name={`stats.custom.${name}`}
+								/>
+							))}
 						</SimpleGrid>
 						<Textarea label="Comment" name="comment" />
 						<Button type="submit">Submit</Button>
@@ -278,17 +268,16 @@ export default function Page() {
 							<MultiSelect
 								label="Statistics to display"
 								data={[
-									...Object.keys(
-										loaderData.userPreferences.fitness.measurements.inbuilt,
-									)
+									...Object.keys(userPreferences.fitness.measurements.inbuilt)
 										.filter(
 											(n) =>
 												// biome-ignore lint/suspicious/noExplicitAny: required
-												(loaderData.userPreferences as any).fitness.measurements
-													.inbuilt[n],
+												(userPreferences as any).fitness.measurements.inbuilt[
+													n
+												],
 										)
 										.map((v) => ({ name: v, value: v })),
-									...loaderData.userPreferences.fitness.measurements.custom.map(
+									...userPreferences.fitness.measurements.custom.map(
 										({ name }) => ({ name, value: `custom.${name}` }),
 									),
 								].map((v) => ({
@@ -332,13 +321,13 @@ export default function Page() {
 								},
 								...([
 									...Object.entries(
-										loaderData.userPreferences.fitness.measurements.inbuilt,
+										userPreferences.fitness.measurements.inbuilt,
 									)
 										.map(([name, enabled]) =>
 											enabled ? `stats.${name}` : null,
 										)
 										.filter(Boolean),
-									...loaderData.userPreferences.fitness.measurements.custom.map(
+									...userPreferences.fitness.measurements.custom.map(
 										(c) => `stats.custom.${c.name}`,
 									),
 								].map((w) => ({
diff --git a/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx
index d275f75496..1673b6e893 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx
@@ -64,26 +64,28 @@ import { withFragment } from "ufo";
 import { z } from "zod";
 import { DisplayExerciseStats } from "~/components/fitness";
 import { dayjsLib, getSetColor } from "~/lib/generals";
-import { getWorkoutStarter, useGetMantineColor } from "~/lib/hooks";
+import {
+	getWorkoutStarter,
+	useGetMantineColor,
+	useUserPreferences,
+} from "~/lib/hooks";
+import { duplicateOldWorkout, getExerciseDetails } from "~/lib/state/workout";
 import {
 	createToastHeaders,
 	getAuthorizationHeader,
-	getUserPreferences,
 	processSubmission,
 	redirectWithToast,
 	serverGqlService,
 } from "~/lib/utilities.server";
-import { duplicateOldWorkout, getExerciseDetails } from "~/lib/workout";
 
 export const loader = unstable_defineLoader(async ({ request, params }) => {
 	const workoutId = params.id;
 	invariant(workoutId, "No ID provided");
-	const [userPreferences, { workoutDetails }] = await Promise.all([
-		getUserPreferences(request),
+	const [{ workoutDetails }] = await Promise.all([
 		serverGqlService.request(
 			WorkoutDetailsDocument,
 			{ workoutId },
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
 	]);
 	let repeatedWorkout = null;
@@ -92,7 +94,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 			await serverGqlService.request(
 				WorkoutDetailsDocument,
 				{ workoutId: workoutDetails.repeatedFrom },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 		repeatedWorkout = {
 			id: workoutDetails.repeatedFrom,
@@ -100,14 +102,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 			doneOn: repeatedWorkoutData.startTime,
 		};
 	}
-	return {
-		workoutId,
-		userPreferences: {
-			unitSystem: userPreferences.fitness.exercises.unitSystem,
-		},
-		workoutDetails,
-		repeatedWorkout,
-	};
+	return { workoutId, workoutDetails, repeatedWorkout };
 });
 
 export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -122,7 +117,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				EditUserWorkoutDocument,
 				{ input: submission },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json({ status: "success", submission } as const, {
 				headers: await createToastHeaders({
@@ -136,7 +131,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				DeleteUserWorkoutDocument,
 				submission,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return redirectWithToast($path("/fitness/workouts/list"), {
 				type: "success",
@@ -156,6 +151,8 @@ const editWorkoutSchema = z.object({
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
+	const unitSystem = userPreferences.fitness.exercises.unitSystem;
 	const [
 		adjustTimeModalOpened,
 		{ open: adjustTimeModalOpen, close: adjustTimeModalClose },
@@ -290,7 +287,7 @@ export default function Page() {
 								<DisplayStat
 									icon={<IconWeight size={16} />}
 									data={displayWeightWithUnit(
-										loaderData.userPreferences.unitSystem,
+										unitSystem,
 										loaderData.workoutDetails.summary.total.weight,
 									)}
 								/>
@@ -299,7 +296,7 @@ export default function Page() {
 								<DisplayStat
 									icon={<IconRun size={16} />}
 									data={displayDistanceWithUnit(
-										loaderData.userPreferences.unitSystem,
+										unitSystem,
 										loaderData.workoutDetails.summary.total.distance,
 									)}
 								/>
@@ -361,6 +358,8 @@ type Exercise =
 
 const DisplayExercise = (props: { exercise: Exercise; idx: number }) => {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
+	const unitSystem = userPreferences.fitness.exercises.unitSystem;
 	const [opened, { toggle }] = useDisclosure(false);
 	const [parent] = useAutoAnimate();
 	const exerciseDetails = useQuery({
@@ -442,7 +441,7 @@ const DisplayExercise = (props: { exercise: Exercise; idx: number }) => {
 									<Text fz="xs">
 										Weight:{" "}
 										{displayWeightWithUnit(
-											loaderData.userPreferences.unitSystem,
+											unitSystem,
 											props.exercise.total.weight,
 										)}
 									</Text>
@@ -454,7 +453,7 @@ const DisplayExercise = (props: { exercise: Exercise; idx: number }) => {
 									<Text fz="xs">
 										Distance:{" "}
 										{displayDistanceWithUnit(
-											loaderData.userPreferences.unitSystem,
+											unitSystem,
 											props.exercise.total.distance,
 										)}
 									</Text>
@@ -509,7 +508,8 @@ const DisplaySet = (props: {
 	idx: number;
 	exerciseLot: ExerciseLot;
 }) => {
-	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
+	const unitSystem = userPreferences.fitness.exercises.unitSystem;
 	const getMantineColor = useGetMantineColor();
 	const [opened, { close, open }] = useDisclosure(false);
 
@@ -558,7 +558,7 @@ const DisplaySet = (props: {
 				<DisplayExerciseStats
 					lot={props.exerciseLot}
 					statistic={props.set.statistic}
-					unit={loaderData.userPreferences.unitSystem}
+					unit={unitSystem}
 				/>
 			</Flex>
 		</Box>
diff --git a/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx b/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx
index eff88a2e56..29d4643fe7 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx
@@ -94,15 +94,7 @@ import { confirmWrapper } from "~/components/confirmation";
 import { DisplayExerciseStats } from "~/components/fitness";
 import events from "~/lib/events";
 import { CurrentWorkoutKey, dayjsLib, getSetColor } from "~/lib/generals";
-import {
-	createToastHeaders,
-	getAuthorizationHeader,
-	getCoreDetails,
-	getCoreEnabledFeatures,
-	getUserPreferences,
-	redirectWithToast,
-	serverGqlService,
-} from "~/lib/utilities.server";
+import { useUserPreferences } from "~/lib/hooks";
 import {
 	type Exercise,
 	type ExerciseSet,
@@ -110,7 +102,14 @@ import {
 	currentWorkoutAtom,
 	currentWorkoutToCreateWorkoutInput,
 	timerAtom,
-} from "~/lib/workout";
+} from "~/lib/state/workout";
+import {
+	createToastHeaders,
+	getAuthorizationHeader,
+	getCoreEnabledFeatures,
+	redirectWithToast,
+	serverGqlService,
+} from "~/lib/utilities.server";
 
 const workoutCookieName = CurrentWorkoutKey;
 const defaultTimerLocalStorageKey = "DefaultExerciseRestTimer";
@@ -123,20 +122,8 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 			type: "error",
 			message: "No workout in progress",
 		});
-	const [coreDetails, userPreferences, coreEnabledFeatures] = await Promise.all(
-		[
-			getCoreDetails(request),
-			getUserPreferences(request),
-			getCoreEnabledFeatures(),
-		],
-	);
+	const [coreEnabledFeatures] = await Promise.all([getCoreEnabledFeatures()]);
 	return {
-		coreDetails,
-		userPreferences: {
-			unitSystem: userPreferences.fitness.exercises.unitSystem,
-			isMeasurementEnabled:
-				userPreferences.featuresEnabled.fitness.measurements,
-		},
 		coreEnabledFeatures: { fileStorage: coreEnabledFeatures.fileStorage },
 	};
 });
@@ -153,7 +140,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			const { createUserWorkout } = await serverGqlService.request(
 				CreateUserWorkoutDocument,
 				workout,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return redirect(
 				$path("/fitness/workouts/:id", { id: createUserWorkout }),
@@ -178,7 +165,8 @@ const deleteUploadedAsset = (key: string) => {
 };
 
 export default function Page() {
-	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
+	const unitSystem = userPreferences.fitness.exercises.unitSystem;
 	const [parent] = useAutoAnimate();
 	const navigate = useNavigate();
 	const [time, setTime] = useState(0);
@@ -300,7 +288,7 @@ export default function Page() {
 									<StatDisplay
 										name="Weight"
 										value={`${displayWeightWithUnit(
-											loaderData.userPreferences.unitSystem,
+											unitSystem,
 											sum(
 												currentWorkout.exercises
 													.flatMap((e) => e.sets)
@@ -439,7 +427,7 @@ export default function Page() {
 									/>
 								))}
 								<Group justify="center">
-									{loaderData.userPreferences.isMeasurementEnabled ? (
+									{userPreferences.featuresEnabled.fitness.measurements ? (
 										<Button
 											component={Link}
 											variant="subtle"
@@ -665,6 +653,8 @@ const ExerciseDisplay = (props: {
 	stopTimer: () => void;
 }) => {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
+	const unitSystem = userPreferences.fitness.exercises.unitSystem;
 	const [parent] = useAutoAnimate();
 	const [currentWorkout, setCurrentWorkout] = useAtom(currentWorkoutAtom);
 	const [currentTimer] = useAtom(timerAtom);
@@ -1046,7 +1036,7 @@ const ExerciseDisplay = (props: {
 							{distanceCol ? (
 								<Text size="xs" style={{ flex: 1 }} ta="center">
 									DISTANCE (
-									{match(loaderData.userPreferences.unitSystem)
+									{match(unitSystem)
 										.with(UserUnitSystem.Metric, () => "KM")
 										.with(UserUnitSystem.Imperial, () => "MI")
 										.exhaustive()}
@@ -1056,7 +1046,7 @@ const ExerciseDisplay = (props: {
 							{weightCol ? (
 								<Text size="xs" style={{ flex: 1 }} ta="center">
 									WEIGHT (
-									{match(loaderData.userPreferences.unitSystem)
+									{match(unitSystem)
 										.with(UserUnitSystem.Metric, () => "KG")
 										.with(UserUnitSystem.Imperial, () => "LB")
 										.exhaustive()}
@@ -1187,7 +1177,7 @@ const ExerciseDisplay = (props: {
 													lot={props.exercise.lot}
 													hideExtras
 													centerText
-													unit={loaderData.userPreferences.unitSystem}
+													unit={unitSystem}
 												/>
 											</Box>
 										) : (
diff --git a/apps/frontend/app/routes/_dashboard.fitness.workouts.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.workouts.list.tsx
index d4dd7fff31..ded569a3bd 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.workouts.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.workouts.list.tsx
@@ -38,14 +38,17 @@ import { zx } from "zodix";
 import { DebouncedSearchInput } from "~/components/common";
 import { getSetStatisticsTextToDisplay } from "~/components/fitness";
 import { dayjsLib } from "~/lib/generals";
-import { getWorkoutStarter, useSearchParam } from "~/lib/hooks";
+import {
+	getWorkoutStarter,
+	useCoreDetails,
+	useSearchParam,
+	useUserPreferences,
+} from "~/lib/hooks";
+import { getDefaultWorkout } from "~/lib/state/workout";
 import {
 	getAuthorizationHeader,
-	getCoreDetails,
-	getUserPreferences,
 	serverGqlService,
 } from "~/lib/utilities.server";
-import { getDefaultWorkout } from "~/lib/workout";
 
 const searchParamsSchema = z.object({
 	page: zx.IntAsString.default("1"),
@@ -56,27 +59,16 @@ export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = unstable_defineLoader(async ({ request }) => {
 	const query = zx.parseQuery(request, searchParamsSchema);
-	const [userPreferences, coreDetails, { userWorkoutList }] = await Promise.all(
-		[
-			getUserPreferences(request),
-			getCoreDetails(request),
-			serverGqlService.request(
-				UserWorkoutListDocument,
-				{
-					input: { page: query.page, query: query.query },
-				},
-				await getAuthorizationHeader(request),
-			),
-		],
-	);
-	return {
-		userPreferences: {
-			unitSystem: userPreferences.fitness.exercises.unitSystem,
-		},
-		coreDetails: { pageLimit: coreDetails.pageLimit },
-		query,
-		userWorkoutList,
-	};
+	const [{ userWorkoutList }] = await Promise.all([
+		serverGqlService.request(
+			UserWorkoutListDocument,
+			{
+				input: { page: query.page, query: query.query },
+			},
+			getAuthorizationHeader(request),
+		),
+	]);
+	return { query, userWorkoutList };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
@@ -85,8 +77,11 @@ export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
+	const coreDetails = useCoreDetails();
 	const [_, { setP }] = useSearchParam();
 	const startWorkout = getWorkoutStarter();
+	const unitSystem = userPreferences.fitness.exercises.unitSystem;
 
 	return (
 		<Container size="xs">
@@ -139,7 +134,7 @@ export default function Page() {
 													<DisplayStat
 														icon={<IconWeight size={16} />}
 														data={displayWeightWithUnit(
-															loaderData.userPreferences.unitSystem,
+															unitSystem,
 															workout.summary.total.weight,
 														)}
 													/>
@@ -178,7 +173,7 @@ export default function Page() {
 													<ExerciseDisplay
 														exercise={exercise}
 														key={`${idx}-${exercise.id}`}
-														unit={loaderData.userPreferences.unitSystem}
+														unit={unitSystem}
 													/>
 												))}
 											</>
@@ -199,8 +194,7 @@ export default function Page() {
 						value={loaderData.query.page}
 						onChange={(v) => setP("page", v.toString())}
 						total={Math.ceil(
-							loaderData.userWorkoutList.details.total /
-								loaderData.coreDetails.pageLimit,
+							loaderData.userWorkoutList.details.total / coreDetails.pageLimit,
 						)}
 					/>
 				</Center>
diff --git a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
index 33d2185764..a9aef00264 100644
--- a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
@@ -35,7 +35,7 @@ import {
 	MediaSource,
 	MetadataListDocument,
 	MetadataSearchDocument,
-	type UserReviewScale,
+	UserReviewScale,
 } from "@ryot/generated/graphql/backend/graphql";
 import { changeCase, startCase } from "@ryot/ts-utils";
 import {
@@ -48,6 +48,7 @@ import {
 	IconSearch,
 	IconSortAscending,
 	IconSortDescending,
+	IconStarFilled,
 } from "@tabler/icons-react";
 import { useState } from "react";
 import invariant from "tiny-invariant";
@@ -55,11 +56,7 @@ import { match } from "ts-pattern";
 import { withoutHost } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
-import {
-	AddEntityToCollectionModal,
-	ApplicationGrid,
-	DebouncedSearchInput,
-} from "~/components/common";
+import { ApplicationGrid, DebouncedSearchInput } from "~/components/common";
 import {
 	type Item,
 	MediaItemWithoutUpdateModal,
@@ -67,23 +64,31 @@ import {
 	commitMedia,
 } from "~/components/media";
 import events from "~/lib/events";
-import { Verb, getLot, getVerb, redirectToQueryParam } from "~/lib/generals";
-import { useSearchParam } from "~/lib/hooks";
+import { Verb, getLot, getVerb } from "~/lib/generals";
+import {
+	useCoreDetails,
+	useSearchParam,
+	useUserCollections,
+	useUserDetails,
+	useUserPreferences,
+} from "~/lib/hooks";
+import {
+	useAddEntityToCollection,
+	useMetadataProgressUpdate,
+	useReviewEntity,
+} from "~/lib/state/media";
 import {
 	getAuthorizationHeader,
-	getCoreDetails,
-	getUserCollectionsList,
-	getUserDetails,
-	getUserPreferences,
 	serverGqlService,
 } from "~/lib/utilities.server";
+import classes from "~/styles/common.module.css";
 
 export type SearchParams = {
 	query?: string;
 };
 
 const defaultFilters = {
-	minecollection: undefined,
+	mineCollection: undefined,
 	mineGeneralFilter: MediaGeneralFilter.All,
 	mineSortOrder: GraphqlSortOrder.Desc,
 	mineSortBy: MediaSortBy.LastSeen,
@@ -111,22 +116,12 @@ const metadataMapping = {
 };
 
 export const loader = unstable_defineLoader(async ({ request, params }) => {
-	const [
-		userDetails,
-		coreDetails,
-		userPreferences,
-		{ latestUserSummary },
-		userCollectionsList,
-	] = await Promise.all([
-		getUserDetails(request),
-		getCoreDetails(request),
-		getUserPreferences(request),
+	const [{ latestUserSummary }] = await Promise.all([
 		serverGqlService.request(
 			LatestUserSummaryDocument,
 			undefined,
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
-		getUserCollectionsList(request),
 	]);
 	const { query, page } = zx.parseQuery(request, {
 		query: z.string().optional(),
@@ -165,7 +160,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 						},
 					},
 				},
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return [{ list: metadataList, url: urlParse }, undefined] as const;
 		})
@@ -183,7 +178,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 						source: urlParse.source,
 					},
 				},
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return [
 				undefined,
@@ -203,12 +198,8 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 		numPage,
 		mediaList,
 		mediaSearch,
-		userId: userDetails.id,
-		collections: userCollectionsList,
 		url: withoutHost(url.href),
-		coreDetails: { pageLimit: coreDetails.pageLimit },
 		mediaInteractedWith: latestUserSummary.media.metadataOverall.interactedWith,
-		userPreferences: { reviewScale: userPreferences.general.reviewScale },
 	};
 });
 
@@ -224,7 +215,11 @@ export const meta = ({ params }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
+	const coreDetails = useCoreDetails();
+	const collections = useUserCollections();
 	const [_, { setP }] = useSearchParam();
+	const [_r, setEntityToReview] = useReviewEntity();
 	const [
 		filtersModalOpened,
 		{ open: openFiltersModal, close: closeFiltersModal },
@@ -236,7 +231,7 @@ export default function Page() {
 			defaultFilters.mineGeneralFilter ||
 		loaderData.mediaList?.url.sortOrder !== defaultFilters.mineSortOrder ||
 		loaderData.mediaList?.url.sortBy !== defaultFilters.mineSortBy ||
-		loaderData.mediaList?.url.collection !== defaultFilters.minecollection;
+		loaderData.mediaList?.url.collection !== defaultFilters.mineCollection;
 
 	return (
 		<Container>
@@ -360,14 +355,14 @@ export default function Page() {
 											)}
 										</ActionIcon>
 									</Flex>
-									{loaderData.collections.length > 0 ? (
+									{collections.length > 0 ? (
 										<Select
 											placeholder="Select a collection"
 											defaultValue={loaderData.mediaList.url.collection?.toString()}
 											data={[
 												{
 													group: "My collections",
-													items: loaderData.collections.map((c) => ({
+													items: collections.map((c) => ({
 														value: c.id.toString(),
 														label: c.name,
 													})),
@@ -390,22 +385,65 @@ export default function Page() {
 									items found
 								</Box>
 								<ApplicationGrid>
-									{loaderData.mediaList.list.items.map((lm) => (
-										<MediaItemWithoutUpdateModal
-											key={lm.data.identifier}
-											item={{
-												...lm.data,
-												publishYear: lm.data.publishYear?.toString(),
-											}}
-											averageRating={lm.averageRating ?? undefined}
-											mediaReason={lm.mediaReason}
-											lot={loaderData.lot}
-											href={$path("/media/item/:id", {
-												id: lm.data.identifier,
-											})}
-											reviewScale={loaderData.userPreferences.reviewScale}
-										/>
-									))}
+									{loaderData.mediaList.list.items.map((lm) => {
+										const averageRating = lm.averageRating;
+										return (
+											<MediaItemWithoutUpdateModal
+												key={lm.data.identifier}
+												item={{
+													...lm.data,
+													publishYear: lm.data.publishYear?.toString(),
+												}}
+												topRight={
+													averageRating ? (
+														<>
+															<IconStarFilled
+																size={12}
+																style={{ color: "#EBE600FF" }}
+															/>
+															<Text c="white" size="xs" fw="bold" pr={4}>
+																{match(userPreferences.general.reviewScale)
+																	.with(UserReviewScale.OutOfFive, () =>
+																		Number.parseFloat(
+																			averageRating.toString(),
+																		).toFixed(1),
+																	)
+																	.with(
+																		UserReviewScale.OutOfHundred,
+																		() => averageRating,
+																	)
+																	.exhaustive()}{" "}
+																{userPreferences.general.reviewScale ===
+																UserReviewScale.OutOfFive
+																	? undefined
+																	: "%"}
+															</Text>
+														</>
+													) : (
+														<IconStarFilled
+															onClick={(e) => {
+																e.preventDefault();
+																setEntityToReview({
+																	entityId: lm.data.identifier,
+																	entityLot: EntityLot.Metadata,
+																	entityTitle: lm.data.title,
+																	metadataLot: loaderData.lot,
+																});
+															}}
+															size={16}
+															className={classes.starIcon}
+														/>
+													)
+												}
+												mediaReason={lm.mediaReason}
+												lot={loaderData.lot}
+												href={$path("/media/item/:id", {
+													id: lm.data.identifier,
+												})}
+												reviewScale={userPreferences.general.reviewScale}
+											/>
+										);
+									})}
 								</ApplicationGrid>
 							</>
 						) : (
@@ -419,7 +457,7 @@ export default function Page() {
 									onChange={(v) => setP("page", v.toString())}
 									total={Math.ceil(
 										loaderData.mediaList.list.details.total /
-											loaderData.coreDetails.pageLimit,
+											coreDetails.pageLimit,
 									)}
 								/>
 							</Center>
@@ -473,7 +511,7 @@ export default function Page() {
 												loaderData.mediaSearch?.url.source ||
 												MediaSource.Anilist
 											}
-											reviewScale={loaderData.userPreferences.reviewScale}
+											reviewScale={userPreferences.general.reviewScale}
 										/>
 									))}
 								</ApplicationGrid>
@@ -489,7 +527,7 @@ export default function Page() {
 									onChange={(v) => setP("page", v.toString())}
 									total={Math.ceil(
 										loaderData.mediaSearch.search.details.total /
-											loaderData.coreDetails.pageLimit,
+											coreDetails.pageLimit,
 									)}
 								/>
 							</Center>
@@ -512,9 +550,11 @@ const MediaSearchItem = (props: {
 	maybeItemId?: string;
 }) => {
 	const navigate = useNavigate();
-	const loaderData = useLoaderData<typeof loader>();
+	const userDetails = useUserDetails();
 	const [isLoading, setIsLoading] = useState(false);
 	const revalidator = useRevalidator();
+	const [_, setMetadataToUpdate] = useMetadataProgressUpdate();
+	const [_a, setAddEntityToCollectionData] = useAddEntityToCollection();
 	const basicCommit = async (e: React.MouseEvent) => {
 		if (props.maybeItemId) return props.maybeItemId;
 		e.preventDefault();
@@ -527,17 +567,6 @@ const MediaSearchItem = (props: {
 		setIsLoading(false);
 		return response;
 	};
-	const [
-		isAddMediaToCollectionModalOpened,
-		{
-			open: openIsAddMediaToCollectionModalOpened,
-			close: closeIsAddMediaToCollectionModalOpened,
-		},
-	] = useDisclosure(false);
-	const [appItemId, setAppItemId] = useState(props.maybeItemId);
-
-	const isShowOrPodcast =
-		props.lot === MediaLot.Show || props.lot === MediaLot.Podcast;
 
 	return (
 		<MediaItemWithoutUpdateModal
@@ -546,7 +575,6 @@ const MediaSearchItem = (props: {
 			reviewScale={props.reviewScale}
 			hasInteracted={props.hasInteracted}
 			imageOverlayForLoadingIndicator={isLoading}
-			noRatingLink
 			noHref
 			onClick={async (e) => {
 				setIsLoading(true);
@@ -555,39 +583,27 @@ const MediaSearchItem = (props: {
 				return navigate($path("/media/item/:id", { id }));
 			}}
 			nameRight={
-				<>
-					<Menu shadow="md">
-						<Menu.Target>
-							<ActionIcon size="xs">
-								<IconDotsVertical />
-							</ActionIcon>
-						</Menu.Target>
-						<Menu.Dropdown>
-							<Menu.Item
-								leftSection={<IconBoxMultiple size={14} />}
-								onClick={async (e) => {
-									if (!appItemId) {
-										const id = await basicCommit(e);
-										setAppItemId(id);
-									}
-									openIsAddMediaToCollectionModalOpened();
-								}}
-							>
-								Add to collection
-							</Menu.Item>
-						</Menu.Dropdown>
-					</Menu>
-					{appItemId ? (
-						<AddEntityToCollectionModal
-							userId={loaderData.userId}
-							opened={isAddMediaToCollectionModalOpened}
-							onClose={closeIsAddMediaToCollectionModalOpened}
-							entityId={appItemId.toString()}
-							entityLot={EntityLot.Media}
-							collections={loaderData.collections}
-						/>
-					) : null}
-				</>
+				<Menu shadow="md">
+					<Menu.Target>
+						<ActionIcon size="xs">
+							<IconDotsVertical />
+						</ActionIcon>
+					</Menu.Target>
+					<Menu.Dropdown>
+						<Menu.Item
+							leftSection={<IconBoxMultiple size={14} />}
+							onClick={async (e) => {
+								const id = await basicCommit(e);
+								setAddEntityToCollectionData({
+									entityId: id,
+									entityLot: EntityLot.Metadata,
+								});
+							}}
+						>
+							Add to collection
+						</Menu.Item>
+					</Menu.Dropdown>
+				</Menu>
 			}
 		>
 			<>
@@ -596,25 +612,11 @@ const MediaSearchItem = (props: {
 					w="100%"
 					size="compact-md"
 					onClick={async (e) => {
-						const id = await basicCommit(e);
-						return navigate(
-							$path(
-								"/media/item/:id",
-								{ id },
-								!isShowOrPodcast
-									? {
-											defaultTab: "actions",
-											openProgressModal: true,
-											[redirectToQueryParam]: loaderData.url,
-										}
-									: { defaultTab: "showSeasons" },
-							),
-						);
+						const metadataId = await basicCommit(e);
+						setMetadataToUpdate({ metadataId });
 					}}
 				>
-					{!isShowOrPodcast
-						? `Mark as ${getVerb(Verb.Read, props.lot)}`
-						: "Show details"}
+					Mark as {getVerb(Verb.Read, props.lot)}
 				</Button>
 				<Button
 					mt="xs"
@@ -626,14 +628,14 @@ const MediaSearchItem = (props: {
 						const id = await basicCommit(e);
 						const form = new FormData();
 						form.append("entityId", id);
-						form.append("entityLot", EntityLot.Media);
-						form.append("creatorUserId", loaderData.userId.toString());
+						form.append("entityLot", EntityLot.Metadata);
+						form.append("creatorUserId", userDetails.id);
 						form.append("collectionName", "Watchlist");
 						await fetch(
 							$path("/actions", { intent: "addEntityToCollection" }),
 							{ body: form, method: "POST", credentials: "include" },
 						);
-						events.addToCollection(EntityLot.Media);
+						events.addToCollection(EntityLot.Metadata);
 						setIsLoading(false);
 						revalidator.revalidate();
 					}}
diff --git a/apps/frontend/app/routes/_dashboard.media.create.tsx b/apps/frontend/app/routes/_dashboard.media.create.tsx
index f0621d9f2f..9d6169523e 100644
--- a/apps/frontend/app/routes/_dashboard.media.create.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.create.tsx
@@ -66,7 +66,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 	const { createCustomMetadata } = await serverGqlService.request(
 		CreateCustomMetadataDocument,
 		{ input },
-		await getAuthorizationHeader(request),
+		getAuthorizationHeader(request),
 	);
 	return redirect($path("/media/item/:id", { id: createCustomMetadata.id }));
 });
diff --git a/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx
index a8a55cb3c8..4bc798b1f3 100644
--- a/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx
@@ -18,8 +18,8 @@ import { z } from "zod";
 import { zx } from "zodix";
 import { ApplicationGrid } from "~/components/common";
 import { BaseDisplayItem } from "~/components/media";
-import { useSearchParam } from "~/lib/hooks";
-import { getCoreDetails, serverGqlService } from "~/lib/utilities.server";
+import { useCoreDetails, useSearchParam } from "~/lib/hooks";
+import { serverGqlService } from "~/lib/utilities.server";
 
 const searchParamsSchema = z.object({
 	page: zx.IntAsString.default("1"),
@@ -31,17 +31,12 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 	const query = zx.parseQuery(request, searchParamsSchema);
 	const genreId = params.id;
 	invariant(genreId, "No ID provided");
-	const [coreDetails, { genreDetails }] = await Promise.all([
-		getCoreDetails(request),
+	const [{ genreDetails }] = await Promise.all([
 		serverGqlService.request(GenreDetailsDocument, {
 			input: { genreId, page: query.page },
 		}),
 	]);
-	return {
-		query,
-		coreDetails: { pageLimit: coreDetails.pageLimit },
-		genreDetails,
-	};
+	return { query, genreDetails };
 });
 
 export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -50,6 +45,7 @@ export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 	const [_, { setP }] = useSearchParam();
 
 	return (
@@ -79,7 +75,7 @@ export default function Page() {
 						onChange={(v) => setP("page", v.toString())}
 						total={Math.ceil(
 							loaderData.genreDetails.contents.details.total /
-								loaderData.coreDetails.pageLimit,
+								coreDetails.pageLimit,
 						)}
 					/>
 				</Center>
diff --git a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
index d7219f91fb..3b70530fc2 100644
--- a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
@@ -22,8 +22,12 @@ import { GenresListDocument } from "@ryot/generated/graphql/backend/graphql";
 import { z } from "zod";
 import { zx } from "zodix";
 import { ApplicationGrid, DebouncedSearchInput } from "~/components/common";
-import { useGetMantineColor, useSearchParam } from "~/lib/hooks";
-import { getCoreDetails, serverGqlService } from "~/lib/utilities.server";
+import {
+	useCoreDetails,
+	useGetMantineColor,
+	useSearchParam,
+} from "~/lib/hooks";
+import { serverGqlService } from "~/lib/utilities.server";
 
 const searchParamsSchema = z.object({
 	page: zx.IntAsString.default("1"),
@@ -34,13 +38,12 @@ export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = unstable_defineLoader(async ({ request }) => {
 	const query = zx.parseQuery(request, searchParamsSchema);
-	const [coreDetails, { genresList }] = await Promise.all([
-		getCoreDetails(request),
+	const [{ genresList }] = await Promise.all([
 		serverGqlService.request(GenresListDocument, {
 			input: { page: query.page, query: query.query },
 		}),
 	]);
-	return { coreDetails, query, listGenres: genresList };
+	return { query, listGenres: genresList };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
@@ -49,6 +52,7 @@ export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 	const getMantineColor = useGetMantineColor();
 	const [_, { setP }] = useSearchParam();
 
@@ -107,8 +111,7 @@ export default function Page() {
 							value={loaderData.query.page}
 							onChange={(v) => setP("page", v.toString())}
 							total={Math.ceil(
-								loaderData.listGenres.details.total /
-									loaderData.coreDetails.pageLimit,
+								loaderData.listGenres.details.total / coreDetails.pageLimit,
 							)}
 						/>
 					</Center>
diff --git a/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx b/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
index ad285c8e74..598b7a8d33 100644
--- a/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
@@ -38,11 +38,13 @@ import {
 	MediaItemWithoutUpdateModal,
 } from "~/components/media";
 import { redirectToQueryParam } from "~/lib/generals";
-import { useSearchParam } from "~/lib/hooks";
+import {
+	useCoreDetails,
+	useSearchParam,
+	useUserPreferences,
+} from "~/lib/hooks";
 import {
 	getAuthorizationHeader,
-	getCoreDetails,
-	getUserPreferences,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
@@ -66,16 +68,12 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 		query: z.string().optional(),
 		page: zx.IntAsString.default("1"),
 	});
-	const [coreDetails, userPreferences] = await Promise.all([
-		getCoreDetails(request),
-		getUserPreferences(request),
-	]);
 	const [list, search] = await match(action)
 		.with(Action.List, async () => {
 			const { metadataGroupsList } = await serverGqlService.request(
 				MetadataGroupsListDocument,
 				{ input: { page, query } },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return [{ list: metadataGroupsList, url: {} }, undefined] as const;
 		})
@@ -91,7 +89,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 			const { metadataGroupSearch } = await serverGqlService.request(
 				MetadataGroupSearchDocument,
 				{ input: { lot, source: urlParse.source, search: { page, query } } },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return [
 				undefined,
@@ -99,15 +97,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 			] as const;
 		})
 		.exhaustive();
-	return {
-		action,
-		coreDetails: { pageLimit: coreDetails.pageLimit },
-		userPreferences: { reviewScale: userPreferences.general.reviewScale },
-		query,
-		page,
-		list,
-		search,
-	};
+	return { action, query, page, list, search };
 });
 
 export const meta = ({ params }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -116,6 +106,7 @@ export const meta = ({ params }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 	const [_, { setP }] = useSearchParam();
 	const navigate = useNavigate();
 
@@ -196,7 +187,7 @@ export default function Page() {
 										onChange={(v) => setP("page", v.toString())}
 										total={Math.ceil(
 											loaderData.list.list.details.total /
-												loaderData.coreDetails.pageLimit,
+												coreDetails.pageLimit,
 										)}
 									/>
 								</Center>
@@ -236,7 +227,7 @@ export default function Page() {
 										onChange={(v) => setP("page", v.toString())}
 										total={Math.ceil(
 											loaderData.search.search.details.total /
-												loaderData.coreDetails.pageLimit,
+												coreDetails.pageLimit,
 										)}
 									/>
 								</Center>
@@ -255,15 +246,15 @@ const GroupSearchItem = (props: {
 	item: Item;
 }) => {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
 	const navigate = useNavigate();
 	const [isLoading, setIsLoading] = useState(false);
 
 	return (
 		<MediaItemWithoutUpdateModal
 			item={props.item}
-			noRatingLink
 			noHref
-			reviewScale={loaderData.userPreferences.reviewScale}
+			reviewScale={userPreferences.general.reviewScale}
 			imageOverlayForLoadingIndicator={isLoading}
 			onClick={async (_) => {
 				if (loaderData.search) {
diff --git a/apps/frontend/app/routes/_dashboard.media.groups.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.groups.item.$id._index.tsx
index 4d0fd0f234..c970105103 100644
--- a/apps/frontend/app/routes/_dashboard.media.groups.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.groups.item.$id._index.tsx
@@ -10,7 +10,6 @@ import {
 	Text,
 	Title,
 } from "@mantine/core";
-import { useDisclosure } from "@mantine/hooks";
 import { unstable_defineLoader } from "@remix-run/node";
 import type { MetaArgs_SingleFetch } from "@remix-run/react";
 import { useLoaderData } from "@remix-run/react";
@@ -24,29 +23,22 @@ import {
 	IconMessageCircle2,
 	IconUser,
 } from "@tabler/icons-react";
-import { useState } from "react";
 import invariant from "tiny-invariant";
 import { z } from "zod";
 import { zx } from "zodix";
-import {
-	AddEntityToCollectionModal,
-	MediaDetailsLayout,
-} from "~/components/common";
+import { MediaDetailsLayout } from "~/components/common";
 import {
 	DisplayCollection,
 	MediaIsPartial,
 	MediaScrollArea,
 	PartialMetadataDisplay,
-	type PostReview,
-	PostReviewModal,
 	ReviewItemDisplay,
 	ToggleMediaMonitorMenuItem,
 } from "~/components/media";
+import { useUserPreferences } from "~/lib/hooks";
+import { useAddEntityToCollection, useReviewEntity } from "~/lib/state/media";
 import {
 	getAuthorizationHeader,
-	getUserCollectionsList,
-	getUserDetails,
-	getUserPreferences,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
@@ -60,31 +52,19 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 	const query = zx.parseQuery(request, searchParamsSchema);
 	const metadataGroupId = params.id;
 	invariant(metadataGroupId, "No ID provided");
-	const [
-		userPreferences,
-		userDetails,
-		{ metadataGroupDetails },
-		{ userMetadataGroupDetails },
-		collections,
-	] = await Promise.all([
-		getUserPreferences(request),
-		getUserDetails(request),
-		serverGqlService.request(MetadataGroupDetailsDocument, { metadataGroupId }),
-		serverGqlService.request(
-			UserMetadataGroupDetailsDocument,
-			{ metadataGroupId },
-			await getAuthorizationHeader(request),
-		),
-		getUserCollectionsList(request),
-	]);
+	const [{ metadataGroupDetails }, { userMetadataGroupDetails }] =
+		await Promise.all([
+			serverGqlService.request(MetadataGroupDetailsDocument, {
+				metadataGroupId,
+			}),
+			serverGqlService.request(
+				UserMetadataGroupDetailsDocument,
+				{ metadataGroupId },
+				getAuthorizationHeader(request),
+			),
+		]);
 	return {
 		query,
-		userPreferences: {
-			reviewScale: userPreferences.general.reviewScale,
-			disableReviews: userPreferences.general.disableReviews,
-		},
-		userDetails,
-		collections,
 		metadataGroupId,
 		metadataGroupDetails,
 		userMetadataGroupDetails,
@@ -97,154 +77,145 @@ export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
-	const [
-		collectionModalOpened,
-		{ open: collectionModalOpen, close: collectionModalClose },
-	] = useDisclosure(false);
-	const [postReviewModalData, setPostReviewModalData] = useState<
-		PostReview | undefined
-	>(undefined);
+	const userPreferences = useUserPreferences();
+	const [_r, setEntityToReview] = useReviewEntity();
+	const [_a, setAddEntityToCollectionData] = useAddEntityToCollection();
 
 	return (
-		<>
-			<PostReviewModal
-				onClose={() => setPostReviewModalData(undefined)}
-				opened={postReviewModalData !== undefined}
-				data={postReviewModalData}
-				entityType="metadataGroup"
-				objectId={loaderData.metadataGroupId.toString()}
-				reviewScale={loaderData.userPreferences.reviewScale}
-				title={loaderData.metadataGroupDetails.details.title}
-			/>
-			<Container>
-				<MediaDetailsLayout
-					images={loaderData.metadataGroupDetails.details.displayImages}
-					externalLink={{
-						source: loaderData.metadataGroupDetails.details.source,
-						lot: loaderData.metadataGroupDetails.details.lot,
-						href: loaderData.metadataGroupDetails.sourceUrl,
-					}}
-				>
-					<Title id="group-title">
-						{loaderData.metadataGroupDetails.details.title}
-					</Title>
-					<Flex id="group-details" wrap="wrap" gap={4}>
-						<Text>
-							{loaderData.metadataGroupDetails.details.parts} media items
-						</Text>
-					</Flex>
-					{loaderData.userMetadataGroupDetails.collections.length > 0 ? (
-						<Group>
-							{loaderData.userMetadataGroupDetails.collections.map((col) => (
-								<DisplayCollection
-									key={col.id}
-									col={col}
-									userId={col.userId}
-									entityId={loaderData.metadataGroupId.toString()}
-									entityLot={EntityLot.MediaGroup}
-								/>
-							))}
-						</Group>
-					) : null}
-					{loaderData.metadataGroupDetails.details.isPartial ? (
-						<MediaIsPartial mediaType="group" />
-					) : null}
-					<Tabs variant="outline" defaultValue={loaderData.query.defaultTab}>
-						<Tabs.List mb="xs">
-							<Tabs.Tab value="media" leftSection={<IconDeviceTv size={16} />}>
-								Media
-							</Tabs.Tab>
-							<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
-								Actions
+		<Container>
+			<MediaDetailsLayout
+				images={loaderData.metadataGroupDetails.details.displayImages}
+				externalLink={{
+					source: loaderData.metadataGroupDetails.details.source,
+					lot: loaderData.metadataGroupDetails.details.lot,
+					href: loaderData.metadataGroupDetails.sourceUrl,
+				}}
+			>
+				<Title id="group-title">
+					{loaderData.metadataGroupDetails.details.title}
+				</Title>
+				<Flex id="group-details" wrap="wrap" gap={4}>
+					<Text>
+						{loaderData.metadataGroupDetails.details.parts} media items
+					</Text>
+				</Flex>
+				{loaderData.userMetadataGroupDetails.collections.length > 0 ? (
+					<Group>
+						{loaderData.userMetadataGroupDetails.collections.map((col) => (
+							<DisplayCollection
+								col={col}
+								key={col.id}
+								creatorUserId={col.userId}
+								entityLot={EntityLot.MetadataGroup}
+								entityId={loaderData.metadataGroupId}
+							/>
+						))}
+					</Group>
+				) : null}
+				{loaderData.metadataGroupDetails.details.isPartial ? (
+					<MediaIsPartial mediaType="group" />
+				) : null}
+				<Tabs variant="outline" defaultValue={loaderData.query.defaultTab}>
+					<Tabs.List mb="xs">
+						<Tabs.Tab value="media" leftSection={<IconDeviceTv size={16} />}>
+							Media
+						</Tabs.Tab>
+						<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
+							Actions
+						</Tabs.Tab>
+						{!userPreferences.general.disableReviews ? (
+							<Tabs.Tab
+								value="reviews"
+								leftSection={<IconMessageCircle2 size={16} />}
+							>
+								Reviews
 							</Tabs.Tab>
-							{!loaderData.userPreferences.disableReviews ? (
-								<Tabs.Tab
-									value="reviews"
-									leftSection={<IconMessageCircle2 size={16} />}
+						) : null}
+					</Tabs.List>
+					<Tabs.Panel value="media">
+						<MediaScrollArea>
+							<SimpleGrid cols={{ base: 3, md: 4, lg: 5 }}>
+								{loaderData.metadataGroupDetails.contents.map((media) => (
+									<PartialMetadataDisplay
+										key={media.identifier}
+										media={media}
+									/>
+								))}
+							</SimpleGrid>
+						</MediaScrollArea>
+					</Tabs.Panel>
+					<Tabs.Panel value="actions">
+						<MediaScrollArea>
+							<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
+								<Button
+									variant="outline"
+									w="100%"
+									onClick={() => {
+										setEntityToReview({
+											entityId: loaderData.metadataGroupId,
+											entityLot: EntityLot.MetadataGroup,
+											entityTitle:
+												loaderData.metadataGroupDetails.details.title,
+										});
+									}}
 								>
-									Reviews
-								</Tabs.Tab>
-							) : null}
-						</Tabs.List>
-						<Tabs.Panel value="media">
-							<MediaScrollArea>
-								<SimpleGrid cols={{ base: 3, md: 4, lg: 5 }}>
-									{loaderData.metadataGroupDetails.contents.map((media) => (
-										<PartialMetadataDisplay
-											key={media.identifier}
-											media={media}
+									Post a review
+								</Button>
+								<Button
+									variant="outline"
+									onClick={() => {
+										setAddEntityToCollectionData({
+											entityId: loaderData.metadataGroupId,
+											entityLot: EntityLot.MetadataGroup,
+											alreadyInCollections:
+												loaderData.userMetadataGroupDetails.collections.map(
+													(c) => c.id,
+												),
+										});
+									}}
+								>
+									Add to collection
+								</Button>
+								<Menu shadow="md">
+									<Menu.Target>
+										<Button variant="outline">More actions</Button>
+									</Menu.Target>
+									<Menu.Dropdown>
+										<ToggleMediaMonitorMenuItem
+											inCollections={loaderData.userMetadataGroupDetails.collections.map(
+												(c) => c.name,
+											)}
+											formValue={loaderData.metadataGroupId}
+											entityLot={EntityLot.MetadataGroup}
 										/>
-									))}
-								</SimpleGrid>
-							</MediaScrollArea>
-						</Tabs.Panel>
-						<Tabs.Panel value="actions">
+									</Menu.Dropdown>
+								</Menu>
+							</SimpleGrid>
+						</MediaScrollArea>
+					</Tabs.Panel>
+					{!userPreferences.general.disableReviews ? (
+						<Tabs.Panel value="reviews">
 							<MediaScrollArea>
-								<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
-									<Button
-										variant="outline"
-										w="100%"
-										onClick={() => {
-											setPostReviewModalData({});
-										}}
-									>
-										Post a review
-									</Button>
-									<Button variant="outline" onClick={collectionModalOpen}>
-										Add to collection
-									</Button>
-									<AddEntityToCollectionModal
-										userId={loaderData.userDetails.id}
-										onClose={collectionModalClose}
-										opened={collectionModalOpened}
-										entityId={loaderData.metadataGroupId.toString()}
-										entityLot={EntityLot.MediaGroup}
-										collections={loaderData.collections}
-									/>
-									<Menu shadow="md">
-										<Menu.Target>
-											<Button variant="outline">More actions</Button>
-										</Menu.Target>
-										<Menu.Dropdown>
-											<ToggleMediaMonitorMenuItem
-												userId={loaderData.userDetails.id}
-												inCollections={loaderData.userMetadataGroupDetails.collections.map(
-													(c) => c.name,
-												)}
-												formValue={loaderData.metadataGroupId}
-												entityLot={EntityLot.MediaGroup}
+								{loaderData.userMetadataGroupDetails.reviews.length > 0 ? (
+									<Stack>
+										{loaderData.userMetadataGroupDetails.reviews.map((r) => (
+											<ReviewItemDisplay
+												review={r}
+												key={r.id}
+												entityId={loaderData.metadataGroupId}
+												title={loaderData.metadataGroupDetails.details.title}
+												entityLot={EntityLot.MetadataGroup}
 											/>
-										</Menu.Dropdown>
-									</Menu>
-								</SimpleGrid>
+										))}
+									</Stack>
+								) : (
+									<Text>No reviews</Text>
+								)}
 							</MediaScrollArea>
 						</Tabs.Panel>
-						{!loaderData.userPreferences.disableReviews ? (
-							<Tabs.Panel value="reviews">
-								<MediaScrollArea>
-									{loaderData.userMetadataGroupDetails.reviews.length > 0 ? (
-										<Stack>
-											{loaderData.userMetadataGroupDetails.reviews.map((r) => (
-												<ReviewItemDisplay
-													review={r}
-													key={r.id}
-													metadataGroupId={loaderData.metadataGroupId}
-													reviewScale={loaderData.userPreferences.reviewScale}
-													user={loaderData.userDetails}
-													title={loaderData.metadataGroupDetails.details.title}
-													entityType="metadataGroup"
-												/>
-											))}
-										</Stack>
-									) : (
-										<Text>No reviews</Text>
-									)}
-								</MediaScrollArea>
-							</Tabs.Panel>
-						) : null}
-					</Tabs>
-				</MediaDetailsLayout>
-			</Container>
-		</>
+					) : null}
+				</Tabs>
+			</MediaDetailsLayout>
+		</Container>
 	);
 }
diff --git a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
index 2740d42a99..80a175bd72 100644
--- a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
@@ -3,11 +3,9 @@ import {
 	ActionIcon,
 	Alert,
 	Anchor,
-	Autocomplete,
 	Avatar,
 	Box,
 	Button,
-	Checkbox,
 	Container,
 	Divider,
 	Flex,
@@ -16,25 +14,18 @@ import {
 	Indicator,
 	Menu,
 	Modal,
-	NumberInput,
 	Paper,
 	ScrollArea,
-	Select,
 	SimpleGrid,
-	Slider,
 	Stack,
 	Tabs,
 	Text,
 	TextInput,
 	Title,
 } from "@mantine/core";
-import { DateInput, DatePickerInput } from "@mantine/dates";
+import { DateInput } from "@mantine/dates";
 import { useDisclosure } from "@mantine/hooks";
-import {
-	redirect,
-	unstable_defineAction,
-	unstable_defineLoader,
-} from "@remix-run/node";
+import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
 import {
 	Form,
 	Link,
@@ -43,7 +34,6 @@ import {
 } from "@remix-run/react";
 import {
 	DeleteSeenItemDocument,
-	DeployBulkProgressUpdateDocument,
 	DeployUpdateMetadataJobDocument,
 	EditSeenItemDocument,
 	EntityLot,
@@ -65,16 +55,11 @@ import {
 } from "@ryot/ts-utils";
 import {
 	IconAlertCircle,
-	IconBook,
-	IconBrandPagekit,
 	IconBulb,
-	IconClock,
-	IconDeviceTv,
 	IconEdit,
 	IconInfoCircle,
 	IconMessageCircle2,
 	IconMovie,
-	IconPercentage,
 	IconPlayerPlay,
 	IconRotateClockwise,
 	IconStarFilled,
@@ -83,15 +68,16 @@ import {
 	IconX,
 } from "@tabler/icons-react";
 import type { HumanizeDurationOptions } from "humanize-duration-ts";
-import { Fragment, type ReactNode, useState } from "react";
+import { type ReactNode, useState } from "react";
 import { GroupedVirtuoso, Virtuoso } from "react-virtuoso";
 import { namedAction } from "remix-utils/named-action";
 import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
+import { withQuery } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
 import {
-	AddEntityToCollectionModal,
+	HiddenLocationInput,
 	MEDIA_DETAILS_HEIGHT,
 	MediaDetailsLayout,
 } from "~/components/common";
@@ -100,20 +86,21 @@ import {
 	MediaIsPartial,
 	MediaScrollArea,
 	PartialMetadataDisplay,
-	type PostReview,
-	PostReviewModal,
 	ReviewItemDisplay,
 	ToggleMediaMonitorMenuItem,
 } from "~/components/media";
 import events from "~/lib/events";
-import { Verb, dayjsLib, getVerb, redirectToQueryParam } from "~/lib/generals";
-import { useGetMantineColor } from "~/lib/hooks";
+import { Verb, dayjsLib, getVerb, queryClient } from "~/lib/generals";
+import { useGetMantineColor, useUserPreferences } from "~/lib/hooks";
+import {
+	useAddEntityToCollection,
+	useMetadataProgressUpdate,
+	useReviewEntity,
+} from "~/lib/state/media";
 import {
+	MetadataIdSchema,
 	createToastHeaders,
 	getAuthorizationHeader,
-	getUserCollectionsList,
-	getUserDetails,
-	getUserPreferences,
 	redirectWithToast,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -125,12 +112,7 @@ import {
 const JUST_WATCH_URL = "https://www.justwatch.com";
 
 const searchParamsSchema = z
-	.object({
-		defaultTab: z.string().optional(),
-		openProgressModal: zx.BoolAsString.optional(),
-		openReviewModal: zx.BoolAsString.optional(),
-		[redirectToQueryParam]: z.string().optional(),
-	})
+	.object({ defaultTab: z.string().optional() })
 	.merge(MetadataSpecificsSchema);
 
 export type SearchParams = z.infer<typeof searchParamsSchema>;
@@ -139,41 +121,15 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 	const query = zx.parseQuery(request, searchParamsSchema);
 	const metadataId = params.id;
 	invariant(metadataId, "No ID provided");
-	const [
-		userPreferences,
-		userDetails,
-		{ metadataDetails },
-		collections,
-		{ userMetadataDetails },
-	] = await Promise.all([
-		getUserPreferences(request),
-		getUserDetails(request),
+	const [{ metadataDetails }, { userMetadataDetails }] = await Promise.all([
 		serverGqlService.request(MetadataDetailsDocument, { metadataId }),
-		getUserCollectionsList(request),
 		serverGqlService.request(
 			UserMetadataDetailsDocument,
 			{ metadataId },
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
 	]);
-	return {
-		query,
-		userPreferences: {
-			reviewScale: userPreferences.general.reviewScale,
-			videosDisabled: userPreferences.general.disableVideos,
-			watchProvidersDisabled: userPreferences.general.disableWatchProviders,
-			peopleEnabled: userPreferences.featuresEnabled.media.people,
-			groupsEnabled: userPreferences.featuresEnabled.media.groups,
-			genresEnabled: userPreferences.featuresEnabled.media.genres,
-			watchProviders: userPreferences.general.watchProviders,
-			disableReviews: userPreferences.general.disableReviews,
-		},
-		userDetails,
-		metadataId,
-		metadataDetails,
-		collections,
-		userMetadataDetails,
-	};
+	return { query, metadataId, metadataDetails, userMetadataDetails };
 });
 
 export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -183,26 +139,12 @@ export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
 export const action = unstable_defineAction(async ({ request }) => {
 	const formData = await request.clone().formData();
 	return namedAction(request, {
-		individualProgressUpdate: async () => {
-			const submission = processSubmission(formData, bulkUpdateSchema);
-			await serverGqlService.request(
-				DeployBulkProgressUpdateDocument,
-				{ input: submission },
-				await getAuthorizationHeader(request),
-			);
-			return Response.json({ status: "success", tt: new Date() } as const, {
-				headers: await createToastHeaders({
-					type: "success",
-					message: "Progress updated successfully",
-				}),
-			});
-		},
 		deleteSeenItem: async () => {
 			const submission = processSubmission(formData, seenIdSchema);
 			await serverGqlService.request(
 				DeleteSeenItemDocument,
 				submission,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json({ status: "success", tt: new Date() } as const, {
 				headers: await createToastHeaders({
@@ -212,11 +154,11 @@ export const action = unstable_defineAction(async ({ request }) => {
 			});
 		},
 		deployUpdateMetadataJob: async () => {
-			const submission = processSubmission(formData, metadataIdSchema);
+			const submission = processSubmission(formData, MetadataIdSchema);
 			await serverGqlService.request(
 				DeployUpdateMetadataJobDocument,
 				submission,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json({ status: "success", tt: new Date() } as const, {
 				headers: await createToastHeaders({
@@ -230,7 +172,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				MergeMetadataDocument,
 				submission,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return redirectWithToast(
 				$path("/media/item/:id", { id: submission.mergeInto }),
@@ -242,7 +184,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				EditSeenItemDocument,
 				{ input: submission },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json({ status: "success", tt: new Date() } as const, {
 				headers: await createToastHeaders({
@@ -251,141 +193,9 @@ export const action = unstable_defineAction(async ({ request }) => {
 				}),
 			});
 		},
-		progressUpdate: async () => {
-			const submission = processSubmission(formData, progressUpdateSchema);
-			const variables = {
-				metadataId: submission.metadataId,
-				progress: "100",
-				date: submission.date,
-				showSeasonNumber: submission.showSeasonNumber,
-				showEpisodeNumber: submission.showEpisodeNumber,
-				podcastEpisodeNumber: submission.podcastEpisodeNumber,
-				animeEpisodeNumber: submission.animeEpisodeNumber,
-				mangaChapterNumber: submission.mangaChapterNumber,
-				mangaVolumeNumber: submission.mangaVolumeNumber,
-				providerWatchedOn: submission.providerWatchedOn,
-			};
-			let needsFinalUpdate = true;
-			const updates = [];
-			const showSpecifics = showSpecificsSchema.parse(
-				JSON.parse(submission.showSpecifics || "[]"),
-			);
-			const podcastSpecifics = podcastSpecificsSchema.parse(
-				JSON.parse(submission.podcastSpecifics || "[]"),
-			);
-			if (submission.metadataLot === MediaLot.Anime) {
-				if (submission.animeEpisodeNumber) {
-					if (submission.animeAllEpisodesBefore) {
-						for (let i = 1; i <= submission.animeEpisodeNumber; i++) {
-							updates.push({
-								...variables,
-								animeEpisodeNumber: i,
-							});
-						}
-						needsFinalUpdate = false;
-					}
-				}
-			}
-			if (submission.metadataLot === MediaLot.Manga) {
-				if (submission.mangaChapterNumber) {
-					if (submission.mangaAllChaptersBefore) {
-						for (let i = 1; i <= submission.mangaChapterNumber; i++) {
-							updates.push({
-								...variables,
-								mangaChapterNumber: i,
-							});
-						}
-						needsFinalUpdate = false;
-					}
-				}
-			}
-			if (submission.metadataLot === MediaLot.Show) {
-				if (submission.completeShow) {
-					for (const season of showSpecifics) {
-						for (const episode of season.episodes) {
-							updates.push({
-								...variables,
-								showSeasonNumber: season.seasonNumber,
-								showEpisodeNumber: episode,
-							});
-						}
-					}
-					needsFinalUpdate = false;
-				}
-				if (submission.onlySeason) {
-					const selectedSeason = showSpecifics.find(
-						(s) => s.seasonNumber === submission.showSeasonNumber,
-					);
-					invariant(selectedSeason, "No season selected");
-					needsFinalUpdate = false;
-					if (submission.showAllSeasonsBefore) {
-						for (const season of showSpecifics) {
-							if (season.seasonNumber > selectedSeason.seasonNumber) break;
-							for (const episode of season.episodes || []) {
-								updates.push({
-									...variables,
-									showSeasonNumber: season.seasonNumber,
-									showEpisodeNumber: episode,
-								});
-							}
-						}
-					} else {
-						for (const episode of selectedSeason.episodes || []) {
-							updates.push({
-								...variables,
-								showEpisodeNumber: episode,
-							});
-						}
-					}
-				}
-			}
-			if (submission.metadataLot === MediaLot.Podcast) {
-				if (submission.completePodcast) {
-					for (const episode of podcastSpecifics) {
-						updates.push({
-							...variables,
-							podcastEpisodeNumber: episode.episodeNumber,
-						});
-					}
-					needsFinalUpdate = false;
-				}
-			}
-			if (needsFinalUpdate) updates.push(variables);
-			const { deployBulkProgressUpdate } = await serverGqlService.request(
-				DeployBulkProgressUpdateDocument,
-				{ input: updates },
-				await getAuthorizationHeader(request),
-			);
-			const headers = {
-				headers: await createToastHeaders({
-					type: !deployBulkProgressUpdate ? "error" : "success",
-					message: !deployBulkProgressUpdate
-						? "Progress was not updated"
-						: "Progress updated successfully",
-				}),
-			};
-			if (submission[redirectToQueryParam])
-				return redirect(submission[redirectToQueryParam], headers);
-			return Response.json(
-				{ status: "success", tt: new Date() } as const,
-				headers,
-			);
-		},
 	});
 });
 
-const metadataIdSchema = z.object({ metadataId: z.string() });
-
-const bulkUpdateSchema = z
-	.object({
-		progress: z.string().optional(),
-		date: z.string().optional(),
-		changeState: z.nativeEnum(SeenState).optional(),
-		providerWatchedOn: z.string().optional(),
-	})
-	.merge(MetadataSpecificsSchema)
-	.merge(metadataIdSchema);
-
 const seenIdSchema = z.object({ seenId: z.string() });
 
 const mergeMetadataSchema = z.object({
@@ -403,65 +213,34 @@ const editSeenItem = z.object({
 	finishedOn: dateString.optional(),
 });
 
-const progressUpdateSchema = z
-	.object({
-		metadataLot: z.nativeEnum(MediaLot),
-		date: z.string().optional(),
-		[redirectToQueryParam]: z.string().optional(),
-		showSpecifics: z.string().optional(),
-		showAllSeasonsBefore: zx.CheckboxAsString.optional(),
-		podcastSpecifics: z.string().optional(),
-		onlySeason: zx.BoolAsString.optional(),
-		completeShow: zx.BoolAsString.optional(),
-		completePodcast: zx.BoolAsString.optional(),
-		animeAllEpisodesBefore: zx.CheckboxAsString.optional(),
-		mangaAllChaptersBefore: zx.CheckboxAsString.optional(),
-		providerWatchedOn: z.string().optional(),
-	})
-	.merge(metadataIdSchema)
-	.merge(MetadataSpecificsSchema);
-
-const showSpecificsSchema = z.array(
-	z.object({ seasonNumber: z.number(), episodes: z.array(z.number()) }),
-);
-
-const podcastSpecificsSchema = z.array(z.object({ episodeNumber: z.number() }));
-
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
 	const getMantineColor = useGetMantineColor();
 	const [tab, setTab] = useState<string | null>(
 		loaderData.query.defaultTab || "overview",
 	);
-	const [
-		progressModalOpened,
-		{ open: progressModalOpen, close: progressModalClose },
-	] = useDisclosure(false);
-	const [
-		collectionModalOpened,
-		{ open: collectionModalOpen, close: collectionModalClose },
-	] = useDisclosure(false);
 	const [
 		mergeMetadataModalOpened,
 		{ open: mergeMetadataModalOpen, close: mergeMetadataModalClose },
 	] = useDisclosure(false);
-	const [updateProgressModalData, setUpdateProgressModalData] = useState<
-		UpdateProgress | undefined
-	>(loaderData.query.openProgressModal ? {} : undefined);
-	const [postReviewModalData, setPostReviewModalData] = useState<
-		PostReview | undefined
-	>(loaderData.query.openReviewModal ? {} : undefined);
+	const [_m, setMetadataToUpdate] = useMetadataProgressUpdate();
+	const [_r, setEntityToReview] = useReviewEntity();
+	const [_a, setAddEntityToCollectionData] = useAddEntityToCollection();
 
 	const PutOnHoldBtn = () => {
 		return (
 			<Form
-				action="?intent=individualProgressUpdate"
+				action={withQuery($path("/actions"), {
+					intent: "individualProgressUpdate",
+				})}
 				method="post"
 				replace
 				onSubmit={() => {
 					events.updateProgress(loaderData.metadataDetails.title);
 				}}
 			>
+				<HiddenLocationInput />
 				<input hidden name="metadataId" defaultValue={loaderData.metadataId} />
 				<input hidden name="changeState" defaultValue={SeenState.OnAHold} />
 				<Menu.Item type="submit">Put on hold</Menu.Item>
@@ -471,13 +250,16 @@ export default function Page() {
 	const DropBtn = () => {
 		return (
 			<Form
-				action="?intent=individualProgressUpdate"
+				action={withQuery($path("/actions"), {
+					intent: "individualProgressUpdate",
+				})}
 				method="post"
 				replace
 				onSubmit={() => {
 					events.updateProgress(loaderData.metadataDetails.title);
 				}}
 			>
+				<HiddenLocationInput />
 				<input hidden name="metadataId" defaultValue={loaderData.metadataId} />
 				<input hidden name="changeState" defaultValue={SeenState.Dropped} />
 				<Menu.Item type="submit">Mark as dropped</Menu.Item>
@@ -494,1251 +276,864 @@ export default function Page() {
 	};
 
 	return (
-		<>
-			<MergeMetadataModal
-				onClose={mergeMetadataModalClose}
-				opened={mergeMetadataModalOpened}
-				metadataId={loaderData.metadataId}
-			/>
-			<ProgressUpdateModal
-				onClose={() => setUpdateProgressModalData(undefined)}
-				opened={updateProgressModalData !== undefined}
-				data={updateProgressModalData}
-			/>
-			<PostReviewModal
-				onClose={() => setPostReviewModalData(undefined)}
-				opened={postReviewModalData !== undefined}
-				data={postReviewModalData}
-				entityType="metadata"
-				objectId={loaderData.metadataId.toString()}
-				reviewScale={loaderData.userPreferences.reviewScale}
-				title={loaderData.metadataDetails.title}
-				lot={loaderData.metadataDetails.lot}
-			/>
-			<Container>
-				<MediaDetailsLayout
-					images={loaderData.metadataDetails.assets.images}
-					externalLink={{
-						source: loaderData.metadataDetails.source,
-						lot: loaderData.metadataDetails.lot,
-						href: loaderData.metadataDetails.sourceUrl,
-					}}
-				>
-					<Box>
-						{loaderData.userPreferences.groupsEnabled &&
-						loaderData.metadataDetails.group ? (
-							<Link
-								to={$path("/media/groups/item/:id", {
-									id: loaderData.metadataDetails.group.id,
-								})}
-								style={{ color: "unset" }}
+		<Container>
+			<MediaDetailsLayout
+				images={loaderData.metadataDetails.assets.images}
+				externalLink={{
+					source: loaderData.metadataDetails.source,
+					lot: loaderData.metadataDetails.lot,
+					href: loaderData.metadataDetails.sourceUrl,
+				}}
+			>
+				<Box>
+					{userPreferences.featuresEnabled.media.groups &&
+					loaderData.metadataDetails.group ? (
+						<Link
+							to={$path("/media/groups/item/:id", {
+								id: loaderData.metadataDetails.group.id,
+							})}
+							style={{ color: "unset" }}
+						>
+							<Text c="dimmed" fs="italic">
+								{loaderData.metadataDetails.group.name} #
+								{loaderData.metadataDetails.group.part}
+							</Text>
+						</Link>
+					) : null}
+					<Title id="media-title">{loaderData.metadataDetails.title}</Title>
+				</Box>
+				{loaderData.userMetadataDetails.collections.length > 0 ? (
+					<Group>
+						{loaderData.userMetadataDetails.collections.map((col) => (
+							<DisplayCollection
+								col={col}
+								key={col.id}
+								creatorUserId={col.userId}
+								entityLot={EntityLot.Metadata}
+								entityId={loaderData.metadataId}
+							/>
+						))}
+					</Group>
+				) : null}
+				{loaderData.metadataDetails.isPartial ? (
+					<MediaIsPartial mediaType="media" />
+				) : null}
+				<Text c="dimmed" fz={{ base: "sm", lg: "md" }}>
+					{[
+						loaderData.metadataDetails.publishDate
+							? dayjsLib(loaderData.metadataDetails.publishDate).format("LL")
+							: loaderData.metadataDetails.publishYear,
+						loaderData.metadataDetails.originalLanguage,
+						loaderData.metadataDetails.productionStatus,
+						loaderData.metadataDetails.bookSpecifics?.pages &&
+							`${loaderData.metadataDetails.bookSpecifics.pages} pages`,
+						loaderData.metadataDetails.podcastSpecifics?.totalEpisodes &&
+							`${loaderData.metadataDetails.podcastSpecifics.totalEpisodes} episodes`,
+						loaderData.metadataDetails.animeSpecifics?.episodes &&
+							`${loaderData.metadataDetails.animeSpecifics.episodes} episodes`,
+						loaderData.metadataDetails.mangaSpecifics?.chapters &&
+							`${loaderData.metadataDetails.mangaSpecifics.chapters} chapters`,
+						loaderData.metadataDetails.mangaSpecifics?.volumes &&
+							`${loaderData.metadataDetails.mangaSpecifics.volumes} volumes`,
+						loaderData.metadataDetails.movieSpecifics?.runtime &&
+							humanizeDuration(
+								dayjsLib
+									.duration(
+										loaderData.metadataDetails.movieSpecifics.runtime,
+										"minute",
+									)
+									.asMilliseconds(),
+							),
+						loaderData.metadataDetails.showSpecifics?.totalSeasons &&
+							`${loaderData.metadataDetails.showSpecifics.totalSeasons} seasons`,
+						loaderData.metadataDetails.showSpecifics?.totalEpisodes &&
+							`${loaderData.metadataDetails.showSpecifics.totalEpisodes} episodes`,
+						loaderData.metadataDetails.showSpecifics?.runtime &&
+							humanizeDuration(
+								dayjsLib
+									.duration(
+										loaderData.metadataDetails.showSpecifics.runtime,
+										"minute",
+									)
+									.asMilliseconds(),
+							),
+						loaderData.metadataDetails.audioBookSpecifics?.runtime &&
+							humanizeDuration(
+								dayjsLib
+									.duration(
+										loaderData.metadataDetails.audioBookSpecifics.runtime,
+										"minute",
+									)
+									.asMilliseconds(),
+							),
+					]
+						.filter(Boolean)
+						.join(" • ")}
+				</Text>
+				{loaderData.metadataDetails.providerRating ||
+				loaderData.userMetadataDetails.averageRating ? (
+					<Group>
+						{loaderData.metadataDetails.providerRating ? (
+							<Paper
+								p={4}
+								display="flex"
+								style={{
+									flexDirection: "column",
+									alignItems: "center",
+									gap: 6,
+								}}
 							>
-								<Text c="dimmed" fs="italic">
-									{loaderData.metadataDetails.group.name} #
-									{loaderData.metadataDetails.group.part}
-								</Text>
-							</Link>
-						) : null}
-						<Title id="media-title">{loaderData.metadataDetails.title}</Title>
-					</Box>
-					{loaderData.userMetadataDetails.collections.length > 0 ? (
-						<Group>
-							{loaderData.userMetadataDetails.collections.map((col) => (
-								<DisplayCollection
-									key={col.id}
-									col={col}
-									userId={col.userId}
-									entityId={loaderData.metadataId.toString()}
-									entityLot={EntityLot.Media}
+								<Image
+									alt="Logo"
+									h={24}
+									w={24}
+									src={`/provider-logos/${match(
+										loaderData.metadataDetails.source,
+									)
+										.with(MediaSource.Anilist, () => "anilist.svg")
+										.with(MediaSource.Audible, () => "audible.svg")
+										.with(MediaSource.GoogleBooks, () => "google-books.svg")
+										.with(MediaSource.Igdb, () => "igdb.svg")
+										.with(MediaSource.Itunes, () => "itunes.svg")
+										.with(MediaSource.Listennotes, () => "listennotes.webp")
+										.with(MediaSource.Mal, () => "mal.svg")
+										.with(MediaSource.MangaUpdates, () => "manga-updates.svg")
+										.with(MediaSource.Openlibrary, () => "openlibrary.svg")
+										.with(MediaSource.Tmdb, () => "tmdb.svg")
+										.with(MediaSource.Vndb, () => "vndb.ico")
+										.with(MediaSource.Custom, () => undefined)
+										.exhaustive()}`}
 								/>
-							))}
-						</Group>
-					) : null}
-					{loaderData.metadataDetails.isPartial ? (
-						<MediaIsPartial mediaType="media" />
-					) : null}
-					<Text c="dimmed" fz={{ base: "sm", lg: "md" }}>
-						{[
-							loaderData.metadataDetails.publishDate
-								? dayjsLib(loaderData.metadataDetails.publishDate).format("LL")
-								: loaderData.metadataDetails.publishYear,
-							loaderData.metadataDetails.originalLanguage,
-							loaderData.metadataDetails.productionStatus,
-							loaderData.metadataDetails.bookSpecifics?.pages &&
-								`${loaderData.metadataDetails.bookSpecifics.pages} pages`,
-							loaderData.metadataDetails.podcastSpecifics?.totalEpisodes &&
-								`${loaderData.metadataDetails.podcastSpecifics.totalEpisodes} episodes`,
-							loaderData.metadataDetails.animeSpecifics?.episodes &&
-								`${loaderData.metadataDetails.animeSpecifics.episodes} episodes`,
-							loaderData.metadataDetails.mangaSpecifics?.chapters &&
-								`${loaderData.metadataDetails.mangaSpecifics.chapters} chapters`,
-							loaderData.metadataDetails.mangaSpecifics?.volumes &&
-								`${loaderData.metadataDetails.mangaSpecifics.volumes} volumes`,
-							loaderData.metadataDetails.movieSpecifics?.runtime &&
-								humanizeDuration(
-									loaderData.metadataDetails.movieSpecifics.runtime * 1000 * 60,
-								),
-							loaderData.metadataDetails.showSpecifics?.totalSeasons &&
-								`${loaderData.metadataDetails.showSpecifics.totalSeasons} seasons`,
-							loaderData.metadataDetails.showSpecifics?.totalEpisodes &&
-								`${loaderData.metadataDetails.showSpecifics.totalEpisodes} episodes`,
-							loaderData.metadataDetails.showSpecifics?.runtime &&
-								humanizeDuration(
-									loaderData.metadataDetails.showSpecifics.runtime * 1000 * 60,
-								),
-							loaderData.metadataDetails.audioBookSpecifics?.runtime &&
-								humanizeDuration(
-									loaderData.metadataDetails.audioBookSpecifics.runtime *
-										1000 *
-										60,
-								),
-						]
-							.filter(Boolean)
-							.join(" • ")}
-					</Text>
-					{loaderData.metadataDetails.providerRating ||
-					loaderData.userMetadataDetails.averageRating ? (
-						<Group>
-							{loaderData.metadataDetails.providerRating ? (
-								<Paper
-									p={4}
-									display="flex"
-									style={{
-										flexDirection: "column",
-										alignItems: "center",
-										gap: 6,
-									}}
-								>
-									<Image
-										alt="Logo"
-										h={24}
-										w={24}
-										src={`/provider-logos/${match(
-											loaderData.metadataDetails.source,
+								<Text fz="sm">
+									{Number(loaderData.metadataDetails.providerRating).toFixed(1)}
+									{match(loaderData.metadataDetails.source)
+										.with(
+											MediaSource.Anilist,
+											MediaSource.Igdb,
+											MediaSource.Listennotes,
+											MediaSource.Tmdb,
+											MediaSource.Vndb,
+											() => "%",
 										)
-											.with(MediaSource.Anilist, () => "anilist.svg")
-											.with(MediaSource.Audible, () => "audible.svg")
-											.with(MediaSource.GoogleBooks, () => "google-books.svg")
-											.with(MediaSource.Igdb, () => "igdb.svg")
-											.with(MediaSource.Itunes, () => "itunes.svg")
-											.with(MediaSource.Listennotes, () => "listennotes.webp")
-											.with(MediaSource.Mal, () => "mal.svg")
-											.with(MediaSource.MangaUpdates, () => "manga-updates.svg")
-											.with(MediaSource.Openlibrary, () => "openlibrary.svg")
-											.with(MediaSource.Tmdb, () => "tmdb.svg")
-											.with(MediaSource.Vndb, () => "vndb.ico")
-											.with(MediaSource.Custom, () => undefined)
-											.exhaustive()}`}
-									/>
-									<Text fz="sm">
-										{Number(loaderData.metadataDetails.providerRating).toFixed(
-											1,
-										)}
-										{match(loaderData.metadataDetails.source)
-											.with(
-												MediaSource.Anilist,
-												MediaSource.Igdb,
-												MediaSource.Listennotes,
-												MediaSource.Tmdb,
-												MediaSource.Vndb,
-												() => "%",
-											)
-											.with(
-												MediaSource.Audible,
-												MediaSource.GoogleBooks,
-												() => "/5",
-											)
-											.with(
-												MediaSource.Mal,
-												MediaSource.MangaUpdates,
-												() => "/10",
-											)
-											.with(
-												MediaSource.Custom,
-												MediaSource.Itunes,
-												MediaSource.Openlibrary,
-												() => undefined,
-											)
-											.exhaustive()}
-									</Text>
-								</Paper>
-							) : null}
-							{loaderData.userMetadataDetails.averageRating ? (
-								<Paper
-									p={4}
-									display="flex"
-									style={{
-										flexDirection: "column",
-										alignItems: "center",
-										gap: 6,
-									}}
-								>
-									<IconStarFilled size={22} style={{ color: "#EBE600FF" }} />
-									<Text fz="sm">
-										{Number(
-											loaderData.userMetadataDetails.averageRating,
-										).toFixed(1)}
-										{loaderData.userPreferences.reviewScale ===
-										UserReviewScale.OutOfFive
-											? undefined
-											: "%"}
-									</Text>
-								</Paper>
-							) : null}
-						</Group>
-					) : null}
-					{loaderData.userMetadataDetails?.inProgress ? (
-						<Alert icon={<IconAlertCircle />} variant="outline">
-							You are currently{" "}
-							{getVerb(Verb.Read, loaderData.metadataDetails.lot)}
-							ing this (
-							{Number(
-								loaderData.userMetadataDetails.inProgress.progress,
-							).toFixed(2)}
-							%)
-						</Alert>
-					) : null}
-					<Tabs variant="outline" value={tab} onChange={(t) => setTab(t)}>
-						<Tabs.List mb="xs">
+										.with(
+											MediaSource.Audible,
+											MediaSource.GoogleBooks,
+											() => "/5",
+										)
+										.with(
+											MediaSource.Mal,
+											MediaSource.MangaUpdates,
+											() => "/10",
+										)
+										.with(
+											MediaSource.Custom,
+											MediaSource.Itunes,
+											MediaSource.Openlibrary,
+											() => undefined,
+										)
+										.exhaustive()}
+								</Text>
+							</Paper>
+						) : null}
+						{loaderData.userMetadataDetails.averageRating ? (
+							<Paper
+								p={4}
+								display="flex"
+								style={{
+									flexDirection: "column",
+									alignItems: "center",
+									gap: 6,
+								}}
+							>
+								<IconStarFilled size={22} style={{ color: "#EBE600FF" }} />
+								<Text fz="sm">
+									{Number(loaderData.userMetadataDetails.averageRating).toFixed(
+										1,
+									)}
+									{userPreferences.general.reviewScale ===
+									UserReviewScale.OutOfFive
+										? undefined
+										: "%"}
+								</Text>
+							</Paper>
+						) : null}
+					</Group>
+				) : null}
+				{loaderData.userMetadataDetails?.inProgress ? (
+					<Alert icon={<IconAlertCircle />} variant="outline">
+						You are currently{" "}
+						{getVerb(Verb.Read, loaderData.metadataDetails.lot)}
+						ing this (
+						{Number(loaderData.userMetadataDetails.inProgress.progress).toFixed(
+							2,
+						)}
+						%)
+					</Alert>
+				) : null}
+				<Tabs variant="outline" value={tab} onChange={(t) => setTab(t)}>
+					<Tabs.List mb="xs">
+						<Tabs.Tab
+							value="overview"
+							leftSection={<IconInfoCircle size={16} />}
+						>
+							Overview
+						</Tabs.Tab>
+						<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
+							Actions
+						</Tabs.Tab>
+						<Tabs.Tab
+							value="history"
+							leftSection={<IconRotateClockwise size={16} />}
+						>
+							History
+						</Tabs.Tab>
+						{loaderData.metadataDetails.lot === MediaLot.Show ? (
 							<Tabs.Tab
-								value="overview"
-								leftSection={<IconInfoCircle size={16} />}
+								value="showSeasons"
+								leftSection={<IconPlayerPlay size={16} />}
 							>
-								Overview
+								Seasons
 							</Tabs.Tab>
-							<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
-								Actions
+						) : null}
+						{loaderData.metadataDetails.lot === MediaLot.Podcast ? (
+							<Tabs.Tab
+								value="podcastEpisodes"
+								leftSection={<IconPlayerPlay size={16} />}
+							>
+								Episodes
 							</Tabs.Tab>
+						) : null}
+						{!userPreferences.general.disableReviews ? (
 							<Tabs.Tab
-								value="history"
-								leftSection={<IconRotateClockwise size={16} />}
+								value="reviews"
+								leftSection={<IconMessageCircle2 size={16} />}
 							>
-								History
+								Reviews
 							</Tabs.Tab>
-							{loaderData.metadataDetails.lot === MediaLot.Show ? (
-								<Tabs.Tab
-									value="showSeasons"
-									leftSection={<IconPlayerPlay size={16} />}
-								>
-									Seasons
-								</Tabs.Tab>
-							) : null}
-							{loaderData.metadataDetails.lot === MediaLot.Podcast ? (
-								<Tabs.Tab
-									value="podcastEpisodes"
-									leftSection={<IconPlayerPlay size={16} />}
-								>
-									Episodes
-								</Tabs.Tab>
-							) : null}
-							{!loaderData.userPreferences.disableReviews ? (
-								<Tabs.Tab
-									value="reviews"
-									leftSection={<IconMessageCircle2 size={16} />}
-								>
-									Reviews
-								</Tabs.Tab>
-							) : null}
+						) : null}
+						<Tabs.Tab value="suggestions" leftSection={<IconBulb size={16} />}>
+							Suggestions
+						</Tabs.Tab>
+						{!userPreferences.general.disableVideos &&
+						(loaderData.metadataDetails.assets.videos.length || 0) > 0 ? (
+							<Tabs.Tab value="videos" leftSection={<IconVideo size={16} />}>
+								Videos
+							</Tabs.Tab>
+						) : null}
+						{!userPreferences.general.disableWatchProviders ? (
 							<Tabs.Tab
-								value="suggestions"
-								leftSection={<IconBulb size={16} />}
+								value="watchProviders"
+								leftSection={<IconMovie size={16} />}
 							>
-								Suggestions
+								Watch On
 							</Tabs.Tab>
-							{!loaderData.userPreferences.videosDisabled &&
-							(loaderData.metadataDetails.assets.videos.length || 0) > 0 ? (
-								<Tabs.Tab value="videos" leftSection={<IconVideo size={16} />}>
-									Videos
-								</Tabs.Tab>
-							) : null}
-							{!loaderData.userPreferences.watchProvidersDisabled ? (
-								<Tabs.Tab
-									value="watchProviders"
-									leftSection={<IconMovie size={16} />}
+						) : null}
+					</Tabs.List>
+					<Tabs.Panel value="overview">
+						<MediaScrollArea>
+							<Stack gap="sm">
+								<SimpleGrid
+									cols={{ base: 3, xl: 4 }}
+									spacing={{ base: "md", lg: "xs" }}
 								>
-									Watch On
-								</Tabs.Tab>
-							) : null}
-						</Tabs.List>
-						<Tabs.Panel value="overview">
-							<MediaScrollArea>
-								<Stack gap="sm">
-									<SimpleGrid
-										cols={{ base: 3, xl: 4 }}
-										spacing={{ base: "md", lg: "xs" }}
-									>
-										{loaderData.userPreferences.genresEnabled
-											? loaderData.metadataDetails.genres
-													.slice(0, 12)
-													.map((g) => (
-														<Group key={g.id} wrap="nowrap">
-															<Box
-																h={11}
-																w={11}
-																style={{ borderRadius: 2, flex: "none" }}
-																bg={getMantineColor(g.name)}
-															/>
-															<Anchor
-																component={Link}
-																to={$path("/media/genre/:id", {
-																	id: g.id,
-																})}
-																fz="sm"
-																truncate
-															>
-																{g.name.trim()}
-															</Anchor>
-														</Group>
-													))
-											: null}
-									</SimpleGrid>
-									{loaderData.metadataDetails.description ? (
-										<div
-											// biome-ignore lint/security/noDangerouslySetInnerHtml: generated by the backend securely
-											dangerouslySetInnerHTML={{
-												__html: loaderData.metadataDetails.description,
-											}}
-										/>
-									) : null}
-									{loaderData.userPreferences.peopleEnabled ? (
-										<Stack>
-											{loaderData.metadataDetails.creators.map((c) => (
-												<Box key={c.name}>
-													<Text fw="bold">{c.name}</Text>
-													<ScrollArea
-														mt="xs"
-														w={{
-															base: 380,
-															xs: 440,
-															sm: 480,
-															md: 520,
-															lg: 580,
-														}}
-													>
-														<Flex gap="md">
-															{c.items.map((creator) => (
-																<Box key={`${creator.id}-${creator.name}`}>
-																	{creator.id ? (
-																		<Anchor
-																			component={Link}
-																			data-creator-id={creator.id}
-																			to={$path("/media/people/item/:id", {
-																				id: creator.id,
-																			})}
-																		>
-																			<MetadataCreator
-																				name={creator.name}
-																				image={creator.image}
-																				character={creator.character}
-																			/>
-																		</Anchor>
-																	) : (
+									{userPreferences.featuresEnabled.media.genres
+										? loaderData.metadataDetails.genres
+												.slice(0, 12)
+												.map((g) => (
+													<Group key={g.id} wrap="nowrap">
+														<Box
+															h={11}
+															w={11}
+															style={{ borderRadius: 2, flex: "none" }}
+															bg={getMantineColor(g.name)}
+														/>
+														<Anchor
+															component={Link}
+															to={$path("/media/genre/:id", {
+																id: g.id,
+															})}
+															fz="sm"
+															truncate
+														>
+															{g.name.trim()}
+														</Anchor>
+													</Group>
+												))
+										: null}
+								</SimpleGrid>
+								{loaderData.metadataDetails.description ? (
+									<div
+										// biome-ignore lint/security/noDangerouslySetInnerHtml: generated by the backend securely
+										dangerouslySetInnerHTML={{
+											__html: loaderData.metadataDetails.description,
+										}}
+									/>
+								) : null}
+								{userPreferences.featuresEnabled.media.people ? (
+									<Stack>
+										{loaderData.metadataDetails.creators.map((c) => (
+											<Box key={c.name}>
+												<Text fw="bold">{c.name}</Text>
+												<ScrollArea
+													mt="xs"
+													w={{
+														base: 380,
+														xs: 440,
+														sm: 480,
+														md: 520,
+														lg: 580,
+													}}
+												>
+													<Flex gap="md">
+														{c.items.map((creator) => (
+															<Box key={`${creator.id}-${creator.name}`}>
+																{creator.id ? (
+																	<Anchor
+																		component={Link}
+																		data-creator-id={creator.id}
+																		to={$path("/media/people/item/:id", {
+																			id: creator.id,
+																		})}
+																	>
 																		<MetadataCreator
 																			name={creator.name}
 																			image={creator.image}
 																			character={creator.character}
 																		/>
-																	)}
-																</Box>
-															))}
-														</Flex>
-													</ScrollArea>
-												</Box>
-											))}
-										</Stack>
-									) : null}
-								</Stack>
-							</MediaScrollArea>
-						</Tabs.Panel>
-						<Tabs.Panel value="actions">
-							<MediaScrollArea>
-								<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
-									{loaderData.userMetadataDetails.inProgress ? (
-										<IndividualProgressModal
-											title={loaderData.metadataDetails.title}
-											progress={Number(
-												loaderData.userMetadataDetails.inProgress.progress,
-											)}
-											inProgress={loaderData.userMetadataDetails.inProgress}
-											metadataId={loaderData.metadataId}
-											onClose={progressModalClose}
-											opened={progressModalOpened}
-											lot={loaderData.metadataDetails.lot}
-											total={
-												loaderData.metadataDetails.audioBookSpecifics
-													?.runtime ||
-												loaderData.metadataDetails.bookSpecifics?.pages ||
-												loaderData.metadataDetails.movieSpecifics?.runtime ||
-												loaderData.metadataDetails.mangaSpecifics?.chapters ||
-												loaderData.metadataDetails.animeSpecifics?.episodes ||
-												loaderData.metadataDetails.visualNovelSpecifics?.length
-											}
-										/>
-									) : null}
-									<Menu shadow="md">
-										<Menu.Target>
-											<Button variant="outline">Update progress</Button>
-										</Menu.Target>
-										<Menu.Dropdown>
-											{loaderData.metadataDetails.lot === MediaLot.Show ? (
-												<>
-													<Menu.Label>Shows</Menu.Label>
-													{loaderData.userMetadataDetails.nextEntry ? (
-														<>
-															<Menu.Item
-																onClick={() => {
-																	setUpdateProgressModalData({
-																		showSeasonNumber:
-																			loaderData.metadataDetails.lot ===
-																			MediaLot.Show
-																				? loaderData.userMetadataDetails
-																						.nextEntry?.season
-																				: undefined,
-																		showEpisodeNumber:
-																			loaderData.metadataDetails.lot ===
-																			MediaLot.Show
-																				? loaderData.userMetadataDetails
-																						.nextEntry?.episode
-																				: undefined,
-																	});
-																}}
-															>
-																Mark{" "}
-																{`S${loaderData.userMetadataDetails.nextEntry?.season}-E${loaderData.userMetadataDetails.nextEntry?.episode}`}{" "}
-																as seen
-															</Menu.Item>
-															<PutOnHoldBtn />
-														</>
-													) : null}
-													{loaderData.userMetadataDetails.history.length !==
-													0 ? (
-														<DropBtn />
-													) : (
-														<Menu.Item disabled>
-															No history. Update from the seasons tab.
-														</Menu.Item>
-													)}
-												</>
-											) : null}
-											{loaderData.metadataDetails.lot === MediaLot.Podcast ? (
-												<>
-													<Menu.Label>Podcasts</Menu.Label>
-													{loaderData.userMetadataDetails.nextEntry ? (
-														<>
-															<Menu.Item
-																onClick={() => {
-																	setUpdateProgressModalData({
-																		podcastEpisodeNumber:
-																			loaderData.metadataDetails.lot ===
-																			MediaLot.Podcast
-																				? loaderData.userMetadataDetails
-																						.nextEntry?.episode
-																				: undefined,
-																	});
-																}}
-															>
-																Mark EP-
-																{
-																	loaderData.userMetadataDetails.nextEntry
-																		?.episode
-																}{" "}
-																as listened
-															</Menu.Item>
-															<PutOnHoldBtn />
-														</>
-													) : null}
-													{loaderData.userMetadataDetails.history.length !==
-													0 ? (
-														<DropBtn />
-													) : (
-														<Menu.Item disabled>
-															No history. Update from the episodes tab.
+																	</Anchor>
+																) : (
+																	<MetadataCreator
+																		name={creator.name}
+																		image={creator.image}
+																		character={creator.character}
+																	/>
+																)}
+															</Box>
+														))}
+													</Flex>
+												</ScrollArea>
+											</Box>
+										))}
+									</Stack>
+								) : null}
+							</Stack>
+						</MediaScrollArea>
+					</Tabs.Panel>
+					<Tabs.Panel value="actions">
+						<MediaScrollArea>
+							<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
+								<Menu shadow="md">
+									<Menu.Target>
+										<Button variant="outline">Update progress</Button>
+									</Menu.Target>
+									<Menu.Dropdown>
+										{loaderData.metadataDetails.lot === MediaLot.Show ? (
+											<>
+												<Menu.Label>Shows</Menu.Label>
+												{loaderData.userMetadataDetails.nextEntry ? (
+													<>
+														<Menu.Item
+															onClick={() => {
+																setMetadataToUpdate({
+																	metadataId: loaderData.metadataId,
+																	showSeasonNumber:
+																		loaderData.metadataDetails.lot ===
+																		MediaLot.Show
+																			? loaderData.userMetadataDetails.nextEntry
+																					?.season
+																			: undefined,
+																	showEpisodeNumber:
+																		loaderData.metadataDetails.lot ===
+																		MediaLot.Show
+																			? loaderData.userMetadataDetails.nextEntry
+																					?.episode
+																			: undefined,
+																});
+															}}
+														>
+															Mark{" "}
+															{`S${loaderData.userMetadataDetails.nextEntry?.season}-E${loaderData.userMetadataDetails.nextEntry?.episode}`}{" "}
+															as seen
 														</Menu.Item>
-													)}
-												</>
-											) : null}
-											{loaderData.userMetadataDetails?.inProgress ? (
-												<>
-													<Menu.Label>In progress</Menu.Label>
-													<Menu.Item onClick={progressModalOpen}>
-														Set progress
+														<PutOnHoldBtn />
+													</>
+												) : null}
+												{loaderData.userMetadataDetails.history.length !== 0 ? (
+													<DropBtn />
+												) : (
+													<Menu.Item disabled>
+														No history. Update from the seasons tab.
 													</Menu.Item>
-													{loaderData.metadataDetails.lot !== MediaLot.Show &&
-													loaderData.metadataDetails.lot !==
-														MediaLot.Podcast ? (
-														<StateChangeButtons />
-													) : null}
-													<Form
-														action="?intent=individualProgressUpdate"
-														method="post"
-														replace
-														onSubmit={() => {
-															events.updateProgress(
-																loaderData.metadataDetails.title,
-															);
-														}}
-													>
-														<input hidden name="progress" defaultValue={100} />
-														<input
-															hidden
-															name="date"
-															defaultValue={formatDateToNaiveDate(new Date())}
-														/>
+												)}
+											</>
+										) : null}
+										{loaderData.metadataDetails.lot === MediaLot.Podcast ? (
+											<>
+												<Menu.Label>Podcasts</Menu.Label>
+												{loaderData.userMetadataDetails.nextEntry ? (
+													<>
 														<Menu.Item
-															type="submit"
-															name="metadataId"
-															value={loaderData.metadataId}
+															onClick={() => {
+																setMetadataToUpdate({
+																	metadataId: loaderData.metadataId,
+																	podcastEpisodeNumber:
+																		loaderData.metadataDetails.lot ===
+																		MediaLot.Podcast
+																			? loaderData.userMetadataDetails.nextEntry
+																					?.episode
+																			: undefined,
+																});
+															}}
 														>
-															I finished it
+															Mark EP-
+															{
+																loaderData.userMetadataDetails.nextEntry
+																	?.episode
+															}{" "}
+															as listened
 														</Menu.Item>
-													</Form>
-												</>
-											) : loaderData.metadataDetails.lot !== MediaLot.Show &&
-												loaderData.metadataDetails.lot !== MediaLot.Podcast ? (
-												<>
-													<Menu.Label>Not in progress</Menu.Label>
-													<Form
-														action="?intent=individualProgressUpdate"
-														method="post"
-														replace
-														onSubmit={() => {
-															events.updateProgress(
-																loaderData.metadataDetails.title,
-															);
-														}}
-													>
-														<input hidden name="progress" defaultValue={0} />
-														{![MediaLot.Anime, MediaLot.Manga].includes(
-															loaderData.metadataDetails.lot,
-														) ? (
-															<Menu.Item
-																type="submit"
-																name="metadataId"
-																value={loaderData.metadataId}
-															>
-																I'm{" "}
-																{getVerb(
-																	Verb.Read,
-																	loaderData.metadataDetails.lot,
-																)}
-																ing it
-															</Menu.Item>
-														) : null}
-													</Form>
-													<Menu.Item
-														onClick={() => {
-															setUpdateProgressModalData({});
-														}}
-													>
-														Add to{" "}
-														{getVerb(Verb.Read, loaderData.metadataDetails.lot)}{" "}
-														history
+														<PutOnHoldBtn />
+													</>
+												) : null}
+												{loaderData.userMetadataDetails.history.length !== 0 ? (
+													<DropBtn />
+												) : (
+													<Menu.Item disabled>
+														No history. Update from the episodes tab.
 													</Menu.Item>
-												</>
-											) : null}
-										</Menu.Dropdown>
-									</Menu>
-									{!loaderData.userPreferences.disableReviews ? (
-										<Button
-											variant="outline"
-											w="100%"
-											onClick={() => {
-												setPostReviewModalData({
-													showSeasonNumber:
-														loaderData.userMetadataDetails?.nextEntry?.season ??
-														undefined,
-													showEpisodeNumber:
-														loaderData.metadataDetails.lot === MediaLot.Show
-															? loaderData.userMetadataDetails?.nextEntry
-																	?.episode ?? undefined
-															: null,
-													podcastEpisodeNumber:
-														loaderData.metadataDetails.lot === MediaLot.Podcast
-															? loaderData.userMetadataDetails?.nextEntry
-																	?.episode ?? undefined
-															: null,
-												});
-											}}
-										>
-											Post a review
-										</Button>
-									) : null}
-									<>
-										<Button variant="outline" onClick={collectionModalOpen}>
-											Add to collection
-										</Button>
-										<AddEntityToCollectionModal
-											userId={loaderData.userDetails.id}
-											onClose={collectionModalClose}
-											opened={collectionModalOpened}
-											entityId={loaderData.metadataId.toString()}
-											entityLot={EntityLot.Media}
-											collections={loaderData.collections}
-										/>
-									</>
-									<Menu shadow="md">
-										<Menu.Target>
-											<Button variant="outline">More actions</Button>
-										</Menu.Target>
-										<Menu.Dropdown>
-											<ToggleMediaMonitorMenuItem
-												userId={loaderData.userDetails.id}
-												inCollections={loaderData.userMetadataDetails.collections.map(
-													(c) => c.name,
 												)}
-												formValue={loaderData.metadataId}
-												entityLot={EntityLot.Media}
-											/>
-											{loaderData.metadataDetails.source !==
-											MediaSource.Custom ? (
+											</>
+										) : null}
+										{loaderData.userMetadataDetails?.inProgress ? (
+											<>
+												<Menu.Label>In progress</Menu.Label>
+												<Menu.Item
+													onClick={() => {
+														setMetadataToUpdate({
+															metadataId: loaderData.metadataId,
+														});
+													}}
+												>
+													Set progress
+												</Menu.Item>
+												{loaderData.metadataDetails.lot !== MediaLot.Show &&
+												loaderData.metadataDetails.lot !== MediaLot.Podcast ? (
+													<StateChangeButtons />
+												) : null}
 												<Form
-													action="?intent=deployUpdateMetadataJob"
+													action={withQuery($path("/actions"), {
+														intent: "individualProgressUpdate",
+													})}
 													method="post"
 													replace
+													onSubmit={() => {
+														events.updateProgress(
+															loaderData.metadataDetails.title,
+														);
+													}}
 												>
+													<HiddenLocationInput />
+													<input hidden name="progress" defaultValue={100} />
+													<input
+														hidden
+														name="date"
+														defaultValue={formatDateToNaiveDate(new Date())}
+													/>
 													<Menu.Item
 														type="submit"
 														name="metadataId"
 														value={loaderData.metadataId}
 													>
-														Update metadata
+														I finished it
 													</Menu.Item>
 												</Form>
-											) : null}
-											<Menu.Item onClick={mergeMetadataModalOpen}>
-												Merge media
-											</Menu.Item>
-										</Menu.Dropdown>
-									</Menu>
-								</SimpleGrid>
-							</MediaScrollArea>
-						</Tabs.Panel>
-						<Tabs.Panel value="history">
-							{loaderData.userMetadataDetails.seenByAllCount > 0 ||
-							loaderData.userMetadataDetails.seenByUserCount > 0 ||
-							loaderData.userMetadataDetails.unitsConsumed ? (
-								<Stack h={MEDIA_DETAILS_HEIGHT}>
-									<Box>
+											</>
+										) : loaderData.metadataDetails.lot !== MediaLot.Show &&
+											loaderData.metadataDetails.lot !== MediaLot.Podcast ? (
+											<>
+												<Menu.Label>Not in progress</Menu.Label>
+												<Form
+													action={withQuery($path("/actions"), {
+														intent: "individualProgressUpdate",
+													})}
+													method="post"
+													replace
+													onSubmit={() => {
+														events.updateProgress(
+															loaderData.metadataDetails.title,
+														);
+													}}
+												>
+													<HiddenLocationInput />
+													<input hidden name="progress" defaultValue={0} />
+													{![MediaLot.Anime, MediaLot.Manga].includes(
+														loaderData.metadataDetails.lot,
+													) ? (
+														<Menu.Item
+															type="submit"
+															name="metadataId"
+															value={loaderData.metadataId}
+														>
+															I'm{" "}
+															{getVerb(
+																Verb.Read,
+																loaderData.metadataDetails.lot,
+															)}
+															ing it
+														</Menu.Item>
+													) : null}
+												</Form>
+												<Menu.Item
+													onClick={() => {
+														setMetadataToUpdate({
+															metadataId: loaderData.metadataId,
+														});
+													}}
+												>
+													Add to{" "}
+													{getVerb(Verb.Read, loaderData.metadataDetails.lot)}{" "}
+													history
+												</Menu.Item>
+											</>
+										) : null}
+									</Menu.Dropdown>
+								</Menu>
+								{!userPreferences.general.disableReviews ? (
+									<Button
+										variant="outline"
+										w="100%"
+										onClick={() => {
+											setEntityToReview({
+												entityId: loaderData.metadataId,
+												entityLot: EntityLot.Metadata,
+												entityTitle: loaderData.metadataDetails.title,
+												metadataLot: loaderData.metadataDetails.lot,
+												existingReview: {
+													showExtraInformation: {
+														episode:
+															loaderData.userMetadataDetails?.nextEntry
+																?.episode || undefined,
+														season:
+															loaderData.userMetadataDetails?.nextEntry
+																?.season || undefined,
+													},
+													podcastExtraInformation: {
+														episode:
+															loaderData.userMetadataDetails?.nextEntry
+																?.episode || undefined,
+													},
+													mangaExtraInformation: {
+														chapter:
+															loaderData.userMetadataDetails?.nextEntry
+																?.chapter || undefined,
+													},
+													animeExtraInformation: {
+														episode:
+															loaderData.userMetadataDetails?.nextEntry
+																?.episode || undefined,
+													},
+												},
+											});
+										}}
+									>
+										Post a review
+									</Button>
+								) : null}
+								<>
+									<Button
+										variant="outline"
+										onClick={() => {
+											setAddEntityToCollectionData({
+												entityId: loaderData.metadataId,
+												entityLot: EntityLot.Metadata,
+												alreadyInCollections:
+													loaderData.userMetadataDetails.collections.map(
+														(c) => c.id,
+													),
+											});
+										}}
+									>
+										Add to collection
+									</Button>
+								</>
+								<Menu shadow="md">
+									<Menu.Target>
+										<Button variant="outline">More actions</Button>
+									</Menu.Target>
+									<Menu.Dropdown>
+										<ToggleMediaMonitorMenuItem
+											inCollections={loaderData.userMetadataDetails.collections.map(
+												(c) => c.name,
+											)}
+											formValue={loaderData.metadataId}
+											entityLot={EntityLot.Metadata}
+										/>
+										{loaderData.metadataDetails.source !==
+										MediaSource.Custom ? (
+											<Form
+												action="?intent=deployUpdateMetadataJob"
+												method="post"
+												replace
+												onSubmit={async () => {
+													await queryClient.invalidateQueries({
+														queryKey: [
+															"metadataDetails",
+															loaderData.metadataId,
+														],
+													});
+												}}
+											>
+												<Menu.Item
+													type="submit"
+													name="metadataId"
+													value={loaderData.metadataId}
+												>
+													Update metadata
+												</Menu.Item>
+											</Form>
+										) : null}
+										<Menu.Item onClick={mergeMetadataModalOpen}>
+											Merge media
+										</Menu.Item>
+										<MergeMetadataModal
+											onClose={mergeMetadataModalClose}
+											opened={mergeMetadataModalOpened}
+											metadataId={loaderData.metadataId}
+										/>
+									</Menu.Dropdown>
+								</Menu>
+							</SimpleGrid>
+						</MediaScrollArea>
+					</Tabs.Panel>
+					<Tabs.Panel value="history">
+						{loaderData.userMetadataDetails.seenByAllCount > 0 ||
+						loaderData.userMetadataDetails.seenByUserCount > 0 ||
+						loaderData.userMetadataDetails.unitsConsumed ? (
+							<Stack h={MEDIA_DETAILS_HEIGHT} gap="xs">
+								<Box>
+									<Text fz={{ base: "sm", md: "md" }}>
+										Seen by all users{" "}
+										{loaderData.userMetadataDetails.seenByAllCount} time
+										{loaderData.userMetadataDetails.seenByAllCount > 1
+											? "s"
+											: ""}{" "}
+										and {loaderData.userMetadataDetails.seenByUserCount} time
+										{loaderData.userMetadataDetails &&
+										loaderData.userMetadataDetails.seenByUserCount > 1
+											? "s"
+											: ""}{" "}
+										by you.
+									</Text>
+									{loaderData.userMetadataDetails.unitsConsumed ? (
 										<Text fz={{ base: "sm", md: "md" }}>
-											Seen by all users{" "}
-											{loaderData.userMetadataDetails.seenByAllCount} time
-											{loaderData.userMetadataDetails.seenByAllCount > 1
-												? "s"
-												: ""}{" "}
-											and {loaderData.userMetadataDetails.seenByUserCount} time
-											{loaderData.userMetadataDetails &&
-											loaderData.userMetadataDetails.seenByUserCount > 1
-												? "s"
-												: ""}{" "}
-											by you.
+											Consumed{" "}
+											{match(loaderData.metadataDetails.lot)
+												.with(
+													MediaLot.AudioBook,
+													MediaLot.Movie,
+													MediaLot.Show,
+													MediaLot.Podcast,
+													MediaLot.VisualNovel,
+													() =>
+														humanizeDuration(
+															(loaderData.userMetadataDetails.unitsConsumed ||
+																0) *
+																1000 *
+																60,
+														),
+												)
+												.otherwise(
+													(v) =>
+														`${loaderData.userMetadataDetails.unitsConsumed} ${match(
+															v,
+														)
+															.with(MediaLot.VideoGame, () => "")
+															.with(MediaLot.Book, () => "pages")
+															.with(MediaLot.Anime, () => "episodes")
+															.with(MediaLot.Manga, () => "chapters")
+															.exhaustive()}`,
+												)}
+											.
 										</Text>
-										{loaderData.userMetadataDetails.unitsConsumed ? (
-											<Text fz={{ base: "sm", md: "md" }}>
-												Consumed{" "}
-												{match(loaderData.metadataDetails.lot)
-													.with(
-														MediaLot.AudioBook,
-														MediaLot.Movie,
-														MediaLot.Show,
-														MediaLot.Podcast,
-														MediaLot.VisualNovel,
-														() =>
-															humanizeDuration(
-																(loaderData.userMetadataDetails.unitsConsumed ||
-																	0) *
-																	1000 *
-																	60,
-															),
-													)
-													.otherwise(
-														(v) =>
-															`${loaderData.userMetadataDetails.unitsConsumed} ${match(
-																v,
-															)
-																.with(MediaLot.VideoGame, () => "")
-																.with(MediaLot.Book, () => "pages")
-																.with(MediaLot.Anime, () => "episodes")
-																.with(MediaLot.Manga, () => "chapters")
-																.exhaustive()}`,
-													)}
-												.
-											</Text>
-										) : null}
-									</Box>
-									<Virtuoso
-										data={loaderData.userMetadataDetails.history}
-										itemContent={(_, history) => (
-											<SeenItem history={history} key={history.id} />
-										)}
-									/>
-								</Stack>
-							) : (
-								<Text>No history</Text>
-							)}
-						</Tabs.Panel>
-						<Tabs.Panel value="showSeasons">
-							{loaderData.metadataDetails.showSpecifics &&
-							loaderData.userMetadataDetails.showProgress ? (
-								<Box h={MEDIA_DETAILS_HEIGHT}>
-									<GroupedVirtuoso
-										groupCounts={loaderData.metadataDetails.showSpecifics.seasons.map(
-											(season) => season.episodes.length,
-										)}
-										groupContent={(index) => (
-											<DisplayShowSeason
-												seasonIdx={index}
-												setData={setUpdateProgressModalData}
-												showProgress={
-													loaderData.userMetadataDetails.showProgress
-												}
-											/>
-										)}
-										itemContent={(index, groupIndex) => (
-											<DisplayShowEpisode
-												overallIdx={index}
-												seasonIdx={groupIndex}
-												setData={setUpdateProgressModalData}
-												seasonProgress={
-													loaderData.userMetadataDetails.showProgress
-												}
-												seasonNumber={
-													// biome-ignore lint/style/noNonNullAssertion: typescript error
-													loaderData.metadataDetails.showSpecifics!.seasons[
-														groupIndex
-													].seasonNumber
-												}
-											/>
-										)}
-									/>
+									) : null}
 								</Box>
-							) : undefined}
-						</Tabs.Panel>
-						{loaderData.metadataDetails.podcastSpecifics ? (
-							<Tabs.Panel value="podcastEpisodes" h={MEDIA_DETAILS_HEIGHT}>
 								<Virtuoso
-									data={loaderData.metadataDetails.podcastSpecifics.episodes}
-									itemContent={(podcastEpisodeIdx, podcastEpisode) => (
-										<DisplayPodcastEpisode
-											key={podcastEpisode.id}
-											episode={podcastEpisode}
-											index={podcastEpisodeIdx}
-											setData={setUpdateProgressModalData}
-											podcastProgress={
-												loaderData.userMetadataDetails.podcastProgress
+									data={loaderData.userMetadataDetails.history}
+									itemContent={(index, history) => (
+										<SeenItem
+											history={history}
+											key={history.id}
+											index={index}
+										/>
+									)}
+								/>
+							</Stack>
+						) : (
+							<Text>No history</Text>
+						)}
+					</Tabs.Panel>
+					<Tabs.Panel value="showSeasons">
+						{loaderData.metadataDetails.showSpecifics &&
+						loaderData.userMetadataDetails.showProgress ? (
+							<Box h={MEDIA_DETAILS_HEIGHT}>
+								<GroupedVirtuoso
+									groupCounts={loaderData.metadataDetails.showSpecifics.seasons.map(
+										(season) => season.episodes.length,
+									)}
+									groupContent={(index) => (
+										<DisplayShowSeason
+											seasonIdx={index}
+											showProgress={loaderData.userMetadataDetails.showProgress}
+										/>
+									)}
+									itemContent={(index, groupIndex) => (
+										<DisplayShowEpisode
+											overallIdx={index}
+											seasonIdx={groupIndex}
+											seasonProgress={
+												loaderData.userMetadataDetails.showProgress
+											}
+											seasonNumber={
+												// biome-ignore lint/style/noNonNullAssertion: typescript error
+												loaderData.metadataDetails.showSpecifics!.seasons[
+													groupIndex
+												].seasonNumber
 											}
 										/>
 									)}
 								/>
-							</Tabs.Panel>
-						) : undefined}
-						{!loaderData.userPreferences.disableReviews ? (
-							<Tabs.Panel value="reviews">
-								{loaderData.userMetadataDetails.reviews.length > 0 ? (
-									<MediaScrollArea>
-										<Stack>
-											{loaderData.userMetadataDetails.reviews.map((r) => (
-												<ReviewItemDisplay
-													entityType="metadata"
-													review={r}
-													key={r.id}
-													metadataId={loaderData.metadataId}
-													reviewScale={loaderData.userPreferences.reviewScale}
-													user={loaderData.userDetails}
-													title={loaderData.metadataDetails.title}
-													lot={loaderData.metadataDetails.lot}
-												/>
-											))}
-										</Stack>
-									</MediaScrollArea>
-								) : (
-									<Text>No reviews</Text>
-								)}
-							</Tabs.Panel>
+							</Box>
 						) : null}
-						<Tabs.Panel value="suggestions">
-							{loaderData.metadataDetails.suggestions.length > 0 ? (
+					</Tabs.Panel>
+					{loaderData.metadataDetails.podcastSpecifics ? (
+						<Tabs.Panel value="podcastEpisodes" h={MEDIA_DETAILS_HEIGHT}>
+							<Virtuoso
+								data={loaderData.metadataDetails.podcastSpecifics.episodes}
+								itemContent={(podcastEpisodeIdx, podcastEpisode) => (
+									<DisplayPodcastEpisode
+										key={podcastEpisode.id}
+										episode={podcastEpisode}
+										index={podcastEpisodeIdx}
+										podcastProgress={
+											loaderData.userMetadataDetails.podcastProgress
+										}
+									/>
+								)}
+							/>
+						</Tabs.Panel>
+					) : null}
+					{!userPreferences.general.disableReviews ? (
+						<Tabs.Panel value="reviews">
+							{loaderData.userMetadataDetails.reviews.length > 0 ? (
 								<MediaScrollArea>
-									<SimpleGrid cols={{ base: 3, md: 4, lg: 5 }}>
-										{loaderData.metadataDetails.suggestions.map((sug) => (
-											<PartialMetadataDisplay
-												key={sug.identifier}
-												media={sug}
+									<Stack>
+										{loaderData.userMetadataDetails.reviews.map((r) => (
+											<ReviewItemDisplay
+												review={r}
+												key={r.id}
+												entityLot={EntityLot.Metadata}
+												entityId={loaderData.metadataId}
+												lot={loaderData.metadataDetails.lot}
+												title={loaderData.metadataDetails.title}
 											/>
 										))}
-									</SimpleGrid>
+									</Stack>
 								</MediaScrollArea>
 							) : (
-								<Text>No suggestions</Text>
+								<Text>No reviews</Text>
 							)}
 						</Tabs.Panel>
-						{!loaderData.userPreferences.videosDisabled ? (
-							<Tabs.Panel value="videos">
+					) : null}
+					<Tabs.Panel value="suggestions">
+						{loaderData.metadataDetails.suggestions.length > 0 ? (
+							<MediaScrollArea>
+								<SimpleGrid cols={{ base: 3, md: 4, lg: 5 }}>
+									{loaderData.metadataDetails.suggestions.map((sug) => (
+										<PartialMetadataDisplay key={sug.identifier} media={sug} />
+									))}
+								</SimpleGrid>
+							</MediaScrollArea>
+						) : (
+							<Text>No suggestions</Text>
+						)}
+					</Tabs.Panel>
+					{!userPreferences.general.disableVideos ? (
+						<Tabs.Panel value="videos">
+							<MediaScrollArea>
+								<Stack>
+									{loaderData.metadataDetails.assets.videos.map((v) => (
+										<Box key={v.videoId}>
+											<iframe
+												width="100%"
+												height={200}
+												src={
+													match(v.source)
+														.with(
+															MetadataVideoSource.Youtube,
+															() => "https://www.youtube.com/embed/",
+														)
+														.with(
+															MetadataVideoSource.Dailymotion,
+															() => "https://www.dailymotion.com/embed/video/",
+														)
+														.with(MetadataVideoSource.Custom, () => "")
+														.exhaustive() + v.videoId
+												}
+												title="YouTube video player"
+												allowFullScreen
+											/>
+										</Box>
+									))}
+								</Stack>
+							</MediaScrollArea>
+						</Tabs.Panel>
+					) : null}
+					{!userPreferences.general.disableWatchProviders ? (
+						<Tabs.Panel value="watchProviders">
+							{loaderData.metadataDetails.watchProviders.length > 0 ? (
 								<MediaScrollArea>
-									<Stack>
-										{loaderData.metadataDetails.assets.videos.map((v) => (
-											<Box key={v.videoId}>
-												<iframe
-													width="100%"
-													height={200}
-													src={
-														match(v.source)
-															.with(
-																MetadataVideoSource.Youtube,
-																() => "https://www.youtube.com/embed/",
-															)
-															.with(
-																MetadataVideoSource.Dailymotion,
-																() =>
-																	"https://www.dailymotion.com/embed/video/",
-															)
-															.with(MetadataVideoSource.Custom, () => "")
-															.exhaustive() + v.videoId
-													}
-													title="YouTube video player"
-													allowFullScreen
-												/>
-											</Box>
-										))}
+									<Stack gap="sm">
+										<Text>
+											JustWatch makes it easy to find out where you can legally
+											watch your favorite movies & TV shows online. Visit{" "}
+											<Anchor href={JUST_WATCH_URL}>JustWatch</Anchor> for more
+											information.
+										</Text>
+										<Text>
+											The following is a list of all available watch providers
+											for this media along with the countries they are available
+											in.
+										</Text>
+										{loaderData.metadataDetails.watchProviders.map(
+											(provider) => (
+												<Flex key={provider.name} align="center" gap="md">
+													<Image
+														src={provider.image}
+														h={80}
+														w={80}
+														radius="md"
+													/>
+													<Text lineClamp={3}>
+														{provider.name}:{" "}
+														<Text size="xs" span>
+															{provider.languages.join(", ")}
+														</Text>
+													</Text>
+												</Flex>
+											),
+										)}
 									</Stack>
 								</MediaScrollArea>
-							</Tabs.Panel>
-						) : null}
-						{!loaderData.userPreferences.watchProvidersDisabled ? (
-							<Tabs.Panel value="watchProviders">
-								{loaderData.metadataDetails.watchProviders.length > 0 ? (
-									<MediaScrollArea>
-										<Stack gap="sm">
-											<Text>
-												JustWatch makes it easy to find out where you can
-												legally watch your favorite movies & TV shows online.
-												Visit <Anchor href={JUST_WATCH_URL}>JustWatch</Anchor>{" "}
-												for more information.
-											</Text>
-											<Text>
-												The following is a list of all available watch providers
-												for this media along with the countries they are
-												available in.
-											</Text>
-											{loaderData.metadataDetails.watchProviders.map(
-												(provider) => (
-													<Flex key={provider.name} align="center" gap="md">
-														<Image
-															src={provider.image}
-															h={80}
-															w={80}
-															radius="md"
-														/>
-														<Text lineClamp={3}>
-															{provider.name}:{" "}
-															<Text size="xs" span>
-																{provider.languages.join(", ")}
-															</Text>
-														</Text>
-													</Flex>
-												),
-											)}
-										</Stack>
-									</MediaScrollArea>
-								) : (
-									<Text>No watch providers</Text>
-								)}
-							</Tabs.Panel>
-						) : null}
-					</Tabs>
-				</MediaDetailsLayout>
-			</Container>
-		</>
-	);
-}
-
-type UpdateProgress = {
-	onlySeason?: boolean;
-	completeShow?: boolean;
-	completePodcast?: boolean;
-	showSeasonNumber?: number | null;
-	showEpisodeNumber?: number | null;
-	podcastEpisodeNumber?: number | null;
-};
-
-const WATCH_TIMES = [
-	"Just Right Now",
-	"I don't remember",
-	"Custom Date",
-] as const;
-
-const ProgressUpdateModal = (props: {
-	opened: boolean;
-	onClose: () => void;
-	data?: UpdateProgress;
-}) => {
-	const loaderData = useLoaderData<typeof loader>();
-	const [selectedDate, setSelectedDate] = useState<Date | null | undefined>(
-		new Date(),
-	);
-	const [watchTime, setWatchTime] =
-		useState<(typeof WATCH_TIMES)[number]>("Just Right Now");
-	const [animeEpisodeNumber, setAnimeEpisodeNumber] = useState<
-		string | undefined
-	>(undefined);
-	const [mangaChapterNumber, setMangaChapterNumber] = useState<
-		string | undefined
-	>(undefined);
-	const [mangaVolumeNumber, setMangaVolumeNumber] = useState<
-		string | undefined
-	>(undefined);
-
-	if (!props.data) return <></>;
-	return (
-		<Modal
-			opened={props.opened}
-			onClose={props.onClose}
-			withCloseButton={false}
-			centered
-		>
-			<Form
-				method="post"
-				action="?intent=progressUpdate"
-				replace
-				onSubmit={() => {
-					props.onClose();
-					events.updateProgress(loaderData.metadataDetails.title);
-				}}
-			>
-				{[
-					...Object.entries(props.data),
-					["metadataId", loaderData.metadataId.toString()],
-					["metadataLot", loaderData.metadataDetails.lot.toString()],
-				].map(([k, v]) => (
-					<Fragment key={k}>
-						{typeof v !== "undefined" ? (
-							<input hidden name={k} defaultValue={v?.toString()} key={k} />
-						) : null}
-					</Fragment>
-				))}
-				{loaderData.query[redirectToQueryParam] ? (
-					<input
-						hidden
-						name={redirectToQueryParam}
-						defaultValue={loaderData.query[redirectToQueryParam]}
-					/>
-				) : null}
-				<Stack>
-					{loaderData.metadataDetails.lot === MediaLot.Anime ? (
-						<>
-							<NumberInput
-								label="Episode"
-								name="animeEpisodeNumber"
-								description="Leaving this empty will mark the whole anime as watched"
-								hideControls
-								value={animeEpisodeNumber}
-								onChange={(e) => setAnimeEpisodeNumber(e.toString())}
-							/>
-							{animeEpisodeNumber ? (
-								<Checkbox
-									label="Mark all episodes before this as watched"
-									name="animeAllEpisodesBefore"
-								/>
-							) : null}
-						</>
-					) : null}
-					{loaderData.metadataDetails.lot === MediaLot.Manga ? (
-						<>
-							<Box>
-								<Text c="dimmed" size="sm">
-									Leaving the following empty will mark the whole manga as
-									watched
-								</Text>
-								<Group wrap="nowrap">
-									<NumberInput
-										label="Chapter"
-										name="mangaChapterNumber"
-										hideControls
-										value={mangaChapterNumber}
-										onChange={(e) => setMangaChapterNumber(e.toString())}
-									/>
-									<Text ta="center" fw="bold" mt="sm">
-										OR
-									</Text>
-									<NumberInput
-										label="Volume"
-										name="mangaVolumeNumber"
-										hideControls
-										value={mangaVolumeNumber}
-										onChange={(e) => setMangaVolumeNumber(e.toString())}
-									/>
-								</Group>
-							</Box>
-							{mangaChapterNumber ? (
-								<Checkbox
-									label="Mark all chapters before this as watched"
-									name="mangaAllChaptersBefore"
-								/>
-							) : null}
-						</>
-					) : null}
-					{loaderData.metadataDetails.lot === MediaLot.Show ? (
-						<>
-							<input
-								hidden
-								name="showSpecifics"
-								defaultValue={JSON.stringify(
-									loaderData.metadataDetails.showSpecifics?.seasons.map(
-										(s) => ({
-											seasonNumber: s.seasonNumber,
-											episodes: s.episodes.map((e) => e.episodeNumber),
-										}),
-									),
-								)}
-							/>
-							{props.data?.onlySeason || props.data?.completeShow ? (
-								<Alert color="yellow" icon={<IconAlertCircle />}>
-									{props.data.onlySeason
-										? `This will mark all episodes of season ${props.data.showSeasonNumber} as seen`
-										: props.data.completeShow
-											? "This will mark all episodes for this show as seen"
-											: null}
-								</Alert>
-							) : null}
-							{!props.data?.completeShow ? (
-								<Select
-									label="Season"
-									data={loaderData.metadataDetails.showSpecifics?.seasons.map(
-										(s) => ({
-											label: `${s.seasonNumber}. ${s.name.toString()}`,
-											value: s.seasonNumber.toString(),
-										}),
-									)}
-									defaultValue={props.data?.showSeasonNumber?.toString()}
-								/>
-							) : null}
-							{props.data?.onlySeason ? (
-								<Checkbox
-									label="Mark all seasons before this as seen"
-									name="showAllSeasonsBefore"
-								/>
-							) : null}
-							{!props.data?.onlySeason &&
-							typeof props.data?.showSeasonNumber !== "undefined" ? (
-								<Select
-									label="Episode"
-									data={
-										loaderData.metadataDetails.showSpecifics?.seasons
-											.find(
-												(s) =>
-													s.seasonNumber ===
-													Number(props.data?.showSeasonNumber),
-											)
-											?.episodes.map((e) => ({
-												label: `${e.episodeNumber}. ${e.name.toString()}`,
-												value: e.episodeNumber.toString(),
-											})) || []
-									}
-									defaultValue={props.data.showEpisodeNumber?.toString()}
-								/>
-							) : null}
-						</>
-					) : null}
-					{loaderData.metadataDetails.lot === MediaLot.Podcast ? (
-						<>
-							<input
-								hidden
-								name="podcastSpecifics"
-								defaultValue={JSON.stringify(
-									loaderData.metadataDetails.podcastSpecifics?.episodes.map(
-										(e) => ({ episodeNumber: e.number }),
-									),
-								)}
-							/>
-							{props.data?.completePodcast ? (
-								<Alert color="yellow" icon={<IconAlertCircle />}>
-									This will mark all episodes for this podcast as seen
-								</Alert>
 							) : (
-								<>
-									<Title order={6}>Select episode</Title>
-									<Autocomplete
-										label="Episode"
-										data={loaderData.metadataDetails.podcastSpecifics?.episodes.map(
-											(se) => ({
-												label: se.title.toString(),
-												value: se.number.toString(),
-											}),
-										)}
-										defaultValue={props.data?.podcastEpisodeNumber?.toString()}
-									/>
-								</>
+								<Text>No watch providers</Text>
 							)}
-						</>
-					) : null}
-					<Select
-						label={`When did you ${getVerb(
-							Verb.Read,
-							loaderData.metadataDetails.lot,
-						)} it?`}
-						data={WATCH_TIMES}
-						value={watchTime}
-						onChange={(v) => {
-							setWatchTime(v as typeof watchTime);
-							match(v)
-								.with(WATCH_TIMES[0], () => setSelectedDate(new Date()))
-								.with(WATCH_TIMES[1], () => setSelectedDate(null))
-								.with(WATCH_TIMES[2], () => setSelectedDate(null));
-						}}
-					/>
-					{watchTime === WATCH_TIMES[2] ? (
-						<DatePickerInput
-							label="Enter exact date"
-							dropdownType="modal"
-							maxDate={new Date()}
-							onChange={setSelectedDate}
-							clearable
-						/>
-					) : null}
-					<Select
-						label={`Where did you ${getVerb(
-							Verb.Read,
-							loaderData.metadataDetails.lot,
-						)} it?`}
-						data={loaderData.userPreferences.watchProviders}
-						name="providerWatchedOn"
-					/>
-					<Button
-						variant="outline"
-						disabled={selectedDate === undefined}
-						type="submit"
-						name="date"
-						value={
-							selectedDate ? formatDateToNaiveDate(selectedDate) : undefined
-						}
-					>
-						Submit
-					</Button>
-				</Stack>
-			</Form>
-		</Modal>
-	);
-};
-
-type AllUserHistory =
-	UserMetadataDetailsQuery["userMetadataDetails"]["history"];
-
-const IndividualProgressModal = (props: {
-	title: string;
-	opened: boolean;
-	onClose: () => void;
-	metadataId: string;
-	progress: number;
-	inProgress: AllUserHistory[number];
-	total?: number | null;
-	lot: MediaLot;
-}) => {
-	const loaderData = useLoaderData<typeof loader>();
-	const [value, setValue] = useState<number | undefined>(props.progress);
-
-	const [updateIcon, text] = match(props.lot)
-		.with(MediaLot.Book, () => [<IconBook size={24} key="element" />, "Pages"])
-		.with(MediaLot.Anime, () => [
-			<IconDeviceTv size={24} key="element" />,
-			"Episodes",
-		])
-		.with(MediaLot.Manga, () => [
-			<IconBrandPagekit size={24} key="element" />,
-			"Chapters",
-		])
-		.with(MediaLot.Movie, MediaLot.VisualNovel, MediaLot.AudioBook, () => [
-			<IconClock size={24} key="element" />,
-			"Minutes",
-		])
-		.otherwise(() => [null, null]);
-
-	return (
-		<Modal
-			opened={props.opened}
-			onClose={props.onClose}
-			withCloseButton={false}
-			centered
-			size="sm"
-		>
-			<Form
-				action="?intent=individualProgressUpdate"
-				method="post"
-				replace
-				onSubmit={() => {
-					events.updateProgress(props.title);
-				}}
-			>
-				<input hidden name="metadataId" defaultValue={props.metadataId} />
-				<input hidden name="progress" value={value} readOnly />
-				<input
-					hidden
-					name="date"
-					defaultValue={formatDateToNaiveDate(new Date())}
-				/>
-				<Stack>
-					<Title order={3}>Set progress</Title>
-					<Group>
-						<Slider
-							showLabelOnHover={false}
-							value={value}
-							onChange={setValue}
-							style={{ flexGrow: 1 }}
-						/>
-						<NumberInput
-							value={value}
-							onChange={(v) => {
-								if (v) setValue(Number(v));
-								else setValue(undefined);
-							}}
-							max={100}
-							min={0}
-							step={1}
-							w="20%"
-							hideControls
-							rightSection={<IconPercentage size={16} />}
-						/>
-					</Group>
-					{props.total ? (
-						<>
-							<Text ta="center" fw="bold">
-								OR
-							</Text>
-							<Flex align="center" gap="xs">
-								<NumberInput
-									defaultValue={((props.total || 1) * (value || 1)) / 100}
-									onChange={(v) => {
-										const value = (Number(v) / (props.total || 1)) * 100;
-										setValue(value);
-									}}
-									max={props.total}
-									min={0}
-									step={1}
-									hideControls
-									leftSection={updateIcon}
-								/>
-								<Text>{text}</Text>
-							</Flex>
-						</>
+						</Tabs.Panel>
 					) : null}
-					<Select
-						data={loaderData.userPreferences.watchProviders}
-						label={`Where did you ${getVerb(
-							Verb.Read,
-							loaderData.metadataDetails.lot,
-						)} it?`}
-						name="providerWatchedOn"
-						defaultValue={props.inProgress.providerWatchedOn}
-					/>
-					<Button variant="outline" type="submit" onClick={props.onClose}>
-						Update
-					</Button>
-					<Button variant="outline" color="red" onClick={props.onClose}>
-						Cancel
-					</Button>
-				</Stack>
-			</Form>
-		</Modal>
+				</Tabs>
+			</MediaDetailsLayout>
+		</Container>
 	);
-};
+}
 
 const MetadataCreator = (props: {
 	name: string;
@@ -1859,7 +1254,10 @@ const DisplaySeasonOrEpisodeDetails = (props: {
 }) => {
 	const display = [
 		props.runtime
-			? humanizeDuration(props.runtime * 1000 * 60, { units: ["h", "m"] })
+			? humanizeDuration(
+					dayjsLib.duration(props.runtime, "minutes").asMilliseconds(),
+					{ units: ["h", "m"] },
+				)
 			: null,
 		props.publishDate ? dayjsLib(props.publishDate).format("ll") : null,
 		props.numEpisodes ? `${props.numEpisodes} episodes` : null,
@@ -1933,7 +1331,7 @@ const DisplaySeasonOrEpisodeDetails = (props: {
 type History =
 	UserMetadataDetailsQuery["userMetadataDetails"]["history"][number];
 
-const SeenItem = (props: { history: History }) => {
+const SeenItem = (props: { history: History; index: number }) => {
 	const loaderData = useLoaderData<typeof loader>();
 	const [opened, { open, close }] = useDisclosure(false);
 	const showExtraInformation = props.history.showExtraInformation
@@ -1981,13 +1379,15 @@ const SeenItem = (props: { history: History }) => {
 
 	const timeSpentInMilliseconds = (props.history.totalTimeSpent || 0) * 1000;
 	const units = ["mo", "d", "h"] as HumanizeDurationOptions["units"];
-	const isLessThanAnHour = timeSpentInMilliseconds < 1000 * 60 * 60;
+	const isLessThanAnHour =
+		timeSpentInMilliseconds < dayjsLib.duration(1, "hour").asMilliseconds();
 	if (isLessThanAnHour) units?.push("m");
 
 	return (
 		<>
 			<Flex
-				my="sm"
+				mb="sm"
+				mt={props.index === 0 ? undefined : "sm"}
 				key={props.history.id}
 				gap={{ base: "xs", md: "lg", xl: "xl" }}
 				data-seen-id={props.history.id}
@@ -2081,13 +1481,14 @@ const SeenItem = (props: { history: History }) => {
 
 const DisplayShowSeason = (props: {
 	seasonIdx: number;
-	setData: (data: UpdateProgress) => void;
 	showProgress: UserMetadataDetailsQuery["userMetadataDetails"]["showProgress"];
 }) => {
 	const loaderData = useLoaderData<typeof loader>();
+	const [_, setMetadataToUpdate] = useMetadataProgressUpdate();
 	const season =
 		loaderData.metadataDetails.showSpecifics?.seasons[props.seasonIdx];
-	const isSeen = (props.showProgress?.[props.seasonIdx]?.timesSeen || 0) > 0;
+	const numTimesSeen = props.showProgress?.[props.seasonIdx]?.timesSeen || 0;
+	const isSeen = numTimesSeen > 0;
 
 	invariant(season, "Season not found");
 
@@ -2097,7 +1498,7 @@ const DisplayShowSeason = (props: {
 				{...season}
 				name={`${season.seasonNumber}. ${season.name}`}
 				numEpisodes={season.episodes.length}
-				displayIndicator={isSeen ? 1 : 0}
+				displayIndicator={numTimesSeen}
 				runtime={season.episodes
 					.map((e) => e.runtime || 0)
 					.reduce((i, a) => i + a, 0)}
@@ -2108,7 +1509,8 @@ const DisplayShowSeason = (props: {
 							variant={isSeen ? "default" : "outline"}
 							color="blue"
 							onClick={() => {
-								props.setData({
+								setMetadataToUpdate({
+									metadataId: loaderData.metadataId,
 									showSeasonNumber: season.seasonNumber,
 									onlySeason: true,
 								});
@@ -2127,10 +1529,10 @@ const DisplayShowEpisode = (props: {
 	seasonIdx: number;
 	overallIdx: number;
 	seasonNumber: number;
-	setData: (data: UpdateProgress) => void;
 	seasonProgress: UserMetadataDetailsQuery["userMetadataDetails"]["showProgress"];
 }) => {
 	const loaderData = useLoaderData<typeof loader>();
+	const [_, setMetadataToUpdate] = useMetadataProgressUpdate();
 	const flattenedEpisodes =
 		loaderData.metadataDetails.showSpecifics?.seasons.flatMap(
 			(season) => season.episodes,
@@ -2157,7 +1559,8 @@ const DisplayShowEpisode = (props: {
 					variant={numTimesEpisodeSeen > 0 ? "default" : "outline"}
 					color="blue"
 					onClick={() => {
-						props.setData({
+						setMetadataToUpdate({
+							metadataId: loaderData.metadataId,
 							showSeasonNumber: props.seasonNumber,
 							showEpisodeNumber: episode.episodeNumber,
 						});
@@ -2173,9 +1576,10 @@ const DisplayShowEpisode = (props: {
 const DisplayPodcastEpisode = (props: {
 	index: number;
 	episode: PodcastEpisode;
-	setData: (data: UpdateProgress) => void;
 	podcastProgress: UserMetadataDetailsQuery["userMetadataDetails"]["podcastProgress"];
 }) => {
+	const loaderData = useLoaderData<typeof loader>();
+	const [_, setMetadataToUpdate] = useMetadataProgressUpdate();
 	const numTimesEpisodeSeen =
 		props.podcastProgress?.[props.index]?.timesSeen || 0;
 
@@ -2193,7 +1597,10 @@ const DisplayPodcastEpisode = (props: {
 					variant={numTimesEpisodeSeen > 0 ? "default" : "outline"}
 					color="blue"
 					onClick={() => {
-						props.setData({ podcastEpisodeNumber: props.episode.number });
+						setMetadataToUpdate({
+							metadataId: loaderData.metadataId,
+							podcastEpisodeNumber: props.episode.number,
+						});
 					}}
 				>
 					{numTimesEpisodeSeen > 0 ? "Re-listen this" : "Mark as listened"}
diff --git a/apps/frontend/app/routes/_dashboard.media.people.$action.tsx b/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
index 37321b5408..9458d04326 100644
--- a/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
@@ -50,11 +50,13 @@ import {
 	MediaItemWithoutUpdateModal,
 } from "~/components/media";
 import { redirectToQueryParam } from "~/lib/generals";
-import { useSearchParam } from "~/lib/hooks";
+import {
+	useCoreDetails,
+	useSearchParam,
+	useUserPreferences,
+} from "~/lib/hooks";
 import {
 	getAuthorizationHeader,
-	getCoreDetails,
-	getUserPreferences,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
@@ -84,10 +86,6 @@ const SEARCH_SOURCES_ALLOWED = [
 
 export const loader = unstable_defineLoader(async ({ request, params }) => {
 	const action = params.action as Action;
-	const [coreDetails, userPreferences] = await Promise.all([
-		getCoreDetails(request),
-		getUserPreferences(request),
-	]);
 	const { query, page } = zx.parseQuery(request, {
 		query: z.string().optional(),
 		page: zx.IntAsString.default("1"),
@@ -111,7 +109,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 						sort: { by: urlParse.sortBy, order: urlParse.orderBy },
 					},
 				},
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return [{ list: peopleList, url: urlParse }, undefined] as const;
 		})
@@ -136,20 +134,12 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 						},
 					},
 				},
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return [undefined, { search: peopleSearch, url: urlParse }] as const;
 		})
 		.exhaustive();
-	return {
-		action,
-		query,
-		page,
-		coreDetails: { pageLimit: coreDetails.pageLimit },
-		userPreferences: { reviewScale: userPreferences.general.reviewScale },
-		peopleList,
-		peopleSearch,
-	};
+	return { action, query, page, peopleList, peopleSearch };
 });
 
 export const meta = ({ params }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -158,6 +148,7 @@ export const meta = ({ params }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 	const navigate = useNavigate();
 	const [_, { setP }] = useSearchParam();
 	const [
@@ -322,7 +313,7 @@ export default function Page() {
 										onChange={(v) => setP("page", v.toString())}
 										total={Math.ceil(
 											loaderData.peopleList.list.details.total /
-												loaderData.coreDetails.pageLimit,
+												coreDetails.pageLimit,
 										)}
 									/>
 								</Center>
@@ -361,7 +352,7 @@ export default function Page() {
 										onChange={(v) => setP("page", v.toString())}
 										total={Math.ceil(
 											loaderData.peopleSearch.search.details.total /
-												loaderData.coreDetails.pageLimit,
+												coreDetails.pageLimit,
 										)}
 									/>
 								</Center>
@@ -380,15 +371,15 @@ const PersonSearchItem = (props: {
 	item: Item;
 }) => {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
 	const navigate = useNavigate();
 	const [isLoading, setIsLoading] = useState(false);
 
 	return (
 		<MediaItemWithoutUpdateModal
 			item={props.item}
-			noRatingLink
 			noHref
-			reviewScale={loaderData.userPreferences.reviewScale}
+			reviewScale={userPreferences.general.reviewScale}
 			imageOverlayForLoadingIndicator={isLoading}
 			onClick={async (_) => {
 				if (loaderData.peopleSearch) {
diff --git a/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx
index debeba917d..b8bfe79ef7 100644
--- a/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx
@@ -13,7 +13,6 @@ import {
 	Text,
 	Title,
 } from "@mantine/core";
-import { useDisclosure } from "@mantine/hooks";
 import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
 import {
 	Form,
@@ -33,30 +32,23 @@ import {
 	IconMessageCircle2,
 	IconUser,
 } from "@tabler/icons-react";
-import { useState } from "react";
 import { namedAction } from "remix-utils/named-action";
 import invariant from "tiny-invariant";
 import { z } from "zod";
 import { zx } from "zodix";
-import {
-	AddEntityToCollectionModal,
-	MediaDetailsLayout,
-} from "~/components/common";
+import { MediaDetailsLayout } from "~/components/common";
 import {
 	DisplayCollection,
 	MediaIsPartial,
 	MediaScrollArea,
-	type PostReview,
-	PostReviewModal,
 	ReviewItemDisplay,
 	ToggleMediaMonitorMenuItem,
 } from "~/components/media";
+import { useUserPreferences } from "~/lib/hooks";
+import { useAddEntityToCollection, useReviewEntity } from "~/lib/state/media";
 import {
 	createToastHeaders,
 	getAuthorizationHeader,
-	getUserCollectionsList,
-	getUserDetails,
-	getUserPreferences,
 	processSubmission,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -71,35 +63,15 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 	const query = zx.parseQuery(request, searchParamsSchema);
 	const personId = params.id;
 	invariant(personId, "No ID provided");
-	const [
-		userPreferences,
-		userDetails,
-		{ personDetails },
-		{ userPersonDetails },
-		collections,
-	] = await Promise.all([
-		getUserPreferences(request),
-		getUserDetails(request),
+	const [{ personDetails }, { userPersonDetails }] = await Promise.all([
 		serverGqlService.request(PersonDetailsDocument, { personId }),
 		serverGqlService.request(
 			UserPersonDetailsDocument,
 			{ personId },
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
-		getUserCollectionsList(request),
 	]);
-	return {
-		query,
-		personId,
-		userPreferences: {
-			reviewScale: userPreferences.general.reviewScale,
-			disableReviews: userPreferences.general.disableReviews,
-		},
-		userDetails,
-		collections,
-		userPersonDetails,
-		personDetails,
-	};
+	return { query, personId, userPersonDetails, personDetails };
 });
 
 export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -114,7 +86,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				DeployUpdatePersonJobDocument,
 				submission,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json({ status: "success", submission } as const, {
 				headers: await createToastHeaders({
@@ -130,243 +102,233 @@ const personIdSchema = z.object({ personId: z.string() });
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
-	const [
-		collectionModalOpened,
-		{ open: collectionModalOpen, close: collectionModalClose },
-	] = useDisclosure(false);
-	const [postReviewModalData, setPostReviewModalData] = useState<
-		PostReview | undefined
-	>(undefined);
+	const userPreferences = useUserPreferences();
+	const [_r, setEntityToReview] = useReviewEntity();
+	const [_a, setAddEntityToCollectionData] = useAddEntityToCollection();
 
 	return (
-		<>
-			<PostReviewModal
-				onClose={() => setPostReviewModalData(undefined)}
-				opened={postReviewModalData !== undefined}
-				data={postReviewModalData}
-				entityType="person"
-				objectId={loaderData.personId.toString()}
-				reviewScale={loaderData.userPreferences.reviewScale}
-				title={loaderData.personDetails.details.name}
-			/>
-			<Container>
-				<MediaDetailsLayout
-					images={loaderData.personDetails.details.displayImages}
-					externalLink={{
-						source: loaderData.personDetails.details.source,
-						href: loaderData.personDetails.sourceUrl,
-					}}
-				>
-					<Title id="creator-title">
-						{loaderData.personDetails.details.name}
-					</Title>
-					<Text c="dimmed" fz={{ base: "sm", lg: "md" }}>
-						{[
-							`${
-								loaderData.personDetails.contents.flatMap((c) => c.items).length
-							} media items`,
-							loaderData.personDetails.details.birthDate &&
-								`Birth: ${loaderData.personDetails.details.birthDate}`,
-							loaderData.personDetails.details.deathDate &&
-								`Death: ${loaderData.personDetails.details.deathDate}`,
-							loaderData.personDetails.details.place &&
-								loaderData.personDetails.details.place,
-							loaderData.personDetails.details.gender,
-						]
-							.filter(Boolean)
-							.join(" • ")}
-						{loaderData.personDetails.details.website ? (
-							<>
-								{" "}
-								•{" "}
-								<Anchor
-									href={loaderData.personDetails.details.website}
-									target="_blank"
-								>
-									Website
-								</Anchor>
-							</>
-						) : null}
-					</Text>
-					{loaderData.userPersonDetails.collections.length > 0 ? (
-						<Group>
-							{loaderData.userPersonDetails.collections.map((col) => (
-								<DisplayCollection
-									key={col.id}
-									col={col}
-									userId={col.userId}
-									entityId={loaderData.personId.toString()}
-									entityLot={EntityLot.Person}
-								/>
-							))}
-						</Group>
-					) : null}
-					{loaderData.personDetails.details.isPartial ? (
-						<MediaIsPartial mediaType="person" />
+		<Container>
+			<MediaDetailsLayout
+				images={loaderData.personDetails.details.displayImages}
+				externalLink={{
+					source: loaderData.personDetails.details.source,
+					href: loaderData.personDetails.sourceUrl,
+				}}
+			>
+				<Title id="creator-title">
+					{loaderData.personDetails.details.name}
+				</Title>
+				<Text c="dimmed" fz={{ base: "sm", lg: "md" }}>
+					{[
+						`${
+							loaderData.personDetails.contents.flatMap((c) => c.items).length
+						} media items`,
+						loaderData.personDetails.details.birthDate &&
+							`Birth: ${loaderData.personDetails.details.birthDate}`,
+						loaderData.personDetails.details.deathDate &&
+							`Death: ${loaderData.personDetails.details.deathDate}`,
+						loaderData.personDetails.details.place &&
+							loaderData.personDetails.details.place,
+						loaderData.personDetails.details.gender,
+					]
+						.filter(Boolean)
+						.join(" • ")}
+					{loaderData.personDetails.details.website ? (
+						<>
+							{" "}
+							•{" "}
+							<Anchor
+								href={loaderData.personDetails.details.website}
+								target="_blank"
+							>
+								Website
+							</Anchor>
+						</>
 					) : null}
-					<Tabs variant="outline" defaultValue={loaderData.query.defaultTab}>
-						<Tabs.List mb="xs">
-							<Tabs.Tab value="media" leftSection={<IconDeviceTv size={16} />}>
-								Media
+				</Text>
+				{loaderData.userPersonDetails.collections.length > 0 ? (
+					<Group>
+						{loaderData.userPersonDetails.collections.map((col) => (
+							<DisplayCollection
+								col={col}
+								key={col.id}
+								creatorUserId={col.userId}
+								entityLot={EntityLot.Person}
+								entityId={loaderData.personId}
+							/>
+						))}
+					</Group>
+				) : null}
+				{loaderData.personDetails.details.isPartial ? (
+					<MediaIsPartial mediaType="person" />
+				) : null}
+				<Tabs variant="outline" defaultValue={loaderData.query.defaultTab}>
+					<Tabs.List mb="xs">
+						<Tabs.Tab value="media" leftSection={<IconDeviceTv size={16} />}>
+							Media
+						</Tabs.Tab>
+						{loaderData.personDetails.details.description ? (
+							<Tabs.Tab
+								value="overview"
+								leftSection={<IconInfoCircle size={16} />}
+							>
+								Overview
 							</Tabs.Tab>
-							{loaderData.personDetails.details.description ? (
-								<Tabs.Tab
-									value="overview"
-									leftSection={<IconInfoCircle size={16} />}
-								>
-									Overview
-								</Tabs.Tab>
-							) : null}
-							{!loaderData.userPreferences.disableReviews ? (
-								<Tabs.Tab
-									value="reviews"
-									leftSection={<IconMessageCircle2 size={16} />}
-								>
-									Reviews
-								</Tabs.Tab>
-							) : null}
-							<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
-								Actions
+						) : null}
+						{!userPreferences.general.disableReviews ? (
+							<Tabs.Tab
+								value="reviews"
+								leftSection={<IconMessageCircle2 size={16} />}
+							>
+								Reviews
 							</Tabs.Tab>
-						</Tabs.List>
-						<Tabs.Panel value="media">
-							<MediaScrollArea>
-								<Stack>
-									{loaderData.personDetails.contents.map((role) => (
-										<Box key={role.name}>
-											<Title order={3} mb="xs" ta="center">
-												{role.name}
-											</Title>
-											<SimpleGrid cols={{ base: 3, md: 4, lg: 5 }}>
-												{role.items.map((item) => (
-													<Anchor
-														key={item.media.id}
-														data-media-id={item.media.id}
-														component={Link}
-														to={$path("/media/item/:id", {
-															id: item.media.id || "",
-														})}
+						) : null}
+						<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
+							Actions
+						</Tabs.Tab>
+					</Tabs.List>
+					<Tabs.Panel value="media">
+						<MediaScrollArea>
+							<Stack>
+								{loaderData.personDetails.contents.map((role) => (
+									<Box key={role.name}>
+										<Title order={3} mb="xs" ta="center">
+											{role.name}
+										</Title>
+										<SimpleGrid cols={{ base: 3, md: 4, lg: 5 }}>
+											{role.items.map((item) => (
+												<Anchor
+													key={item.media.id}
+													data-media-id={item.media.id}
+													component={Link}
+													to={$path("/media/item/:id", {
+														id: item.media.id || "",
+													})}
+												>
+													<Avatar
+														imageProps={{ loading: "lazy" }}
+														src={item.media.image}
+														radius="sm"
+														h={100}
+														w={85}
+														mx="auto"
+														alt={item.media.title}
+														styles={{ image: { objectPosition: "top" } }}
+													/>
+													<Text
+														c="dimmed"
+														size="xs"
+														ta="center"
+														lineClamp={2}
+														mt={4}
 													>
-														<Avatar
-															imageProps={{ loading: "lazy" }}
-															src={item.media.image}
-															radius="sm"
-															h={100}
-															w={85}
-															mx="auto"
-															alt={item.media.title}
-															styles={{ image: { objectPosition: "top" } }}
-														/>
-														<Text
-															c="dimmed"
-															size="xs"
-															ta="center"
-															lineClamp={2}
-															mt={4}
-														>
-															{item.media.title}{" "}
-															{item.character ? `as ${item.character}` : ""}
-														</Text>
-													</Anchor>
-												))}
-											</SimpleGrid>
-										</Box>
-									))}
-								</Stack>
+														{item.media.title}{" "}
+														{item.character ? `as ${item.character}` : ""}
+													</Text>
+												</Anchor>
+											))}
+										</SimpleGrid>
+									</Box>
+								))}
+							</Stack>
+						</MediaScrollArea>
+					</Tabs.Panel>
+					{loaderData.personDetails.details.description ? (
+						<Tabs.Panel value="overview">
+							<MediaScrollArea>
+								<div
+									// biome-ignore lint/security/noDangerouslySetInnerHtml: generated by the backend securely
+									dangerouslySetInnerHTML={{
+										__html: loaderData.personDetails.details.description,
+									}}
+								/>
 							</MediaScrollArea>
 						</Tabs.Panel>
-						{loaderData.personDetails.details.description ? (
-							<Tabs.Panel value="overview">
-								<MediaScrollArea>
-									<div
-										// biome-ignore lint/security/noDangerouslySetInnerHtml: generated by the backend securely
-										dangerouslySetInnerHTML={{
-											__html: loaderData.personDetails.details.description,
-										}}
-									/>
-								</MediaScrollArea>
-							</Tabs.Panel>
-						) : null}
-						<Tabs.Panel value="actions">
+					) : null}
+					<Tabs.Panel value="actions">
+						<MediaScrollArea>
+							<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
+								<Button
+									variant="outline"
+									w="100%"
+									onClick={() => {
+										setEntityToReview({
+											entityId: loaderData.personId,
+											entityLot: EntityLot.Person,
+											entityTitle: loaderData.personDetails.details.name,
+										});
+									}}
+								>
+									Post a review
+								</Button>
+								<Button
+									variant="outline"
+									onClick={() => {
+										setAddEntityToCollectionData({
+											entityId: loaderData.personId,
+											entityLot: EntityLot.Person,
+											alreadyInCollections:
+												loaderData.userPersonDetails.collections.map(
+													(c) => c.id,
+												),
+										});
+									}}
+								>
+									Add to collection
+								</Button>
+								<Menu shadow="md">
+									<Menu.Target>
+										<Button variant="outline">More actions</Button>
+									</Menu.Target>
+									<Menu.Dropdown>
+										<ToggleMediaMonitorMenuItem
+											inCollections={loaderData.userPersonDetails.collections.map(
+												(c) => c.name,
+											)}
+											formValue={loaderData.personId}
+											entityLot={EntityLot.Person}
+										/>
+										<Form
+											action="?intent=deployUpdatePersonJob"
+											method="post"
+											replace
+										>
+											<Menu.Item
+												type="submit"
+												name="personId"
+												value={loaderData.personId}
+											>
+												Update person
+											</Menu.Item>
+										</Form>
+									</Menu.Dropdown>
+								</Menu>
+							</SimpleGrid>
+						</MediaScrollArea>
+					</Tabs.Panel>
+					{!userPreferences.general.disableReviews ? (
+						<Tabs.Panel value="reviews">
 							<MediaScrollArea>
-								<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
-									<Button
-										variant="outline"
-										w="100%"
-										onClick={() => {
-											setPostReviewModalData({});
-										}}
-									>
-										Post a review
-									</Button>
-									<Button variant="outline" onClick={collectionModalOpen}>
-										Add to collection
-									</Button>
-									<Menu shadow="md">
-										<Menu.Target>
-											<Button variant="outline">More actions</Button>
-										</Menu.Target>
-										<Menu.Dropdown>
-											<ToggleMediaMonitorMenuItem
-												userId={loaderData.userDetails.id}
-												inCollections={loaderData.userPersonDetails.collections.map(
-													(c) => c.name,
-												)}
-												formValue={loaderData.personId}
+								{loaderData.userPersonDetails.reviews.length > 0 ? (
+									<Stack>
+										{loaderData.userPersonDetails.reviews.map((r) => (
+											<ReviewItemDisplay
+												review={r}
+												key={r.id}
 												entityLot={EntityLot.Person}
+												entityId={loaderData.personId}
+												title={loaderData.personDetails.details.name}
 											/>
-											<Form
-												action="?intent=deployUpdatePersonJob"
-												method="post"
-												replace
-											>
-												<Menu.Item
-													type="submit"
-													name="personId"
-													value={loaderData.personId}
-												>
-													Update person
-												</Menu.Item>
-											</Form>
-										</Menu.Dropdown>
-									</Menu>
-									<AddEntityToCollectionModal
-										userId={loaderData.userDetails.id}
-										onClose={collectionModalClose}
-										opened={collectionModalOpened}
-										entityId={loaderData.personId.toString()}
-										entityLot={EntityLot.Person}
-										collections={loaderData.collections}
-									/>
-								</SimpleGrid>
+										))}
+									</Stack>
+								) : (
+									<Text>No reviews</Text>
+								)}
 							</MediaScrollArea>
 						</Tabs.Panel>
-						{!loaderData.userPreferences.disableReviews ? (
-							<Tabs.Panel value="reviews">
-								<MediaScrollArea>
-									{loaderData.userPersonDetails.reviews.length > 0 ? (
-										<Stack>
-											{loaderData.userPersonDetails.reviews.map((r) => (
-												<ReviewItemDisplay
-													review={r}
-													key={r.id}
-													personId={loaderData.personId}
-													title={loaderData.personDetails.details.name}
-													user={loaderData.userDetails}
-													reviewScale={loaderData.userPreferences.reviewScale}
-													entityType="person"
-												/>
-											))}
-										</Stack>
-									) : (
-										<Text>No reviews</Text>
-									)}
-								</MediaScrollArea>
-							</Tabs.Panel>
-						) : null}
-					</Tabs>
-				</MediaDetailsLayout>
-			</Container>
-		</>
+					) : null}
+				</Tabs>
+			</MediaDetailsLayout>
+		</Container>
 	);
 }
diff --git a/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx b/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx
index 3bdc0625e8..3c01157c06 100644
--- a/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx
@@ -51,10 +51,10 @@ import { z } from "zod";
 import { confirmWrapper } from "~/components/confirmation";
 import events from "~/lib/events";
 import { dayjsLib } from "~/lib/generals";
+import { useCoreDetails } from "~/lib/hooks";
 import {
 	createToastHeaders,
 	getAuthorizationHeader,
-	getCoreDetails,
 	getCoreEnabledFeatures,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -64,27 +64,21 @@ import {
 } from "~/lib/utilities.server";
 
 export const loader = unstable_defineLoader(async ({ request }) => {
-	const [coreDetails, coreEnabledFeatures, { importReports }, { userExports }] =
+	const [coreEnabledFeatures, { importReports }, { userExports }] =
 		await Promise.all([
-			getCoreDetails(request),
 			getCoreEnabledFeatures(),
 			serverGqlService.request(
 				ImportReportsDocument,
 				undefined,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			),
 			serverGqlService.request(
 				UserExportsDocument,
 				undefined,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			),
 		]);
-	return {
-		coreEnabledFeatures,
-		importReports,
-		userExports,
-		coreDetails: { docsLink: coreDetails.docsLink },
-	};
+	return { coreEnabledFeatures, importReports, userExports };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
@@ -140,7 +134,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				DeployImportJobDocument,
 				{ input: { source, ...values } },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json(
 				{ status: "success", generateAuthToken: false } as const,
@@ -157,7 +151,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				DeployExportJobDocument,
 				toExport,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json(
 				{ status: "success", generateAuthToken: false } as const,
@@ -212,6 +206,7 @@ const deployExportForm = z.object({
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 	const [deployImportSource, setDeployImportSource] = useState<ImportSource>();
 
 	const fetcher = useFetcher<typeof action>();
@@ -242,7 +237,7 @@ export default function Page() {
 									<Anchor
 										size="xs"
 										href={withFragment(
-											`${loaderData.coreDetails.docsLink}/importing.html`,
+											`${coreDetails.docsLink}/importing.html`,
 											match(deployImportSource)
 												.with(ImportSource.Goodreads, () => "goodreads")
 												.with(ImportSource.Mal, () => "myanimelist")
@@ -501,7 +496,7 @@ export default function Page() {
 								<Group>
 									<Anchor
 										size="xs"
-										href={`${loaderData.coreDetails.docsLink}/guides/exporting.html`}
+										href={`${coreDetails.docsLink}/guides/exporting.html`}
 										target="_blank"
 									>
 										Docs
diff --git a/apps/frontend/app/routes/_dashboard.settings.integrations.tsx b/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
index c54f76577b..c340a7fa75 100644
--- a/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
@@ -63,7 +63,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 		serverGqlService.request(
 			UserIntegrationsDocument,
 			undefined,
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
 	]);
 	return { userIntegrations };
@@ -81,7 +81,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				DeleteUserIntegrationDocument,
 				submission,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json(
 				{ status: "success", generateAuthToken: false } as const,
@@ -98,7 +98,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				CreateUserIntegrationDocument,
 				{ input: submission },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json(
 				{ status: "success", generateAuthToken: false } as const,
@@ -114,7 +114,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			const { generateAuthToken } = await serverGqlService.request(
 				GenerateAuthTokenDocument,
 				undefined,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json({ status: "success", generateAuthToken } as const);
 		},
@@ -255,7 +255,7 @@ const DisplayIntegration = (props: { integration: Integration }) => {
 								Triggered:{" "}
 								{dayjsLib(props.integration.lastTriggeredOn).fromNow()}
 							</Text>
-						) : undefined}
+						) : null}
 					</Box>
 					<Group>
 						{!YANK_INTEGRATIONS.includes(props.integration.source) ? (
@@ -355,7 +355,7 @@ const CreateIntegrationModal = (props: {
 								max={100}
 							/>
 						</Group>
-					) : undefined}
+					) : null}
 					{match(source)
 						.with(IntegrationSource.Audiobookshelf, () => (
 							<>
diff --git a/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx b/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
index 06389a2c69..0138419ca2 100644
--- a/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
@@ -7,32 +7,23 @@ import {
 	Text,
 	Title,
 } from "@mantine/core";
-import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
-import {
-	Form,
-	type MetaArgs_SingleFetch,
-	useLoaderData,
-} from "@remix-run/react";
+import { unstable_defineAction } from "@remix-run/node";
+import { Form, type MetaArgs_SingleFetch } from "@remix-run/react";
 import {
 	BackgroundJob,
 	DeployBackgroundJobDocument,
 	UserLot,
 } from "@ryot/generated/graphql/backend/graphql";
 import { z } from "zod";
+import { useUserDetails } from "~/lib/hooks";
 import {
 	createToastHeaders,
 	getAuthorizationHeader,
-	getUserDetails,
 	processSubmission,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
-export const loader = unstable_defineLoader(async ({ request }) => {
-	const [userDetails] = await Promise.all([getUserDetails(request)]);
-	return { userDetails };
-});
-
-export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
+export const meta = (_args: MetaArgs_SingleFetch) => {
 	return [{ title: "Miscellaneous settings | Ryot" }];
 };
 
@@ -42,7 +33,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 	await serverGqlService.request(
 		DeployBackgroundJobDocument,
 		submission,
-		await getAuthorizationHeader(request),
+		getAuthorizationHeader(request),
 	);
 	return Response.json({ status: "success" } as const, {
 		headers: await createToastHeaders({
@@ -57,7 +48,7 @@ const jobSchema = z.object({
 });
 
 export default function Page() {
-	const loaderData = useLoaderData<typeof loader>();
+	const userDetails = useUserDetails();
 
 	return (
 		<Container size="lg">
@@ -68,7 +59,7 @@ export default function Page() {
 						cols={{ base: 1, lg: 2 }}
 						spacing={{ base: "xl", md: "md" }}
 					>
-						{loaderData.userDetails.lot === UserLot.Admin ? (
+						{userDetails.lot === UserLot.Admin ? (
 							<>
 								<Stack>
 									<Box>
diff --git a/apps/frontend/app/routes/_dashboard.settings.notifications.tsx b/apps/frontend/app/routes/_dashboard.settings.notifications.tsx
index 265983b745..9dcaf759b1 100644
--- a/apps/frontend/app/routes/_dashboard.settings.notifications.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.notifications.tsx
@@ -41,27 +41,23 @@ import { z } from "zod";
 import { zx } from "zodix";
 import { confirmWrapper } from "~/components/confirmation";
 import { dayjsLib } from "~/lib/generals";
+import { useCoreDetails } from "~/lib/hooks";
 import {
 	createToastHeaders,
 	getAuthorizationHeader,
-	getCoreDetails,
 	processSubmission,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
 export const loader = unstable_defineLoader(async ({ request }) => {
-	const [coreDetails, { userNotificationPlatforms }] = await Promise.all([
-		getCoreDetails(request),
+	const [{ userNotificationPlatforms }] = await Promise.all([
 		serverGqlService.request(
 			UserNotificationPlatformsDocument,
 			undefined,
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
 	]);
-	return {
-		userNotificationPlatforms,
-		coreDetails: { docsLink: coreDetails.docsLink },
-	};
+	return { userNotificationPlatforms };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
@@ -76,7 +72,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				CreateUserNotificationPlatformDocument,
 				{ input: submission },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json({ status: "success", submission } as const);
 		},
@@ -85,7 +81,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			await serverGqlService.request(
 				DeleteUserNotificationPlatformDocument,
 				submission,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json({ status: "success", submission } as const, {
 				headers: await createToastHeaders({
@@ -98,7 +94,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			const { testUserNotificationPlatforms } = await serverGqlService.request(
 				TestUserNotificationPlatformsDocument,
 				undefined,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json({ status: "success" } as const, {
 				headers: await createToastHeaders({
@@ -125,6 +121,7 @@ const createSchema = z.object({
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 	const [
 		createUserNotificationPlatformModalOpened,
 		{
@@ -298,7 +295,7 @@ export default function Page() {
 																Make sure
 																<Anchor
 																	size="xs"
-																	href={`${loaderData.coreDetails.docsLink}/configuration.html?h=smtp#all-parameters`}
+																	href={`${coreDetails.docsLink}/configuration.html?h=smtp#all-parameters`}
 																	target="_blank"
 																>
 																	{" "}
diff --git a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
index aa50b7170d..33d9dc235d 100644
--- a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
@@ -47,15 +47,13 @@ import { Fragment, useState } from "react";
 import { match } from "ts-pattern";
 import { z } from "zod";
 import { zx } from "zodix";
-import { AUTH_COOKIE_NAME } from "~/lib/generals";
+import { useUserDetails, useUserPreferences } from "~/lib/hooks";
 import {
 	combineHeaders,
 	createToastHeaders,
+	getAuthorizationCookie,
 	getAuthorizationHeader,
-	getCookieValue,
 	getCookiesForApplication,
-	getUserDetails,
-	getUserPreferences,
 	serverGqlService,
 } from "~/lib/utilities.server";
 import classes from "~/styles/preferences.module.css";
@@ -66,15 +64,7 @@ const searchSchema = z.object({
 
 export const loader = unstable_defineLoader(async ({ request }) => {
 	const query = zx.parseQuery(request, searchSchema);
-	const [userPreferences, userDetails] = await Promise.all([
-		getUserPreferences(request),
-		getUserDetails(request),
-	]);
-	return {
-		query,
-		userDetails: { isDemo: userDetails.isDemo },
-		userPreferences,
-	};
+	return { query };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
@@ -105,10 +95,10 @@ export const action = unstable_defineAction(async ({ request }) => {
 		await serverGqlService.request(
 			UpdateUserPreferenceDocument,
 			{ input },
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		);
 	}
-	const token = getCookieValue(request, AUTH_COOKIE_NAME);
+	const token = getAuthorizationCookie(request);
 	const applicationHeaders = await getCookiesForApplication(token);
 	const toastHeaders = await createToastHeaders({
 		message: "Preferences updated",
@@ -122,8 +112,10 @@ export const action = unstable_defineAction(async ({ request }) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const userPreferences = useUserPreferences();
+	const userDetails = useUserDetails();
 	const [dashboardElements, setDashboardElements] = useState(
-		loaderData.userPreferences.general.dashboard,
+		userPreferences.general.dashboard,
 	);
 	const [toUpdatePreferences, updateUserPreferencesHandler] = useListState<
 		[string, string]
@@ -172,7 +164,7 @@ export default function Page() {
 								color="red"
 								variant="outline"
 								onClick={async (e) => {
-									if (!loaderData.userDetails.isDemo) {
+									if (!userDetails.isDemo) {
 										if (
 											!confirm(
 												"This will reset all your preferences to default. Are you sure you want to continue?",
@@ -196,7 +188,7 @@ export default function Page() {
 						</Tooltip>
 					</Form>
 				</Group>
-				{loaderData.userDetails.isDemo ? (
+				{userDetails.isDemo ? (
 					<Alert icon={<IconAlertCircle />} variant="outline" color="violet">
 						{notificationContent.message}
 					</Alert>
@@ -218,7 +210,7 @@ export default function Page() {
 						<Text mb="md">The different sections on the dashboard.</Text>
 						<DragDropContext
 							onDragEnd={({ destination, source }) => {
-								if (!loaderData.userDetails.isDemo) {
+								if (!userDetails.isDemo) {
 									const newOrder = reorder(dashboardElements, {
 										from: source.index,
 										to: destination?.index || 0,
@@ -252,24 +244,24 @@ export default function Page() {
 								<Fragment key={facet}>
 									<Title order={4}>{startCase(facet)}</Title>
 									<SimpleGrid cols={2}>
-										{Object.entries(
-											loaderData.userPreferences.featuresEnabled[facet],
-										).map(([name, isEnabled]) => (
-											<Switch
-												key={name}
-												size="xs"
-												label={changeCase(snakeCase(name))}
-												defaultChecked={isEnabled}
-												disabled={!!loaderData.userDetails.isDemo}
-												onChange={(ev) => {
-													const lot = snakeCase(name);
-													appendPref(
-														`features_enabled.${facet}.${lot}`,
-														String(ev.currentTarget.checked),
-													);
-												}}
-											/>
-										))}
+										{Object.entries(userPreferences.featuresEnabled[facet]).map(
+											([name, isEnabled]) => (
+												<Switch
+													key={name}
+													size="xs"
+													label={changeCase(snakeCase(name))}
+													defaultChecked={isEnabled}
+													disabled={!!userDetails.isDemo}
+													onChange={(ev) => {
+														const lot = snakeCase(name);
+														appendPref(
+															`features_enabled.${facet}.${lot}`,
+															String(ev.currentTarget.checked),
+														);
+													}}
+												/>
+											),
+										)}
 									</SimpleGrid>
 								</Fragment>
 							))}
@@ -280,8 +272,8 @@ export default function Page() {
 							<TagsInput
 								label="Watch providers"
 								placeholder="Enter more providers"
-								defaultValue={loaderData.userPreferences.general.watchProviders}
-								disabled={!!loaderData.userDetails.isDemo}
+								defaultValue={userPreferences.general.watchProviders}
+								disabled={!!userDetails.isDemo}
 								onChange={(val) => {
 									appendPref("general.watch_providers", JSON.stringify(val));
 								}}
@@ -320,8 +312,8 @@ export default function Page() {
 												() => 'Do not display the "Watch On" tab',
 											)
 											.exhaustive()}
-										defaultChecked={loaderData.userPreferences.general[name]}
-										disabled={!!loaderData.userDetails.isDemo}
+										defaultChecked={userPreferences.general[name]}
+										disabled={!!userDetails.isDemo}
 										onChange={(ev) => {
 											appendPref(
 												`general.${snakeCase(name)}`,
@@ -337,8 +329,8 @@ export default function Page() {
 										label: startCase(snakeCase(c)),
 										value: c,
 									}))}
-									defaultValue={loaderData.userPreferences.general.reviewScale}
-									disabled={!!loaderData.userDetails.isDemo}
+									defaultValue={userPreferences.general.reviewScale}
+									disabled={!!userDetails.isDemo}
 									onChange={(val) => {
 										if (val) appendPref("general.review_scale", val);
 									}}
@@ -351,10 +343,8 @@ export default function Page() {
 							<Switch
 								size="xs"
 								label="Whether notifications will be sent"
-								defaultChecked={
-									loaderData.userPreferences.notifications.enabled
-								}
-								disabled={!!loaderData.userDetails.isDemo}
+								defaultChecked={userPreferences.notifications.enabled}
+								disabled={!!userDetails.isDemo}
 								onChange={(ev) => {
 									appendPref(
 										"notifications.enabled",
@@ -417,16 +407,16 @@ export default function Page() {
 												() => "New media is associated with a person",
 											)
 											.exhaustive()}
-										defaultChecked={loaderData.userPreferences.notifications.toSend.includes(
+										defaultChecked={userPreferences.notifications.toSend.includes(
 											name,
 										)}
 										disabled={
-											!!loaderData.userDetails.isDemo ||
-											!loaderData.userPreferences.notifications.enabled
+											!!userDetails.isDemo ||
+											!userPreferences.notifications.enabled
 										}
 										onChange={() => {
 											const alreadyToSend = new Set(
-												loaderData.userPreferences.notifications.toSend,
+												userPreferences.notifications.toSend,
 											);
 											const alreadyHas = alreadyToSend.has(name);
 											if (!alreadyHas) alreadyToSend.add(name);
@@ -452,10 +442,8 @@ export default function Page() {
 								<NumberInput
 									size="xs"
 									label="Number of elements in exercise history"
-									defaultValue={
-										loaderData.userPreferences.fitness.exercises.saveHistory
-									}
-									disabled={!!loaderData.userDetails.isDemo}
+									defaultValue={userPreferences.fitness.exercises.saveHistory}
+									disabled={!!userDetails.isDemo}
 									onChange={(num) => {
 										if (num)
 											appendPref("fitness.exercises.save_history", String(num));
@@ -468,8 +456,8 @@ export default function Page() {
 										value: c.toLowerCase(),
 										label: startCase(c.toLowerCase()),
 									}))}
-									defaultValue={loaderData.userPreferences.fitness.exercises.unitSystem.toLowerCase()}
-									disabled={!!loaderData.userDetails.isDemo}
+									defaultValue={userPreferences.fitness.exercises.unitSystem.toLowerCase()}
+									disabled={!!userDetails.isDemo}
 									onChange={(val) => {
 										if (val) appendPref("fitness.exercises.unit_system", val);
 									}}
@@ -497,14 +485,14 @@ export default function Page() {
 							<Text>The default measurements you want to keep track of.</Text>
 							<SimpleGrid cols={2}>
 								{Object.entries(
-									loaderData.userPreferences.fitness.measurements.inbuilt,
+									userPreferences.fitness.measurements.inbuilt,
 								).map(([name, isEnabled]) => (
 									<Switch
 										size="xs"
 										key={name}
 										label={changeCase(snakeCase(name))}
 										defaultChecked={isEnabled}
-										disabled={!!loaderData.userDetails.isDemo}
+										disabled={!!userDetails.isDemo}
 										onChange={(ev) => {
 											appendPref(
 												`fitness.measurements.inbuilt.${snakeCase(name)}`,
@@ -518,11 +506,11 @@ export default function Page() {
 								label="The custom metrics you want to keep track of"
 								description="The name of the attribute along with the data type. Only decimal data type is supported."
 								defaultValue={JSON.stringify(
-									loaderData.userPreferences.fitness.measurements.custom,
+									userPreferences.fitness.measurements.custom,
 									null,
 									4,
 								)}
-								disabled={!!loaderData.userDetails.isDemo}
+								disabled={!!userDetails.isDemo}
 								autosize
 								formatOnBlur
 								onChange={(v) => {
@@ -542,13 +530,12 @@ const EditDashboardElement = (props: {
 	index: number;
 	appendPref: (property: string, value: string) => void;
 }) => {
-	const loaderData = useLoaderData<typeof loader>();
-	const focusedElementIndex =
-		loaderData.userPreferences.general.dashboard.findIndex(
-			(de) => de.section === props.lot,
-		);
-	const focusedElement =
-		loaderData.userPreferences.general.dashboard[focusedElementIndex];
+	const userPreferences = useUserPreferences();
+	const userDetails = useUserDetails();
+	const focusedElementIndex = userPreferences.general.dashboard.findIndex(
+		(de) => de.section === props.lot,
+	);
+	const focusedElement = userPreferences.general.dashboard[focusedElementIndex];
 
 	return (
 		<Draggable index={props.index} draggableId={props.lot}>
@@ -582,11 +569,11 @@ const EditDashboardElement = (props: {
 							label="Hidden"
 							labelPosition="left"
 							defaultChecked={focusedElement.hidden}
-							disabled={!!loaderData.userDetails.isDemo}
+							disabled={!!userDetails.isDemo}
 							onChange={(ev) => {
 								const newValue = ev.currentTarget.checked;
 								const newDashboardData = Array.from(
-									loaderData.userPreferences.general.dashboard,
+									userPreferences.general.dashboard,
 								);
 								newDashboardData[focusedElementIndex].hidden = newValue;
 								props.appendPref(
@@ -602,11 +589,11 @@ const EditDashboardElement = (props: {
 								label="Number of elements"
 								size="xs"
 								defaultValue={focusedElement.numElements}
-								disabled={!!loaderData.userDetails.isDemo}
+								disabled={!!userDetails.isDemo}
 								onChange={(num) => {
 									if (typeof num === "number") {
 										const newDashboardData = Array.from(
-											loaderData.userPreferences.general.dashboard,
+											userPreferences.general.dashboard,
 										);
 										newDashboardData[focusedElementIndex].numElements = num;
 										props.appendPref(
diff --git a/apps/frontend/app/routes/_dashboard.settings.profile.tsx b/apps/frontend/app/routes/_dashboard.settings.profile.tsx
index 21c1a4da3d..edc2f15c74 100644
--- a/apps/frontend/app/routes/_dashboard.settings.profile.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.profile.tsx
@@ -6,30 +6,24 @@ import {
 	TextInput,
 	Title,
 } from "@mantine/core";
-import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
-import {
-	type MetaArgs_SingleFetch,
-	useFetcher,
-	useLoaderData,
-} from "@remix-run/react";
+import { unstable_defineAction } from "@remix-run/node";
+import { type MetaArgs_SingleFetch, useFetcher } from "@remix-run/react";
 import { UpdateUserDocument } from "@ryot/generated/graphql/backend/graphql";
 import { useRef } from "react";
 import { z } from "zod";
 import { confirmWrapper } from "~/components/confirmation";
+import { useUserDetails } from "~/lib/hooks";
 import {
+	combineHeaders,
 	createToastHeaders,
+	getAuthorizationCookie,
 	getAuthorizationHeader,
-	getUserDetails,
+	getCookiesForApplication,
 	serverGqlService,
 } from "~/lib/utilities.server";
 import { processSubmission } from "~/lib/utilities.server";
 
-export const loader = unstable_defineLoader(async ({ request }) => {
-	const [userDetails] = await Promise.all([getUserDetails(request)]);
-	return { userDetails };
-});
-
-export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
+export const meta = (_args: MetaArgs_SingleFetch) => {
 	return [{ title: "Profile Settings | Ryot" }];
 };
 
@@ -39,13 +33,15 @@ export const action = unstable_defineAction(async ({ request }) => {
 	await serverGqlService.request(
 		UpdateUserDocument,
 		{ input: submission },
-		await getAuthorizationHeader(request),
+		getAuthorizationHeader(request),
 	);
+	const token = getAuthorizationCookie(request);
+	const applicationHeaders = await getCookiesForApplication(token);
+	const toastHeaders = await createToastHeaders({
+		message: "Profile updated. Please login again for changes to take effect.",
+	});
 	return Response.json({ status: "success", submission } as const, {
-		headers: await createToastHeaders({
-			message:
-				"Profile updated. Please login again for changes to take effect.",
-		}),
+		headers: combineHeaders(toastHeaders, applicationHeaders),
 	});
 });
 
@@ -56,7 +52,7 @@ const updateProfileFormSchema = z.object({
 });
 
 export default function Page() {
-	const loaderData = useLoaderData<typeof loader>();
+	const userDetails = useUserDetails();
 	const fetcher = useFetcher<typeof action>();
 	const formRef = useRef<HTMLFormElement>(null);
 
@@ -69,29 +65,28 @@ export default function Page() {
 						<TextInput
 							readOnly
 							description="Database generated user ID"
-							defaultValue={loaderData.userDetails.id}
+							defaultValue={userDetails.id}
 						/>
 						<TextInput
 							label="Username"
 							name="username"
-							disabled={Boolean(loaderData.userDetails.isDemo)}
+							disabled={Boolean(userDetails.isDemo)}
 							description={
-								loaderData.userDetails.isDemo &&
+								userDetails.isDemo &&
 								"Username can not be changed for the demo user"
 							}
-							defaultValue={loaderData.userDetails.name}
+							defaultValue={userDetails.name}
 						/>
 						<PasswordInput
 							label="Password"
 							name="password"
 							disabled={
-								Boolean(loaderData.userDetails.isDemo) ||
-								Boolean(loaderData.userDetails.oidcIssuerId)
+								Boolean(userDetails.isDemo) || Boolean(userDetails.oidcIssuerId)
 							}
 							description={
-								loaderData.userDetails.oidcIssuerId
+								userDetails.oidcIssuerId
 									? "Not applicable since this user was created via OIDC"
-									: loaderData.userDetails.isDemo
+									: userDetails.isDemo
 										? "Password can not be changed for the demo user"
 										: undefined
 							}
diff --git a/apps/frontend/app/routes/_dashboard.settings.users.tsx b/apps/frontend/app/routes/_dashboard.settings.users.tsx
index 6b76b3d957..0e48273275 100644
--- a/apps/frontend/app/routes/_dashboard.settings.users.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.users.tsx
@@ -31,21 +31,19 @@ import { confirmWrapper } from "~/components/confirmation";
 import {
 	createToastHeaders,
 	getAuthorizationHeader,
-	getCoreDetails,
 	processSubmission,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
 export const loader = unstable_defineLoader(async ({ request }) => {
-	const [coreDetails, { usersList }] = await Promise.all([
-		getCoreDetails(request),
+	const [{ usersList }] = await Promise.all([
 		serverGqlService.request(
 			UsersListDocument,
 			undefined,
-			await getAuthorizationHeader(request),
+			getAuthorizationHeader(request),
 		),
 	]);
-	return { coreDetails, usersList };
+	return { usersList };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
@@ -60,7 +58,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			const { deleteUser } = await serverGqlService.request(
 				DeleteUserDocument,
 				submission,
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			return Response.json({ status: "success", submission } as const, {
 				headers: await createToastHeaders({
@@ -76,7 +74,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 			const { registerUser } = await serverGqlService.request(
 				RegisterUserDocument,
 				{ input: { password: submission } },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			const success = registerUser.__typename === "StringIdObject";
 			return Response.json({ status: "success", submission } as const, {
diff --git a/apps/frontend/app/routes/_dashboard.tsx b/apps/frontend/app/routes/_dashboard.tsx
index c90ff068f2..01cda70945 100644
--- a/apps/frontend/app/routes/_dashboard.tsx
+++ b/apps/frontend/app/routes/_dashboard.tsx
@@ -1,62 +1,119 @@
 import { useAutoAnimate } from "@formkit/auto-animate/react";
 import { $path } from "@ignisda/remix-routes";
 import {
+	Alert,
 	Anchor,
 	AppShell,
 	Box,
 	Burger,
+	Button,
 	Center,
+	Checkbox,
 	Collapse,
 	Flex,
 	Group,
 	Image,
+	Input,
+	Loader,
+	Modal,
+	NumberInput,
+	Rating,
 	ScrollArea,
+	SegmentedControl,
+	Select,
+	Slider,
 	Stack,
 	Text,
+	TextInput,
+	Textarea,
 	ThemeIcon,
+	Title,
 	UnstyledButton,
 	useDirection,
 	useMantineTheme,
 } from "@mantine/core";
+import { DateInput, DatePickerInput, DateTimePicker } from "@mantine/dates";
 import { upperFirst, useDisclosure, useLocalStorage } from "@mantine/hooks";
 import { unstable_defineLoader } from "@remix-run/node";
 import { Form, Link, NavLink, Outlet, useLoaderData } from "@remix-run/react";
-import { UserLot } from "@ryot/generated/graphql/backend/graphql";
-import { changeCase } from "@ryot/ts-utils";
 import {
+	CollectionExtraInformationLot,
+	type CoreDetails,
+	EntityLot,
+	MediaLot,
+	type MetadataDetailsQuery,
+	type UserCollectionsListQuery,
+	UserLot,
+	type UserMetadataDetailsQuery,
+	UserReviewScale,
+	Visibility,
+} from "@ryot/generated/graphql/backend/graphql";
+import { changeCase, formatDateToNaiveDate, groupBy } from "@ryot/ts-utils";
+import {
+	IconAlertCircle,
 	IconArchive,
+	IconBook,
+	IconBrandPagekit,
 	IconCalendar,
 	IconChevronLeft,
 	IconChevronRight,
+	IconClock,
 	IconDeviceSpeaker,
+	IconDeviceTv,
 	IconHome2,
 	IconLogout,
 	IconMoon,
+	IconPercentage,
 	IconSettings,
 	IconStretching,
 	IconSun,
 } from "@tabler/icons-react";
 import { produce } from "immer";
-import { joinURL } from "ufo";
+import { useState } from "react";
+import { Fragment } from "react/jsx-runtime";
+import { match } from "ts-pattern";
+import { joinURL, withQuery } from "ufo";
 import { HiddenLocationInput } from "~/components/common";
-import { LOGO_IMAGE_URL, getLot } from "~/lib/generals";
+import events from "~/lib/events";
 import {
-	redirectIfNotAuthenticatedOrUpdated,
-	serverVariables,
-} from "~/lib/utilities.server";
+	CORE_DETAILS_COOKIE_NAME,
+	LOGO_IMAGE_URL,
+	Verb,
+	getLot,
+	getVerb,
+	queryClient,
+} from "~/lib/generals";
+import {
+	useMetadataDetails,
+	useUserCollections,
+	useUserDetails,
+	useUserMetadataDetails,
+	useUserPreferences,
+} from "~/lib/hooks";
 import {
-	colorSchemeCookie,
-	getCoreDetails,
+	type UpdateProgressData,
+	useAddEntityToCollection,
+	useMetadataProgressUpdate,
+	useReviewEntity,
+} from "~/lib/state/media";
+import {
+	serverVariables as envData,
+	getCachedUserCollectionsList,
+	getCookieValue,
 	getUserPreferences,
+	redirectIfNotAuthenticatedOrUpdated,
 } from "~/lib/utilities.server";
+import { colorSchemeCookie } from "~/lib/utilities.server";
 import classes from "~/styles/dashboard.module.css";
 
 export const loader = unstable_defineLoader(async ({ request }) => {
 	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
-	const [userPreferences, coreDetails] = await Promise.all([
+	const [userPreferences, userCollections] = await Promise.all([
 		getUserPreferences(request),
-		getCoreDetails(request),
+		getCachedUserCollectionsList(request),
 	]);
+	const details = getCookieValue(request, CORE_DETAILS_COOKIE_NAME);
+	const coreDetails = JSON.parse(details) as CoreDetails;
 
 	const mediaLinks = [
 		...(Object.entries(userPreferences.featuresEnabled.media || {})
@@ -132,7 +189,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 		{ label: "Integrations", link: $path("/settings/integrations") },
 		{ label: "Notifications", link: $path("/settings/notifications") },
 		{ label: "Miscellaneous", link: $path("/settings/miscellaneous") },
-		userDetails.__typename === "User" && userDetails.lot === UserLot.Admin
+		userDetails.lot === UserLot.Admin
 			? { label: "Users", link: $path("/settings/users") }
 			: undefined,
 	].filter((link) => link !== undefined);
@@ -142,21 +199,22 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 	);
 
 	const shouldHaveUmami =
-		serverVariables.FRONTEND_UMAMI_SCRIPT_URL &&
-		serverVariables.FRONTEND_UMAMI_WEBSITE_ID &&
-		!serverVariables.DISABLE_TELEMETRY &&
+		envData.FRONTEND_UMAMI_SCRIPT_URL &&
+		envData.FRONTEND_UMAMI_WEBSITE_ID &&
+		!envData.DISABLE_TELEMETRY &&
 		!userDetails.isDemo;
 
 	return {
-		envData: serverVariables,
+		envData,
 		mediaLinks,
 		userDetails,
 		coreDetails,
 		fitnessLinks,
 		settingsLinks,
+		userPreferences,
 		shouldHaveUmami,
+		userCollections,
 		currentColorScheme,
-		userPreferences,
 	};
 });
 
@@ -184,9 +242,45 @@ export default function Layout() {
 	const theme = useMantineTheme();
 	const [opened, { toggle }] = useDisclosure(false);
 	const Icon = loaderData.currentColorScheme === "dark" ? IconSun : IconMoon;
+	const [metadataToUpdate, setMetadataToUpdate] = useMetadataProgressUpdate();
+	const closeMetadataProgressUpdateModal = () => setMetadataToUpdate(null);
+	const [entityToReview, setEntityToReview] = useReviewEntity();
+	const closeReviewEntityModal = () => setEntityToReview(null);
+	const [addEntityToCollectionData, setAddEntityToCollectionData] =
+		useAddEntityToCollection();
+	const closeAddEntityToCollectionModal = () =>
+		setAddEntityToCollectionData(null);
 
 	return (
 		<>
+			<Modal
+				onClose={closeMetadataProgressUpdateModal}
+				opened={metadataToUpdate !== null}
+				withCloseButton={false}
+				centered
+			>
+				<MetadataProgressUpdateForm
+					closeMetadataProgressUpdateModal={closeMetadataProgressUpdateModal}
+				/>
+			</Modal>
+			<Modal
+				onClose={() => setEntityToReview(null)}
+				opened={entityToReview !== null}
+				withCloseButton={false}
+				centered
+			>
+				<ReviewEntityForm closeReviewEntityModal={closeReviewEntityModal} />
+			</Modal>
+			<Modal
+				onClose={closeAddEntityToCollectionModal}
+				opened={addEntityToCollectionData !== null}
+				withCloseButton={false}
+				centered
+			>
+				<AddEntityToCollectionForm
+					closeAddEntityToCollectionModal={closeAddEntityToCollectionModal}
+				/>
+			</Modal>
 			<AppShell
 				w="100%"
 				padding={0}
@@ -229,7 +323,7 @@ export default function Layout() {
 									)
 								}
 							/>
-						) : undefined}
+						) : null}
 						{loaderData.userPreferences.featuresEnabled.fitness.enabled ? (
 							<LinksGroup
 								label="Fitness"
@@ -245,7 +339,7 @@ export default function Layout() {
 								}
 								links={loaderData.fitnessLinks}
 							/>
-						) : undefined}
+						) : null}
 						{loaderData.userPreferences.featuresEnabled.others.calendar ? (
 							<LinksGroup
 								label="Calendar"
@@ -454,10 +548,10 @@ function LinksGroup({
 									: "none",
 							}}
 						/>
-					) : undefined}
+					) : null}
 				</Group>
 			</UnstyledButton>
-			{hasLinks ? <Collapse in={opened}>{items}</Collapse> : undefined}
+			{hasLinks ? <Collapse in={opened}>{items}</Collapse> : null}
 		</>
 	);
 }
@@ -492,3 +586,791 @@ const Footer = () => {
 		</Stack>
 	);
 };
+
+const WATCH_TIMES = [
+	"Just Right Now",
+	"I don't remember",
+	"Custom Date",
+] as const;
+
+const MetadataProgressUpdateForm = ({
+	closeMetadataProgressUpdateModal,
+}: {
+	closeMetadataProgressUpdateModal: () => void;
+}) => {
+	const [metadataToUpdate] = useMetadataProgressUpdate();
+
+	const { data: metadataDetails } = useMetadataDetails(
+		metadataToUpdate?.metadataId,
+	);
+	const { data: userMetadataDetails } = useUserMetadataDetails(
+		metadataToUpdate?.metadataId,
+	);
+
+	if (!metadataDetails || !metadataToUpdate || !userMetadataDetails)
+		return (
+			<Center p="lg">
+				<Loader type="dots" />
+			</Center>
+		);
+
+	const onSubmit = () => {
+		queryClient.removeQueries({
+			queryKey: ["userMetadataDetails", metadataToUpdate.metadataId],
+		});
+		events.updateProgress(metadataDetails.title);
+		closeMetadataProgressUpdateModal();
+	};
+
+	return userMetadataDetails.inProgress ? (
+		<MetadataInProgressUpdateForm
+			onSubmit={onSubmit}
+			metadataDetails={metadataDetails}
+			metadataToUpdate={metadataToUpdate}
+			inProgress={userMetadataDetails.inProgress}
+		/>
+	) : (
+		<NewProgressUpdateForm
+			onSubmit={onSubmit}
+			metadataDetails={metadataDetails}
+			metadataToUpdate={metadataToUpdate}
+		/>
+	);
+};
+
+type InProgress = UserMetadataDetailsQuery["userMetadataDetails"]["inProgress"];
+
+const MetadataInProgressUpdateForm = ({
+	onSubmit,
+	inProgress,
+	metadataDetails,
+	metadataToUpdate,
+}: {
+	onSubmit: () => void;
+	inProgress: NonNullable<InProgress>;
+	metadataToUpdate: UpdateProgressData;
+	metadataDetails: MetadataDetailsQuery["metadataDetails"];
+}) => {
+	const userPreferences = useUserPreferences();
+	const total =
+		metadataDetails.audioBookSpecifics?.runtime ||
+		metadataDetails.bookSpecifics?.pages ||
+		metadataDetails.movieSpecifics?.runtime ||
+		metadataDetails.mangaSpecifics?.chapters ||
+		metadataDetails.animeSpecifics?.episodes ||
+		metadataDetails.visualNovelSpecifics?.length;
+	const progress = Number(inProgress.progress);
+	const [value, setValue] = useState<number | undefined>(progress);
+
+	const [updateIcon, text] = match(metadataDetails.lot)
+		.with(MediaLot.Book, () => [<IconBook size={24} key="element" />, "Pages"])
+		.with(MediaLot.Anime, () => [
+			<IconDeviceTv size={24} key="element" />,
+			"Episodes",
+		])
+		.with(MediaLot.Manga, () => [
+			<IconBrandPagekit size={24} key="element" />,
+			"Chapters",
+		])
+		.with(MediaLot.Movie, MediaLot.VisualNovel, MediaLot.AudioBook, () => [
+			<IconClock size={24} key="element" />,
+			"Minutes",
+		])
+		.otherwise(() => [null, null]);
+
+	return (
+		<Form
+			method="post"
+			onSubmit={onSubmit}
+			action={withQuery($path("/actions"), {
+				intent: "individualProgressUpdate",
+			})}
+		>
+			<HiddenLocationInput hash={metadataToUpdate.pageFragment} />
+			<input
+				hidden
+				name="metadataId"
+				defaultValue={metadataToUpdate.metadataId}
+			/>
+			<input hidden name="progress" value={value} readOnly />
+			<input
+				hidden
+				name="date"
+				defaultValue={formatDateToNaiveDate(new Date())}
+			/>
+			<Stack mt="sm">
+				<Group>
+					<Slider
+						max={100}
+						min={0}
+						step={1}
+						showLabelOnHover={false}
+						value={value}
+						onChange={setValue}
+						style={{ flexGrow: 1 }}
+					/>
+					<NumberInput
+						value={value}
+						onChange={(v) => {
+							if (v) setValue(Number(v));
+							else setValue(undefined);
+						}}
+						max={100}
+						min={0}
+						step={1}
+						w="20%"
+						hideControls
+						rightSection={<IconPercentage size={16} />}
+					/>
+				</Group>
+				{total ? (
+					<>
+						<Text ta="center" fw="bold">
+							OR
+						</Text>
+						<Flex align="center" gap="xs">
+							<NumberInput
+								defaultValue={((total || 1) * (value || 1)) / 100}
+								onChange={(v) => {
+									const value = (Number(v) / (total || 1)) * 100;
+									setValue(value);
+								}}
+								max={total}
+								min={0}
+								step={1}
+								hideControls
+								leftSection={updateIcon}
+							/>
+							<Text>{text}</Text>
+						</Flex>
+					</>
+				) : null}
+				<Select
+					data={userPreferences.general.watchProviders}
+					label={`Where did you ${getVerb(Verb.Read, metadataDetails.lot)} it?`}
+					name="providerWatchedOn"
+					defaultValue={inProgress.providerWatchedOn}
+				/>
+				<Button variant="outline" type="submit">
+					Update
+				</Button>
+			</Stack>
+		</Form>
+	);
+};
+
+const NewProgressUpdateForm = ({
+	onSubmit,
+	metadataDetails,
+	metadataToUpdate,
+}: {
+	onSubmit: () => void;
+	metadataToUpdate: UpdateProgressData;
+	metadataDetails: MetadataDetailsQuery["metadataDetails"];
+}) => {
+	const userPreferences = useUserPreferences();
+	const [_, setMetadataToUpdate] = useMetadataProgressUpdate();
+
+	const [selectedDate, setSelectedDate] = useState<Date | null | undefined>(
+		new Date(),
+	);
+	const [watchTime, setWatchTime] =
+		useState<(typeof WATCH_TIMES)[number]>("Just Right Now");
+	const [animeEpisodeNumber, setAnimeEpisodeNumber] = useState<
+		string | undefined
+	>(undefined);
+	const [mangaChapterNumber, setMangaChapterNumber] = useState<
+		string | undefined
+	>(undefined);
+	const [mangaVolumeNumber, setMangaVolumeNumber] = useState<
+		string | undefined
+	>(undefined);
+
+	return (
+		<Form
+			method="post"
+			onSubmit={onSubmit}
+			action={withQuery($path("/actions"), { intent: "progressUpdate" })}
+		>
+			{[
+				...Object.entries(metadataToUpdate),
+				["metadataLot", metadataDetails.lot],
+			].map(([k, v]) => (
+				<Fragment key={k}>
+					{typeof v !== "undefined" ? (
+						<input hidden readOnly name={k} value={v?.toString()} />
+					) : null}
+				</Fragment>
+			))}
+			<HiddenLocationInput hash={metadataToUpdate.pageFragment} />
+			<Stack>
+				{metadataDetails.lot === MediaLot.Anime ? (
+					<>
+						<NumberInput
+							label="Episode"
+							name="animeEpisodeNumber"
+							description="Leaving this empty will mark the whole anime as watched"
+							hideControls
+							value={animeEpisodeNumber}
+							onChange={(e) => setAnimeEpisodeNumber(e.toString())}
+						/>
+						{animeEpisodeNumber ? (
+							<Checkbox
+								label="Mark all episodes before this as watched"
+								name="animeAllEpisodesBefore"
+							/>
+						) : null}
+					</>
+				) : null}
+				{metadataDetails.lot === MediaLot.Manga ? (
+					<>
+						<Box>
+							<Text c="dimmed" size="sm">
+								Leaving the following empty will mark the whole manga as watched
+							</Text>
+							<Group wrap="nowrap">
+								<NumberInput
+									label="Chapter"
+									name="mangaChapterNumber"
+									hideControls
+									value={mangaChapterNumber}
+									onChange={(e) => setMangaChapterNumber(e.toString())}
+								/>
+								<Text ta="center" fw="bold" mt="sm">
+									OR
+								</Text>
+								<NumberInput
+									label="Volume"
+									name="mangaVolumeNumber"
+									hideControls
+									value={mangaVolumeNumber}
+									onChange={(e) => setMangaVolumeNumber(e.toString())}
+								/>
+							</Group>
+						</Box>
+						{mangaChapterNumber ? (
+							<Checkbox
+								label="Mark all chapters before this as watched"
+								name="mangaAllChaptersBefore"
+							/>
+						) : null}
+					</>
+				) : null}
+				{metadataDetails.lot === MediaLot.Show ? (
+					<>
+						<input
+							hidden
+							name="showSpecifics"
+							defaultValue={JSON.stringify(
+								metadataDetails.showSpecifics?.seasons.map((s) => ({
+									seasonNumber: s.seasonNumber,
+									episodes: s.episodes.map((e) => e.episodeNumber),
+								})),
+							)}
+						/>
+						{metadataToUpdate.onlySeason || metadataToUpdate.completeShow ? (
+							<Alert color="yellow" icon={<IconAlertCircle />}>
+								{metadataToUpdate.onlySeason
+									? `This will mark all episodes of season ${metadataToUpdate.showSeasonNumber} as seen`
+									: metadataToUpdate.completeShow
+										? "This will mark all episodes for this show as seen"
+										: null}
+							</Alert>
+						) : null}
+						{!metadataToUpdate.completeShow ? (
+							<Select
+								label="Season"
+								required
+								data={metadataDetails.showSpecifics?.seasons.map((s) => ({
+									label: `${s.seasonNumber}. ${s.name.toString()}`,
+									value: s.seasonNumber.toString(),
+								}))}
+								value={metadataToUpdate.showSeasonNumber?.toString()}
+								onChange={(v) => {
+									setMetadataToUpdate(
+										produce(metadataToUpdate, (draft) => {
+											draft.showSeasonNumber = Number(v);
+										}),
+									);
+								}}
+								searchable
+								limit={50}
+							/>
+						) : null}
+						{metadataToUpdate?.onlySeason ? (
+							<Checkbox
+								label="Mark all seasons before this as seen"
+								name="showAllSeasonsBefore"
+							/>
+						) : null}
+						{!metadataToUpdate.onlySeason &&
+						typeof metadataToUpdate.showSeasonNumber !== "undefined" ? (
+							<Select
+								label="Episode"
+								required
+								data={
+									metadataDetails.showSpecifics?.seasons
+										.find(
+											(s) =>
+												s.seasonNumber ===
+												Number(metadataToUpdate.showSeasonNumber),
+										)
+										?.episodes.map((e) => ({
+											label: `${e.episodeNumber}. ${e.name.toString()}`,
+											value: e.episodeNumber.toString(),
+										})) || []
+								}
+								value={metadataToUpdate.showEpisodeNumber?.toString()}
+								onChange={(v) => {
+									setMetadataToUpdate(
+										produce(metadataToUpdate, (draft) => {
+											draft.showEpisodeNumber = Number(v);
+										}),
+									);
+								}}
+								searchable
+								limit={50}
+							/>
+						) : null}
+					</>
+				) : null}
+				{metadataDetails.lot === MediaLot.Podcast ? (
+					<>
+						<input
+							hidden
+							name="podcastSpecifics"
+							defaultValue={JSON.stringify(
+								metadataDetails.podcastSpecifics?.episodes.map((e) => ({
+									episodeNumber: e.number,
+								})),
+							)}
+						/>
+						{metadataToUpdate.completePodcast ? (
+							<Alert color="yellow" icon={<IconAlertCircle />}>
+								This will mark all episodes for this podcast as seen
+							</Alert>
+						) : (
+							<>
+								<Text fw="bold">Select episode</Text>
+								<Select
+									required
+									label="Episode"
+									data={metadataDetails.podcastSpecifics?.episodes.map(
+										(se) => ({
+											label: se.title.toString(),
+											value: se.number.toString(),
+										}),
+									)}
+									value={metadataToUpdate.podcastEpisodeNumber?.toString()}
+									onChange={(v) => {
+										setMetadataToUpdate(
+											produce(metadataToUpdate, (draft) => {
+												draft.podcastEpisodeNumber = Number(v);
+											}),
+										);
+									}}
+									searchable
+									limit={50}
+								/>
+							</>
+						)}
+					</>
+				) : null}
+				<Select
+					label={`When did you ${getVerb(Verb.Read, metadataDetails.lot)} it?`}
+					data={WATCH_TIMES}
+					value={watchTime}
+					onChange={(v) => {
+						setWatchTime(v as typeof watchTime);
+						match(v)
+							.with(WATCH_TIMES[0], () => setSelectedDate(new Date()))
+							.with(WATCH_TIMES[1], () => setSelectedDate(null))
+							.with(WATCH_TIMES[2], () => setSelectedDate(null));
+					}}
+				/>
+				{watchTime === WATCH_TIMES[2] ? (
+					<DatePickerInput
+						required
+						label="Enter exact date"
+						dropdownType="modal"
+						maxDate={new Date()}
+						onChange={setSelectedDate}
+						clearable
+					/>
+				) : null}
+				<Select
+					label={`Where did you ${getVerb(Verb.Read, metadataDetails.lot)} it?`}
+					data={userPreferences.general.watchProviders}
+					name="providerWatchedOn"
+				/>
+				<Button
+					variant="outline"
+					disabled={selectedDate === undefined}
+					type="submit"
+					name="date"
+					value={selectedDate ? formatDateToNaiveDate(selectedDate) : undefined}
+				>
+					Submit
+				</Button>
+			</Stack>
+		</Form>
+	);
+};
+
+const ReviewEntityForm = ({
+	closeReviewEntityModal,
+}: {
+	closeReviewEntityModal: () => void;
+}) => {
+	const userPreferences = useUserPreferences();
+	const [entityToReview] = useReviewEntity();
+
+	if (!entityToReview) return null;
+
+	return (
+		<Form
+			method="post"
+			action="/actions?intent=performReviewAction"
+			replace
+			onSubmit={() => {
+				events.postReview(entityToReview.entityTitle);
+				closeReviewEntityModal();
+			}}
+		>
+			<input
+				hidden
+				name={match(entityToReview.entityLot)
+					.with(EntityLot.Metadata, () => "metadataId")
+					.with(EntityLot.MetadataGroup, () => "metadataGroupId")
+					.with(EntityLot.Person, () => "personId")
+					.with(EntityLot.Collection, () => "collection")
+					.run()}
+				value={entityToReview.entityId}
+				readOnly
+			/>
+			<HiddenLocationInput />
+			{entityToReview.existingReview?.id ? (
+				<input
+					hidden
+					name="reviewId"
+					value={entityToReview.existingReview.id}
+				/>
+			) : null}
+			<Stack>
+				<Flex align="center" gap="xl">
+					{match(userPreferences.general.reviewScale)
+						.with(UserReviewScale.OutOfFive, () => (
+							<Flex gap="sm" mt="lg">
+								<Input.Label>Rating:</Input.Label>
+								<Rating
+									name="rating"
+									defaultValue={
+										entityToReview.existingReview?.rating
+											? Number(entityToReview.existingReview.rating)
+											: undefined
+									}
+									fractions={2}
+								/>
+							</Flex>
+						))
+						.with(UserReviewScale.OutOfHundred, () => (
+							<NumberInput
+								label="Rating"
+								name="rating"
+								min={0}
+								max={100}
+								step={1}
+								w="40%"
+								hideControls
+								rightSection={<IconPercentage size={16} />}
+								defaultValue={
+									entityToReview.existingReview?.rating
+										? Number(entityToReview.existingReview.rating)
+										: undefined
+								}
+							/>
+						))
+						.exhaustive()}
+					<Checkbox label="This review is a spoiler" mt="lg" name="isSpoiler" />
+				</Flex>
+				{entityToReview.metadataLot === MediaLot.Show ? (
+					<Flex gap="md">
+						<NumberInput
+							label="Season"
+							name="showSeasonNumber"
+							hideControls
+							defaultValue={
+								typeof entityToReview.existingReview?.showExtraInformation
+									?.season === "number"
+									? entityToReview.existingReview.showExtraInformation.season
+									: undefined
+							}
+						/>
+						<NumberInput
+							label="Episode"
+							name="showEpisodeNumber"
+							hideControls
+							defaultValue={
+								typeof entityToReview.existingReview?.showExtraInformation
+									?.episode === "number"
+									? entityToReview.existingReview.showExtraInformation.episode
+									: undefined
+							}
+						/>
+					</Flex>
+				) : null}
+				{entityToReview.metadataLot === MediaLot.Podcast ? (
+					<NumberInput
+						label="Episode"
+						name="podcastEpisodeNumber"
+						hideControls
+						defaultValue={
+							typeof entityToReview.existingReview?.podcastExtraInformation
+								?.episode === "number"
+								? entityToReview.existingReview.podcastExtraInformation.episode
+								: undefined
+						}
+					/>
+				) : null}
+				{entityToReview.metadataLot === MediaLot.Anime ? (
+					<NumberInput
+						label="Episode"
+						name="animeEpisodeNumber"
+						hideControls
+						defaultValue={
+							typeof entityToReview.existingReview?.animeExtraInformation
+								?.episode === "number"
+								? entityToReview.existingReview.animeExtraInformation.episode
+								: undefined
+						}
+					/>
+				) : null}
+				{entityToReview.metadataLot === MediaLot.Manga ? (
+					<>
+						<Group wrap="nowrap">
+							<NumberInput
+								label="Chapter"
+								name="mangaChapterNumber"
+								hideControls
+								defaultValue={
+									typeof entityToReview.existingReview?.mangaExtraInformation
+										?.chapter === "number"
+										? entityToReview.existingReview.mangaExtraInformation
+												.chapter
+										: undefined
+								}
+							/>
+							<Text ta="center" fw="bold" mt="sm">
+								OR
+							</Text>
+							<NumberInput
+								label="Volume"
+								name="mangaVolumeNumber"
+								hideControls
+								defaultValue={
+									typeof entityToReview.existingReview?.mangaExtraInformation
+										?.volume === "number"
+										? entityToReview.existingReview.mangaExtraInformation.volume
+										: undefined
+								}
+							/>
+						</Group>
+					</>
+				) : null}
+				<Textarea
+					label="Review"
+					name="text"
+					description="Markdown is supported"
+					autoFocus
+					minRows={10}
+					maxRows={20}
+					autosize
+					defaultValue={
+						entityToReview.existingReview?.textOriginal ?? undefined
+					}
+				/>
+				<Box>
+					<Input.Label>Visibility</Input.Label>
+					<SegmentedControl
+						fullWidth
+						data={Object.entries(Visibility).map(([k, v]) => ({
+							label: changeCase(k),
+							value: v,
+						}))}
+						defaultValue={
+							entityToReview.existingReview?.visibility ?? Visibility.Public
+						}
+						name="visibility"
+					/>
+				</Box>
+				<Button mt="md" type="submit" w="100%">
+					{entityToReview.existingReview?.id ? "Update" : "Submit"}
+				</Button>
+			</Stack>
+		</Form>
+	);
+};
+
+type Collection = UserCollectionsListQuery["userCollectionsList"][number];
+
+const AddEntityToCollectionForm = ({
+	closeAddEntityToCollectionModal,
+}: {
+	closeAddEntityToCollectionModal: () => void;
+}) => {
+	const userDetails = useUserDetails();
+	const collections = useUserCollections();
+	const [selectedCollection, setSelectedCollection] =
+		useState<Collection | null>(null);
+	const [ownedOn, setOwnedOn] = useState<Date | null>();
+	const [addEntityToCollectionData, _] = useAddEntityToCollection();
+
+	if (!addEntityToCollectionData) return null;
+
+	const selectData = Object.entries(
+		groupBy(collections, (c) =>
+			c.creator.id === userDetails.id ? "You" : c.creator.name,
+		),
+	).map(([g, items]) => ({
+		group: g,
+		items: items.map((c) => ({
+			label: c.name,
+			value: c.id.toString(),
+			disabled: addEntityToCollectionData.alreadyInCollections?.includes(
+				c.id.toString(),
+			),
+		})),
+	}));
+
+	return (
+		<Form
+			action="/actions?intent=addEntityToCollection"
+			method="post"
+			onSubmit={() => {
+				closeAddEntityToCollectionModal();
+			}}
+		>
+			<input
+				readOnly
+				hidden
+				name="entityId"
+				value={addEntityToCollectionData.entityId}
+			/>
+			<input
+				readOnly
+				hidden
+				name="entityLot"
+				value={addEntityToCollectionData.entityLot}
+			/>
+			<HiddenLocationInput />
+			<Stack>
+				<Title order={3}>Select collection</Title>
+				<Select
+					searchable
+					data={selectData}
+					nothingFoundMessage="Nothing found..."
+					value={selectedCollection?.id.toString()}
+					onChange={(v) => {
+						if (v) {
+							const collection = collections.find((c) => c.id === v);
+							if (collection) setSelectedCollection(collection);
+						}
+					}}
+				/>
+				{selectedCollection ? (
+					<>
+						<input
+							readOnly
+							hidden
+							name="collectionName"
+							value={selectedCollection.name}
+						/>
+						<input
+							readOnly
+							hidden
+							name="creatorUserId"
+							value={selectedCollection.creator.id}
+						/>
+						{selectedCollection.informationTemplate?.map((template) => (
+							<Fragment key={template.name}>
+								{match(template.lot)
+									.with(CollectionExtraInformationLot.String, () => (
+										<TextInput
+											name={`information.${template.name}`}
+											label={template.name}
+											description={template.description}
+											required={!!template.required}
+											defaultValue={template.defaultValue || undefined}
+										/>
+									))
+									.with(CollectionExtraInformationLot.Number, () => (
+										<NumberInput
+											name={`information.${template.name}`}
+											label={template.name}
+											description={template.description}
+											required={!!template.required}
+											defaultValue={
+												template.defaultValue
+													? Number(template.defaultValue)
+													: undefined
+											}
+										/>
+									))
+									.with(CollectionExtraInformationLot.Date, () => (
+										<>
+											<DateInput
+												label={template.name}
+												description={template.description}
+												required={!!template.required}
+												onChange={setOwnedOn}
+												value={ownedOn}
+												defaultValue={
+													template.defaultValue
+														? new Date(template.defaultValue)
+														: undefined
+												}
+											/>
+											<input
+												readOnly
+												hidden
+												name={`information.${template.name}`}
+												value={
+													ownedOn ? formatDateToNaiveDate(ownedOn) : undefined
+												}
+											/>
+										</>
+									))
+									.with(CollectionExtraInformationLot.DateTime, () => (
+										<DateTimePicker
+											name={`information.${template.name}`}
+											label={template.name}
+											description={template.description}
+											required={!!template.required}
+										/>
+									))
+									.exhaustive()}
+							</Fragment>
+						))}
+					</>
+				) : null}
+				<Button
+					disabled={!selectedCollection}
+					variant="outline"
+					type="submit"
+					onClick={() =>
+						events.addToCollection(addEntityToCollectionData.entityLot)
+					}
+				>
+					Set
+				</Button>
+				<Button
+					variant="outline"
+					color="red"
+					onClick={closeAddEntityToCollectionModal}
+				>
+					Cancel
+				</Button>
+			</Stack>
+		</Form>
+	);
+};
diff --git a/apps/frontend/app/routes/actions.tsx b/apps/frontend/app/routes/actions.tsx
index 1a3438af8f..a7ec67240f 100644
--- a/apps/frontend/app/routes/actions.tsx
+++ b/apps/frontend/app/routes/actions.tsx
@@ -12,11 +12,13 @@ import {
 	CreateReviewCommentDocument,
 	DeleteReviewDocument,
 	DeleteS3ObjectDocument,
+	DeployBulkProgressUpdateDocument,
 	EntityLot,
 	MediaLot,
 	MediaSource,
 	PostReviewDocument,
 	RemoveEntityFromCollectionDocument,
+	SeenState,
 	Visibility,
 } from "@ryot/generated/graphql/backend/graphql";
 import { isEmpty, omitBy } from "@ryot/ts-utils";
@@ -26,6 +28,7 @@ import { z } from "zod";
 import { zx } from "zodix";
 import { redirectToQueryParam } from "~/lib/generals";
 import {
+	MetadataIdSchema,
 	MetadataSpecificsSchema,
 	colorSchemeCookie,
 	createToastHeaders,
@@ -33,6 +36,7 @@ import {
 	getAuthorizationHeader,
 	getLogoutCookies,
 	processSubmission,
+	removeCachedUserCollectionsList,
 	s3FileUploader,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -53,7 +57,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 			const { commitMetadata } = await serverGqlService.request(
 				CommitMetadataDocument,
 				{ input: submission },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			returnData = { commitMedia: commitMetadata };
 		})
@@ -86,7 +90,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 						},
 					},
 				},
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			returnData = { commitPerson };
 		})
@@ -95,7 +99,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 			const { commitMetadataGroup } = await serverGqlService.request(
 				CommitMetadataGroupDocument,
 				{ input: submission },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			returnData = { commitMetadataGroup };
 		})
@@ -113,7 +117,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 			redirectTo = $path("/auth");
 			response.headers = extendResponseHeaders(
 				response.headers,
-				await getLogoutCookies(),
+				getLogoutCookies(),
 			);
 		})
 		.with("createReviewComment", async () => {
@@ -121,7 +125,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 			await serverGqlService.request(
 				CreateReviewCommentDocument,
 				{ input: submission },
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 			response.headers = extendResponseHeaders(
 				response.headers,
@@ -137,6 +141,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 			);
 		})
 		.with("addEntityToCollection", async () => {
+			removeCachedUserCollectionsList(request);
 			const [submission, input] =
 				getChangeCollectionToEntityVariables(formData);
 			const addTo = [submission.collectionName];
@@ -152,7 +157,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 							information: omitBy(submission.information || {}, isEmpty),
 						},
 					},
-					await getAuthorizationHeader(request),
+					getAuthorizationHeader(request),
 				);
 			}
 			response.headers = extendResponseHeaders(
@@ -164,6 +169,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 			);
 		})
 		.with("removeEntityFromCollection", async () => {
+			removeCachedUserCollectionsList(request);
 			const [submission, input] =
 				getChangeCollectionToEntityVariables(formData);
 			await serverGqlService.request(
@@ -175,7 +181,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 						creatorUserId: submission.creatorUserId,
 					},
 				},
-				await getAuthorizationHeader(request),
+				getAuthorizationHeader(request),
 			);
 		})
 		.with("performReviewAction", async () => {
@@ -185,7 +191,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 				await serverGqlService.request(
 					DeleteReviewDocument,
 					{ reviewId: submission.reviewId },
-					await getAuthorizationHeader(request),
+					getAuthorizationHeader(request),
 				);
 				response.headers = extendResponseHeaders(
 					response.headers,
@@ -198,7 +204,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 				await serverGqlService.request(
 					PostReviewDocument,
 					{ input: submission },
-					await getAuthorizationHeader(request),
+					getAuthorizationHeader(request),
 				);
 				response.headers = extendResponseHeaders(
 					response.headers,
@@ -209,6 +215,137 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 				);
 			}
 		})
+		.with("progressUpdate", async () => {
+			const submission = processSubmission(formData, progressUpdateSchema);
+			const variables = {
+				metadataId: submission.metadataId,
+				progress: "100",
+				date: submission.date,
+				showSeasonNumber: submission.showSeasonNumber,
+				showEpisodeNumber: submission.showEpisodeNumber,
+				podcastEpisodeNumber: submission.podcastEpisodeNumber,
+				animeEpisodeNumber: submission.animeEpisodeNumber,
+				mangaChapterNumber: submission.mangaChapterNumber,
+				mangaVolumeNumber: submission.mangaVolumeNumber,
+				providerWatchedOn: submission.providerWatchedOn,
+			};
+			let needsFinalUpdate = true;
+			const updates = [];
+			const showSpecifics = showSpecificsSchema.parse(
+				JSON.parse(submission.showSpecifics || "[]"),
+			);
+			const podcastSpecifics = podcastSpecificsSchema.parse(
+				JSON.parse(submission.podcastSpecifics || "[]"),
+			);
+			if (submission.metadataLot === MediaLot.Anime) {
+				if (submission.animeEpisodeNumber) {
+					if (submission.animeAllEpisodesBefore) {
+						for (let i = 1; i <= submission.animeEpisodeNumber; i++) {
+							updates.push({
+								...variables,
+								animeEpisodeNumber: i,
+							});
+						}
+						needsFinalUpdate = false;
+					}
+				}
+			}
+			if (submission.metadataLot === MediaLot.Manga) {
+				if (submission.mangaChapterNumber) {
+					if (submission.mangaAllChaptersBefore) {
+						for (let i = 1; i <= submission.mangaChapterNumber; i++) {
+							updates.push({
+								...variables,
+								mangaChapterNumber: i,
+							});
+						}
+						needsFinalUpdate = false;
+					}
+				}
+			}
+			if (submission.metadataLot === MediaLot.Show) {
+				if (submission.completeShow) {
+					for (const season of showSpecifics) {
+						for (const episode of season.episodes) {
+							updates.push({
+								...variables,
+								showSeasonNumber: season.seasonNumber,
+								showEpisodeNumber: episode,
+							});
+						}
+					}
+					needsFinalUpdate = false;
+				}
+				if (submission.onlySeason) {
+					const selectedSeason = showSpecifics.find(
+						(s) => s.seasonNumber === submission.showSeasonNumber,
+					);
+					invariant(selectedSeason, "No season selected");
+					needsFinalUpdate = false;
+					if (submission.showAllSeasonsBefore) {
+						for (const season of showSpecifics) {
+							if (season.seasonNumber > selectedSeason.seasonNumber) break;
+							for (const episode of season.episodes || []) {
+								updates.push({
+									...variables,
+									showSeasonNumber: season.seasonNumber,
+									showEpisodeNumber: episode,
+								});
+							}
+						}
+					} else {
+						for (const episode of selectedSeason.episodes || []) {
+							updates.push({
+								...variables,
+								showEpisodeNumber: episode,
+							});
+						}
+					}
+				}
+			}
+			if (submission.metadataLot === MediaLot.Podcast) {
+				if (submission.completePodcast) {
+					for (const episode of podcastSpecifics) {
+						updates.push({
+							...variables,
+							podcastEpisodeNumber: episode.episodeNumber,
+						});
+					}
+					needsFinalUpdate = false;
+				}
+			}
+			if (needsFinalUpdate) updates.push(variables);
+			const { deployBulkProgressUpdate } = await serverGqlService.request(
+				DeployBulkProgressUpdateDocument,
+				{ input: updates },
+				getAuthorizationHeader(request),
+			);
+			response.headers = extendResponseHeaders(
+				response.headers,
+				await createToastHeaders({
+					type: !deployBulkProgressUpdate ? "error" : "success",
+					message: !deployBulkProgressUpdate
+						? "Progress was not updated"
+						: "Progress updated successfully",
+				}),
+			);
+			redirectTo = submission[redirectToQueryParam];
+		})
+		.with("individualProgressUpdate", async () => {
+			const submission = processSubmission(formData, bulkUpdateSchema);
+			await serverGqlService.request(
+				DeployBulkProgressUpdateDocument,
+				{ input: submission },
+				getAuthorizationHeader(request),
+			);
+			response.headers = extendResponseHeaders(
+				response.headers,
+				await createToastHeaders({
+					message: "Progress updated successfully",
+					type: "success",
+				}),
+			);
+		})
 		.run();
 	if (redirectTo) {
 		response.headers.append("Location", redirectTo.toString());
@@ -268,9 +405,11 @@ const getChangeCollectionToEntityVariables = (formData: FormData) => {
 		changeCollectionToEntitySchema.passthrough(),
 	);
 	const metadataId =
-		submission.entityLot === EntityLot.Media ? submission.entityId : undefined;
+		submission.entityLot === EntityLot.Metadata
+			? submission.entityId
+			: undefined;
 	const metadataGroupId =
-		submission.entityLot === EntityLot.MediaGroup
+		submission.entityLot === EntityLot.MetadataGroup
 			? submission.entityId
 			: undefined;
 	const personId =
@@ -281,11 +420,40 @@ const getChangeCollectionToEntityVariables = (formData: FormData) => {
 			: undefined;
 	return [
 		submission,
-		{
-			metadataId,
-			metadataGroupId,
-			exerciseId,
-			personId,
-		},
+		{ metadataId, metadataGroupId, exerciseId, personId },
 	] as const;
 };
+
+const progressUpdateSchema = z
+	.object({
+		metadataLot: z.nativeEnum(MediaLot),
+		date: z.string().optional(),
+		[redirectToQueryParam]: z.string().optional(),
+		showSpecifics: z.string().optional(),
+		showAllSeasonsBefore: zx.CheckboxAsString.optional(),
+		podcastSpecifics: z.string().optional(),
+		onlySeason: zx.BoolAsString.optional(),
+		completeShow: zx.BoolAsString.optional(),
+		completePodcast: zx.BoolAsString.optional(),
+		animeAllEpisodesBefore: zx.CheckboxAsString.optional(),
+		mangaAllChaptersBefore: zx.CheckboxAsString.optional(),
+		providerWatchedOn: z.string().optional(),
+	})
+	.merge(MetadataIdSchema)
+	.merge(MetadataSpecificsSchema);
+
+const showSpecificsSchema = z.array(
+	z.object({ seasonNumber: z.number(), episodes: z.array(z.number()) }),
+);
+
+const podcastSpecificsSchema = z.array(z.object({ episodeNumber: z.number() }));
+
+const bulkUpdateSchema = z
+	.object({
+		progress: z.string().optional(),
+		date: z.string().optional(),
+		changeState: z.nativeEnum(SeenState).optional(),
+		providerWatchedOn: z.string().optional(),
+	})
+	.merge(MetadataSpecificsSchema)
+	.merge(MetadataIdSchema);
diff --git a/apps/frontend/app/routes/api.auth.tsx b/apps/frontend/app/routes/api.auth.tsx
index 076de92974..bc2d60c719 100644
--- a/apps/frontend/app/routes/api.auth.tsx
+++ b/apps/frontend/app/routes/api.auth.tsx
@@ -6,12 +6,9 @@ import {
 	LoginUserDocument,
 	RegisterUserDocument,
 } from "@ryot/generated/graphql/backend/graphql";
-import { type CookieSerializeOptions, serialize } from "cookie";
 import { z } from "zod";
 import { zx } from "zodix";
-import { AUTH_COOKIE_NAME } from "~/lib/generals";
 import {
-	combineHeaders,
 	getCookiesForApplication,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -30,7 +27,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 		email: getOidcToken.email,
 		issuerId: getOidcToken.subject,
 	};
-	const [{ coreDetails }] = await Promise.all([
+	await Promise.all([
 		serverGqlService.request(CoreDetailsDocument),
 		serverGqlService.request(RegisterUserDocument, {
 			input: { oidc: oidcInput },
@@ -40,19 +37,8 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 		input: { oidc: oidcInput },
 	});
 	if (loginUser.__typename === "LoginResponse") {
-		const cookies = await getCookiesForApplication(loginUser.apiKey);
-		const options = {
-			maxAge: coreDetails.tokenValidForDays * 24 * 60 * 60,
-			path: "/",
-		} satisfies CookieSerializeOptions;
-		return redirect($path("/"), {
-			headers: combineHeaders(
-				{
-					"set-cookie": serialize(AUTH_COOKIE_NAME, loginUser.apiKey, options),
-				},
-				cookies,
-			),
-		});
+		const headers = await getCookiesForApplication(loginUser.apiKey);
+		return redirect($path("/"), { headers });
 	}
 	return Response.json({ input });
 });
diff --git a/apps/frontend/app/routes/auth.tsx b/apps/frontend/app/routes/auth.tsx
index 0354e748b6..0543e68fe2 100644
--- a/apps/frontend/app/routes/auth.tsx
+++ b/apps/frontend/app/routes/auth.tsx
@@ -33,16 +33,14 @@ import {
 } from "@ryot/generated/graphql/backend/graphql";
 import { startCase } from "@ryot/ts-utils";
 import { IconAt } from "@tabler/icons-react";
-import { serialize } from "cookie";
 import { namedAction } from "remix-utils/named-action";
 import { safeRedirect } from "remix-utils/safe-redirect";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
-import { AUTH_COOKIE_NAME, redirectToQueryParam } from "~/lib/generals";
+import { redirectToQueryParam } from "~/lib/generals";
 import {
-	combineHeaders,
 	createToastHeaders,
 	getCookiesForApplication,
 	getCoreEnabledFeatures,
@@ -144,20 +142,8 @@ export const action = unstable_defineAction(async ({ request }) => {
 				let redirectUrl = $path("/");
 				if (submission[redirectToQueryParam])
 					redirectUrl = safeRedirect(submission[redirectToQueryParam]);
-				const cookies = await getCookiesForApplication(loginUser.apiKey);
-				const options = { maxAge: submission.tokenValidForDays * 24 * 60 * 60 };
-				return redirect(redirectUrl, {
-					headers: combineHeaders(
-						{
-							"set-cookie": serialize(
-								AUTH_COOKIE_NAME,
-								loginUser.apiKey,
-								options,
-							),
-						},
-						cookies,
-					),
-				});
+				const headers = await getCookiesForApplication(loginUser.apiKey);
+				return redirect(redirectUrl, { headers });
 			}
 			const message = match(loginUser.error)
 				.with(
diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts
index a433bba351..8f73592dcc 100644
--- a/libs/generated/src/graphql/backend/gql.ts
+++ b/libs/generated/src/graphql/backend/gql.ts
@@ -25,7 +25,7 @@ const documents = {
     "query GenresList($input: SearchInput!) {\n  genresList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items {\n      id\n      name\n      numItems\n    }\n  }\n}": types.GenresListDocument,
     "query ImportReports {\n  importReports {\n    id\n    source\n    startedOn\n    finishedOn\n    wasSuccess\n    details {\n      import {\n        total\n      }\n      failedItems {\n        lot\n        step\n        identifier\n        error\n      }\n    }\n  }\n}": types.ImportReportsDocument,
     "query LatestUserSummary {\n  latestUserSummary {\n    calculatedOn\n    fitness {\n      measurementsRecorded\n      exercisesInteractedWith\n      workouts {\n        recorded\n        duration\n        weight\n      }\n    }\n    media {\n      metadataOverall {\n        reviewed\n        interactedWith\n      }\n      peopleOverall {\n        reviewed\n        interactedWith\n      }\n      manga {\n        chapters\n        read\n      }\n      books {\n        pages\n        read\n      }\n      movies {\n        runtime\n        watched\n      }\n      anime {\n        episodes\n        watched\n      }\n      podcasts {\n        runtime\n        played\n        playedEpisodes\n      }\n      visualNovels {\n        played\n        runtime\n      }\n      videoGames {\n        played\n      }\n      shows {\n        runtime\n        watchedEpisodes\n        watchedSeasons\n        watched\n      }\n      audioBooks {\n        runtime\n        played\n      }\n    }\n  }\n}": types.LatestUserSummaryDocument,
-    "query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    title\n    lot\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    publishYear\n    publishDate\n    providerRating\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    suggestions {\n      ...PartialMetadataPart\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n  }\n}": types.MetadataDetailsDocument,
+    "query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    publishYear\n    publishDate\n    providerRating\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    suggestions {\n      ...PartialMetadataPart\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n  }\n}": types.MetadataDetailsDocument,
     "query MetadataGroupDetails($metadataGroupId: String!) {\n  metadataGroupDetails(metadataGroupId: $metadataGroupId) {\n    details {\n      id\n      title\n      lot\n      source\n      displayImages\n      parts\n      isPartial\n    }\n    sourceUrl\n    contents {\n      ...PartialMetadataPart\n    }\n  }\n}": types.MetadataGroupDetailsDocument,
     "query MetadataGroupSearch($input: MetadataGroupSearchInput!) {\n  metadataGroupSearch(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items {\n      identifier\n      name\n      image\n      parts\n    }\n  }\n}": types.MetadataGroupSearchDocument,
     "query MetadataGroupsList($input: SearchInput!) {\n  metadataGroupsList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items {\n      id\n      title\n      lot\n      parts\n      image\n    }\n  }\n}": types.MetadataGroupsListDocument,
@@ -112,7 +112,7 @@ export function graphql(source: "query LatestUserSummary {\n  latestUserSummary
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    title\n    lot\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    publishYear\n    publishDate\n    providerRating\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    suggestions {\n      ...PartialMetadataPart\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n  }\n}"): (typeof documents)["query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    title\n    lot\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    publishYear\n    publishDate\n    providerRating\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    suggestions {\n      ...PartialMetadataPart\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n  }\n}"];
+export function graphql(source: "query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    publishYear\n    publishDate\n    providerRating\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    suggestions {\n      ...PartialMetadataPart\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n  }\n}"): (typeof documents)["query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    publishYear\n    publishDate\n    providerRating\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    suggestions {\n      ...PartialMetadataPart\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n  }\n}"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts
index 8f3d004495..746316ae64 100644
--- a/libs/generated/src/graphql/backend/graphql.ts
+++ b/libs/generated/src/graphql/backend/graphql.ts
@@ -348,8 +348,8 @@ export type EntityAssetsInput = {
 export enum EntityLot {
   Collection = 'COLLECTION',
   Exercise = 'EXERCISE',
-  Media = 'MEDIA',
-  MediaGroup = 'MEDIA_GROUP',
+  Metadata = 'METADATA',
+  MetadataGroup = 'METADATA_GROUP',
   Person = 'PERSON'
 }
 
@@ -2753,7 +2753,7 @@ export type MetadataDetailsQueryVariables = Exact<{
 }>;
 
 
-export type MetadataDetailsQuery = { metadataDetails: { title: string, lot: MediaLot, source: MediaSource, isNsfw?: boolean | null, isPartial?: boolean | null, sourceUrl?: string | null, identifier: string, description?: string | null, publishYear?: number | null, publishDate?: string | null, providerRating?: string | null, productionStatus?: string | null, originalLanguage?: string | null, genres: Array<{ id: string, name: string }>, group?: { id: string, name: string, part: number } | null, assets: { images: Array<string>, videos: Array<{ videoId: string, source: MetadataVideoSource }> }, creators: Array<{ name: string, items: Array<{ id?: string | null, name: string, image?: string | null, character?: string | null }> }>, suggestions: Array<{ id: string, lot: MediaLot, source: MediaSource, identifier: string, title: string, image?: string | null }>, watchProviders: Array<{ name: string, image?: string | null, languages: Array<string> }>, animeSpecifics?: { episodes?: number | null } | null, audioBookSpecifics?: { runtime?: number | null } | null, bookSpecifics?: { pages?: number | null } | null, movieSpecifics?: { runtime?: number | null } | null, mangaSpecifics?: { volumes?: number | null, chapters?: number | null } | null, podcastSpecifics?: { totalEpisodes: number, episodes: Array<{ id: string, title: string, overview?: string | null, thumbnail?: string | null, number: number, runtime?: number | null, publishDate: string }> } | null, showSpecifics?: { totalSeasons?: number | null, totalEpisodes?: number | null, runtime?: number | null, seasons: Array<{ id: number, seasonNumber: number, name: string, overview?: string | null, backdropImages: Array<string>, posterImages: Array<string>, episodes: Array<{ id: number, name: string, posterImages: Array<string>, episodeNumber: number, publishDate?: string | null, overview?: string | null, runtime?: number | null }> }> } | null, visualNovelSpecifics?: { length?: number | null } | null, videoGameSpecifics?: { platforms: Array<string> } | null } };
+export type MetadataDetailsQuery = { metadataDetails: { id: string, lot: MediaLot, title: string, source: MediaSource, isNsfw?: boolean | null, isPartial?: boolean | null, sourceUrl?: string | null, identifier: string, description?: string | null, publishYear?: number | null, publishDate?: string | null, providerRating?: string | null, productionStatus?: string | null, originalLanguage?: string | null, genres: Array<{ id: string, name: string }>, group?: { id: string, name: string, part: number } | null, assets: { images: Array<string>, videos: Array<{ videoId: string, source: MetadataVideoSource }> }, creators: Array<{ name: string, items: Array<{ id?: string | null, name: string, image?: string | null, character?: string | null }> }>, suggestions: Array<{ id: string, lot: MediaLot, source: MediaSource, identifier: string, title: string, image?: string | null }>, watchProviders: Array<{ name: string, image?: string | null, languages: Array<string> }>, animeSpecifics?: { episodes?: number | null } | null, audioBookSpecifics?: { runtime?: number | null } | null, bookSpecifics?: { pages?: number | null } | null, movieSpecifics?: { runtime?: number | null } | null, mangaSpecifics?: { volumes?: number | null, chapters?: number | null } | null, podcastSpecifics?: { totalEpisodes: number, episodes: Array<{ id: string, title: string, overview?: string | null, thumbnail?: string | null, number: number, runtime?: number | null, publishDate: string }> } | null, showSpecifics?: { totalSeasons?: number | null, totalEpisodes?: number | null, runtime?: number | null, seasons: Array<{ id: number, seasonNumber: number, name: string, overview?: string | null, backdropImages: Array<string>, posterImages: Array<string>, episodes: Array<{ id: number, name: string, posterImages: Array<string>, episodeNumber: number, publishDate?: string | null, overview?: string | null, runtime?: number | null }> }> } | null, visualNovelSpecifics?: { length?: number | null } | null, videoGameSpecifics?: { platforms: Array<string> } | null } };
 
 export type MetadataGroupDetailsQueryVariables = Exact<{
   metadataGroupId: Scalars['String']['input'];
@@ -3031,7 +3031,7 @@ export const GenreDetailsDocument = {"kind":"Document","definitions":[{"kind":"O
 export const GenresListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GenresList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SearchInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"genresList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"numItems"}}]}}]}}]}}]} as unknown as DocumentNode<GenresListQuery, GenresListQueryVariables>;
 export const ImportReportsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ImportReports"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"importReports"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"startedOn"}},{"kind":"Field","name":{"kind":"Name","value":"finishedOn"}},{"kind":"Field","name":{"kind":"Name","value":"wasSuccess"}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"import"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"failedItems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"step"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]}}]} as unknown as DocumentNode<ImportReportsQuery, ImportReportsQueryVariables>;
 export const LatestUserSummaryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LatestUserSummary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"latestUserSummary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"calculatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"measurementsRecorded"}},{"kind":"Field","name":{"kind":"Name","value":"exercisesInteractedWith"}},{"kind":"Field","name":{"kind":"Name","value":"workouts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"recorded"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"media"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataOverall"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reviewed"}},{"kind":"Field","name":{"kind":"Name","value":"interactedWith"}}]}},{"kind":"Field","name":{"kind":"Name","value":"peopleOverall"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reviewed"}},{"kind":"Field","name":{"kind":"Name","value":"interactedWith"}}]}},{"kind":"Field","name":{"kind":"Name","value":"manga"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapters"}},{"kind":"Field","name":{"kind":"Name","value":"read"}}]}},{"kind":"Field","name":{"kind":"Name","value":"books"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pages"}},{"kind":"Field","name":{"kind":"Name","value":"read"}}]}},{"kind":"Field","name":{"kind":"Name","value":"movies"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"watched"}}]}},{"kind":"Field","name":{"kind":"Name","value":"anime"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodes"}},{"kind":"Field","name":{"kind":"Name","value":"watched"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcasts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"played"}},{"kind":"Field","name":{"kind":"Name","value":"playedEpisodes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"visualNovels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"played"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}},{"kind":"Field","name":{"kind":"Name","value":"videoGames"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"played"}}]}},{"kind":"Field","name":{"kind":"Name","value":"shows"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"watchedEpisodes"}},{"kind":"Field","name":{"kind":"Name","value":"watchedSeasons"}},{"kind":"Field","name":{"kind":"Name","value":"watched"}}]}},{"kind":"Field","name":{"kind":"Name","value":"audioBooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"played"}}]}}]}}]}}]}}]} as unknown as DocumentNode<LatestUserSummaryQuery, LatestUserSummaryQueryVariables>;
-export const MetadataDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"isNsfw"}},{"kind":"Field","name":{"kind":"Name","value":"isPartial"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"publishYear"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}},{"kind":"Field","name":{"kind":"Name","value":"providerRating"}},{"kind":"Field","name":{"kind":"Name","value":"productionStatus"}},{"kind":"Field","name":{"kind":"Name","value":"originalLanguage"}},{"kind":"Field","name":{"kind":"Name","value":"genres"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"part"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"images"}},{"kind":"Field","name":{"kind":"Name","value":"videos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"videoId"}},{"kind":"Field","name":{"kind":"Name","value":"source"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"creators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"character"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"suggestions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PartialMetadataPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"watchProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"languages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"audioBookSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bookSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"movieSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"volumes"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"thumbnail"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalEpisodes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalSeasons"}},{"kind":"Field","name":{"kind":"Name","value":"totalEpisodes"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"seasons"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"seasonNumber"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"backdropImages"}},{"kind":"Field","name":{"kind":"Name","value":"posterImages"}},{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"posterImages"}},{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"visualNovelSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"length"}}]}},{"kind":"Field","name":{"kind":"Name","value":"videoGameSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"platforms"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialMetadataPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PartialMetadata"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"image"}}]}}]} as unknown as DocumentNode<MetadataDetailsQuery, MetadataDetailsQueryVariables>;
+export const MetadataDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"isNsfw"}},{"kind":"Field","name":{"kind":"Name","value":"isPartial"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"publishYear"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}},{"kind":"Field","name":{"kind":"Name","value":"providerRating"}},{"kind":"Field","name":{"kind":"Name","value":"productionStatus"}},{"kind":"Field","name":{"kind":"Name","value":"originalLanguage"}},{"kind":"Field","name":{"kind":"Name","value":"genres"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"part"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"images"}},{"kind":"Field","name":{"kind":"Name","value":"videos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"videoId"}},{"kind":"Field","name":{"kind":"Name","value":"source"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"creators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"character"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"suggestions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PartialMetadataPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"watchProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"languages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"audioBookSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bookSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"movieSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"volumes"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"thumbnail"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalEpisodes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalSeasons"}},{"kind":"Field","name":{"kind":"Name","value":"totalEpisodes"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"seasons"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"seasonNumber"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"backdropImages"}},{"kind":"Field","name":{"kind":"Name","value":"posterImages"}},{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"posterImages"}},{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"visualNovelSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"length"}}]}},{"kind":"Field","name":{"kind":"Name","value":"videoGameSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"platforms"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialMetadataPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PartialMetadata"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"image"}}]}}]} as unknown as DocumentNode<MetadataDetailsQuery, MetadataDetailsQueryVariables>;
 export const MetadataGroupDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataGroupDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataGroupId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataGroupDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataGroupId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataGroupId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"displayImages"}},{"kind":"Field","name":{"kind":"Name","value":"parts"}},{"kind":"Field","name":{"kind":"Name","value":"isPartial"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"contents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PartialMetadataPart"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialMetadataPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PartialMetadata"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"image"}}]}}]} as unknown as DocumentNode<MetadataGroupDetailsQuery, MetadataGroupDetailsQueryVariables>;
 export const MetadataGroupSearchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataGroupSearch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MetadataGroupSearchInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataGroupSearch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"parts"}}]}}]}}]}}]} as unknown as DocumentNode<MetadataGroupSearchQuery, MetadataGroupSearchQueryVariables>;
 export const MetadataGroupsListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataGroupsList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SearchInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataGroupsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"parts"}},{"kind":"Field","name":{"kind":"Name","value":"image"}}]}}]}}]}}]} as unknown as DocumentNode<MetadataGroupsListQuery, MetadataGroupsListQueryVariables>;
diff --git a/libs/graphql/src/backend/queries/MetadataDetails.gql b/libs/graphql/src/backend/queries/MetadataDetails.gql
index d9c2991467..1c7370fda7 100644
--- a/libs/graphql/src/backend/queries/MetadataDetails.gql
+++ b/libs/graphql/src/backend/queries/MetadataDetails.gql
@@ -1,7 +1,8 @@
 query MetadataDetails($metadataId: String!) {
   metadataDetails(metadataId: $metadataId) {
-    title
+    id
     lot
+    title
     source
     isNsfw
     isPartial


Test patch: