Compare commits
No commits in common. "cargo-leptos" and "main" have entirely different histories.
cargo-lept
...
main
15
application/.gitignore
vendored
15
application/.gitignore
vendored
|
@ -1,13 +1,2 @@
|
||||||
# Generated by Cargo
|
/target
|
||||||
# will have compiled files and executables
|
/dist
|
||||||
/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,119 +2,46 @@
|
||||||
name = "application"
|
name = "application"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
authors = ["xeovalyte <me+gitea@xeovalyte.dev>"]
|
||||||
|
|
||||||
[lib]
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
crate-type = ["cdylib", "rlib"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.7", optional = true, features = [ "ws", "macros" ] }
|
leptos = { version = "0.6", features = ["csr"] }
|
||||||
|
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 = { version = "0.6", features = [] }
|
leptos-use = "0.10.2"
|
||||||
leptos_axum = { version = "0.6", optional = true }
|
serde = "1.0.196"
|
||||||
leptos_meta = { version = "0.6", features = [] }
|
serde_json = "1.0.113"
|
||||||
leptos_router = { version = "0.6", features = [] }
|
rand = "0.8.5"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
gloo-timers = "0.3.0"
|
||||||
tower = { version = "0.4", optional = true }
|
strsim = "0.11.0"
|
||||||
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" ] }
|
|
||||||
|
|
||||||
[features]
|
# utils
|
||||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate", "leptos_toaster/hydrate"]
|
# strum = { version = "0.25", features = ["derive", "strum_macros"] }
|
||||||
ssr = [
|
# strum_macros = "0.25"
|
||||||
"dep:surrealdb",
|
|
||||||
"dep:axum",
|
[dependencies.web-sys]
|
||||||
"dep:tokio",
|
version = "0.3"
|
||||||
"dep:tower",
|
features = [
|
||||||
"dep:tower-http",
|
"Document",
|
||||||
"dep:leptos_axum",
|
"Window",
|
||||||
"leptos/ssr",
|
"Element",
|
||||||
"leptos-use/ssr",
|
"ScrollIntoViewOptions",
|
||||||
"leptos_meta/ssr",
|
"ScrollLogicalPosition",
|
||||||
"leptos_router/ssr",
|
"ScrollBehavior",
|
||||||
"dep:tracing",
|
|
||||||
"leptos_toaster/ssr"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Defines a size-optimized profile for the WASM bundle in release mode
|
[dev-dependencies]
|
||||||
[profile.wasm-release]
|
wasm-bindgen = "0.2"
|
||||||
inherits = "release"
|
wasm-bindgen-test = "0.3"
|
||||||
|
|
||||||
|
|
||||||
|
[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"
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
# 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"]
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
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,88 +3,77 @@
|
||||||
<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 Axum Starter Template
|
# Leptos Client-Side Rendered (CSR) App Starter Template
|
||||||
|
|
||||||
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).
|
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.
|
||||||
|
|
||||||
## Creating your template repo
|
## Creating your repo from the template
|
||||||
|
|
||||||
If you don't have `cargo-leptos` installed you can install it with
|
This template requires you to have `cargo-generate` installed. You can install it with
|
||||||
|
|
||||||
```bash
|
```sh
|
||||||
cargo install cargo-leptos
|
cargo install cargo-generate
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run
|
|
||||||
```bash
|
To set up your project with this template, run
|
||||||
cargo leptos new --git leptos-rs/start-axum
|
|
||||||
|
```sh
|
||||||
|
cargo generate --git https://github.com/leptos-community/start-csr
|
||||||
```
|
```
|
||||||
|
|
||||||
to generate a new project template.
|
to generate your new project, then
|
||||||
|
|
||||||
```bash
|
```sh
|
||||||
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`.
|
|
||||||
|
|
||||||
## Running your project
|
By default, this template uses Rust `nightly` and requires that you've installed the `wasm` compilation target for your toolchain.
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo leptos watch
|
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].
|
||||||
|
|
||||||
|
|
||||||
|
If you don't have Rust nightly, you can install it with
|
||||||
|
```sh
|
||||||
|
rustup toolchain install nightly --allow-downgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installing Additional Tools
|
You can add the `wasm` compilation target to rust using
|
||||||
|
```sh
|
||||||
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.
|
rustup target add wasm32-unknown-unknown
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
## Testing Your Project
|
## Developing your Leptos CSR project
|
||||||
```bash
|
|
||||||
cargo leptos end-to-end
|
To develop your Leptos CSR project, running
|
||||||
|
|
||||||
|
```sh
|
||||||
|
trunk serve --port 3000 --open
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
will open your app in your default browser at `http://localhost:3000`.
|
||||||
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
|
||||||
```
|
```
|
||||||
|
|
||||||
Cargo-leptos uses Playwright as the end-to-end test tool.
|
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.
|
||||||
Tests are located in end2end/tests directory.
|
|
||||||
|
|
||||||
## Executing a Server on a Remote Machine Without the Toolchain
|
For further information about hosting Leptos CSR apps, please refer to [the Leptos Book chapter on deployment available here][deploy-csr].
|
||||||
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`
|
|
||||||
|
|
||||||
Copy these files to your remote server. The directory structure should be:
|
[Leptos]: https://github.com/leptos-rs/leptos
|
||||||
```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.
|
|
||||||
|
|
||||||
## Licensing
|
[Trunk]: https://github.com/trunk-rs/trunk
|
||||||
|
[Trunk-instructions]: https://trunkrs.dev/assets/
|
||||||
|
|
||||||
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.
|
[deploy-csr]: https://book.leptos.dev/deployment/csr.html
|
74
application/end2end/package-lock.json
generated
74
application/end2end/package-lock.json
generated
|
@ -1,74 +0,0 @@
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"name": "end2end",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"devDependencies": {
|
|
||||||
"@playwright/test": "^1.28.0"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
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;
|
|
|
@ -1,9 +0,0 @@
|
||||||
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!");
|
|
||||||
});
|
|
15
application/index.html
Normal file
15
application/index.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!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>
|
196
application/public/styles.scss
Normal file
196
application/public/styles.scss
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
$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,3 +1,2 @@
|
||||||
|
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "nightly"
|
channel = "nightly"
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
#![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>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod header;
|
|
||||||
pub mod participant;
|
|
||||||
pub mod participants;
|
|
|
@ -1,18 +0,0 @@
|
||||||
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>
|
|
||||||
}
|
|
||||||
}
|
|
2
application/src/components/mod.rs
Normal file
2
application/src/components/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod navbar;
|
||||||
|
pub mod toast;
|
18
application/src/components/navbar.rs
Normal file
18
application/src/components/navbar.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,359 +0,0 @@
|
||||||
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>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
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>
|
|
||||||
}
|
|
||||||
}
|
|
18
application/src/components/toast.rs
Normal file
18
application/src/components/toast.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,72 +0,0 @@
|
||||||
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>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
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,15 +1,89 @@
|
||||||
pub mod app;
|
use leptos::*;
|
||||||
pub mod error_template;
|
use leptos_meta::*;
|
||||||
#[cfg(feature = "ssr")]
|
use leptos_router::*;
|
||||||
pub mod fileserv;
|
|
||||||
pub mod pages;
|
|
||||||
pub mod util;
|
|
||||||
pub mod components;
|
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
// Modules
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
mod components;
|
||||||
pub fn hydrate() {
|
mod pages;
|
||||||
use crate::app::*;
|
mod util;
|
||||||
console_error_panic_hook::set_once();
|
|
||||||
leptos::mount_to_body(App);
|
// Top-Level pages
|
||||||
|
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,45 +1,14 @@
|
||||||
#![feature(diagnostic_namespace)]
|
use application::App;
|
||||||
#[cfg(feature = "ssr")]
|
use leptos::*;
|
||||||
#[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};
|
|
||||||
|
|
||||||
application::util::surrealdb::connect()
|
fn main() {
|
||||||
.await
|
// set up logging
|
||||||
.expect("Database connection failed");
|
_ = console_log::init_with_level(log::Level::Debug);
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
mount_to_body(|| {
|
||||||
// For deployment these variables are:
|
view! {
|
||||||
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
<App />
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
pub mod add_participant;
|
|
||||||
pub mod add_time;
|
|
||||||
pub mod groups;
|
|
||||||
pub mod index;
|
|
|
@ -1,144 +0,0 @@
|
||||||
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>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,231 +0,0 @@
|
||||||
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>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,228 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
13
application/src/pages/home.rs
Normal file
13
application/src/pages/home.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use leptos::*;
|
||||||
|
|
||||||
|
/// Default Home Page
|
||||||
|
#[component]
|
||||||
|
pub fn Home() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1>"Welcome to WRB Timings"</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,34 +0,0 @@
|
||||||
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() />
|
|
||||||
}
|
|
||||||
}
|
|
32
application/src/pages/login.rs
Normal file
32
application/src/pages/login.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
5
application/src/pages/mod.rs
Normal file
5
application/src/pages/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod home;
|
||||||
|
pub mod login;
|
||||||
|
pub mod not_found;
|
||||||
|
pub mod participants;
|
||||||
|
pub mod times;
|
7
application/src/pages/not_found.rs
Normal file
7
application/src/pages/not_found.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
use leptos::*;
|
||||||
|
|
||||||
|
/// 404 Not Found Page
|
||||||
|
#[component]
|
||||||
|
pub fn NotFound() -> impl IntoView {
|
||||||
|
view! { <h1>"Uh oh!" <br/> "We couldn't find that page!"</h1> }
|
||||||
|
}
|
12
application/src/pages/participants.rs
Normal file
12
application/src/pages/participants.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
90
application/src/pages/participants/add.rs
Normal file
90
application/src/pages/participants/add.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
12
application/src/pages/times.rs
Normal file
12
application/src/pages/times.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
209
application/src/pages/times/add.rs
Normal file
209
application/src/pages/times/add.rs
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
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 websocket;
|
pub mod toast;
|
|
@ -1,69 +1,387 @@
|
||||||
|
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;
|
||||||
|
|
||||||
pub mod client;
|
#[derive(Serialize)]
|
||||||
pub mod schemas;
|
#[serde(untagged)]
|
||||||
|
enum SurrealId {
|
||||||
|
String(String),
|
||||||
|
Integer(u32),
|
||||||
|
}
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
#[derive(Serialize)]
|
||||||
if #[cfg(feature = "ssr")] {
|
struct SurrealRequest {
|
||||||
use once_cell::sync::Lazy;
|
id: SurrealId,
|
||||||
use surrealdb::engine::remote::ws::{Client, Ws};
|
method: String,
|
||||||
use surrealdb::opt::auth::Root;
|
params: Vec<SurrealParams>,
|
||||||
use surrealdb::Surreal;
|
}
|
||||||
|
|
||||||
pub static DB: Lazy<Surreal<Client>> = Lazy::new(Surreal::init);
|
#[derive(Serialize)]
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub async fn connect() -> Result<(), ServerFnError> {
|
pub struct Participant {
|
||||||
DB.connect::<Ws>("localhost:80").await?;
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
DB.signin(Root {
|
pub group: String,
|
||||||
username: "root",
|
|
||||||
password: "root",
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
DB.use_ns("wrb").use_db("timings").await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[derive(Clone, Debug)]
|
||||||
pub async fn get_participants() -> Result<Vec<schemas::Participant>, ServerFnError> {
|
pub struct ParticipantsContext {
|
||||||
let participant_records: Vec<schemas::ParticipantRecord> = DB.select("participant").await?;
|
pub read: ReadSignal<Vec<Participant>>,
|
||||||
|
write: WriteSignal<Vec<Participant>>,
|
||||||
|
}
|
||||||
|
|
||||||
let mut participants: Vec<schemas::Participant> = vec![];
|
#[derive(Clone)]
|
||||||
|
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>,
|
||||||
|
}
|
||||||
|
|
||||||
participant_records.iter().for_each(|participant| {
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
participants.push(schemas::Participant {
|
struct SurrealResponse {
|
||||||
id: participant.id.id.to_string(),
|
id: Option<u32>,
|
||||||
name: participant.name.clone(),
|
result: SurrealResult,
|
||||||
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,
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(participants)
|
create_effect(move |prev_value| {
|
||||||
}
|
let status = ready_state.get();
|
||||||
|
|
||||||
pub fn init_participants() {
|
if prev_value != Some(status.clone()) && status == ConnectionReadyState::Open {
|
||||||
let participants = create_local_resource(|| (), |_| async move { get_participants().await });
|
let (token, _, _) = use_local_storage::<String, FromToStringCodec>("surrealdb-token");
|
||||||
|
|
||||||
create_effect(move |_| {
|
if token.get().is_empty() {
|
||||||
participants.and_then(|data: &Vec<schemas::Participant>| {
|
set_loading.set(false);
|
||||||
let participants_context = use_context::<schemas::ParticipantsContext>().unwrap();
|
return status;
|
||||||
|
|
||||||
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()),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
participants_context.set(participants_new);
|
let request = SurrealRequest {
|
||||||
});
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
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,
|
|
||||||
}
|
|
51
application/src/util/toast.rs
Normal file
51
application/src/util/toast.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
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);
|
||||||
|
}
|
|
@ -1,36 +0,0 @@
|
||||||
#[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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
|
@ -1,55 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
.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%;
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,41 +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;
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
.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": 1713344939,
|
"lastModified": 1710951922,
|
||||||
"narHash": "sha256-jpHkAt0sG2/J7ueKnG7VvLLkBYUMQbXQ2L8OBpVG53s=",
|
"narHash": "sha256-FOOBJ3DQenLpTNdxMHR2CpGZmYuctb92gF0lpiirZ30=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e402c3eb6d88384ca6c52ef1c53e61bdc9b84ddd",
|
"rev": "f091af045dff8347d66d186a62d42aceff159456",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -54,11 +54,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs-unstable": {
|
"nixpkgs-unstable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1713297878,
|
"lastModified": 1711001935,
|
||||||
"narHash": "sha256-hOkzkhLT59wR8VaMbh1ESjtZLbGi+XNaBN6h49SPqEc=",
|
"narHash": "sha256-URtGpHue7HHZK0mrHnSf8wJ6OmMKYSsoLmJybrOLFSQ=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "66adc1e47f8784803f2deb6cacd5e07264ec2d5c",
|
"rev": "20f77aa09916374aa3141cbc605c955626762c9a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -98,11 +98,11 @@
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1713492869,
|
"lastModified": 1711073443,
|
||||||
"narHash": "sha256-Zv+ZQq3X+EH6oogkXaJ8dGN8t1v26kPZgC5bki04GnM=",
|
"narHash": "sha256-PpNb4xq7U5Q/DdX40qe7CijUsqhVVM3VZrhN0+c6Lcw=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "1e9264d1214d3db00c795b41f75d55b5e153758e",
|
"rev": "eec55ba9fcde6be4c63942827247e42afef7fafe",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
21
flake.nix
21
flake.nix
|
@ -25,26 +25,23 @@
|
||||||
{
|
{
|
||||||
devShells.default = mkShell {
|
devShells.default = mkShell {
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
openssh
|
trunk
|
||||||
pkg-config
|
|
||||||
cargo-insta
|
|
||||||
llvmPackages_latest.llvm
|
|
||||||
llvmPackages_latest.bintools
|
|
||||||
# llvmPackages_17.clangNoLibc
|
|
||||||
zlib.out
|
|
||||||
dart-sass
|
dart-sass
|
||||||
llvmPackages_17.lld
|
unstable.rust-analyzer
|
||||||
(rust-bin.selectLatestNightlyWith ( toolchain: toolchain.default.override {
|
llvmPackages.clangNoLibc
|
||||||
|
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