Added basic exercise addition and deletion
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,3 +2,5 @@
|
|||||||
.direnv
|
.direnv
|
||||||
.envrc
|
.envrc
|
||||||
node_modules
|
node_modules
|
||||||
|
.data
|
||||||
|
.env
|
||||||
|
136
Cargo.lock
generated
136
Cargo.lock
generated
@@ -146,6 +146,12 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -436,7 +442,19 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasi 0.14.2+wasi-0.2.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -725,6 +743,16 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.77"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -865,7 +893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1078,6 +1106,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@@ -1105,7 +1139,7 @@ version = "0.6.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1169,7 +1203,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom",
|
"getrandom 0.2.16",
|
||||||
"libc",
|
"libc",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
@@ -1453,6 +1487,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
"webpki-roots 0.26.11",
|
"webpki-roots 0.26.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1533,6 +1568,7 @@ dependencies = [
|
|||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1570,6 +1606,7 @@ dependencies = [
|
|||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1595,6 +1632,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1935,6 +1973,18 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -1959,12 +2009,79 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.14.2+wasi-0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasite"
|
name = "wasite"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "0.26.11"
|
version = "0.26.11"
|
||||||
@@ -2163,11 +2280,21 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rt"
|
||||||
|
version = "0.39.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "workout"
|
name = "workout"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"dotenvy",
|
||||||
"maud",
|
"maud",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
@@ -2176,6 +2303,7 @@ dependencies = [
|
|||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@@ -6,7 +6,7 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8", feature = [ "form" ] }
|
axum = { version = "0.8", feature = [ "form" ] }
|
||||||
tokio = { version = "1.45", features = ["full"] }
|
tokio = { version = "1.45", features = ["full"] }
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "tls-rustls", "migrate"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "tls-rustls", "migrate", "uuid"] }
|
||||||
maud = { version = "0.27", features = [ "axum" ] }
|
maud = { version = "0.27", features = [ "axum" ] }
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
|
|
||||||
@@ -15,3 +15,6 @@ tracing-subscriber = { version = "0.3", features = [ "env-filter" ] }
|
|||||||
|
|
||||||
tower = { version = "0.5.2", features = ["util"] }
|
tower = { version = "0.5.2", features = ["util"] }
|
||||||
tower-http = { version = "0.6.1", features = ["fs", "trace"] }
|
tower-http = { version = "0.6.1", features = ["fs", "trace"] }
|
||||||
|
|
||||||
|
dotenvy = "0.15"
|
||||||
|
uuid = { version = "1.17.0", features = ["v4", "serde"] }
|
||||||
|
@@ -8,14 +8,16 @@
|
|||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
"Courier New", monospace;
|
"Courier New", monospace;
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
--container-md: 28rem;
|
--text-xs: 0.75rem;
|
||||||
--container-xl: 36rem;
|
--text-xs--line-height: calc(1 / 0.75);
|
||||||
--text-xl: 1.25rem;
|
--text-xl: 1.25rem;
|
||||||
--text-xl--line-height: calc(1.75 / 1.25);
|
--text-xl--line-height: calc(1.75 / 1.25);
|
||||||
--text-2xl: 1.5rem;
|
--text-2xl: 1.5rem;
|
||||||
--text-2xl--line-height: calc(2 / 1.5);
|
--text-2xl--line-height: calc(2 / 1.5);
|
||||||
--text-3xl: 1.875rem;
|
--text-3xl: 1.875rem;
|
||||||
--text-3xl--line-height: calc(2.25 / 1.875);
|
--text-3xl--line-height: calc(2.25 / 1.875);
|
||||||
|
--font-weight-semibold: 600;
|
||||||
|
--font-weight-bold: 700;
|
||||||
--default-font-family: var(--font-sans);
|
--default-font-family: var(--font-sans);
|
||||||
--default-mono-font-family: var(--font-mono);
|
--default-mono-font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
@@ -449,6 +451,59 @@
|
|||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
:where(.list-row) {
|
||||||
|
--list-grid-cols: minmax(0, auto) 1fr;
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
gap: calc(0.25rem * 4);
|
||||||
|
border-radius: var(--radius-box);
|
||||||
|
padding: calc(0.25rem * 4);
|
||||||
|
word-break: break-word;
|
||||||
|
grid-template-columns: var(--list-grid-cols);
|
||||||
|
&:has(.list-col-grow:nth-child(1)) {
|
||||||
|
--list-grid-cols: 1fr;
|
||||||
|
}
|
||||||
|
&:has(.list-col-grow:nth-child(2)) {
|
||||||
|
--list-grid-cols: minmax(0, auto) 1fr;
|
||||||
|
}
|
||||||
|
&:has(.list-col-grow:nth-child(3)) {
|
||||||
|
--list-grid-cols: minmax(0, auto) minmax(0, auto) 1fr;
|
||||||
|
}
|
||||||
|
&:has(.list-col-grow:nth-child(4)) {
|
||||||
|
--list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr;
|
||||||
|
}
|
||||||
|
&:has(.list-col-grow:nth-child(5)) {
|
||||||
|
--list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr;
|
||||||
|
}
|
||||||
|
&:has(.list-col-grow:nth-child(6)) {
|
||||||
|
--list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto)
|
||||||
|
minmax(0, auto) 1fr;
|
||||||
|
}
|
||||||
|
:not(.list-col-wrap) {
|
||||||
|
grid-row-start: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& > :not(:last-child) {
|
||||||
|
&.list-row, .list-row {
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
border-bottom: var(--border) solid;
|
||||||
|
inset-inline: var(--radius-box);
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(0.25rem * 0);
|
||||||
|
border-color: var(--color-base-content);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.input {
|
.input {
|
||||||
cursor: text;
|
cursor: text;
|
||||||
border: var(--border) solid #0000;
|
border: var(--border) solid #0000;
|
||||||
@@ -549,98 +604,6 @@
|
|||||||
inset-inline-end: 0.75em;
|
inset-inline-end: 0.75em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.select {
|
|
||||||
border: var(--border) solid #0000;
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-shrink: 1;
|
|
||||||
appearance: none;
|
|
||||||
align-items: center;
|
|
||||||
gap: calc(0.25rem * 1.5);
|
|
||||||
background-color: var(--color-base-100);
|
|
||||||
padding-inline-start: calc(0.25rem * 4);
|
|
||||||
padding-inline-end: calc(0.25rem * 7);
|
|
||||||
vertical-align: middle;
|
|
||||||
width: clamp(3rem, 20rem, 100%);
|
|
||||||
height: var(--size);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
touch-action: manipulation;
|
|
||||||
border-start-start-radius: var(--join-ss, var(--radius-field));
|
|
||||||
border-start-end-radius: var(--join-se, var(--radius-field));
|
|
||||||
border-end-start-radius: var(--join-es, var(--radius-field));
|
|
||||||
border-end-end-radius: var(--join-ee, var(--radius-field));
|
|
||||||
background-image: linear-gradient(45deg, #0000 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, #0000 50%);
|
|
||||||
background-position: calc(100% - 20px) calc(1px + 50%), calc(100% - 16.1px) calc(1px + 50%);
|
|
||||||
background-size: 4px 4px, 4px 4px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
box-shadow: 0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset;
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset;
|
|
||||||
}
|
|
||||||
border-color: var(--input-color);
|
|
||||||
--input-color: var(--color-base-content);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
--input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
|
||||||
}
|
|
||||||
--size: calc(var(--size-field, 0.25rem) * 10);
|
|
||||||
[dir="rtl"] & {
|
|
||||||
background-position: calc(0% + 12px) calc(1px + 50%), calc(0% + 16px) calc(1px + 50%);
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
margin-inline-start: calc(0.25rem * -4);
|
|
||||||
margin-inline-end: calc(0.25rem * -7);
|
|
||||||
width: calc(100% + 2.75rem);
|
|
||||||
appearance: none;
|
|
||||||
padding-inline-start: calc(0.25rem * 4);
|
|
||||||
padding-inline-end: calc(0.25rem * 7);
|
|
||||||
height: calc(100% - 2px);
|
|
||||||
background: inherit;
|
|
||||||
border-radius: inherit;
|
|
||||||
border-style: none;
|
|
||||||
&:focus, &:focus-within {
|
|
||||||
--tw-outline-style: none;
|
|
||||||
outline-style: none;
|
|
||||||
@media (forced-colors: active) {
|
|
||||||
outline: 2px solid transparent;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-inline-end: calc(0.25rem * -5.5);
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:focus, &:focus-within {
|
|
||||||
--input-color: var(--color-base-content);
|
|
||||||
box-shadow: 0 1px var(--input-color);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000);
|
|
||||||
}
|
|
||||||
outline: 2px solid var(--input-color);
|
|
||||||
outline-offset: 2px;
|
|
||||||
isolation: isolate;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
&:has(> select[disabled]), &:is(:disabled, [disabled]) {
|
|
||||||
cursor: not-allowed;
|
|
||||||
border-color: var(--color-base-200);
|
|
||||||
background-color: var(--color-base-200);
|
|
||||||
color: var(--color-base-content);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
color: color-mix(in oklab, var(--color-base-content) 40%, transparent);
|
|
||||||
}
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--color-base-content);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
color: color-mix(in oklab, var(--color-base-content) 20%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:has(> select[disabled]) > select[disabled] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
border: var(--border) solid var(--input-color, var(--color-base-content));
|
border: var(--border) solid var(--input-color, var(--color-base-content));
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
@@ -785,6 +748,9 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.list-col-wrap {
|
||||||
|
grid-row-start: 2;
|
||||||
|
}
|
||||||
.label {
|
.label {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -846,6 +812,19 @@
|
|||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.btn-square {
|
||||||
|
padding-inline: calc(0.25rem * 0);
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
}
|
||||||
|
.size-\[1\.2em\] {
|
||||||
|
width: 1.2em;
|
||||||
|
height: 1.2em;
|
||||||
|
}
|
||||||
|
.size-\[1\.6em\] {
|
||||||
|
width: 1.6em;
|
||||||
|
height: 1.6em;
|
||||||
|
}
|
||||||
.h-full {
|
.h-full {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -858,12 +837,6 @@
|
|||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.w-md {
|
|
||||||
width: var(--container-md);
|
|
||||||
}
|
|
||||||
.w-xl {
|
|
||||||
width: var(--container-xl);
|
|
||||||
}
|
|
||||||
.link {
|
.link {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
@@ -887,42 +860,77 @@
|
|||||||
margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));
|
margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.space-y-3 {
|
|
||||||
:where(& > :not(:last-child)) {
|
|
||||||
--tw-space-y-reverse: 0;
|
|
||||||
margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));
|
|
||||||
margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.space-y-5 {
|
|
||||||
:where(& > :not(:last-child)) {
|
|
||||||
--tw-space-y-reverse: 0;
|
|
||||||
margin-block-start: calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));
|
|
||||||
margin-block-end: calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.rounded-box {
|
.rounded-box {
|
||||||
border-radius: var(--radius-box);
|
border-radius: var(--radius-box);
|
||||||
}
|
}
|
||||||
.rounded-box {
|
.rounded-box {
|
||||||
border-radius: var(--radius-box);
|
border-radius: var(--radius-box);
|
||||||
}
|
}
|
||||||
.border {
|
.bg-base-100 {
|
||||||
border-style: var(--tw-border-style);
|
background-color: var(--color-base-100);
|
||||||
border-width: 1px;
|
|
||||||
}
|
|
||||||
.border-base-300 {
|
|
||||||
border-color: var(--color-base-300);
|
|
||||||
}
|
}
|
||||||
.bg-base-200 {
|
.bg-base-200 {
|
||||||
background-color: var(--color-base-200);
|
background-color: var(--color-base-200);
|
||||||
}
|
}
|
||||||
.p-4 {
|
.fill-primary-content {
|
||||||
padding: calc(var(--spacing) * 4);
|
fill: var(--color-primary-content);
|
||||||
}
|
}
|
||||||
.p-10 {
|
.p-10 {
|
||||||
padding: calc(var(--spacing) * 10);
|
padding: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
|
.py-2 {
|
||||||
|
padding-block: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.py-5 {
|
||||||
|
padding-block: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
|
.text-xs {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
line-height: var(--tw-leading, var(--text-xs--line-height));
|
||||||
|
}
|
||||||
|
.font-bold {
|
||||||
|
--tw-font-weight: var(--font-weight-bold);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
.font-semibold {
|
||||||
|
--tw-font-weight: var(--font-weight-semibold);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
.text-primary-content {
|
||||||
|
color: var(--color-primary-content);
|
||||||
|
}
|
||||||
|
.opacity-60 {
|
||||||
|
opacity: 60%;
|
||||||
|
}
|
||||||
|
.shadow-md {
|
||||||
|
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
.btn-ghost {
|
||||||
|
&:not(.btn-active, :hover, :active:focus, :focus-visible) {
|
||||||
|
--btn-shadow: "";
|
||||||
|
--btn-bg: #0000;
|
||||||
|
--btn-border: #0000;
|
||||||
|
--btn-noise: none;
|
||||||
|
&:not(:disabled, [disabled], .btn-disabled) {
|
||||||
|
outline-color: currentColor;
|
||||||
|
--btn-fg: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (hover: none) {
|
||||||
|
&:hover:not(.btn-active, :active, :focus-visible, :disabled, [disabled], .btn-disabled) {
|
||||||
|
--btn-shadow: "";
|
||||||
|
--btn-bg: #0000;
|
||||||
|
--btn-border: #0000;
|
||||||
|
--btn-noise: none;
|
||||||
|
--btn-fg: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-error {
|
||||||
|
--btn-color: var(--color-error);
|
||||||
|
--btn-fg: var(--color-error-content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: var(--text-3xl);
|
font-size: var(--text-3xl);
|
||||||
@@ -1194,16 +1202,94 @@ h3 {
|
|||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0;
|
initial-value: 0;
|
||||||
}
|
}
|
||||||
@property --tw-border-style {
|
@property --tw-font-weight {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: solid;
|
}
|
||||||
|
@property --tw-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-shadow-alpha {
|
||||||
|
syntax: "<percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow-alpha {
|
||||||
|
syntax: "<percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
|
@property --tw-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-ring-inset {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-width {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0px;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: #fff;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
}
|
}
|
||||||
@layer properties {
|
@layer properties {
|
||||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||||
*, ::before, ::after, ::backdrop {
|
*, ::before, ::after, ::backdrop {
|
||||||
--tw-space-y-reverse: 0;
|
--tw-space-y-reverse: 0;
|
||||||
--tw-border-style: solid;
|
--tw-font-weight: initial;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-color: initial;
|
||||||
|
--tw-shadow-alpha: 100%;
|
||||||
|
--tw-inset-shadow: 0 0 #0000;
|
||||||
|
--tw-inset-shadow-color: initial;
|
||||||
|
--tw-inset-shadow-alpha: 100%;
|
||||||
|
--tw-ring-color: initial;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-inset-ring-color: initial;
|
||||||
|
--tw-inset-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-inset: initial;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
build.rs
Normal file
3
build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=migrations");
|
||||||
|
}
|
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:17
|
||||||
|
volumes:
|
||||||
|
- ./.data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: "password"
|
||||||
|
POSTGRES_USER: "postgres"
|
||||||
|
POSTGRES_DB: "postgres"
|
||||||
|
|
@@ -27,6 +27,7 @@
|
|||||||
mprocs
|
mprocs
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
watchexec
|
watchexec
|
||||||
|
sqlx-cli
|
||||||
] ++ pkgs.lib.optionals pkg.stdenv.isDarwin [
|
] ++ pkgs.lib.optionals pkg.stdenv.isDarwin [
|
||||||
darwin.apple_sdk.frameworks.SystemConfiguration
|
darwin.apple_sdk.frameworks.SystemConfiguration
|
||||||
];
|
];
|
||||||
|
8
migrations/20250708192552_add_tables.down.sql
Normal file
8
migrations/20250708192552_add_tables.down.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
DROP TABLE exercise_categories;
|
||||||
|
DROP TABLE exercise_muscle_groups;
|
||||||
|
DROP TABLE workout_exercises;
|
||||||
|
DROP TABLE categories;
|
||||||
|
DROP TABLE muscle_groups;
|
||||||
|
DROP TABLE exercises;
|
||||||
|
DROP TABLE workouts;
|
65
migrations/20250708192552_add_tables.sql
Normal file
65
migrations/20250708192552_add_tables.sql
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
CREATE TABLE exercises (
|
||||||
|
exercise_id uuid PRIMARY KEY,
|
||||||
|
name varchar NOT NULL,
|
||||||
|
description varchar NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE muscle_groups (
|
||||||
|
muscle_group_id varchar(16) PRIMARY KEY,
|
||||||
|
name varchar NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE categories (
|
||||||
|
category_id varchar(16) PRIMARY KEY,
|
||||||
|
name varchar NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE workouts (
|
||||||
|
workout_id uuid PRIMARY KEY,
|
||||||
|
name varchar NOT NULL,
|
||||||
|
description varchar NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE workout_exercises (
|
||||||
|
workout_id uuid NOT NULL,
|
||||||
|
exercise_id uuid NOT NULL,
|
||||||
|
PRIMARY KEY (workout_id, exercise_id),
|
||||||
|
FOREIGN KEY (workout_id) REFERENCES workouts(workout_id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (exercise_id) REFERENCES exercises(exercise_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE exercise_muscle_groups (
|
||||||
|
exercise_id uuid NOT NULL,
|
||||||
|
muscle_group_id varchar(16) NOT NULL,
|
||||||
|
PRIMARY KEY (exercise_id, muscle_group_id),
|
||||||
|
FOREIGN KEY (exercise_id) REFERENCES exercises(exercise_id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (muscle_group_id) REFERENCES muscle_groups(muscle_group_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE exercise_categories (
|
||||||
|
exercise_id uuid NOT NULL,
|
||||||
|
category_id varchar(16) NOT NULL,
|
||||||
|
PRIMARY KEY (exercise_id, category_id),
|
||||||
|
FOREIGN KEY (exercise_id) REFERENCES exercises(exercise_id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- Initialize muscle data
|
||||||
|
INSERT INTO muscle_groups (muscle_group_id, name) VALUES
|
||||||
|
('chest', 'Chest'),
|
||||||
|
('back', 'Back'),
|
||||||
|
('arms', 'Arms'),
|
||||||
|
('legs', 'Legs'),
|
||||||
|
('shoulders', 'Shoulders');
|
||||||
|
|
||||||
|
-- Initialize caregory data
|
||||||
|
INSERT INTO categories (category_id, name) VALUES
|
||||||
|
('strength', 'Strength'),
|
||||||
|
('cardio', 'Cardio');
|
@@ -1,6 +1,6 @@
|
|||||||
procs:
|
procs:
|
||||||
# database:
|
database:
|
||||||
# shell: "docker compose up"
|
shell: "docker compose up db"
|
||||||
backend:
|
backend:
|
||||||
shell: "watchexec -w src -r cargo run"
|
shell: "watchexec -w src -r cargo run"
|
||||||
cwd: "./"
|
cwd: "./"
|
||||||
|
15
src/database.rs
Normal file
15
src/database.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||||
|
|
||||||
|
pub async fn connect_database() -> PgPool {
|
||||||
|
let db_connection_str =
|
||||||
|
std::env::var("DATABASE_URL").expect("Could not find DATABASE_URL environment variable");
|
||||||
|
|
||||||
|
PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.acquire_timeout(Duration::from_secs(3))
|
||||||
|
.connect(&db_connection_str)
|
||||||
|
.await
|
||||||
|
.expect("Could not connect to database")
|
||||||
|
}
|
3
src/icons.rs
Normal file
3
src/icons.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod eye;
|
||||||
|
|
||||||
|
pub use eye::eye;
|
9
src/icons/eye.rs
Normal file
9
src/icons/eye.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use maud::{Markup, html};
|
||||||
|
|
||||||
|
pub fn eye() -> Markup {
|
||||||
|
html! {
|
||||||
|
svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 256 256" {
|
||||||
|
path fill="currentColor" d="M247.31 124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57 61.26 162.88 48 128 48S61.43 61.26 36.34 86.35C17.51 105.18 9 124 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208s66.57-13.26 91.66-38.34c18.83-18.83 27.3-37.61 27.65-38.4a8 8 0 0 0 0-6.5M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.5 133.5 0 0 1 25 128a133.3 133.3 0 0 1 23.07-30.75C70.33 75.19 97.22 64 128 64s57.67 11.19 79.93 33.25A133.5 133.5 0 0 1 231.05 128c-7.21 13.46-38.62 64-103.05 64m0-112a48 48 0 1 0 48 48a48.05 48.05 0 0 0-48-48m0 80a32 32 0 1 1 32-32a32 32 0 0 1-32 32" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
src/main.rs
25
src/main.rs
@@ -2,8 +2,16 @@ use axum::Router;
|
|||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
use crate::util::AppState;
|
||||||
|
|
||||||
|
mod database;
|
||||||
|
mod icons;
|
||||||
mod layouts;
|
mod layouts;
|
||||||
|
mod models;
|
||||||
mod pages;
|
mod pages;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
pub use util::error::AppError;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -15,10 +23,25 @@ async fn main() {
|
|||||||
.with(tracing_subscriber::fmt::layer())
|
.with(tracing_subscriber::fmt::layer())
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
dotenvy::dotenv().expect("Could not initialize dotenvy");
|
||||||
|
|
||||||
|
// Connect to databse
|
||||||
|
let pool = database::connect_database().await;
|
||||||
|
|
||||||
|
// Migrate
|
||||||
|
sqlx::migrate!("./migrations")
|
||||||
|
.run(&pool)
|
||||||
|
.await
|
||||||
|
.expect("Database migrations failed");
|
||||||
|
|
||||||
|
// Initialize global app state
|
||||||
|
let app_state = AppState { pool };
|
||||||
|
|
||||||
// build our application with a route
|
// build our application with a route
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest_service("/assets", ServeDir::new("assets"))
|
.nest_service("/assets", ServeDir::new("assets"))
|
||||||
.merge(pages::routes());
|
.merge(pages::routes())
|
||||||
|
.with_state(app_state);
|
||||||
|
|
||||||
// run it
|
// run it
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
||||||
|
9
src/models.rs
Normal file
9
src/models.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
pub mod categories;
|
||||||
|
pub mod exercises;
|
||||||
|
pub mod muscle_groups;
|
||||||
|
pub mod workouts;
|
||||||
|
|
||||||
|
pub use categories::Category;
|
||||||
|
pub use exercises::Exercise;
|
||||||
|
pub use muscle_groups::MuscleGroup;
|
||||||
|
pub use workouts::Workout;
|
8
src/models/categories.rs
Normal file
8
src/models/categories.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use sqlx::{PgPool, prelude::FromRow};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, FromRow)]
|
||||||
|
pub struct Category {
|
||||||
|
pub category_id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
23
src/models/exercises.rs
Normal file
23
src/models/exercises.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use sqlx::{PgPool, prelude::FromRow};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, FromRow)]
|
||||||
|
pub struct Exercise {
|
||||||
|
pub exercise_id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Exercise {
|
||||||
|
pub async fn from_id(id: Uuid, conn: &PgPool) -> Result<Self, sqlx::Error> {
|
||||||
|
let exercise = sqlx::query_as!(
|
||||||
|
Exercise,
|
||||||
|
"SELECT exercise_id, name, description FROM exercises WHERE exercise_id = $1",
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_one(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(exercise)
|
||||||
|
}
|
||||||
|
}
|
7
src/models/muscle_groups.rs
Normal file
7
src/models/muscle_groups.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
use sqlx::{PgPool, prelude::FromRow};
|
||||||
|
|
||||||
|
#[derive(Debug, FromRow)]
|
||||||
|
pub struct MuscleGroup {
|
||||||
|
pub muscle_group_id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
8
src/models/muscles.rs
Normal file
8
src/models/muscles.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use sqlx::{PgPool, prelude::FromRow};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, FromRow)]
|
||||||
|
pub struct Muscle {
|
||||||
|
pub muscle_id: i16,
|
||||||
|
pub name: String,
|
||||||
|
}
|
9
src/models/workouts.rs
Normal file
9
src/models/workouts.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use sqlx::{PgPool, prelude::FromRow};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, FromRow)]
|
||||||
|
pub struct Workout {
|
||||||
|
exercise_id: Uuid,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
}
|
@@ -1,9 +1,11 @@
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::util::AppState;
|
||||||
|
|
||||||
mod exercises;
|
mod exercises;
|
||||||
mod index;
|
mod index;
|
||||||
|
|
||||||
pub fn routes() -> Router {
|
pub fn routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.merge(index::routes())
|
.merge(index::routes())
|
||||||
.nest("/exercises", exercises::routes())
|
.nest("/exercises", exercises::routes())
|
||||||
|
@@ -1,20 +1,45 @@
|
|||||||
use crate::layouts;
|
use crate::{AppError, icons, layouts, util::AppState};
|
||||||
use axum::{Router, routing::get};
|
use axum::{Router, extract::State, routing::get};
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
|
|
||||||
|
mod id;
|
||||||
mod new;
|
mod new;
|
||||||
|
|
||||||
pub fn routes() -> Router {
|
pub fn routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(page))
|
.route("/", get(page))
|
||||||
.nest("/new", new::routes())
|
.nest("/new", new::routes())
|
||||||
|
.nest("/{id}", id::routes())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn page() -> Markup {
|
async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||||
|
let exercises = sqlx::query_as!(
|
||||||
|
crate::models::Exercise,
|
||||||
|
"SELECT exercise_id, name, description FROM exercises"
|
||||||
|
)
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let content = html! {
|
let content = html! {
|
||||||
h1 { "Exercises" }
|
h1 { "Exercises" }
|
||||||
a href="/exercises/new" { "new exercise +" }
|
a href="/exercises/new" { "new exercise +" }
|
||||||
|
ul class="list" {
|
||||||
|
@for exercise in exercises {
|
||||||
|
li hx-get={ "/exercises/" (exercise.exercise_id) } hx-target="body" hx-push-url="true" class="list-row" {
|
||||||
|
div {}
|
||||||
|
div {
|
||||||
|
div class="font-bold" { (exercise.name) }
|
||||||
|
div class="text-xs" { (exercise.description) }
|
||||||
|
}
|
||||||
|
a class="btn btn-square btn-ghost" {
|
||||||
|
div class="size-[1.6em]" {
|
||||||
|
(icons::eye())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
layouts::desktop(content, "Exercises")
|
Ok(layouts::desktop(content, "Exercises"))
|
||||||
}
|
}
|
||||||
|
50
src/pages/exercises/id.rs
Normal file
50
src/pages/exercises/id.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use crate::{AppError, layouts, util::AppState};
|
||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
extract::{Path, State},
|
||||||
|
http::HeaderMap,
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::get,
|
||||||
|
};
|
||||||
|
use maud::{Markup, html};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new().route("/", get(page).delete(delete))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn page(State(state): State<AppState>, Path(id): Path<Uuid>) -> Result<Markup, AppError> {
|
||||||
|
let exercise = sqlx::query_as!(
|
||||||
|
crate::models::Exercise,
|
||||||
|
"SELECT exercise_id, name, description FROM exercises WHERE exercise_id = $1",
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_one(&state.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let content = html! {
|
||||||
|
h1 { (exercise.name) }
|
||||||
|
p { (exercise.description) }
|
||||||
|
button hx-delete={ "/exercises/" (exercise.exercise_id) } hx-confirm="Are you sure that you want to delete this exercise?" class="btn btn-error" { "Delete Exercise" }
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(layouts::desktop(content, "Exercises"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
crate::models::Exercise,
|
||||||
|
"DELETE FROM exercises WHERE exercise_id = $1",
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.execute(&state.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("hx-location", "/exercises".parse().unwrap());
|
||||||
|
|
||||||
|
Ok((headers, html! {}))
|
||||||
|
}
|
@@ -1,49 +1,56 @@
|
|||||||
use crate::layouts;
|
use crate::{AppError, layouts, util::AppState};
|
||||||
use axum::{Router, routing::get};
|
use axum::{Form, Router, extract::State, http::StatusCode, routing::get};
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
pub fn routes() -> Router {
|
pub fn routes() -> Router<AppState> {
|
||||||
Router::new().route("/", get(page))
|
Router::new().route("/", get(page).post(submit))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn page() -> Markup {
|
async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||||
|
let muscles = sqlx::query_as!(crate::models::MuscleGroup, "SELECT * FROM muscle_groups")
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let categories = sqlx::query_as!(crate::models::Category, "SELECT * FROM categories")
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let content = html! {
|
let content = html! {
|
||||||
h1 class="mb-5" { "New Exercise" }
|
h1 class="mb-5" { "New Exercise" }
|
||||||
|
|
||||||
forum class="space-y-1" {
|
form hx-post="/exercises/new" class="space-y-1" {
|
||||||
fieldset class="fieldset" {
|
fieldset class="fieldset" {
|
||||||
legend class="fieldset-legend" { "Name" }
|
legend class="fieldset-legend" { "Name" }
|
||||||
input class="input" {}
|
input required="true" name="name" class="input" {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset class="fieldset" {
|
fieldset class="fieldset" {
|
||||||
legend class="fieldset-legend" { "Description" }
|
legend class="fieldset-legend" { "Description" }
|
||||||
textarea class="textarea" {}
|
textarea required="true" name="description" class="textarea" {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset class="fieldset" {
|
fieldset class="fieldset" {
|
||||||
legend class="fieldset-legend" { "Muscle Group" }
|
legend class="fieldset-legend" { "Muscle Group" }
|
||||||
|
|
||||||
|
@for muscle in muscles {
|
||||||
|
@let name = muscle.name;
|
||||||
label class="label" {
|
label class="label" {
|
||||||
input type="checkbox" checked="checked" class="checkbox" {}
|
input type="checkbox" class="checkbox" {}
|
||||||
"Chest"
|
(name)
|
||||||
}
|
}
|
||||||
label class="label" {
|
|
||||||
input type="checkbox" checked="checked" class="checkbox" {}
|
|
||||||
"Body"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset class="fieldset" {
|
fieldset class="fieldset" {
|
||||||
legend class="fieldset-legend" { "Equipment" }
|
legend class="fieldset-legend" { "Category" }
|
||||||
|
|
||||||
|
@for category in categories {
|
||||||
|
@let name = category.name;
|
||||||
label class="label" {
|
label class="label" {
|
||||||
input type="checkbox" checked="checked" class="checkbox" {}
|
input type="checkbox" class="checkbox" {}
|
||||||
"Weigted plates"
|
(name)
|
||||||
}
|
}
|
||||||
label class="label" {
|
|
||||||
input type="checkbox" checked="checked" class="checkbox" {}
|
|
||||||
"Jump Rope"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,5 +58,33 @@ async fn page() -> Markup {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
layouts::desktop(content, "New Exercise")
|
Ok(layouts::desktop(content, "New Exercise"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct FormData {
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn submit(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(form): Form<FormData>,
|
||||||
|
) -> Result<Markup, AppError> {
|
||||||
|
let exercise_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO exercises (exercise_id, name, description) VALUES ($1, $2, $3)",
|
||||||
|
exercise_id,
|
||||||
|
form.name,
|
||||||
|
form.description
|
||||||
|
)
|
||||||
|
.execute(&state.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(html! {
|
||||||
|
p {
|
||||||
|
"New exercise has been created!"
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
use crate::layouts;
|
use crate::{layouts, util::AppState};
|
||||||
use axum::{Router, routing::get};
|
use axum::{Router, routing::get};
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
|
|
||||||
pub fn routes() -> Router {
|
pub fn routes() -> Router<AppState> {
|
||||||
Router::new().route("/", get(page))
|
Router::new().route("/", get(page))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
8
src/util.rs
Normal file
8
src/util.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub pool: PgPool,
|
||||||
|
}
|
27
src/util/error.rs
Normal file
27
src/util/error.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum AppError {
|
||||||
|
DatabaseError(sqlx::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, message) = match self {
|
||||||
|
AppError::DatabaseError(err) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Error with database".to_owned(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
(status, message).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for AppError {
|
||||||
|
fn from(value: sqlx::Error) -> Self {
|
||||||
|
Self::DatabaseError(value)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user