Compare commits
No commits in common. "main" and "cargo-leptos" have entirely different histories.
main
...
cargo-lept
15
application/.gitignore
vendored
15
application/.gitignore
vendored
@ -1,2 +1,13 @@
|
|||||||
/target
|
# Generated by Cargo
|
||||||
/dist
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
pkg
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# node e2e test tools and outputs
|
||||||
|
node_modules/
|
||||||
|
test-results/
|
||||||
|
end2end/playwright-report/
|
||||||
|
playwright/.cache/
|
||||||
|
@ -2,46 +2,119 @@
|
|||||||
name = "application"
|
name = "application"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["xeovalyte <me+gitea@xeovalyte.dev>"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
leptos = { version = "0.6", features = ["csr"] }
|
axum = { version = "0.7", optional = true, features = [ "ws", "macros" ] }
|
||||||
leptos_meta = { version = "0.6", features = ["csr"] }
|
|
||||||
leptos_router = { version = "0.6", features = ["csr"] }
|
|
||||||
console_log = "1"
|
|
||||||
log = "0.4"
|
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
leptos-use = "0.10.2"
|
leptos = { version = "0.6", features = [] }
|
||||||
serde = "1.0.196"
|
leptos_axum = { version = "0.6", optional = true }
|
||||||
serde_json = "1.0.113"
|
leptos_meta = { version = "0.6", features = [] }
|
||||||
rand = "0.8.5"
|
leptos_router = { version = "0.6", features = [] }
|
||||||
gloo-timers = "0.3.0"
|
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||||
strsim = "0.11.0"
|
tower = { version = "0.4", optional = true }
|
||||||
|
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||||
|
wasm-bindgen = "=0.2.92"
|
||||||
|
thiserror = "1"
|
||||||
|
tracing = { version = "0.1", optional = true }
|
||||||
|
http = "1"
|
||||||
|
surrealdb = { version = "1.3.1", optional = true }
|
||||||
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
serde_json = "1.0.115"
|
||||||
|
cfg-if = "1.0.0"
|
||||||
|
once_cell = "1.19.0"
|
||||||
|
futures = "0.3.30"
|
||||||
|
uuid = "1.8.0"
|
||||||
|
leptos-use = "0.10.6"
|
||||||
|
strsim = "0.11.1"
|
||||||
|
web-sys = { version = "0.3.69", features = ["Document", "Window", "Element", "ScrollIntoViewOptions", "ScrollLogicalPosition", "ScrollBehavior" ] }
|
||||||
|
leptos_toaster = { version = "0.1.7", optional = true, features = [ "builtin_toast" ] }
|
||||||
|
|
||||||
# utils
|
[features]
|
||||||
# strum = { version = "0.25", features = ["derive", "strum_macros"] }
|
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate", "leptos_toaster/hydrate"]
|
||||||
# strum_macros = "0.25"
|
ssr = [
|
||||||
|
"dep:surrealdb",
|
||||||
[dependencies.web-sys]
|
"dep:axum",
|
||||||
version = "0.3"
|
"dep:tokio",
|
||||||
features = [
|
"dep:tower",
|
||||||
"Document",
|
"dep:tower-http",
|
||||||
"Window",
|
"dep:leptos_axum",
|
||||||
"Element",
|
"leptos/ssr",
|
||||||
"ScrollIntoViewOptions",
|
"leptos-use/ssr",
|
||||||
"ScrollLogicalPosition",
|
"leptos_meta/ssr",
|
||||||
"ScrollBehavior",
|
"leptos_router/ssr",
|
||||||
|
"dep:tracing",
|
||||||
|
"leptos_toaster/ssr"
|
||||||
]
|
]
|
||||||
|
|
||||||
[dev-dependencies]
|
# Defines a size-optimized profile for the WASM bundle in release mode
|
||||||
wasm-bindgen = "0.2"
|
[profile.wasm-release]
|
||||||
wasm-bindgen-test = "0.3"
|
inherits = "release"
|
||||||
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
opt-level = 'z'
|
opt-level = 'z'
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
|
[package.metadata.leptos]
|
||||||
|
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||||
|
output-name = "application"
|
||||||
|
|
||||||
|
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||||
|
site-root = "target/site"
|
||||||
|
|
||||||
|
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||||
|
# Defaults to pkg
|
||||||
|
site-pkg-dir = "pkg"
|
||||||
|
|
||||||
|
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||||
|
style-file = "style/main.scss"
|
||||||
|
# Assets source dir. All files found here will be copied and synchronized to site-root.
|
||||||
|
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
|
||||||
|
#
|
||||||
|
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||||
|
assets-dir = "public"
|
||||||
|
|
||||||
|
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||||
|
site-addr = "0.0.0.0:3000"
|
||||||
|
|
||||||
|
# The port to use for automatic reload monitoring
|
||||||
|
reload-port = 3001
|
||||||
|
|
||||||
|
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||||
|
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||||
|
# This binary name can be checked in Powershell with Get-Command npx
|
||||||
|
end2end-cmd = "npx playwright test"
|
||||||
|
end2end-dir = "end2end"
|
||||||
|
|
||||||
|
# The browserlist query used for optimizing the CSS.
|
||||||
|
browserquery = "defaults"
|
||||||
|
|
||||||
|
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||||
|
env = "DEV"
|
||||||
|
|
||||||
|
# The features to use when compiling the bin target
|
||||||
|
#
|
||||||
|
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||||
|
bin-features = ["ssr"]
|
||||||
|
|
||||||
|
# If the --no-default-features flag should be used when compiling the bin target
|
||||||
|
#
|
||||||
|
# Optional. Defaults to false.
|
||||||
|
bin-default-features = false
|
||||||
|
|
||||||
|
# The features to use when compiling the lib target
|
||||||
|
#
|
||||||
|
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||||
|
lib-features = ["hydrate"]
|
||||||
|
|
||||||
|
# If the --no-default-features flag should be used when compiling the lib target
|
||||||
|
#
|
||||||
|
# Optional. Defaults to false.
|
||||||
|
lib-default-features = false
|
||||||
|
|
||||||
|
# The profile to use for the lib target when compiling for release
|
||||||
|
#
|
||||||
|
# Optional. Defaults to "release".
|
||||||
|
lib-profile-release = "wasm-release"
|
||||||
|
31
application/Dockerfile
Normal file
31
application/Dockerfile
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Get started with a build env with Rust nightly
|
||||||
|
FROM rustlang/rust:nightly-alpine as builder
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk add --no-cache bash curl npm libc-dev binaryen
|
||||||
|
|
||||||
|
RUN npm install -g sass
|
||||||
|
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/leptos-rs/cargo-leptos/releases/latest/download/cargo-leptos-installer.sh | sh
|
||||||
|
|
||||||
|
# Add the WASM target
|
||||||
|
RUN rustup target add wasm32-unknown-unknown
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN cargo leptos build --release -vv
|
||||||
|
|
||||||
|
FROM rustlang/rust:nightly-alpine as runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /work/target/release/leptos_start /app/
|
||||||
|
COPY --from=builder /work/target/site /app/site
|
||||||
|
COPY --from=builder /work/Cargo.toml /app/
|
||||||
|
|
||||||
|
EXPOSE $PORT
|
||||||
|
ENV LEPTOS_SITE_ROOT=./site
|
||||||
|
|
||||||
|
CMD ["/app/leptos_start"]
|
||||||
|
|
24
application/LICENSE
Normal file
24
application/LICENSE
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <https://unlicense.org>
|
@ -3,77 +3,88 @@
|
|||||||
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
|
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
|
||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
# Leptos Client-Side Rendered (CSR) App Starter Template
|
# Leptos Axum Starter Template
|
||||||
|
|
||||||
This is a template for use with the [Leptos][Leptos] web framework using the [Trunk][Trunk] tool to compile and serve your app in development.
|
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool using [Axum](https://github.com/tokio-rs/axum).
|
||||||
|
|
||||||
## Creating your repo from the template
|
## Creating your template repo
|
||||||
|
|
||||||
This template requires you to have `cargo-generate` installed. You can install it with
|
If you don't have `cargo-leptos` installed you can install it with
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
cargo install cargo-generate
|
cargo install cargo-leptos
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then run
|
||||||
To set up your project with this template, run
|
```bash
|
||||||
|
cargo leptos new --git leptos-rs/start-axum
|
||||||
```sh
|
|
||||||
cargo generate --git https://github.com/leptos-community/start-csr
|
|
||||||
```
|
```
|
||||||
|
|
||||||
to generate your new project, then
|
to generate a new project template.
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
cd application
|
cd application
|
||||||
```
|
```
|
||||||
|
|
||||||
to go to your newly created project.
|
to go to your newly created project.
|
||||||
|
Feel free to explore the project structure, but the best place to start with your application code is in `src/app.rs`.
|
||||||
|
Addtionally, Cargo.toml may need updating as new versions of the dependencies are released, especially if things are not working after a `cargo update`.
|
||||||
|
|
||||||
By default, this template uses Rust `nightly` and requires that you've installed the `wasm` compilation target for your toolchain.
|
## Running your project
|
||||||
|
|
||||||
|
```bash
|
||||||
Sass and Tailwind are also supported by the Trunk build tool, but are optional additions: [see here for more info on how to set those up with Trunk][Trunk-instructions].
|
cargo leptos watch
|
||||||
|
|
||||||
|
|
||||||
If you don't have Rust nightly, you can install it with
|
|
||||||
```sh
|
|
||||||
rustup toolchain install nightly --allow-downgrade
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can add the `wasm` compilation target to rust using
|
## Installing Additional Tools
|
||||||
```sh
|
|
||||||
rustup target add wasm32-unknown-unknown
|
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
|
||||||
|
|
||||||
|
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
|
||||||
|
2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
|
||||||
|
3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
|
||||||
|
4. `npm install -g sass` - install `dart-sass` (should be optional in future
|
||||||
|
|
||||||
|
## Compiling for Release
|
||||||
|
```bash
|
||||||
|
cargo leptos build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Will generate your server binary in target/server/release and your site package in target/site
|
||||||
|
|
||||||
## Developing your Leptos CSR project
|
## Testing Your Project
|
||||||
|
```bash
|
||||||
To develop your Leptos CSR project, running
|
cargo leptos end-to-end
|
||||||
|
|
||||||
```sh
|
|
||||||
trunk serve --port 3000 --open
|
|
||||||
```
|
```
|
||||||
|
|
||||||
will open your app in your default browser at `http://localhost:3000`.
|
```bash
|
||||||
|
cargo leptos end-to-end --release
|
||||||
|
|
||||||
## Deploying your Leptos CSR project
|
|
||||||
|
|
||||||
To build a Leptos CSR app for release, use the command
|
|
||||||
|
|
||||||
```sh
|
|
||||||
trunk build --release
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This will output the files necessary to run your app into the `dist` folder; you can then use any static site host to serve these files.
|
Cargo-leptos uses Playwright as the end-to-end test tool.
|
||||||
|
Tests are located in end2end/tests directory.
|
||||||
|
|
||||||
For further information about hosting Leptos CSR apps, please refer to [the Leptos Book chapter on deployment available here][deploy-csr].
|
## Executing a Server on a Remote Machine Without the Toolchain
|
||||||
|
After running a `cargo leptos build --release` the minimum files needed are:
|
||||||
|
|
||||||
|
1. The server binary located in `target/server/release`
|
||||||
|
2. The `site` directory and all files within located in `target/site`
|
||||||
|
|
||||||
[Leptos]: https://github.com/leptos-rs/leptos
|
Copy these files to your remote server. The directory structure should be:
|
||||||
|
```text
|
||||||
|
application
|
||||||
|
site/
|
||||||
|
```
|
||||||
|
Set the following environment variables (updating for your project as needed):
|
||||||
|
```text
|
||||||
|
LEPTOS_OUTPUT_NAME="application"
|
||||||
|
LEPTOS_SITE_ROOT="site"
|
||||||
|
LEPTOS_SITE_PKG_DIR="pkg"
|
||||||
|
LEPTOS_SITE_ADDR="127.0.0.1:3000"
|
||||||
|
LEPTOS_RELOAD_PORT="3001"
|
||||||
|
```
|
||||||
|
Finally, run the server binary.
|
||||||
|
|
||||||
[Trunk]: https://github.com/trunk-rs/trunk
|
## Licensing
|
||||||
[Trunk-instructions]: https://trunkrs.dev/assets/
|
|
||||||
|
|
||||||
[deploy-csr]: https://book.leptos.dev/deployment/csr.html
|
This template itself is released under the Unlicense. You should replace the LICENSE for your own application with an appropriate license if you plan to release it publicly.
|
||||||
|
74
application/end2end/package-lock.json
generated
Normal file
74
application/end2end/package-lock.json
generated
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "end2end",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "end2end",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.28.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||||
|
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"playwright-core": "1.28.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "18.11.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||||
|
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||||
|
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@playwright/test": {
|
||||||
|
"version": "1.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||||
|
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"playwright-core": "1.28.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"version": "18.11.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||||
|
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"playwright-core": {
|
||||||
|
"version": "1.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||||
|
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
application/end2end/package.json
Normal file
13
application/end2end/package.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "end2end",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.28.0"
|
||||||
|
}
|
||||||
|
}
|
107
application/end2end/playwright.config.ts
Normal file
107
application/end2end/playwright.config.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||||
|
import { devices } from "@playwright/test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// require('dotenv').config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
const config: PlaywrightTestConfig = {
|
||||||
|
testDir: "./tests",
|
||||||
|
/* Maximum time one test can run for. */
|
||||||
|
timeout: 30 * 1000,
|
||||||
|
expect: {
|
||||||
|
/**
|
||||||
|
* Maximum time expect() should wait for the condition to be met.
|
||||||
|
* For example in `await expect(locator).toHaveText();`
|
||||||
|
*/
|
||||||
|
timeout: 5000,
|
||||||
|
},
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: "html",
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||||
|
actionTimeout: 0,
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
// baseURL: 'http://localhost:3000',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: {
|
||||||
|
...devices["Desktop Chrome"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "firefox",
|
||||||
|
use: {
|
||||||
|
...devices["Desktop Firefox"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "webkit",
|
||||||
|
use: {
|
||||||
|
...devices["Desktop Safari"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: {
|
||||||
|
// ...devices['Pixel 5'],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: {
|
||||||
|
// ...devices['iPhone 12'],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: {
|
||||||
|
// channel: 'msedge',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: {
|
||||||
|
// channel: 'chrome',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||||
|
// outputDir: 'test-results/',
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
// webServer: {
|
||||||
|
// command: 'npm run start',
|
||||||
|
// port: 3000,
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
9
application/end2end/tests/example.spec.ts
Normal file
9
application/end2end/tests/example.spec.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test("homepage has title and links to intro page", async ({ page }) => {
|
||||||
|
await page.goto("http://localhost:3000/");
|
||||||
|
|
||||||
|
await expect(page).toHaveTitle("Welcome to Leptos");
|
||||||
|
|
||||||
|
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
|
||||||
|
});
|
@ -1,15 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<!-- Add a plain CSS file: see https://trunkrs.dev/assets/#css -->
|
|
||||||
<!-- If using Tailwind with Leptos CSR, see https://trunkrs.dev/assets/#tailwind instead-->
|
|
||||||
<link data-trunk rel="sass" href="public/styles.scss" />
|
|
||||||
|
|
||||||
<!-- Include favicon in dist output: see https://trunkrs.dev/assets/#icon -->
|
|
||||||
<link data-trunk rel="icon" href="public/favicon.ico" />
|
|
||||||
|
|
||||||
<!-- include support for `wasm-bindgen --weak-refs` - see: https://rustwasm.github.io/docs/wasm-bindgen/reference/weak-references.html -->
|
|
||||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs />
|
|
||||||
</head>
|
|
||||||
<body></body>
|
|
||||||
</html>
|
|
@ -1,196 +0,0 @@
|
|||||||
$primary-color: #eb6330;
|
|
||||||
$primary-color-light: hsl(16.36, 82.38%, 55.49%, 0.8);
|
|
||||||
$secondary-color: #465651;
|
|
||||||
$accent-color: #89969f;
|
|
||||||
$primary-bg-color: #0d0b0b;
|
|
||||||
$secondary-bg-color: #151719;
|
|
||||||
$secondary-bg-color-light: hsl(204, 11%, 12%, 1);
|
|
||||||
$secondary-bg-color-lighter: hsl(204, 11%, 15%, 1);
|
|
||||||
$accent-bg-color: #181a19;
|
|
||||||
$text-color: #f3efef;
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
height: 100vh;
|
|
||||||
max-width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0;
|
|
||||||
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: $primary-bg-color;
|
|
||||||
color: $text-color;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
max-width: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
color: $text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.route-active {
|
|
||||||
color: $primary-color !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
background-color: $secondary-bg-color;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar > a {
|
|
||||||
margin: 0px 20px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 100ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar > a:hover {
|
|
||||||
background-color: $primary-color-light;
|
|
||||||
color: $text-color !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.db-connection {
|
|
||||||
position: absolute;
|
|
||||||
left: 0px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-add-link {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 0;
|
|
||||||
background-color: $secondary-bg-color;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 15px;
|
|
||||||
border: dashed $secondary-bg-color-lighter;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-add-link:hover {
|
|
||||||
background-color: $secondary-bg-color-light;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,select {
|
|
||||||
background-color: $secondary-bg-color-light;
|
|
||||||
border: none;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: $text-color;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=password] {
|
|
||||||
margin-top: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=submit]:hover {
|
|
||||||
background-color: $secondary-bg-color-lighter;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
margin: 40px auto 0 auto;
|
|
||||||
width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toastcontainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
gap: 10px;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 999;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toastcontainer span {
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 300px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toastcontainer span:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
background-color: #f29826;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: #d9534f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
background-color: #2181d4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
background-color: #4bb24b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participants-container {
|
|
||||||
height: 200px;
|
|
||||||
overflow: scroll;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
list-style-type: none;
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participants-container li {
|
|
||||||
text-align: left;
|
|
||||||
background-color: $secondary-bg-color-light;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participants-container li:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: $secondary-bg-color-lighter;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participants-container .selected {
|
|
||||||
border: 1px solid $primary-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-input-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin: 10px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-input-container input[type=number] {
|
|
||||||
width: 30px;
|
|
||||||
text-align: center;
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-input-container input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
@ -1,2 +1,3 @@
|
|||||||
|
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "nightly"
|
channel = "nightly"
|
||||||
|
58
application/src/app.rs
Normal file
58
application/src/app.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#![feature(diagnostic_namespace)]
|
||||||
|
use crate::error_template::{AppError, ErrorTemplate};
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_meta::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
|
||||||
|
use crate::components;
|
||||||
|
use crate::pages;
|
||||||
|
use crate::util;
|
||||||
|
use leptos_toaster::{Toaster, ToasterPosition};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn App() -> impl IntoView {
|
||||||
|
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||||
|
provide_meta_context();
|
||||||
|
let participants: util::surrealdb::schemas::ParticipantsContext = create_rw_signal(vec![]);
|
||||||
|
let participant: RwSignal<Option<String>> = create_rw_signal(None);
|
||||||
|
|
||||||
|
provide_context(participant);
|
||||||
|
|
||||||
|
provide_context(participants);
|
||||||
|
util::surrealdb::init_participants();
|
||||||
|
util::websocket::client::init_websocket();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
// injects a stylesheet into the document <head>
|
||||||
|
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
||||||
|
<Stylesheet id="leptos" href="/pkg/application.css"/>
|
||||||
|
|
||||||
|
// sets the document title
|
||||||
|
<Title text="WRB Timings"/>
|
||||||
|
|
||||||
|
// content for this welcome page
|
||||||
|
<Router fallback=|| {
|
||||||
|
let mut outside_errors = Errors::default();
|
||||||
|
outside_errors.insert_with_default_key(AppError::NotFound);
|
||||||
|
view! {
|
||||||
|
<ErrorTemplate outside_errors/>
|
||||||
|
}
|
||||||
|
.into_view()
|
||||||
|
}>
|
||||||
|
<Toaster
|
||||||
|
position=ToasterPosition::BottomCenter
|
||||||
|
>
|
||||||
|
<components::header::Header />
|
||||||
|
<components::participant::Modal />
|
||||||
|
<main>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" view=pages::index::HomePage />
|
||||||
|
<Route path="/add-participant" view=pages::add_participant::AddParticipant />
|
||||||
|
<Route path="/add-time" view=pages::add_time::AddTime />
|
||||||
|
<Route path="/groups" view=pages::groups::Groups />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</Toaster>
|
||||||
|
</Router>
|
||||||
|
}
|
||||||
|
}
|
3
application/src/components.rs
Normal file
3
application/src/components.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod header;
|
||||||
|
pub mod participant;
|
||||||
|
pub mod participants;
|
18
application/src/components/header.rs
Normal file
18
application/src/components/header.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
use leptos::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
use leptos_use::core::ConnectionReadyState;
|
||||||
|
|
||||||
|
/// Renders the home page of your application.
|
||||||
|
#[component]
|
||||||
|
pub fn Header() -> impl IntoView {
|
||||||
|
let ready_state = use_context::<Signal<ConnectionReadyState>>();
|
||||||
|
// Creates a reactive value to update the button
|
||||||
|
view! {
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<A href="/">"WRB Timings"</A>
|
||||||
|
<div>"Connection: " { move || format!("{}", ready_state.unwrap().get()) }</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +0,0 @@
|
|||||||
pub mod navbar;
|
|
||||||
pub mod toast;
|
|
@ -1,18 +0,0 @@
|
|||||||
use crate::util;
|
|
||||||
use leptos::*;
|
|
||||||
use leptos_router::*;
|
|
||||||
|
|
||||||
/// Navigation bar
|
|
||||||
#[component]
|
|
||||||
pub fn Navbar() -> impl IntoView {
|
|
||||||
let websocket = expect_context::<util::surrealdb::SurrealContext>();
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div class="navbar">
|
|
||||||
<span class="db-connection">"Connection: "{ move || websocket.ready_state.get().to_string()}</span>
|
|
||||||
<A href="/" active_class="route-active">Home</A>
|
|
||||||
<A href="/participants" active_class="route-active">Deelnemers</A>
|
|
||||||
<A href="/times" active_class="route-active">Tijden</A>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
359
application/src/components/participant.rs
Normal file
359
application/src/components/participant.rs
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
use crate::util::surrealdb::{client::Time, schemas};
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_toaster::{Toast, ToastId, ToastVariant, Toasts};
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature = "ssr")] {
|
||||||
|
use crate::util::surrealdb::{DB, schemas::Participant};
|
||||||
|
use crate::util::websocket::{server, ParticipantsAction};
|
||||||
|
use leptos::logging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(ModifyParticipant)]
|
||||||
|
async fn modify_participant(
|
||||||
|
mut participant: schemas::ParticipantUpdate,
|
||||||
|
id: String,
|
||||||
|
) -> Result<(), ServerFnError> {
|
||||||
|
let websocket_state = &server::WEBSOCKET_STATE;
|
||||||
|
|
||||||
|
let updated: Option<schemas::ParticipantRecord> = DB
|
||||||
|
.update(("participant", &id))
|
||||||
|
.content(&participant)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match updated {
|
||||||
|
Some(participant_updated) => {
|
||||||
|
logging::log!(
|
||||||
|
"Updated participant: {} ({})",
|
||||||
|
participant_updated.name,
|
||||||
|
participant_updated.group
|
||||||
|
);
|
||||||
|
let action = ParticipantsAction::Replace {
|
||||||
|
participant: Participant {
|
||||||
|
name: participant_updated.name.clone(),
|
||||||
|
group: participant_updated.group.clone(),
|
||||||
|
id,
|
||||||
|
events: participant_updated.events.clone(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match websocket_state.apply(action) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => Err(ServerFnError::new("Error sending websocket action")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(ServerFnError::ServerError(String::from(
|
||||||
|
"Could not update participant",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the home page of your application.
|
||||||
|
#[component]
|
||||||
|
pub fn Modal() -> impl IntoView {
|
||||||
|
let participant_id = use_context::<RwSignal<Option<String>>>().unwrap();
|
||||||
|
let participants = use_context::<schemas::ParticipantsContext>().unwrap();
|
||||||
|
let toasts_context = expect_context::<Toasts>();
|
||||||
|
|
||||||
|
let time_lifesaver = create_rw_signal(Time {
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0,
|
||||||
|
milliseconds: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let time_hindernis = create_rw_signal(Time {
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0,
|
||||||
|
milliseconds: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let time_popduiken = create_rw_signal(Time {
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0,
|
||||||
|
milliseconds: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let name = create_rw_signal(String::from(""));
|
||||||
|
let group = create_rw_signal(String::from(""));
|
||||||
|
|
||||||
|
let participant = move || {
|
||||||
|
let x = participants
|
||||||
|
.get()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|item| item.id == participant_id.get().unwrap_or("".to_string()))
|
||||||
|
.collect::<Vec<schemas::ParticipantSignal>>();
|
||||||
|
|
||||||
|
match x.get(0) {
|
||||||
|
Some(participant) => {
|
||||||
|
let participant_clone = participant.value.get_untracked();
|
||||||
|
|
||||||
|
name.set(participant_clone.name);
|
||||||
|
group.set(participant_clone.group);
|
||||||
|
|
||||||
|
time_lifesaver.set(Time::from_milliseconds(match participant_clone.events {
|
||||||
|
Some(ref events) => events.lifesaver.unwrap_or(0),
|
||||||
|
None => 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
time_hindernis.set(Time::from_milliseconds(match participant_clone.events {
|
||||||
|
Some(ref events) => events.hindernis.unwrap_or(0),
|
||||||
|
None => 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
time_popduiken.set(Time::from_milliseconds(match participant_clone.events {
|
||||||
|
Some(ref events) => events.popduiken.unwrap_or(0),
|
||||||
|
None => 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
Some(participant.value)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let modify_participant_action =
|
||||||
|
create_action(|input: &(schemas::ParticipantUpdate, String)| {
|
||||||
|
let input = input.to_owned();
|
||||||
|
async move { modify_participant(input.0, input.1).await }
|
||||||
|
});
|
||||||
|
|
||||||
|
let form_submit = move |ev: ev::SubmitEvent| {
|
||||||
|
ev.prevent_default();
|
||||||
|
|
||||||
|
let p = schemas::ParticipantUpdate {
|
||||||
|
name: name.get(),
|
||||||
|
group: group.get(),
|
||||||
|
events: Some(schemas::Events {
|
||||||
|
lifesaver: Some(time_lifesaver.get().as_milliseconds()),
|
||||||
|
hindernis: Some(time_hindernis.get().as_milliseconds()),
|
||||||
|
popduiken: Some(time_popduiken.get().as_milliseconds()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
modify_participant_action.dispatch((p, participant().unwrap().get().id));
|
||||||
|
|
||||||
|
let toast_id = ToastId::new();
|
||||||
|
|
||||||
|
toasts_context.toast(
|
||||||
|
view! {
|
||||||
|
<Toast
|
||||||
|
toast_id
|
||||||
|
variant=ToastVariant::Success
|
||||||
|
title=view! { "Successfully modified participant" }.into_view()
|
||||||
|
/>
|
||||||
|
},
|
||||||
|
Some(toast_id),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
participant_id.set(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
{ move || match participant() {
|
||||||
|
Some(_p) => view! {
|
||||||
|
<div class="modal-background">
|
||||||
|
<div class="modal">
|
||||||
|
<form on:submit=form_submit>
|
||||||
|
<h2>"Deelnemer bewerken"</h2>
|
||||||
|
<label>Naam</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
on:input=move |ev| {
|
||||||
|
name.set(event_target_value(&ev))
|
||||||
|
}
|
||||||
|
prop:value=name
|
||||||
|
/>
|
||||||
|
<label>Groep</label>
|
||||||
|
<select
|
||||||
|
name="group"
|
||||||
|
autocomplete="off"
|
||||||
|
on:change=move |ev| {
|
||||||
|
group.set(event_target_value(&ev))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectOption value=group is="A1" />
|
||||||
|
<SelectOption value=group is="A2" />
|
||||||
|
<SelectOption value=group is="A3" />
|
||||||
|
<SelectOption value=group is="A4" />
|
||||||
|
<SelectOption value=group is="A5" />
|
||||||
|
<SelectOption value=group is="A6" />
|
||||||
|
|
||||||
|
<SelectOption value=group is="B1" />
|
||||||
|
<SelectOption value=group is="B2" />
|
||||||
|
<SelectOption value=group is="B3" />
|
||||||
|
<SelectOption value=group is="B4" />
|
||||||
|
<SelectOption value=group is="B5" />
|
||||||
|
<SelectOption value=group is="B6" />
|
||||||
|
|
||||||
|
<SelectOption value=group is="C1" />
|
||||||
|
<SelectOption value=group is="C2" />
|
||||||
|
<SelectOption value=group is="C3" />
|
||||||
|
<SelectOption value=group is="C4" />
|
||||||
|
<SelectOption value=group is="C5" />
|
||||||
|
<SelectOption value=group is="C6" />
|
||||||
|
|
||||||
|
<SelectOption value=group is="D1" />
|
||||||
|
<SelectOption value=group is="D2" />
|
||||||
|
<SelectOption value=group is="D3" />
|
||||||
|
<SelectOption value=group is="D4" />
|
||||||
|
<SelectOption value=group is="D5" />
|
||||||
|
<SelectOption value=group is="D6" />
|
||||||
|
|
||||||
|
<SelectOption value=group is="Z1" />
|
||||||
|
<SelectOption value=group is="Z2" />
|
||||||
|
<SelectOption value=group is="Z3" />
|
||||||
|
<SelectOption value=group is="Z4" />
|
||||||
|
<SelectOption value=group is="Z5" />
|
||||||
|
<SelectOption value=group is="Z6" />
|
||||||
|
</select>
|
||||||
|
<label>Tijd Lifesaver</label>
|
||||||
|
<div class="time">
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="mm"
|
||||||
|
min=0
|
||||||
|
max=99
|
||||||
|
on:input=move |ev| {
|
||||||
|
time_lifesaver.update(|time| time.minutes = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time_lifesaver.get().minutes
|
||||||
|
/>
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="ss"
|
||||||
|
min=0
|
||||||
|
max=59
|
||||||
|
on:input=move |ev| {
|
||||||
|
time_lifesaver.update(|time| time.seconds = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time_lifesaver.get().seconds
|
||||||
|
/>
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="ms"
|
||||||
|
min=0
|
||||||
|
max=99
|
||||||
|
on:input=move |ev| {
|
||||||
|
time_lifesaver.update(|time| time.milliseconds = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time_lifesaver.get().milliseconds
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label>Tijd Hindernis</label>
|
||||||
|
<div class="time">
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="mm"
|
||||||
|
min=0
|
||||||
|
max=99
|
||||||
|
on:input=move |ev| {
|
||||||
|
time_hindernis.update(|time| time.minutes = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time_hindernis.get().minutes
|
||||||
|
/>
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="ss"
|
||||||
|
min=0
|
||||||
|
max=59
|
||||||
|
on:input=move |ev| {
|
||||||
|
time_hindernis.update(|time| time.seconds = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time_hindernis.get().seconds
|
||||||
|
/>
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="ms"
|
||||||
|
min=0
|
||||||
|
max=99
|
||||||
|
on:input=move |ev| {
|
||||||
|
time_hindernis.update(|time| time.milliseconds = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time_hindernis.get().milliseconds
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label>Tijd Popduiken</label>
|
||||||
|
<div class="time">
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="mm"
|
||||||
|
min=0
|
||||||
|
max=99
|
||||||
|
on:input=move |ev| {
|
||||||
|
time_popduiken.update(|time| time.minutes = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time_popduiken.get().minutes
|
||||||
|
/>
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="ss"
|
||||||
|
min=0
|
||||||
|
max=59
|
||||||
|
on:input=move |ev| {
|
||||||
|
time_popduiken.update(|time| time.seconds = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time_popduiken.get().seconds
|
||||||
|
/>
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="ms"
|
||||||
|
min=0
|
||||||
|
max=99
|
||||||
|
on:input=move |ev| {
|
||||||
|
time_popduiken.update(|time| time.milliseconds = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time_popduiken.get().milliseconds
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Deelnemer bewerken" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}.into_view(),
|
||||||
|
None => view! {}.into_view()
|
||||||
|
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SelectOption(is: &'static str, value: RwSignal<String>) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<option
|
||||||
|
value=is
|
||||||
|
selected=move || value.get() == is
|
||||||
|
>
|
||||||
|
{is}
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
}
|
74
application/src/components/participants.rs
Normal file
74
application/src/components/participants.rs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
use crate::util::surrealdb::{
|
||||||
|
client::Time,
|
||||||
|
schemas::{self, ParticipantSignal},
|
||||||
|
};
|
||||||
|
use leptos::*;
|
||||||
|
|
||||||
|
/// Renders the home page of your application.
|
||||||
|
#[component]
|
||||||
|
pub fn Participants(
|
||||||
|
participants: Signal<Vec<schemas::ParticipantSignal>>,
|
||||||
|
#[prop(default = true)] show_group: bool,
|
||||||
|
#[prop(default = true)] show_lifesaver: bool,
|
||||||
|
#[prop(default = true)] show_hindernis: bool,
|
||||||
|
#[prop(default = true)] show_popduiken: bool,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let participant_modal = use_context::<RwSignal<Option<String>>>().unwrap();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<table class="participants-table">
|
||||||
|
<tr>
|
||||||
|
<th>"Naam"</th>
|
||||||
|
<Show when=move || show_group>
|
||||||
|
<th>"Groep"</th>
|
||||||
|
</Show>
|
||||||
|
<Show when=move || show_popduiken>
|
||||||
|
<th>"Popduiken"</th>
|
||||||
|
</Show>
|
||||||
|
<Show when=move || show_hindernis>
|
||||||
|
<th>"Hindernis"</th>
|
||||||
|
</Show>
|
||||||
|
<Show when=move || show_lifesaver>
|
||||||
|
<th>"Lifesaver"</th>
|
||||||
|
</Show>
|
||||||
|
</tr>
|
||||||
|
<For
|
||||||
|
each=move || participants.get()
|
||||||
|
key=|state| state.id.clone()
|
||||||
|
let:child
|
||||||
|
>
|
||||||
|
<tr on:click=move |_| participant_modal.set(Some(child.id.clone()))>
|
||||||
|
<td>{ move || child.value.get().name }</td>
|
||||||
|
<Show when=move || show_group>
|
||||||
|
<td>{ move || child.value.get().group }</td>
|
||||||
|
</Show>
|
||||||
|
{ move || match child.value.get().events {
|
||||||
|
Some(events) => view! {
|
||||||
|
<Show when=move || show_popduiken>
|
||||||
|
<td>{ Time::from_milliseconds_to_string(events.popduiken.unwrap_or(0)) }</td>
|
||||||
|
</Show>
|
||||||
|
<Show when=move || show_hindernis>
|
||||||
|
<td>{ Time::from_milliseconds_to_string(events.hindernis.unwrap_or(0)) }</td>
|
||||||
|
</Show>
|
||||||
|
<Show when=move || show_lifesaver>
|
||||||
|
<td>{ Time::from_milliseconds_to_string(events.lifesaver.unwrap_or(0)) }</td>
|
||||||
|
</Show>
|
||||||
|
},
|
||||||
|
None => view! {
|
||||||
|
<Show when=move || show_popduiken>
|
||||||
|
<td>"0"</td>
|
||||||
|
</Show>
|
||||||
|
<Show when=move || show_hindernis>
|
||||||
|
<td>"0"</td>
|
||||||
|
</Show>
|
||||||
|
<Show when=move || show_lifesaver>
|
||||||
|
<td>"0"</td>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</For>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +0,0 @@
|
|||||||
use crate::util;
|
|
||||||
use leptos::*;
|
|
||||||
|
|
||||||
/// Navigation bar
|
|
||||||
#[component]
|
|
||||||
pub fn Toasts() -> impl IntoView {
|
|
||||||
let notifications = expect_context::<util::toast::NotificationsContext>();
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div class="toastcontainer">
|
|
||||||
{move || notifications.notifications.get().into_iter()
|
|
||||||
.map(|n| view! {
|
|
||||||
<span on:click=move |_| util::toast::remove_toast((*n.id).to_string()) class=n.option>{n.text}</span>
|
|
||||||
}
|
|
||||||
).collect_view()}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
72
application/src/error_template.rs
Normal file
72
application/src/error_template.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
use http::status::StatusCode;
|
||||||
|
use leptos::*;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Error)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error("Not Found")]
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppError {
|
||||||
|
pub fn status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
AppError::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A basic function to display errors served by the error boundaries.
|
||||||
|
// Feel free to do more complicated things here than just displaying the error.
|
||||||
|
#[component]
|
||||||
|
pub fn ErrorTemplate(
|
||||||
|
#[prop(optional)] outside_errors: Option<Errors>,
|
||||||
|
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let errors = match outside_errors {
|
||||||
|
Some(e) => create_rw_signal(e),
|
||||||
|
None => match errors {
|
||||||
|
Some(e) => e,
|
||||||
|
None => panic!("No Errors found and we expected errors!"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Get Errors from Signal
|
||||||
|
let errors = errors.get_untracked();
|
||||||
|
|
||||||
|
// Downcast lets us take a type that implements `std::error::Error`
|
||||||
|
let errors: Vec<AppError> = errors
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||||
|
.collect();
|
||||||
|
println!("Errors: {errors:#?}");
|
||||||
|
|
||||||
|
// Only the response code for the first error is actually sent from the server
|
||||||
|
// this may be customized by the specific application
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
{
|
||||||
|
use leptos_axum::ResponseOptions;
|
||||||
|
let response = use_context::<ResponseOptions>();
|
||||||
|
if let Some(response) = response {
|
||||||
|
response.set_status(errors[0].status_code());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
|
||||||
|
<For
|
||||||
|
// a function that returns the items we're iterating over; a signal is fine
|
||||||
|
each= move || {errors.clone().into_iter().enumerate()}
|
||||||
|
// a unique key for each item as a reference
|
||||||
|
key=|(index, _error)| *index
|
||||||
|
// renders each item to a view
|
||||||
|
children=move |error| {
|
||||||
|
let error_string = error.1.to_string();
|
||||||
|
let error_code= error.1.status_code();
|
||||||
|
view! {
|
||||||
|
<h2>{error_code.to_string()}</h2>
|
||||||
|
<p>"Error: " {error_string}</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
42
application/src/fileserv.rs
Normal file
42
application/src/fileserv.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::State,
|
||||||
|
response::IntoResponse,
|
||||||
|
http::{Request, Response, StatusCode, Uri},
|
||||||
|
};
|
||||||
|
use axum::response::Response as AxumResponse;
|
||||||
|
use tower::ServiceExt;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
use leptos::*;
|
||||||
|
use crate::app::App;
|
||||||
|
|
||||||
|
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||||
|
let root = options.site_root.clone();
|
||||||
|
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||||
|
|
||||||
|
if res.status() == StatusCode::OK {
|
||||||
|
res.into_response()
|
||||||
|
} else {
|
||||||
|
let handler = leptos_axum::render_app_to_stream(options.to_owned(), App);
|
||||||
|
handler(req).await.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_static_file(
|
||||||
|
uri: Uri,
|
||||||
|
root: &str,
|
||||||
|
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||||
|
let req = Request::builder()
|
||||||
|
.uri(uri.clone())
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||||
|
// This path is relative to the cargo root
|
||||||
|
match ServeDir::new(root).oneshot(req).await {
|
||||||
|
Ok(res) => Ok(res.into_response()),
|
||||||
|
Err(err) => Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Something went wrong: {err}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
@ -1,89 +1,15 @@
|
|||||||
use leptos::*;
|
pub mod app;
|
||||||
use leptos_meta::*;
|
pub mod error_template;
|
||||||
use leptos_router::*;
|
#[cfg(feature = "ssr")]
|
||||||
|
pub mod fileserv;
|
||||||
|
pub mod pages;
|
||||||
|
pub mod util;
|
||||||
|
pub mod components;
|
||||||
|
|
||||||
// Modules
|
#[cfg(feature = "hydrate")]
|
||||||
mod components;
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
mod pages;
|
pub fn hydrate() {
|
||||||
mod util;
|
use crate::app::*;
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
// Top-Level pages
|
leptos::mount_to_body(App);
|
||||||
use crate::pages::home::Home;
|
|
||||||
use crate::pages::login;
|
|
||||||
use crate::pages::not_found::NotFound;
|
|
||||||
use crate::pages::participants;
|
|
||||||
use crate::pages::times;
|
|
||||||
|
|
||||||
/// An app router which renders the homepage and handles 404's
|
|
||||||
#[component]
|
|
||||||
pub fn App() -> impl IntoView {
|
|
||||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
|
||||||
provide_meta_context();
|
|
||||||
util::surrealdb::init_surrealdb();
|
|
||||||
util::toast::init_toast();
|
|
||||||
|
|
||||||
let websocket = expect_context::<util::surrealdb::SurrealContext>();
|
|
||||||
let _participants = use_context::<util::surrealdb::ParticipantsContext>()
|
|
||||||
.expect("Could not find participants context");
|
|
||||||
|
|
||||||
view! {
|
|
||||||
|
|
||||||
// injects info into HTML tag from application code
|
|
||||||
<Html
|
|
||||||
lang="en"
|
|
||||||
dir="ltr"
|
|
||||||
attr:data-theme="light"
|
|
||||||
/>
|
|
||||||
|
|
||||||
// sets the document title
|
|
||||||
<Title text="WRB Timings"/>
|
|
||||||
|
|
||||||
// injects metadata in the <head> of the page
|
|
||||||
<Meta charset="UTF-8" />
|
|
||||||
<Meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
|
|
||||||
|
|
||||||
<ErrorBoundary
|
|
||||||
fallback=|errors| view! {
|
|
||||||
<h1>"Uh oh! Something went wrong!"</h1>
|
|
||||||
|
|
||||||
<p>"Errors: "</p>
|
|
||||||
// Render a list of errors as strings - good for development purposes
|
|
||||||
<ul>
|
|
||||||
{move || errors.get()
|
|
||||||
.into_iter()
|
|
||||||
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
|
|
||||||
.collect_view()
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Router>
|
|
||||||
<components::toast::Toasts />
|
|
||||||
<components::navbar::Navbar />
|
|
||||||
<main>
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" view=move || {
|
|
||||||
view! {
|
|
||||||
// only show the outlet if the signin was successfull
|
|
||||||
<Show when=move || !websocket.loading.get() fallback=|| view! { <p>"Connection to database..."</p>}>
|
|
||||||
<Show when=move || websocket.authenticated.get() fallback=login::Login>
|
|
||||||
<Outlet/>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
<Route path="/" view=Home />
|
|
||||||
<Route path="/participants" view=participants::Participants />
|
|
||||||
<Route path="/participants/add" view=participants::add::Add />
|
|
||||||
<Route path="/times" view=times::Times />
|
|
||||||
<Route path="/times/add" view=times::add::Add />
|
|
||||||
<Route path="/*" view=NotFound />
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
</main>
|
|
||||||
</Router>
|
|
||||||
|
|
||||||
</ErrorBoundary>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,45 @@
|
|||||||
use application::App;
|
#![feature(diagnostic_namespace)]
|
||||||
use leptos::*;
|
#[cfg(feature = "ssr")]
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
use application::fileserv::file_and_error_handler;
|
||||||
|
use application::{app::*, util::websocket::server};
|
||||||
|
use axum::{routing::get, Router};
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
|
|
||||||
fn main() {
|
application::util::surrealdb::connect()
|
||||||
// set up logging
|
.await
|
||||||
_ = console_log::init_with_level(log::Level::Debug);
|
.expect("Database connection failed");
|
||||||
console_error_panic_hook::set_once();
|
|
||||||
|
|
||||||
mount_to_body(|| {
|
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
||||||
view! {
|
// For deployment these variables are:
|
||||||
<App />
|
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
||||||
}
|
// Alternately a file can be specified such as Some("Cargo.toml")
|
||||||
})
|
// The file would need to be included with the executable when moved to deployment
|
||||||
|
let conf = get_configuration(None).await.unwrap();
|
||||||
|
let leptos_options = conf.leptos_options;
|
||||||
|
let addr = leptos_options.site_addr;
|
||||||
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
|
// build our application with a route
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/ws", get(server::websocket_handler))
|
||||||
|
.leptos_routes(&leptos_options, routes, App)
|
||||||
|
.fallback(file_and_error_handler)
|
||||||
|
.with_state(leptos_options);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||||
|
logging::log!("listening on http://{}", &addr);
|
||||||
|
|
||||||
|
axum::serve(listener, app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
|
pub fn main() {
|
||||||
|
// no client-side main function
|
||||||
|
// unless we want this to work with e.g., Trunk for a purely client-side app
|
||||||
|
// see lib.rs for hydration function instead
|
||||||
}
|
}
|
||||||
|
4
application/src/pages.rs
Normal file
4
application/src/pages.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod add_participant;
|
||||||
|
pub mod add_time;
|
||||||
|
pub mod groups;
|
||||||
|
pub mod index;
|
144
application/src/pages/add_participant.rs
Normal file
144
application/src/pages/add_participant.rs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
use leptos::*;
|
||||||
|
use leptos_toaster::{Toast, ToastId, ToastVariant, Toasts};
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature = "ssr")] {
|
||||||
|
use crate::util::surrealdb::{DB, schemas};
|
||||||
|
use crate::util::websocket::{server, ParticipantsAction};
|
||||||
|
use leptos::logging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(AddParticipant)]
|
||||||
|
async fn add_participant(name: String, group: String) -> Result<(), ServerFnError> {
|
||||||
|
let websocket_state = &server::WEBSOCKET_STATE;
|
||||||
|
|
||||||
|
let created: Vec<schemas::ParticipantRecord> = DB
|
||||||
|
.create("participant")
|
||||||
|
.content(schemas::NewParticipant { name, group })
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match created.first() {
|
||||||
|
Some(participant) => {
|
||||||
|
logging::log!(
|
||||||
|
"Created participant: {} ({})",
|
||||||
|
participant.name,
|
||||||
|
participant.group
|
||||||
|
);
|
||||||
|
|
||||||
|
let action = ParticipantsAction::Add {
|
||||||
|
participant: schemas::Participant {
|
||||||
|
name: participant.name.clone(),
|
||||||
|
group: participant.group.clone(),
|
||||||
|
id: participant.id.id.to_string(),
|
||||||
|
events: participant.events.clone(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match websocket_state.apply(action) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => Err(ServerFnError::new("Error sending websocket action")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(ServerFnError::ServerError(String::from(
|
||||||
|
"Could not create participant",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the home page of your application.
|
||||||
|
#[component]
|
||||||
|
pub fn AddParticipant() -> impl IntoView {
|
||||||
|
let toasts_context = expect_context::<Toasts>();
|
||||||
|
|
||||||
|
let name = create_rw_signal("".to_string());
|
||||||
|
let group = create_rw_signal("A1".to_string());
|
||||||
|
|
||||||
|
let name_input_ref: NodeRef<html::Input> = create_node_ref();
|
||||||
|
|
||||||
|
let form_submit_action = create_action(|input: &(String, String)| {
|
||||||
|
let input = input.to_owned();
|
||||||
|
|
||||||
|
async move { add_participant(input.0, input.1).await }
|
||||||
|
});
|
||||||
|
|
||||||
|
let form_submit = move |ev: ev::SubmitEvent| {
|
||||||
|
ev.prevent_default();
|
||||||
|
|
||||||
|
form_submit_action.dispatch((name.get(), group.get()));
|
||||||
|
|
||||||
|
let toast_id = ToastId::new();
|
||||||
|
|
||||||
|
toasts_context.toast(
|
||||||
|
view! {
|
||||||
|
<Toast
|
||||||
|
toast_id
|
||||||
|
variant=ToastVariant::Success
|
||||||
|
title=view! { "Successfully added participant" }.into_view()
|
||||||
|
/>
|
||||||
|
},
|
||||||
|
Some(toast_id),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
name.set("".to_string());
|
||||||
|
let _ = name_input_ref.get().unwrap().focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<h2>"Deelnemer toevoegen"</h2>
|
||||||
|
<form on:submit=form_submit>
|
||||||
|
<label>Naam</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
node_ref=name_input_ref
|
||||||
|
on:input=move |ev| {
|
||||||
|
name.set(event_target_value(&ev));
|
||||||
|
}
|
||||||
|
prop:value=name
|
||||||
|
/>
|
||||||
|
<label>Groep</label>
|
||||||
|
<select on:change=move |ev| {
|
||||||
|
let new_value = event_target_value(&ev);
|
||||||
|
group.set(new_value);
|
||||||
|
} autocomplete="off">
|
||||||
|
<option value="A1">A1</option>
|
||||||
|
<option value="A2">A2</option>
|
||||||
|
<option value="A3">A3</option>
|
||||||
|
<option value="A4">A4</option>
|
||||||
|
<option value="A5">A5</option>
|
||||||
|
<option value="A6">A6</option>
|
||||||
|
|
||||||
|
<option value="B1">B1</option>
|
||||||
|
<option value="B2">B2</option>
|
||||||
|
<option value="B3">B3</option>
|
||||||
|
<option value="B4">B4</option>
|
||||||
|
<option value="B5">B5</option>
|
||||||
|
<option value="B6">B6</option>
|
||||||
|
|
||||||
|
<option value="C1">C1</option>
|
||||||
|
<option value="C2">C2</option>
|
||||||
|
<option value="C3">C3</option>
|
||||||
|
<option value="C4">C4</option>
|
||||||
|
<option value="C5">C5</option>
|
||||||
|
<option value="C6">C6</option>
|
||||||
|
|
||||||
|
<option value="D1">D1</option>
|
||||||
|
<option value="D2">D2</option>
|
||||||
|
<option value="D3">D3</option>
|
||||||
|
<option value="D4">D4</option>
|
||||||
|
<option value="D5">D5</option>
|
||||||
|
<option value="D6">D6</option>
|
||||||
|
|
||||||
|
<option value="Z1">Z1</option>
|
||||||
|
<option value="Z2">Z2</option>
|
||||||
|
<option value="Z3">Z3</option>
|
||||||
|
<option value="Z4">Z4</option>
|
||||||
|
<option value="Z5">Z5</option>
|
||||||
|
<option value="Z6">Z6</option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" value="Deelnemer toevoegen" />
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
231
application/src/pages/add_time.rs
Normal file
231
application/src/pages/add_time.rs
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
use crate::util::surrealdb::{client::sort_participants, client::Time, schemas};
|
||||||
|
use leptos::{ev::keydown, *};
|
||||||
|
use leptos_toaster::{Toast, ToastId, ToastVariant, Toasts};
|
||||||
|
use leptos_use::*;
|
||||||
|
use web_sys::ScrollIntoViewOptions;
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature = "ssr")] {
|
||||||
|
use crate::util::surrealdb::{DB, schemas::Participant};
|
||||||
|
use crate::util::websocket::{server, ParticipantsAction};
|
||||||
|
use surrealdb::opt::PatchOp;
|
||||||
|
use leptos::logging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(AddTime)]
|
||||||
|
async fn add_time(
|
||||||
|
mut participant: schemas::Participant,
|
||||||
|
event: String,
|
||||||
|
time: u32,
|
||||||
|
) -> Result<(), ServerFnError> {
|
||||||
|
let websocket_state = &server::WEBSOCKET_STATE;
|
||||||
|
|
||||||
|
let updated: Option<schemas::ParticipantRecord> = DB
|
||||||
|
.update(("participant", &participant.id))
|
||||||
|
.patch(PatchOp::replace(&("/events/".to_owned() + &event), time))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match updated {
|
||||||
|
Some(participant_updated) => {
|
||||||
|
logging::log!(
|
||||||
|
"Updated participant: {} ({})",
|
||||||
|
participant_updated.name,
|
||||||
|
participant_updated.group
|
||||||
|
);
|
||||||
|
let action = ParticipantsAction::Replace {
|
||||||
|
participant: Participant {
|
||||||
|
name: participant_updated.name.clone(),
|
||||||
|
group: participant_updated.group.clone(),
|
||||||
|
id: participant.id,
|
||||||
|
events: participant_updated.events.clone(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match websocket_state.apply(action) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => Err(ServerFnError::new("Error sending websocket action")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(ServerFnError::ServerError(String::from(
|
||||||
|
"Could not update participant",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the home page of your application.
|
||||||
|
#[component]
|
||||||
|
pub fn AddTime() -> impl IntoView {
|
||||||
|
let participants = use_context::<schemas::ParticipantsContext>().unwrap();
|
||||||
|
let toasts_context = expect_context::<Toasts>();
|
||||||
|
|
||||||
|
let container_ref: NodeRef<html::Ul> = create_node_ref();
|
||||||
|
let name_input_ref: NodeRef<html::Input> = create_node_ref();
|
||||||
|
|
||||||
|
let event = create_rw_signal("lifesaver".to_string());
|
||||||
|
let name = create_rw_signal("".to_string());
|
||||||
|
let time = create_rw_signal(Time {
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0,
|
||||||
|
milliseconds: 0,
|
||||||
|
});
|
||||||
|
let selected_index = create_rw_signal::<usize>(0);
|
||||||
|
|
||||||
|
let participants_sorted =
|
||||||
|
create_memo(move |_| sort_participants(participants.get(), name.get()));
|
||||||
|
|
||||||
|
let add_time_action = create_action(|input: &(schemas::Participant, String, u32)| {
|
||||||
|
let input = input.to_owned();
|
||||||
|
async move { add_time(input.0, input.1, input.2).await }
|
||||||
|
});
|
||||||
|
|
||||||
|
let form_submit = move |ev: ev::SubmitEvent| {
|
||||||
|
ev.prevent_default();
|
||||||
|
|
||||||
|
let participant = &participants_sorted.get()[selected_index.get()];
|
||||||
|
add_time_action.dispatch((
|
||||||
|
participant.value.get(),
|
||||||
|
event.get(),
|
||||||
|
time.get().as_milliseconds(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let toast_id = ToastId::new();
|
||||||
|
|
||||||
|
toasts_context.toast(
|
||||||
|
view! {
|
||||||
|
<Toast
|
||||||
|
toast_id
|
||||||
|
variant=ToastVariant::Success
|
||||||
|
title=view! { "Successfully added time" }.into_view()
|
||||||
|
/>
|
||||||
|
},
|
||||||
|
Some(toast_id),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
name.set("".to_string());
|
||||||
|
time.set(Time {
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0,
|
||||||
|
milliseconds: 0,
|
||||||
|
});
|
||||||
|
let _ = name_input_ref.get().unwrap().focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = use_event_listener(name_input_ref, keydown, move |evt| {
|
||||||
|
match evt.key().as_str() {
|
||||||
|
"ArrowDown" => selected_index.update(|x| {
|
||||||
|
let len = participants.get_untracked().len();
|
||||||
|
if *x != len {
|
||||||
|
*x += 1;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
"ArrowUp" => selected_index.update(|x| {
|
||||||
|
if *x != 0 {
|
||||||
|
*x -= 1;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
"Enter" => evt.prevent_default(),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
let el: web_sys::Element = container_ref
|
||||||
|
.get_untracked()
|
||||||
|
.unwrap()
|
||||||
|
.children()
|
||||||
|
.item(selected_index.get_untracked().try_into().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
el.scroll_into_view_with_scroll_into_view_options(
|
||||||
|
&ScrollIntoViewOptions::new().block(web_sys::ScrollLogicalPosition::Center),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<h2>"Tijd toevoegen"</h2>
|
||||||
|
<form on:submit=form_submit>
|
||||||
|
<label>Onderdeel</label>
|
||||||
|
<select autocomplete="off"
|
||||||
|
on:change=move |ev| {
|
||||||
|
event.set(event_target_value(&ev))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="lifesaver">"Lifesaver"</option>
|
||||||
|
<option value="hindernis">"Hindernis"</option>
|
||||||
|
<option value="popduiken">"Popduiken"</option>
|
||||||
|
</select>
|
||||||
|
<label>Naam</label>
|
||||||
|
<div class="autocomplete">
|
||||||
|
<input type="text"
|
||||||
|
name="name"
|
||||||
|
autocomplete="off"
|
||||||
|
autofocus=true
|
||||||
|
node_ref=name_input_ref
|
||||||
|
on:input=move |ev| {
|
||||||
|
name.set(event_target_value(&ev));
|
||||||
|
selected_index.set(0);
|
||||||
|
}
|
||||||
|
prop:value=name
|
||||||
|
/>
|
||||||
|
<ul node_ref=container_ref tabindex=-1>
|
||||||
|
{move || participants_sorted.get().into_iter().enumerate().map(|(i, participant)| view! {
|
||||||
|
<li on:click=move |_| selected_index.set(i) class:selected=move || selected_index.get() == i>{participant.value.get().name + " " + "(" + &participant.value.get().group + ")" }</li>
|
||||||
|
}).collect_view()}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<label>Tijd</label>
|
||||||
|
<div class="time">
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="mm"
|
||||||
|
min=0
|
||||||
|
max=99
|
||||||
|
on:input=move |ev| {
|
||||||
|
time.update(|time| time.minutes = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time.get().minutes
|
||||||
|
/>
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="ss"
|
||||||
|
min=0
|
||||||
|
max=59
|
||||||
|
on:input=move |ev| {
|
||||||
|
time.update(|time| time.seconds = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time.get().seconds
|
||||||
|
/>
|
||||||
|
<input type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="ms"
|
||||||
|
min=0
|
||||||
|
max=99
|
||||||
|
on:input=move |ev| {
|
||||||
|
time.update(|time| time.milliseconds = match event_target_value(&ev).parse::<u32>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prop:value=move || time.get().milliseconds
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Tijd toevoegen" />
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SelectOption(is: &'static str, value: ReadSignal<String>) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<option
|
||||||
|
value=is
|
||||||
|
selected=move || value.get() == is
|
||||||
|
>
|
||||||
|
{is}
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
}
|
228
application/src/pages/groups.rs
Normal file
228
application/src/pages/groups.rs
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
use crate::components::{self, participant};
|
||||||
|
use crate::util::surrealdb::schemas;
|
||||||
|
use futures::stream::ForEach;
|
||||||
|
use leptos::*;
|
||||||
|
|
||||||
|
/// Renders the home page of your application.
|
||||||
|
#[component]
|
||||||
|
pub fn Groups() -> impl IntoView {
|
||||||
|
let group_hour = create_rw_signal("A");
|
||||||
|
let group_lane = create_rw_signal("1");
|
||||||
|
|
||||||
|
let participants_context = use_context::<schemas::ParticipantsContext>().unwrap();
|
||||||
|
|
||||||
|
let participants_filtered: Memo<Vec<schemas::ParticipantSignal>> = create_memo(move |_| {
|
||||||
|
participants_context
|
||||||
|
.get()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|participant| {
|
||||||
|
participant.value.get().group == group_hour.get().to_owned() + group_lane.get()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
let (participants_lifesaver, participants_hindernis, participants_popduiken) =
|
||||||
|
sort_by_events(participants_filtered);
|
||||||
|
|
||||||
|
let lifesaver_best = create_memo(move |_| match participants_lifesaver.get().get(0) {
|
||||||
|
Some(p) => match p.value.get().events {
|
||||||
|
Some(e) => e.lifesaver.unwrap_or(0),
|
||||||
|
None => 6_000_000,
|
||||||
|
},
|
||||||
|
None => 6_000_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
let hindernis_best = create_memo(move |_| match participants_hindernis.get().get(0) {
|
||||||
|
Some(p) => match p.value.get().events {
|
||||||
|
Some(e) => e.hindernis.unwrap_or(0),
|
||||||
|
None => 6_000_000,
|
||||||
|
},
|
||||||
|
None => 6_000_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
let popduiken_best = create_memo(move |_| match participants_popduiken.get().get(0) {
|
||||||
|
Some(p) => match p.value.get().events {
|
||||||
|
Some(e) => e.popduiken.unwrap_or(0),
|
||||||
|
None => 6_000_000,
|
||||||
|
},
|
||||||
|
None => 6_000_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
let total_score_participants = sort_by_total_score(
|
||||||
|
participants_filtered,
|
||||||
|
lifesaver_best,
|
||||||
|
hindernis_best,
|
||||||
|
popduiken_best,
|
||||||
|
);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<h2>"Groups"</h2>
|
||||||
|
<div class="groups-select-container">
|
||||||
|
<div class="groups-select-row">
|
||||||
|
<div on:click=move |_| group_hour.set("A") class:selected=move || group_hour.get() == "A">"A"</div>
|
||||||
|
<div on:click=move |_| group_hour.set("B") class:selected=move || group_hour.get() == "B">"B"</div>
|
||||||
|
<div on:click=move |_| group_hour.set("C") class:selected=move || group_hour.get() == "C">"C"</div>
|
||||||
|
<div on:click=move |_| group_hour.set("D") class:selected=move || group_hour.get() == "D">"D"</div>
|
||||||
|
<div on:click=move |_| group_hour.set("Z") class:selected=move || group_hour.get() == "Z">"Z"</div>
|
||||||
|
</div>
|
||||||
|
<div class="groups-select-row">
|
||||||
|
<div on:click=move |_| group_lane.set("1") class:selected=move || group_lane.get() == "1">"1"</div>
|
||||||
|
<div on:click=move |_| group_lane.set("2") class:selected=move || group_lane.get() == "2">"2"</div>
|
||||||
|
<div on:click=move |_| group_lane.set("3") class:selected=move || group_lane.get() == "3">"3"</div>
|
||||||
|
<div on:click=move |_| group_lane.set("4") class:selected=move || group_lane.get() == "4">"4"</div>
|
||||||
|
<div on:click=move |_| group_lane.set("5") class:selected=move || group_lane.get() == "5">"5"</div>
|
||||||
|
<div on:click=move |_| group_lane.set("6") class:selected=move || group_lane.get() == "6">"6"</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>"Algemene score"</h3>
|
||||||
|
<components::participants::Participants participants=total_score_participants.into() show_group=false />
|
||||||
|
<div class="events-container">
|
||||||
|
<div>
|
||||||
|
<h3>"Popduiken"</h3>
|
||||||
|
<components::participants::Participants participants=participants_popduiken.into() show_group=false show_hindernis=false show_lifesaver=false />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>"Hindernis"</h3>
|
||||||
|
<components::participants::Participants participants=participants_hindernis.into() show_group=false show_popduiken=false show_lifesaver=false />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>"Lifesaver"</h3>
|
||||||
|
<components::participants::Participants participants=participants_lifesaver.into() show_group=false show_popduiken=false show_hindernis=false />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_by_total_score(
|
||||||
|
participants_filtered: Memo<Vec<schemas::ParticipantSignal>>,
|
||||||
|
lifesaver_best: Memo<u32>,
|
||||||
|
popduiken_best: Memo<u32>,
|
||||||
|
hindernis_best: Memo<u32>,
|
||||||
|
) -> Memo<Vec<schemas::ParticipantSignal>> {
|
||||||
|
let total_score_participants: Memo<Vec<schemas::ParticipantSignal>> = create_memo(move |_| {
|
||||||
|
let mut all_participants: Vec<(usize, schemas::ParticipantSignal)> = participants_filtered
|
||||||
|
.get()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut lifesaver_best = lifesaver_best.get();
|
||||||
|
let mut popduiken_best = popduiken_best.get();
|
||||||
|
let mut hindernis_best = hindernis_best.get();
|
||||||
|
|
||||||
|
if lifesaver_best == 0 {
|
||||||
|
lifesaver_best = 10 * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if popduiken_best == 0 {
|
||||||
|
popduiken_best = 10 * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if hindernis_best == 0 {
|
||||||
|
hindernis_best = 10 * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
all_participants.sort_by(|(_, a), (_, b)| {
|
||||||
|
let part_a = match a.value.get().events {
|
||||||
|
Some(events) => {
|
||||||
|
((events.lifesaver.unwrap_or(6_000_000) * 100) / lifesaver_best)
|
||||||
|
+ ((events.hindernis.unwrap_or(6_000_000) * 100) / hindernis_best)
|
||||||
|
+ ((events.popduiken.unwrap_or(6_000_000) * 100) / popduiken_best)
|
||||||
|
}
|
||||||
|
None => 1000,
|
||||||
|
};
|
||||||
|
let part_b = match b.value.get().events {
|
||||||
|
Some(events) => {
|
||||||
|
((events.lifesaver.unwrap_or(6_000_000) * 100) / lifesaver_best)
|
||||||
|
+ ((events.hindernis.unwrap_or(6_000_000) * 100) / hindernis_best)
|
||||||
|
+ ((events.popduiken.unwrap_or(6_000_000) * 100) / popduiken_best)
|
||||||
|
}
|
||||||
|
None => 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
part_a.cmp(&part_b)
|
||||||
|
});
|
||||||
|
|
||||||
|
all_participants
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, value)| value)
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
total_score_participants
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_by_events(
|
||||||
|
participants_filtered: Memo<Vec<schemas::ParticipantSignal>>,
|
||||||
|
) -> (
|
||||||
|
Memo<Vec<schemas::ParticipantSignal>>,
|
||||||
|
Memo<Vec<schemas::ParticipantSignal>>,
|
||||||
|
Memo<Vec<schemas::ParticipantSignal>>,
|
||||||
|
) {
|
||||||
|
let lifesaver: Memo<Vec<schemas::ParticipantSignal>> = create_memo(move |_| {
|
||||||
|
let mut participants: Vec<(usize, schemas::ParticipantSignal)> = participants_filtered
|
||||||
|
.get()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.collect();
|
||||||
|
participants.sort_by(|(_, a), (_, b)| {
|
||||||
|
let event_a = match a.value.get().events {
|
||||||
|
Some(events) => events.lifesaver.unwrap_or(6_000_000),
|
||||||
|
None => 6_000_000,
|
||||||
|
};
|
||||||
|
let event_b = match b.value.get().events {
|
||||||
|
Some(events) => events.lifesaver.unwrap_or(5_999_100),
|
||||||
|
None => 6_000_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
event_a.cmp(&event_b)
|
||||||
|
});
|
||||||
|
|
||||||
|
participants.into_iter().map(|(_, value)| value).collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
let hindernis: Memo<Vec<schemas::ParticipantSignal>> = create_memo(move |_| {
|
||||||
|
let mut participants: Vec<(usize, schemas::ParticipantSignal)> = participants_filtered
|
||||||
|
.get()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.collect();
|
||||||
|
participants.sort_by(|(_, a), (_, b)| {
|
||||||
|
let event_a = match a.value.get().events {
|
||||||
|
Some(events) => events.hindernis.unwrap_or(6_000_000),
|
||||||
|
None => 6_000_000,
|
||||||
|
};
|
||||||
|
let event_b = match b.value.get().events {
|
||||||
|
Some(events) => events.hindernis.unwrap_or(6_000_000),
|
||||||
|
None => 6_000_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
event_a.cmp(&event_b)
|
||||||
|
});
|
||||||
|
|
||||||
|
participants.into_iter().map(|(_, value)| value).collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
let popduiken: Memo<Vec<schemas::ParticipantSignal>> = create_memo(move |_| {
|
||||||
|
let mut participants: Vec<(usize, schemas::ParticipantSignal)> = participants_filtered
|
||||||
|
.get()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.collect();
|
||||||
|
participants.sort_by(|(_, a), (_, b)| {
|
||||||
|
let event_a = match a.value.get().events {
|
||||||
|
Some(events) => events.popduiken.unwrap_or(6_000_000),
|
||||||
|
None => 6_000_000,
|
||||||
|
};
|
||||||
|
let event_b = match b.value.get().events {
|
||||||
|
Some(events) => events.popduiken.unwrap_or(6_000_000),
|
||||||
|
None => 6_000_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
event_a.cmp(&event_b)
|
||||||
|
});
|
||||||
|
|
||||||
|
participants.into_iter().map(|(_, value)| value).collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
(lifesaver, hindernis, popduiken)
|
||||||
|
}
|
@ -1,13 +0,0 @@
|
|||||||
use leptos::*;
|
|
||||||
|
|
||||||
/// Default Home Page
|
|
||||||
#[component]
|
|
||||||
pub fn Home() -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<h1>"Welcome to WRB Timings"</h1>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
34
application/src/pages/index.rs
Normal file
34
application/src/pages/index.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use crate::components;
|
||||||
|
use crate::util::surrealdb::{client::sort_participants, schemas};
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
|
||||||
|
/// Renders the home page of your application.
|
||||||
|
#[component]
|
||||||
|
pub fn HomePage() -> impl IntoView {
|
||||||
|
let participants_context = use_context::<schemas::ParticipantsContext>().unwrap();
|
||||||
|
|
||||||
|
let name = create_rw_signal("".to_string());
|
||||||
|
|
||||||
|
let participants_sorted =
|
||||||
|
create_memo(move |_| sort_participants(participants_context.get(), name.get()));
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="actions-container">
|
||||||
|
<A href="/add-participant">"Deelnemer toevoegen"</A>
|
||||||
|
<A href="/add-time">"Tijd toevoegen"</A>
|
||||||
|
<A href="/groups">"Group view"</A>
|
||||||
|
</div>
|
||||||
|
<input type="search"
|
||||||
|
class="participants-search"
|
||||||
|
placeholder="Search"
|
||||||
|
autocomplete="off"
|
||||||
|
autofocus=true
|
||||||
|
on:input=move |ev| {
|
||||||
|
name.set(event_target_value(&ev));
|
||||||
|
}
|
||||||
|
prop:value=name
|
||||||
|
/>
|
||||||
|
<components::participants::Participants participants=participants_sorted.into() />
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
use crate::util;
|
|
||||||
use leptos::*;
|
|
||||||
|
|
||||||
/// Login page
|
|
||||||
#[component]
|
|
||||||
pub fn Login() -> impl IntoView {
|
|
||||||
let websocket = expect_context::<util::surrealdb::SurrealContext>();
|
|
||||||
|
|
||||||
let input_element: NodeRef<html::Input> = create_node_ref();
|
|
||||||
|
|
||||||
let on_submit = move |ev: leptos::ev::SubmitEvent| {
|
|
||||||
ev.prevent_default();
|
|
||||||
|
|
||||||
let value = input_element
|
|
||||||
.get()
|
|
||||||
.expect("<input> should be mounted")
|
|
||||||
.value();
|
|
||||||
|
|
||||||
websocket.signin(value)
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<form class="login" on:submit=on_submit>
|
|
||||||
<h1>"WRB Timings"</h1>
|
|
||||||
<input type="password"
|
|
||||||
node_ref=input_element
|
|
||||||
placeholder="Password"
|
|
||||||
/>
|
|
||||||
<input type="submit" value="Submit" />
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
pub mod home;
|
|
||||||
pub mod login;
|
|
||||||
pub mod not_found;
|
|
||||||
pub mod participants;
|
|
||||||
pub mod times;
|
|
@ -1,7 +0,0 @@
|
|||||||
use leptos::*;
|
|
||||||
|
|
||||||
/// 404 Not Found Page
|
|
||||||
#[component]
|
|
||||||
pub fn NotFound() -> impl IntoView {
|
|
||||||
view! { <h1>"Uh oh!" <br/> "We couldn't find that page!"</h1> }
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
use leptos::*;
|
|
||||||
use leptos_router::*;
|
|
||||||
|
|
||||||
pub mod add;
|
|
||||||
|
|
||||||
/// Default Home Page
|
|
||||||
#[component]
|
|
||||||
pub fn Participants() -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<A class="btn-add-link" href="/participants/add">"Deelnemer toevoegen"</A>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
use leptos::*;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn SelectOption(is: &'static str, value: ReadSignal<String>) -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<option
|
|
||||||
value=is
|
|
||||||
selected=move || value.get() == is
|
|
||||||
>
|
|
||||||
{is}
|
|
||||||
</option>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigation bar
|
|
||||||
#[component]
|
|
||||||
pub fn Add() -> impl IntoView {
|
|
||||||
let websocket = expect_context::<crate::util::surrealdb::SurrealContext>();
|
|
||||||
|
|
||||||
let (name, set_name) = create_signal(String::from(""));
|
|
||||||
let (group, set_group) = create_signal(String::from("A1"));
|
|
||||||
let (error, set_error) = create_signal(String::from(""));
|
|
||||||
|
|
||||||
let on_submit = move |ev: leptos::ev::SubmitEvent| {
|
|
||||||
ev.prevent_default();
|
|
||||||
set_error.set(String::from(""));
|
|
||||||
|
|
||||||
match websocket.add_person(name.get(), group.get()) {
|
|
||||||
Ok(_) => set_name.set(String::from("")),
|
|
||||||
Err(err) => set_error.set(err),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
view! {
|
|
||||||
<h1>"Deelnemer toevoegen"</h1>
|
|
||||||
<form class="add" on:submit=on_submit>
|
|
||||||
<h1>"WRB Timings"</h1>
|
|
||||||
<select
|
|
||||||
on:change=move |ev| {
|
|
||||||
set_group.set(event_target_value(&ev));
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectOption value=group is="A1"/>
|
|
||||||
<SelectOption value=group is="A2"/>
|
|
||||||
<SelectOption value=group is="A3"/>
|
|
||||||
<SelectOption value=group is="A4"/>
|
|
||||||
<SelectOption value=group is="A5"/>
|
|
||||||
<SelectOption value=group is="A6"/>
|
|
||||||
|
|
||||||
<SelectOption value=group is="B1"/>
|
|
||||||
<SelectOption value=group is="B2"/>
|
|
||||||
<SelectOption value=group is="B3"/>
|
|
||||||
<SelectOption value=group is="B4"/>
|
|
||||||
<SelectOption value=group is="B5"/>
|
|
||||||
<SelectOption value=group is="B6"/>
|
|
||||||
|
|
||||||
<SelectOption value=group is="C1"/>
|
|
||||||
<SelectOption value=group is="C2"/>
|
|
||||||
<SelectOption value=group is="C3"/>
|
|
||||||
<SelectOption value=group is="C4"/>
|
|
||||||
<SelectOption value=group is="C5"/>
|
|
||||||
<SelectOption value=group is="C6"/>
|
|
||||||
|
|
||||||
<SelectOption value=group is="D1"/>
|
|
||||||
<SelectOption value=group is="D2"/>
|
|
||||||
<SelectOption value=group is="D3"/>
|
|
||||||
<SelectOption value=group is="D4"/>
|
|
||||||
<SelectOption value=group is="D5"/>
|
|
||||||
<SelectOption value=group is="D6"/>
|
|
||||||
|
|
||||||
<SelectOption value=group is="Z1"/>
|
|
||||||
<SelectOption value=group is="Z2"/>
|
|
||||||
<SelectOption value=group is="Z3"/>
|
|
||||||
<SelectOption value=group is="Z4"/>
|
|
||||||
<SelectOption value=group is="Z5"/>
|
|
||||||
<SelectOption value=group is="Z6"/>
|
|
||||||
</select>
|
|
||||||
<input type="text"
|
|
||||||
placeholder="Name"
|
|
||||||
on:input=move |ev| {
|
|
||||||
set_name.set(event_target_value(&ev));
|
|
||||||
}
|
|
||||||
prop:value=name
|
|
||||||
/>
|
|
||||||
<input type="submit" value="Submit" />
|
|
||||||
</form>
|
|
||||||
<p class="error">
|
|
||||||
{ error }
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
use leptos::*;
|
|
||||||
use leptos_router::*;
|
|
||||||
|
|
||||||
pub mod add;
|
|
||||||
|
|
||||||
/// Default Home Page
|
|
||||||
#[component]
|
|
||||||
pub fn Times() -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<A class="btn-add-link" href="/times/add">"Tijd toevoegen"</A>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,209 +0,0 @@
|
|||||||
use crate::util::surrealdb;
|
|
||||||
use leptos::{ev::keydown, *};
|
|
||||||
use leptos_use::*;
|
|
||||||
use strsim::normalized_damerau_levenshtein;
|
|
||||||
use web_sys::{ScrollIntoViewOptions, ScrollLogicalPosition};
|
|
||||||
|
|
||||||
use crate::util::surrealdb::Participant;
|
|
||||||
|
|
||||||
// Functions to sort all the participants and include a search term
|
|
||||||
fn sort_participants(participants: Vec<Participant>, search: String) -> Vec<Participant> {
|
|
||||||
let mut filtered_sorted_list: Vec<(Participant, f64)> = participants
|
|
||||||
.into_iter()
|
|
||||||
.map(|participant| {
|
|
||||||
(
|
|
||||||
participant.clone(),
|
|
||||||
normalized_damerau_levenshtein(
|
|
||||||
&participant.name.to_lowercase(),
|
|
||||||
&search.to_lowercase(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
filtered_sorted_list.sort_by(|a, b| {
|
|
||||||
let (_, sim_score_a) = a;
|
|
||||||
let (_, sim_score_b) = b;
|
|
||||||
sim_score_b
|
|
||||||
.partial_cmp(sim_score_a)
|
|
||||||
.unwrap_or(std::cmp::Ordering::Equal)
|
|
||||||
});
|
|
||||||
|
|
||||||
filtered_sorted_list
|
|
||||||
.into_iter()
|
|
||||||
.map(|(item, _)| item)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn SelectOption(is: &'static str, value: ReadSignal<String>) -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<option
|
|
||||||
value=is
|
|
||||||
selected=move || value.get() == is
|
|
||||||
>
|
|
||||||
{is}
|
|
||||||
</option>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigation bar
|
|
||||||
#[component]
|
|
||||||
pub fn Add() -> impl IntoView {
|
|
||||||
let websocket = expect_context::<crate::util::surrealdb::SurrealContext>();
|
|
||||||
let participants = expect_context::<crate::util::surrealdb::ParticipantsContext>();
|
|
||||||
|
|
||||||
let container_ref: NodeRef<html::Ul> = create_node_ref();
|
|
||||||
let name_ref: NodeRef<html::Input> = create_node_ref();
|
|
||||||
|
|
||||||
let (name, set_name) = create_signal(String::from(""));
|
|
||||||
let (event, set_event) = create_signal(String::from("lifesaver"));
|
|
||||||
let (error, set_error) = create_signal(String::from(""));
|
|
||||||
let (minutes, set_minutes) = create_signal::<u64>(0);
|
|
||||||
let (seconds, set_seconds) = create_signal::<u64>(0);
|
|
||||||
let (miliseconds, set_miliseconds) = create_signal::<u64>(0);
|
|
||||||
let (selected, set_selected) = create_signal::<usize>(0);
|
|
||||||
|
|
||||||
let participants_sorted =
|
|
||||||
create_memo(move |_| sort_participants(participants.read.get(), name.get()));
|
|
||||||
|
|
||||||
let _ = use_event_listener(use_document(), keydown, move |evt| {
|
|
||||||
if evt.key() == "ArrowDown".to_string() {
|
|
||||||
set_selected.update(|x| {
|
|
||||||
let len = participants.read.get_untracked().len();
|
|
||||||
if *x != len {
|
|
||||||
*x += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if evt.key() == "ArrowUp".to_string() {
|
|
||||||
set_selected.update(|x| {
|
|
||||||
if *x != 0 {
|
|
||||||
*x -= 1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let el: web_sys::Element = container_ref
|
|
||||||
.get_untracked()
|
|
||||||
.unwrap()
|
|
||||||
.children()
|
|
||||||
.item(selected.get_untracked().try_into().unwrap())
|
|
||||||
.expect("No element found");
|
|
||||||
|
|
||||||
el.scroll_into_view_with_scroll_into_view_options(
|
|
||||||
&ScrollIntoViewOptions::new().block(ScrollLogicalPosition::Center),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let on_submit = move |ev: leptos::ev::SubmitEvent| {
|
|
||||||
ev.prevent_default();
|
|
||||||
set_error.set(String::from(""));
|
|
||||||
|
|
||||||
let person = &participants_sorted.get()[selected.get()];
|
|
||||||
|
|
||||||
match websocket.add_time(
|
|
||||||
surrealdb::CreateTimeParam::new(
|
|
||||||
person.clone().id,
|
|
||||||
minutes.get(),
|
|
||||||
seconds.get(),
|
|
||||||
miliseconds.get(),
|
|
||||||
),
|
|
||||||
event.get(),
|
|
||||||
) {
|
|
||||||
Ok(_) => {
|
|
||||||
set_name.set(String::from(""));
|
|
||||||
set_minutes.set(0);
|
|
||||||
set_seconds.set(0);
|
|
||||||
set_miliseconds.set(0);
|
|
||||||
|
|
||||||
let _ = name_ref.get().unwrap().focus();
|
|
||||||
}
|
|
||||||
Err(err) => set_error.set(err),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<h1>"Tijd toevoegen"</h1>
|
|
||||||
<form class="add" on:submit=on_submit>
|
|
||||||
<div class="time-input-container">
|
|
||||||
<span>"Onderdeel:"</span>
|
|
||||||
<select
|
|
||||||
on:change=move |ev| {
|
|
||||||
set_event.set(event_target_value(&ev));
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectOption value=event is="lifesaver"/>
|
|
||||||
<SelectOption value=event is="popduiken"/>
|
|
||||||
<SelectOption value=event is="hindernis"/>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<input type="text"
|
|
||||||
placeholder="Name"
|
|
||||||
tabindex=0
|
|
||||||
node_ref=name_ref
|
|
||||||
on:input=move |ev| {
|
|
||||||
set_name.set(event_target_value(&ev));
|
|
||||||
set_selected.set(0);
|
|
||||||
}
|
|
||||||
prop:value=name
|
|
||||||
/>
|
|
||||||
<ul class="participants-container" tabindex=-1 node_ref=container_ref>
|
|
||||||
{move || participants_sorted.get().into_iter().enumerate().map(|(i, participant)| view! {
|
|
||||||
<li on:click=move |_| set_selected.set(i) class:selected=move || selected.get() == i>{participant.name + " " + "(" + &participant.group[6..].to_string() + ")" }</li>
|
|
||||||
}).collect_view()}
|
|
||||||
</ul>
|
|
||||||
<div class="time-input-container">
|
|
||||||
<span>"Tijd:"</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
tabindex=0
|
|
||||||
placeholder="mm"
|
|
||||||
min=0
|
|
||||||
max=99
|
|
||||||
on:input=move |ev| {
|
|
||||||
let x = event_target_value(&ev).parse::<u64>();
|
|
||||||
match x {
|
|
||||||
Ok(x) => set_minutes.set(x),
|
|
||||||
Err(_) => set_minutes.set(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prop:value=minutes
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
tabindex=0
|
|
||||||
placeholder="ss"
|
|
||||||
min=0
|
|
||||||
max=59
|
|
||||||
on:input=move |ev| {
|
|
||||||
let x = event_target_value(&ev).parse::<u64>();
|
|
||||||
match x {
|
|
||||||
Ok(x) => set_seconds.set(x),
|
|
||||||
Err(_) => set_seconds.set(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prop:value=seconds
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
tabindex=0
|
|
||||||
placeholder="ms"
|
|
||||||
min=0
|
|
||||||
max=99
|
|
||||||
on:input=move |ev| {
|
|
||||||
let x = event_target_value(&ev).parse::<u64>();
|
|
||||||
match x {
|
|
||||||
Ok(x) => set_miliseconds.set(x),
|
|
||||||
Err(_) => set_miliseconds.set(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prop:value=miliseconds
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input type="submit" tabindex=0 value="Submit" />
|
|
||||||
</form>
|
|
||||||
<p class="error">
|
|
||||||
{ error }
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,2 +1,2 @@
|
|||||||
pub mod surrealdb;
|
pub mod surrealdb;
|
||||||
pub mod toast;
|
pub mod websocket;
|
@ -1,387 +1,69 @@
|
|||||||
use crate::util;
|
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_use::{
|
|
||||||
core::ConnectionReadyState,
|
|
||||||
storage::use_local_storage,
|
|
||||||
use_websocket,
|
|
||||||
utils::{FromToStringCodec, StringCodec},
|
|
||||||
UseWebsocketReturn,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::json;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
pub mod client;
|
||||||
#[serde(untagged)]
|
pub mod schemas;
|
||||||
enum SurrealId {
|
|
||||||
String(String),
|
|
||||||
Integer(u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
cfg_if::cfg_if! {
|
||||||
struct SurrealRequest {
|
if #[cfg(feature = "ssr")] {
|
||||||
id: SurrealId,
|
use once_cell::sync::Lazy;
|
||||||
method: String,
|
use surrealdb::engine::remote::ws::{Client, Ws};
|
||||||
params: Vec<SurrealParams>,
|
use surrealdb::opt::auth::Root;
|
||||||
}
|
use surrealdb::Surreal;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
pub static DB: Lazy<Surreal<Client>> = Lazy::new(Surreal::init);
|
||||||
#[serde(untagged)]
|
|
||||||
enum SurrealParams {
|
|
||||||
Participant,
|
|
||||||
SigninParam(SigninParam),
|
|
||||||
CreatePersonParam(CreatePersonParam),
|
|
||||||
CreateTimeParam(CreateTimeParam),
|
|
||||||
String(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SigninParam {
|
|
||||||
user: String,
|
|
||||||
pass: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct CreatePersonParam {
|
|
||||||
name: String,
|
|
||||||
group: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct CreateTimeParam {
|
|
||||||
person_id: String,
|
|
||||||
time: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CreateTimeParam {
|
|
||||||
pub fn new(person_id: String, minutes: u64, seconds: u64, miliseconds: u64) -> Self {
|
|
||||||
Self {
|
|
||||||
person_id,
|
|
||||||
time: minutes * 60 * 1000 + seconds * 1000 + miliseconds * 10,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[cfg(feature = "ssr")]
|
||||||
pub struct Participant {
|
pub async fn connect() -> Result<(), ServerFnError> {
|
||||||
pub id: String,
|
DB.connect::<Ws>("localhost:80").await?;
|
||||||
pub name: String,
|
|
||||||
pub group: String,
|
DB.signin(Root {
|
||||||
|
username: "root",
|
||||||
|
password: "root",
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
DB.use_ns("wrb").use_db("timings").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[server]
|
||||||
pub struct ParticipantsContext {
|
pub async fn get_participants() -> Result<Vec<schemas::Participant>, ServerFnError> {
|
||||||
pub read: ReadSignal<Vec<Participant>>,
|
let participant_records: Vec<schemas::ParticipantRecord> = DB.select("participant").await?;
|
||||||
write: WriteSignal<Vec<Participant>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
let mut participants: Vec<schemas::Participant> = vec![];
|
||||||
pub struct SurrealContext {
|
|
||||||
pub message: Signal<Option<String>>,
|
|
||||||
send: Rc<dyn Fn(&str)>,
|
|
||||||
pub ready_state: Signal<ConnectionReadyState>,
|
|
||||||
pub authenticated: ReadSignal<bool>,
|
|
||||||
pub set_authenticated: WriteSignal<bool>,
|
|
||||||
pub loading: ReadSignal<bool>,
|
|
||||||
pub set_loading: WriteSignal<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
participant_records.iter().for_each(|participant| {
|
||||||
struct SurrealResponse {
|
participants.push(schemas::Participant {
|
||||||
id: Option<u32>,
|
id: participant.id.id.to_string(),
|
||||||
result: SurrealResult,
|
name: participant.name.clone(),
|
||||||
}
|
group: participant.group.clone(),
|
||||||
|
events: participant.events.clone(),
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
})
|
||||||
struct SurrealResultError {
|
|
||||||
code: i32,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
struct SurrealResponseError {
|
|
||||||
id: u32,
|
|
||||||
error: SurrealResultError,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
struct SurrealAction {
|
|
||||||
id: String,
|
|
||||||
action: String,
|
|
||||||
result: Participant,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
enum SurrealResult {
|
|
||||||
String(String),
|
|
||||||
Participants(Vec<Participant>),
|
|
||||||
Action(SurrealAction),
|
|
||||||
Null,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SurrealContext {
|
|
||||||
pub fn new(
|
|
||||||
message: Signal<Option<String>>,
|
|
||||||
send: Rc<dyn Fn(&str)>,
|
|
||||||
ready_state: Signal<ConnectionReadyState>,
|
|
||||||
authenticated: ReadSignal<bool>,
|
|
||||||
set_authenticated: WriteSignal<bool>,
|
|
||||||
loading: ReadSignal<bool>,
|
|
||||||
set_loading: WriteSignal<bool>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
message,
|
|
||||||
send,
|
|
||||||
ready_state,
|
|
||||||
authenticated,
|
|
||||||
set_authenticated,
|
|
||||||
loading,
|
|
||||||
set_loading,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a string to surrealDB
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn send(&self, message: &str) {
|
|
||||||
(self.send)(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// sigin SurrealDB
|
|
||||||
pub fn signin(&self, pass: String) {
|
|
||||||
log::debug!("Signing into surrealdb");
|
|
||||||
|
|
||||||
let request = SurrealRequest {
|
|
||||||
id: SurrealId::Integer(0),
|
|
||||||
method: String::from("signin"),
|
|
||||||
params: vec![SurrealParams::SigninParam(SigninParam {
|
|
||||||
user: String::from("root"),
|
|
||||||
pass,
|
|
||||||
})],
|
|
||||||
};
|
|
||||||
|
|
||||||
self.send(&json!(request).to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_person(&self, name: String, group: String) -> Result<(), String> {
|
|
||||||
if name.is_empty() {
|
|
||||||
return Err(String::from("Name cannot be empty"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = SurrealRequest {
|
|
||||||
id: SurrealId::Integer(10),
|
|
||||||
method: String::from("create"),
|
|
||||||
params: vec![
|
|
||||||
SurrealParams::String(String::from("person")),
|
|
||||||
SurrealParams::CreatePersonParam(CreatePersonParam {
|
|
||||||
name,
|
|
||||||
group: "group:".to_owned() + &group,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(self.send(&json!(request).to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_time(&self, body: CreateTimeParam, event: String) -> Result<(), String> {
|
|
||||||
let request = SurrealRequest {
|
|
||||||
id: SurrealId::Integer(20),
|
|
||||||
method: String::from("create"),
|
|
||||||
params: vec![
|
|
||||||
SurrealParams::String(event),
|
|
||||||
SurrealParams::CreateTimeParam(body),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(self.send(&json!(request).to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_surrealdb() {
|
|
||||||
// Initialize a connection to surrealDB
|
|
||||||
let UseWebsocketReturn {
|
|
||||||
ready_state,
|
|
||||||
message,
|
|
||||||
send,
|
|
||||||
..
|
|
||||||
} = use_websocket("ws://localhost:80/rpc");
|
|
||||||
|
|
||||||
// Create reactive signals for the websocket connection to surrealDB
|
|
||||||
let (participants, set_participants) = create_signal::<Vec<Participant>>(vec![]);
|
|
||||||
let (authenticated, set_authenticated) = create_signal::<bool>(false);
|
|
||||||
let (loading, set_loading) = create_signal::<bool>(true);
|
|
||||||
|
|
||||||
// Bind the websocket connection to a context
|
|
||||||
let websocket = SurrealContext::new(
|
|
||||||
message,
|
|
||||||
Rc::new(send.clone()),
|
|
||||||
ready_state,
|
|
||||||
authenticated,
|
|
||||||
set_authenticated,
|
|
||||||
loading,
|
|
||||||
set_loading,
|
|
||||||
);
|
|
||||||
|
|
||||||
provide_context(websocket.clone());
|
|
||||||
provide_context(ParticipantsContext {
|
|
||||||
read: participants,
|
|
||||||
write: set_participants,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
create_effect(move |prev_value| {
|
Ok(participants)
|
||||||
let status = ready_state.get();
|
}
|
||||||
|
|
||||||
if prev_value != Some(status.clone()) && status == ConnectionReadyState::Open {
|
pub fn init_participants() {
|
||||||
let (token, _, _) = use_local_storage::<String, FromToStringCodec>("surrealdb-token");
|
let participants = create_local_resource(|| (), |_| async move { get_participants().await });
|
||||||
|
|
||||||
if token.get().is_empty() {
|
create_effect(move |_| {
|
||||||
set_loading.set(false);
|
participants.and_then(|data: &Vec<schemas::Participant>| {
|
||||||
return status;
|
let participants_context = use_context::<schemas::ParticipantsContext>().unwrap();
|
||||||
|
|
||||||
|
let mut participants_new: Vec<schemas::ParticipantSignal> = vec![];
|
||||||
|
|
||||||
|
for participant in data {
|
||||||
|
participants_new.push(schemas::ParticipantSignal {
|
||||||
|
id: participant.id.clone(),
|
||||||
|
value: create_rw_signal(participant.clone()),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = SurrealRequest {
|
participants_context.set(participants_new);
|
||||||
id: SurrealId::Integer(0),
|
});
|
||||||
method: String::from("authenticate"),
|
|
||||||
params: vec![SurrealParams::String(token.get())],
|
|
||||||
};
|
|
||||||
|
|
||||||
websocket.send(&json!(request).to_string());
|
|
||||||
};
|
|
||||||
|
|
||||||
status
|
|
||||||
});
|
|
||||||
|
|
||||||
// Watch for a message recieved from the surrealDB connection
|
|
||||||
create_effect(move |prev_value| {
|
|
||||||
let msg = message.get();
|
|
||||||
|
|
||||||
if prev_value != Some(msg.clone()) {
|
|
||||||
match msg {
|
|
||||||
Some(ref text) => {
|
|
||||||
log::debug!("{:?}", text);
|
|
||||||
surrealdb_response(text.to_string());
|
|
||||||
}
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Function to execute when SurrealDB returns a message
|
|
||||||
fn surrealdb_response(response: String) {
|
|
||||||
let response: SurrealResponse = match serde_json::from_str(&response) {
|
|
||||||
Ok(res) => res,
|
|
||||||
Err(_) => {
|
|
||||||
handle_error(response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match response.id {
|
|
||||||
Some(0) => use_surrealdb(response.clone()),
|
|
||||||
Some(1) => get_participants(),
|
|
||||||
Some(2) => log::debug!("Subscribed to live timings"),
|
|
||||||
Some(5) => got_participants(response.result.clone()),
|
|
||||||
Some(_) => (),
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
match response.result {
|
|
||||||
SurrealResult::Action(action) => handle_action(action),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Function to execute when an error is returned from Surrealdb
|
|
||||||
fn handle_error(response: String) {
|
|
||||||
let response: SurrealResponseError = match serde_json::from_str(&response) {
|
|
||||||
Ok(res) => res,
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("{}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if response.id == 0 && response.error.code == -32000 {
|
|
||||||
let (_, _, remove_token) =
|
|
||||||
use_local_storage::<String, FromToStringCodec>("surrealdb-token");
|
|
||||||
let websocket = expect_context::<SurrealContext>();
|
|
||||||
|
|
||||||
remove_token();
|
|
||||||
websocket.set_loading.set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Function to execute when DB signin is succesful
|
|
||||||
fn use_surrealdb(response: SurrealResponse) {
|
|
||||||
util::toast::add_toast(
|
|
||||||
"Succesfully signed into DB".to_string(),
|
|
||||||
"success".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let (_, set_token, _) = use_local_storage::<String, FromToStringCodec>("surrealdb-token");
|
|
||||||
|
|
||||||
match response.result {
|
|
||||||
SurrealResult::String(str) => set_token.set(str),
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
|
|
||||||
let websocket = expect_context::<SurrealContext>();
|
|
||||||
|
|
||||||
websocket.set_authenticated.set(true);
|
|
||||||
websocket.set_loading.set(false);
|
|
||||||
|
|
||||||
let request = SurrealRequest {
|
|
||||||
id: SurrealId::Integer(1),
|
|
||||||
method: String::from("use"),
|
|
||||||
params: vec![
|
|
||||||
SurrealParams::String(String::from("wrb")),
|
|
||||||
SurrealParams::String(String::from("timings")),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
websocket.send(&json!(request).to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Function to get all participants and subscribe to changes
|
|
||||||
fn get_participants() {
|
|
||||||
let websocket = expect_context::<SurrealContext>();
|
|
||||||
|
|
||||||
let request = SurrealRequest {
|
|
||||||
id: SurrealId::Integer(5),
|
|
||||||
method: String::from("select"),
|
|
||||||
params: vec![SurrealParams::String(String::from("person"))],
|
|
||||||
};
|
|
||||||
|
|
||||||
websocket.send(&json!(request).to_string());
|
|
||||||
|
|
||||||
let request = SurrealRequest {
|
|
||||||
id: SurrealId::Integer(2),
|
|
||||||
method: String::from("live"),
|
|
||||||
params: vec![SurrealParams::String(String::from("person"))],
|
|
||||||
};
|
|
||||||
|
|
||||||
websocket.send(&json!(request).to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Function that will execute when participants are recieved
|
|
||||||
fn got_participants(result: SurrealResult) {
|
|
||||||
let participants_context = expect_context::<ParticipantsContext>();
|
|
||||||
|
|
||||||
if let SurrealResult::Participants(mut value) = result {
|
|
||||||
let mut participants = participants_context.read.get();
|
|
||||||
participants.append(&mut value);
|
|
||||||
participants_context.write.set(participants);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Function to call when an action is recieved from surrealDB
|
|
||||||
fn handle_action(action: SurrealAction) {
|
|
||||||
let participants_context = expect_context::<ParticipantsContext>();
|
|
||||||
|
|
||||||
let mut participants = participants_context.read.get();
|
|
||||||
participants.push(action.result);
|
|
||||||
participants_context.write.set(participants);
|
|
||||||
}
|
|
||||||
|
71
application/src/util/surrealdb/client.rs
Normal file
71
application/src/util/surrealdb/client.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use crate::util::surrealdb::schemas;
|
||||||
|
use leptos::*;
|
||||||
|
use strsim::normalized_damerau_levenshtein;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Time {
|
||||||
|
pub minutes: u32,
|
||||||
|
pub seconds: u32,
|
||||||
|
pub milliseconds: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Time {
|
||||||
|
pub fn as_milliseconds(&self) -> u32 {
|
||||||
|
self.minutes * 60 * 1000 + self.seconds * 1000 + self.milliseconds * 10
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_milliseconds_to_string(mut time: u32) -> String {
|
||||||
|
let milliseconds = (time % 1000) / 10;
|
||||||
|
time /= 1000;
|
||||||
|
let seconds = time % 60;
|
||||||
|
time /= 60;
|
||||||
|
let minutes = time;
|
||||||
|
|
||||||
|
format!("{minutes:02}:{seconds:02}:{milliseconds:02}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_milliseconds(mut time: u32) -> Self {
|
||||||
|
let milliseconds = (time % 1000) / 10;
|
||||||
|
time /= 1000;
|
||||||
|
let seconds = time % 60;
|
||||||
|
time /= 60;
|
||||||
|
let minutes = time;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
milliseconds,
|
||||||
|
seconds,
|
||||||
|
minutes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sort_participants(
|
||||||
|
participants: Vec<schemas::ParticipantSignal>,
|
||||||
|
search: String,
|
||||||
|
) -> Vec<schemas::ParticipantSignal> {
|
||||||
|
let mut filtered_sorted_list: Vec<(schemas::ParticipantSignal, f64)> = participants
|
||||||
|
.into_iter()
|
||||||
|
.map(|participant| {
|
||||||
|
(
|
||||||
|
participant.clone(),
|
||||||
|
normalized_damerau_levenshtein(
|
||||||
|
&participant.value.get().name.to_lowercase(),
|
||||||
|
&search.to_lowercase(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
filtered_sorted_list.sort_by(|a, b| {
|
||||||
|
let (_, sim_score_a) = a;
|
||||||
|
let (_, sim_score_b) = b;
|
||||||
|
sim_score_b
|
||||||
|
.partial_cmp(sim_score_a)
|
||||||
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
});
|
||||||
|
|
||||||
|
filtered_sorted_list
|
||||||
|
.into_iter()
|
||||||
|
.map(|(item, _)| item)
|
||||||
|
.collect()
|
||||||
|
}
|
63
application/src/util/surrealdb/schemas.rs
Normal file
63
application/src/util/surrealdb/schemas.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature = "ssr")] {
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use leptos::RwSignal;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Hash, Eq)]
|
||||||
|
pub struct Events {
|
||||||
|
pub lifesaver: Option<u32>,
|
||||||
|
pub hindernis: Option<u32>,
|
||||||
|
pub popduiken: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct ParticipantRecord {
|
||||||
|
pub id: Thing,
|
||||||
|
pub name: String,
|
||||||
|
pub group: String,
|
||||||
|
pub events: Option<Events>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
|
pub struct Participant {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub group: String,
|
||||||
|
pub events: Option<Events>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Participant {
|
||||||
|
pub fn as_update(&self) -> ParticipantUpdate {
|
||||||
|
ParticipantUpdate {
|
||||||
|
name: self.name.clone(),
|
||||||
|
group: self.group.clone(),
|
||||||
|
events: self.events.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
|
pub struct ParticipantUpdate {
|
||||||
|
pub name: String,
|
||||||
|
pub group: String,
|
||||||
|
pub events: Option<Events>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
|
pub struct ParticipantSignal {
|
||||||
|
pub id: String,
|
||||||
|
pub value: RwSignal<Participant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ParticipantsContext = RwSignal<Vec<ParticipantSignal>>;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct NewParticipant {
|
||||||
|
pub name: String,
|
||||||
|
pub group: String,
|
||||||
|
}
|
@ -1,51 +0,0 @@
|
|||||||
use gloo_timers::future::TimeoutFuture;
|
|
||||||
use leptos::*;
|
|
||||||
use rand::distributions::{Alphanumeric, DistString};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct NotificationsContext {
|
|
||||||
pub notifications: ReadSignal<Vec<ToastNotification>>,
|
|
||||||
pub set_notifications: WriteSignal<Vec<ToastNotification>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ToastNotification {
|
|
||||||
pub text: String,
|
|
||||||
pub option: String, // error, warning, info, success
|
|
||||||
pub id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_toast() {
|
|
||||||
let (notifications, set_notifications) = create_signal::<Vec<ToastNotification>>(vec![]);
|
|
||||||
provide_context(NotificationsContext {
|
|
||||||
notifications,
|
|
||||||
set_notifications,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_toast(text: String, option: String) {
|
|
||||||
let context = expect_context::<NotificationsContext>();
|
|
||||||
|
|
||||||
let id = Alphanumeric.sample_string(&mut rand::thread_rng(), 4);
|
|
||||||
|
|
||||||
let mut vec = context.notifications.get();
|
|
||||||
vec.push(ToastNotification {
|
|
||||||
text,
|
|
||||||
option,
|
|
||||||
id: id.clone(),
|
|
||||||
});
|
|
||||||
context.set_notifications.set(vec);
|
|
||||||
|
|
||||||
spawn_local(async {
|
|
||||||
TimeoutFuture::new(5000).await;
|
|
||||||
remove_toast(id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_toast(id: String) {
|
|
||||||
let context = expect_context::<NotificationsContext>();
|
|
||||||
|
|
||||||
let mut vec = context.notifications.get_untracked();
|
|
||||||
vec.retain(|x| x.id != id);
|
|
||||||
context.set_notifications.set(vec);
|
|
||||||
}
|
|
36
application/src/util/websocket.rs
Normal file
36
application/src/util/websocket.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub mod server;
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::util::surrealdb::schemas::Participant;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub enum ParticipantsAction {
|
||||||
|
Add { participant: Participant },
|
||||||
|
Replace { participant: Participant },
|
||||||
|
Remove { id: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub fn apply_participants_patch(mut participants: Vec<Participant>, action: ParticipantsAction) {
|
||||||
|
match action {
|
||||||
|
ParticipantsAction::Add { participant } => {
|
||||||
|
participants.push(participant);
|
||||||
|
}
|
||||||
|
ParticipantsAction::Remove { id } => {
|
||||||
|
participants.retain(|participant| participant.id != id);
|
||||||
|
}
|
||||||
|
ParticipantsAction::Replace { participant } => {
|
||||||
|
if let Some(index) = participants
|
||||||
|
.iter()
|
||||||
|
.position(|item| item.id == participant.id)
|
||||||
|
{
|
||||||
|
let _ = std::mem::replace(&mut participants[index], participant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
55
application/src/util/websocket/client.rs
Normal file
55
application/src/util/websocket/client.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
use crate::util::surrealdb::schemas;
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_use::*;
|
||||||
|
|
||||||
|
use super::ParticipantsAction;
|
||||||
|
|
||||||
|
pub fn init_websocket() {
|
||||||
|
let UseWebsocketReturn {
|
||||||
|
ready_state,
|
||||||
|
message,
|
||||||
|
..
|
||||||
|
} = use_websocket("ws://192.168.0.150:3000/ws");
|
||||||
|
|
||||||
|
provide_context(ready_state);
|
||||||
|
|
||||||
|
let participants_context = use_context::<schemas::ParticipantsContext>().unwrap();
|
||||||
|
let owner = Owner::current().unwrap();
|
||||||
|
|
||||||
|
create_effect(move |_| {
|
||||||
|
if let Some(m) = message.get() {
|
||||||
|
with_owner(owner, || handle_message(&participants_context, &m));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_message(&participants_context: &schemas::ParticipantsContext, message: &str) {
|
||||||
|
let action: ParticipantsAction = match serde_json::from_str(message) {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(_err) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logging::log!("Recieved action: {:?}", action);
|
||||||
|
|
||||||
|
match action {
|
||||||
|
ParticipantsAction::Add { participant } => participants_context.update(|participants| {
|
||||||
|
participants.push(schemas::ParticipantSignal {
|
||||||
|
id: participant.id.clone(),
|
||||||
|
value: create_rw_signal(participant),
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ParticipantsAction::Remove { id } => participants_context
|
||||||
|
.update(|participants| participants.retain(|participant| participant.id != id)),
|
||||||
|
ParticipantsAction::Replace { participant } => {
|
||||||
|
let participants = participants_context.get();
|
||||||
|
for participant_signal in participants {
|
||||||
|
if participant_signal.id == participant.id {
|
||||||
|
participant_signal.value.set(participant);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
92
application/src/util/websocket/server.rs
Normal file
92
application/src/util/websocket/server.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use crate::util::websocket;
|
||||||
|
use axum::{
|
||||||
|
extract::ws::{Message, WebSocket, WebSocketUpgrade},
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use futures::{sink::SinkExt, stream::StreamExt};
|
||||||
|
use leptos::LeptosOptions;
|
||||||
|
use leptos_router::RouteListing;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use tokio::sync::{broadcast, Mutex};
|
||||||
|
|
||||||
|
use std::{collections::HashSet, sync::Arc};
|
||||||
|
|
||||||
|
pub static WEBSOCKET_STATE: Lazy<WebSocketState> = Lazy::new(|| {
|
||||||
|
let client_set = Arc::new(Mutex::new(HashSet::<uuid::Uuid>::new()));
|
||||||
|
let (tx, _rx) = broadcast::channel(100);
|
||||||
|
|
||||||
|
WebSocketState { client_set, tx }
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WebSocketState {
|
||||||
|
pub client_set: Arc<Mutex<HashSet<uuid::Uuid>>>,
|
||||||
|
pub tx: broadcast::Sender<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebSocketState {
|
||||||
|
pub fn apply(&self, action: websocket::ParticipantsAction) -> Result<(), serde_json::Error> {
|
||||||
|
let message = serde_json::to_string(&action)?;
|
||||||
|
let _ = self.tx.send(message);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, axum::extract::FromRef)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub leptos_options: LeptosOptions,
|
||||||
|
pub websocket_state: Arc<WebSocketState>,
|
||||||
|
pub routes: Vec<RouteListing>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn websocket_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
|
||||||
|
ws.on_upgrade(|socket| websocket(socket))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn websocket(stream: WebSocket) {
|
||||||
|
let state = &WEBSOCKET_STATE;
|
||||||
|
|
||||||
|
let (mut sender, mut receiver) = stream.split();
|
||||||
|
|
||||||
|
let mut client_set = state.client_set.lock().await;
|
||||||
|
|
||||||
|
let uuid = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
client_set.insert(uuid);
|
||||||
|
drop(client_set);
|
||||||
|
|
||||||
|
let mut rx = state.tx.subscribe();
|
||||||
|
|
||||||
|
let msg = format!("{uuid} joined");
|
||||||
|
println!("{uuid} joined");
|
||||||
|
let _ = state.tx.send(msg);
|
||||||
|
|
||||||
|
let mut send_task = tokio::spawn(async move {
|
||||||
|
while let Ok(msg) = rx.recv().await {
|
||||||
|
if sender.send(Message::Text(msg)).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tx = state.tx.clone();
|
||||||
|
let uuid_clone = uuid.clone();
|
||||||
|
|
||||||
|
let mut recv_task = tokio::spawn(async move {
|
||||||
|
while let Some(Ok(Message::Text(text))) = receiver.next().await {
|
||||||
|
let _ = tx.send(format!("{uuid_clone}: {text}"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = (&mut send_task) => recv_task.abort(),
|
||||||
|
_ = (&mut recv_task) => send_task.abort(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let msg = format!("{uuid} left");
|
||||||
|
println!("{uuid} left");
|
||||||
|
let _ = state.tx.send(msg);
|
||||||
|
|
||||||
|
state.client_set.lock().await.remove(&uuid);
|
||||||
|
}
|
72
application/style/forms.scss
Normal file
72
application/style/forms.scss
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
margin: 40px auto 0 auto;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form label {
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete ul {
|
||||||
|
background-color: $secondary-bg-color-lighter;
|
||||||
|
list-style: none;
|
||||||
|
text-align: left;
|
||||||
|
padding: 5px;
|
||||||
|
margin-top: -15px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete ul .selected {
|
||||||
|
background-color: $secondary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete ul li {
|
||||||
|
padding: 3px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,select {
|
||||||
|
background-color: $secondary-bg-color-light;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: $text-color;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit]:hover {
|
||||||
|
background-color: $secondary-bg-color-lighter;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .time {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .time input {
|
||||||
|
display: flex;
|
||||||
|
width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .time input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
37
application/style/groups.scss
Normal file
37
application/style/groups.scss
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
.groups-select-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groups-select-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groups-select-row div {
|
||||||
|
background-color: $secondary-bg-color-light;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 50px;
|
||||||
|
padding: 2px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groups-select-row div:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: $secondary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groups-select-row .selected {
|
||||||
|
background-color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-container div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
27
application/style/header.scss
Normal file
27
application/style/header.scss
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
header {
|
||||||
|
width: 100%;
|
||||||
|
background-color: $secondary-bg-color;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container a {
|
||||||
|
margin-right: auto;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
color: $text-color;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container a:hover {
|
||||||
|
margin: 0;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
57
application/style/index.scss
Normal file
57
application/style/index.scss
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
color: $text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-container a {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 0;
|
||||||
|
background-color: $secondary-bg-color;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 15px;
|
||||||
|
border: dashed $secondary-bg-color-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-container a:hover {
|
||||||
|
background-color: $secondary-bg-color-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-search {
|
||||||
|
margin-top: 40px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-table {
|
||||||
|
margin-top: 0px;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-table th {
|
||||||
|
position: sticky;
|
||||||
|
background-color: $secondary-bg-color-lighter;
|
||||||
|
z-index: 100;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-table th,td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-table tr:hover {
|
||||||
|
background-color: $secondary-color !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-table tr:nth-child(odd) {
|
||||||
|
background-color: $secondary-bg-color-light;
|
||||||
|
}
|
41
application/style/main.scss
Normal file
41
application/style/main.scss
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
$primary-color: #eb6330;
|
||||||
|
$primary-color-light: hsl(16.36, 82.38%, 55.49%, 0.8);
|
||||||
|
$secondary-color: #465651;
|
||||||
|
$accent-color: #89969f;
|
||||||
|
$primary-bg-color: #0d0b0b;
|
||||||
|
$secondary-bg-color: #151719;
|
||||||
|
$secondary-bg-color-light: hsl(204, 11%, 12%, 1);
|
||||||
|
$secondary-bg-color-lighter: hsl(204, 11%, 15%, 1);
|
||||||
|
$accent-bg-color: #181a19;
|
||||||
|
$text-color: #f3efef;
|
||||||
|
|
||||||
|
@import "forms";
|
||||||
|
@import "header";
|
||||||
|
@import "index";
|
||||||
|
@import "groups";
|
||||||
|
@import "modal";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
max-width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: $primary-bg-color;
|
||||||
|
color: $text-color;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
28
application/style/modal.scss
Normal file
28
application/style/modal.scss
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.modal-background {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
z-index: 200;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 800px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: $secondary-bg-color;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
18
flake.lock
18
flake.lock
@ -38,11 +38,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1710951922,
|
"lastModified": 1713344939,
|
||||||
"narHash": "sha256-FOOBJ3DQenLpTNdxMHR2CpGZmYuctb92gF0lpiirZ30=",
|
"narHash": "sha256-jpHkAt0sG2/J7ueKnG7VvLLkBYUMQbXQ2L8OBpVG53s=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "f091af045dff8347d66d186a62d42aceff159456",
|
"rev": "e402c3eb6d88384ca6c52ef1c53e61bdc9b84ddd",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -54,11 +54,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-unstable": {
|
"nixpkgs-unstable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1711001935,
|
"lastModified": 1713297878,
|
||||||
"narHash": "sha256-URtGpHue7HHZK0mrHnSf8wJ6OmMKYSsoLmJybrOLFSQ=",
|
"narHash": "sha256-hOkzkhLT59wR8VaMbh1ESjtZLbGi+XNaBN6h49SPqEc=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "20f77aa09916374aa3141cbc605c955626762c9a",
|
"rev": "66adc1e47f8784803f2deb6cacd5e07264ec2d5c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -98,11 +98,11 @@
|
|||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1711073443,
|
"lastModified": 1713492869,
|
||||||
"narHash": "sha256-PpNb4xq7U5Q/DdX40qe7CijUsqhVVM3VZrhN0+c6Lcw=",
|
"narHash": "sha256-Zv+ZQq3X+EH6oogkXaJ8dGN8t1v26kPZgC5bki04GnM=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "eec55ba9fcde6be4c63942827247e42afef7fafe",
|
"rev": "1e9264d1214d3db00c795b41f75d55b5e153758e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
21
flake.nix
21
flake.nix
@ -25,23 +25,26 @@
|
|||||||
{
|
{
|
||||||
devShells.default = mkShell {
|
devShells.default = mkShell {
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
trunk
|
openssh
|
||||||
|
pkg-config
|
||||||
|
cargo-insta
|
||||||
|
llvmPackages_latest.llvm
|
||||||
|
llvmPackages_latest.bintools
|
||||||
|
# llvmPackages_17.clangNoLibc
|
||||||
|
zlib.out
|
||||||
dart-sass
|
dart-sass
|
||||||
unstable.rust-analyzer
|
llvmPackages_17.lld
|
||||||
llvmPackages.clangNoLibc
|
(rust-bin.selectLatestNightlyWith ( toolchain: toolchain.default.override {
|
||||||
llvmPackages.lld
|
|
||||||
dap
|
|
||||||
(rust-bin.stable.latest.default.override {
|
|
||||||
extensions= [ "rust-src" "rust-analyzer" ];
|
extensions= [ "rust-src" "rust-analyzer" ];
|
||||||
targets = [ "wasm32-unknown-unknown" ];
|
targets = [ "wasm32-unknown-unknown" ];
|
||||||
})
|
}))
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
alias grep=ripgrep
|
alias grep=ripgrep
|
||||||
export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin
|
export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin
|
||||||
export CC=clang
|
# export CC=clang
|
||||||
export CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_LINKER=lld
|
# export CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_LINKER=lld
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user