yes
This commit is contained in:
@@ -2,12 +2,14 @@
|
|||||||
name = "vulkan-cts-analyzer"
|
name = "vulkan-cts-analyzer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Kbz-8 <kbz_8.code@proton.me>"]
|
authors = ["Kbz-8 <kbz_8.code@proton.me>"]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
csv = "1.4.0"
|
csv = "1.4.0"
|
||||||
dioxus = { version = "0.7.2", features = ["router"] }
|
dioxus = { version = "0.7.2", features = ["router"] }
|
||||||
dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false }
|
dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false }
|
||||||
|
reqwest = "0.12.25"
|
||||||
|
strum = { version = "0.27.2", default-features = false, features = ["derive"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["web"]
|
default = ["web"]
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
/* This file contains the global styles for the styled dioxus components. You only
|
|
||||||
* need to import this file once in your project root.
|
|
||||||
*/
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
|
|
||||||
|
|
||||||
body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--secondary-color-4);
|
|
||||||
font-family: Inter, sans-serif;
|
|
||||||
font-optical-sizing: auto;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--dark: initial;
|
|
||||||
--light: ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
--dark: ;
|
|
||||||
--light: initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Primary colors */
|
|
||||||
--primary-color: var(--dark, #000) var(--light, #fff);
|
|
||||||
--primary-color-1: var(--dark, #0e0e0e) var(--light, #fbfbfb);
|
|
||||||
--primary-color-2: var(--dark, #0a0a0a) var(--light, #fff);
|
|
||||||
--primary-color-3: var(--dark, #141313) var(--light, #f8f8f8);
|
|
||||||
--primary-color-4: var(--dark, #1a1a1a) var(--light, #f8f8f8);
|
|
||||||
--primary-color-5: var(--dark, #262626) var(--light, #f5f5f5);
|
|
||||||
--primary-color-6: var(--dark, #232323) var(--light, #e5e5e5);
|
|
||||||
--primary-color-7: var(--dark, #3e3e3e) var(--light, #b0b0b0);
|
|
||||||
|
|
||||||
/* Secondary colors */
|
|
||||||
--secondary-color: var(--dark, #fff) var(--light, #000);
|
|
||||||
--secondary-color-1: var(--dark, #fafafa) var(--light, #000);
|
|
||||||
--secondary-color-2: var(--dark, #e6e6e6) var(--light, #0d0d0d);
|
|
||||||
--secondary-color-3: var(--dark, #dcdcdc) var(--light, #2b2b2b);
|
|
||||||
--secondary-color-4: var(--dark, #d4d4d4) var(--light, #111);
|
|
||||||
--secondary-color-5: var(--dark, #a1a1a1) var(--light, #848484);
|
|
||||||
--secondary-color-6: var(--dark, #5d5d5d) var(--light, #d0d0d0);
|
|
||||||
|
|
||||||
/* Highlight colors */
|
|
||||||
--focused-border-color: var(--dark, #2b7fff) var(--light, #2b7fff);
|
|
||||||
--primary-success-color: var(--dark, #02271c) var(--light, #ecfdf5);
|
|
||||||
--secondary-success-color: var(--dark, #b6fae3) var(--light, #10b981);
|
|
||||||
--primary-warning-color: var(--dark, #342203) var(--light, #fffbeb);
|
|
||||||
--secondary-warning-color: var(--dark, #feeac7) var(--light, #f59e0b);
|
|
||||||
--primary-error-color: var(--dark, #a22e2e) var(--light, #dc2626);
|
|
||||||
--secondary-error-color: var(--dark, #9b1c1c) var(--light, #ef4444);
|
|
||||||
--contrast-error-color: var(--dark, var(--secondary-color-3))
|
|
||||||
var(--light, var(--primary-color));
|
|
||||||
--primary-info-color: var(--dark, var(--primary-color-5))
|
|
||||||
var(--light, var(--primary-color));
|
|
||||||
--secondary-info-color: var(--dark, var(--primary-color-7))
|
|
||||||
var(--light, var(--secondary-color-3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modern browsers with `scrollbar-*` support */
|
|
||||||
@supports (scrollbar-width: auto) {
|
|
||||||
:not(:hover) {
|
|
||||||
scrollbar-color: rgb(0 0 0 / 0%) rgb(0 0 0 / 0%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
scrollbar-color: var(--secondary-color-2) rgb(0 0 0 / 0%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Legacy browsers with `::-webkit-scrollbar-*` support */
|
|
||||||
@supports selector(::-webkit-scrollbar) {
|
|
||||||
:root::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
// AUTOGENERTED Components module
|
// AUTOGENERTED Components module
|
||||||
pub mod skeleton;
|
pub mod select;
|
||||||
|
pub mod toast;
|
||||||
|
|||||||
116
src/components/select/component.rs
git.filemode.normal_file
116
src/components/select/component.rs
git.filemode.normal_file
@@ -0,0 +1,116 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_primitives::select::{
|
||||||
|
self, SelectGroupLabelProps, SelectGroupProps, SelectListProps, SelectOptionProps, SelectProps,
|
||||||
|
SelectTriggerProps, SelectValueProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Select<T: Clone + PartialEq + 'static>(props: SelectProps<T>) -> Element {
|
||||||
|
rsx! {
|
||||||
|
document::Link { rel: "stylesheet", href: asset!("./style.css") }
|
||||||
|
select::Select {
|
||||||
|
class: "select",
|
||||||
|
value: props.value,
|
||||||
|
default_value: props.default_value,
|
||||||
|
on_value_change: props.on_value_change,
|
||||||
|
disabled: props.disabled,
|
||||||
|
name: props.name,
|
||||||
|
placeholder: props.placeholder,
|
||||||
|
roving_loop: props.roving_loop,
|
||||||
|
typeahead_timeout: props.typeahead_timeout,
|
||||||
|
attributes: props.attributes,
|
||||||
|
{props.children}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SelectTrigger(props: SelectTriggerProps) -> Element {
|
||||||
|
rsx! {
|
||||||
|
select::SelectTrigger { attributes: props.attributes,
|
||||||
|
{props.children}
|
||||||
|
svg {
|
||||||
|
class: "select-expand-icon",
|
||||||
|
view_box: "0 0 24 24",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
polyline { points: "6 9 12 15 18 9" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SelectValue(props: SelectValueProps) -> Element {
|
||||||
|
rsx! {
|
||||||
|
select::SelectValue { attributes: props.attributes }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SelectList(props: SelectListProps) -> Element {
|
||||||
|
rsx! {
|
||||||
|
select::SelectList {
|
||||||
|
class: "select-list",
|
||||||
|
id: props.id,
|
||||||
|
attributes: props.attributes,
|
||||||
|
{props.children}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SelectGroup(props: SelectGroupProps) -> Element {
|
||||||
|
rsx! {
|
||||||
|
select::SelectGroup {
|
||||||
|
class: "select-group",
|
||||||
|
disabled: props.disabled,
|
||||||
|
id: props.id,
|
||||||
|
attributes: props.attributes,
|
||||||
|
{props.children}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SelectGroupLabel(props: SelectGroupLabelProps) -> Element {
|
||||||
|
rsx! {
|
||||||
|
select::SelectGroupLabel {
|
||||||
|
class: "select-group-label",
|
||||||
|
id: props.id,
|
||||||
|
attributes: props.attributes,
|
||||||
|
{props.children}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SelectOption<T: Clone + PartialEq + 'static>(props: SelectOptionProps<T>) -> Element {
|
||||||
|
rsx! {
|
||||||
|
select::SelectOption::<T> {
|
||||||
|
class: "select-option",
|
||||||
|
value: props.value,
|
||||||
|
text_value: props.text_value,
|
||||||
|
disabled: props.disabled,
|
||||||
|
id: props.id,
|
||||||
|
index: props.index,
|
||||||
|
aria_label: props.aria_label,
|
||||||
|
aria_roledescription: props.aria_roledescription,
|
||||||
|
attributes: props.attributes,
|
||||||
|
{props.children}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SelectItemIndicator() -> Element {
|
||||||
|
rsx! {
|
||||||
|
select::SelectItemIndicator {
|
||||||
|
svg {
|
||||||
|
class: "select-check-icon",
|
||||||
|
view_box: "0 0 24 24",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
path { d: "M5 13l4 4L19 7" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/components/select/mod.rs
git.filemode.normal_file
2
src/components/select/mod.rs
git.filemode.normal_file
@@ -0,0 +1,2 @@
|
|||||||
|
mod component;
|
||||||
|
pub use component::*;
|
||||||
150
src/components/select/style.css
git.filemode.normal_file
150
src/components/select/style.css
git.filemode.normal_file
@@ -0,0 +1,150 @@
|
|||||||
|
.select {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-trigger {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.25rem;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border-radius: calc(0.5rem);
|
||||||
|
background: none;
|
||||||
|
background: var(--primary-color-3);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--primary-color-7);
|
||||||
|
color: var(--secondary-color-4);
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 0.25rem;
|
||||||
|
transition: background-color 100ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-trigger span[data-placeholder="true"] {
|
||||||
|
color: var(--secondary-color-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select[data-state="open"] .select-trigger {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-expand-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--secondary-color-5);
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-check-icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--secondary-color-5);
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select[data-disabled="true"] .select-trigger {
|
||||||
|
color: var(--secondary-color-5);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-trigger:hover:not([data-disabled="true"]),
|
||||||
|
.select-trigger:focus-visible {
|
||||||
|
background: var(--light, var(--primary-color-4))
|
||||||
|
var(--dark, var(--primary-color-5));
|
||||||
|
color: var(--secondary-color-1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-list {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
background: var(--primary-color-5);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--primary-color-7);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform-origin: top;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-list[data-state="closed"] {
|
||||||
|
animation: select-list-animate-out 150ms ease-in forwards;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes select-list-animate-out {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-list[data-state="open"] {
|
||||||
|
animation: select-list-animate-in 150ms ease-out forwards;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes select-list-animate-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: calc(0.5rem - 0.25rem);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option[data-disabled="true"] {
|
||||||
|
color: var(--secondary-color-5);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option:hover:not([data-disabled="true"]),
|
||||||
|
.select-option:focus-visible {
|
||||||
|
background: var(--primary-color-7);
|
||||||
|
color: var(--secondary-color-1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-group-label {
|
||||||
|
padding: 4px 12px;
|
||||||
|
color: var(--secondary-color-5);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-disabled="true"] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Skeleton(#[props(extends=GlobalAttributes)] attributes: Vec<Attribute>) -> Element {
|
|
||||||
rsx! {
|
|
||||||
document::Link { rel: "stylesheet", href: asset!("./style.css") }
|
|
||||||
div { class: "skeleton", ..attributes }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
mod component;
|
|
||||||
pub use component::*;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
.skeleton {
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
||||||
background-color: var(--primary-color-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeleton-pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
61.8% {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
src/components/toast/component.rs
git.filemode.normal_file
15
src/components/toast/component.rs
git.filemode.normal_file
@@ -0,0 +1,15 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_primitives::toast::{self, ToastProviderProps};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ToastProvider(props: ToastProviderProps) -> Element {
|
||||||
|
rsx! {
|
||||||
|
document::Link { rel: "stylesheet", href: asset!("./style.css") }
|
||||||
|
toast::ToastProvider {
|
||||||
|
default_duration: props.default_duration,
|
||||||
|
max_toasts: props.max_toasts,
|
||||||
|
render_toast: props.render_toast,
|
||||||
|
{props.children}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/components/toast/mod.rs
git.filemode.normal_file
2
src/components/toast/mod.rs
git.filemode.normal_file
@@ -0,0 +1,2 @@
|
|||||||
|
mod component;
|
||||||
|
pub use component::*;
|
||||||
180
src/components/toast/style.css
git.filemode.normal_file
180
src/components/toast/style.css
git.filemode.normal_file
@@ -0,0 +1,180 @@
|
|||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 20px;
|
||||||
|
max-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-item {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
z-index: calc(var(--toast-count) - var(--toast-index));
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 18rem;
|
||||||
|
height: 4rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--primary-color-7);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-top: -4rem;
|
||||||
|
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
||||||
|
filter: brightness(calc(0.5 + 0.5 * (1 - ((var(--toast-index) + 1) / 4))));
|
||||||
|
opacity: calc(1 - var(--toast-hidden));
|
||||||
|
transform: scale(
|
||||||
|
calc(100% - var(--toast-index) * 5%),
|
||||||
|
calc(100% - var(--toast-index) * 2%)
|
||||||
|
);
|
||||||
|
transition: transform 0.2s ease, margin-top 0.2s ease, opacity 0.2s ease;
|
||||||
|
|
||||||
|
--toast-hidden: calc(min(max(0, var(--toast-index) - 2), 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-container:not(:hover, :focus-within)
|
||||||
|
.toast[data-toast-even]:not([data-top]) {
|
||||||
|
animation: slide-up-even 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-container:not(:hover, :focus-within)
|
||||||
|
.toast[data-toast-odd]:not([data-top]) {
|
||||||
|
animation: slide-up-odd 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up-even {
|
||||||
|
from {
|
||||||
|
transform: translateY(0.5rem)
|
||||||
|
scale(
|
||||||
|
calc(100% - var(--toast-index) * 5%),
|
||||||
|
calc(100% - var(--toast-index) * 2%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0)
|
||||||
|
scale(
|
||||||
|
calc(100% - var(--toast-index) * 5%),
|
||||||
|
calc(100% - var(--toast-index) * 2%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up-odd {
|
||||||
|
from {
|
||||||
|
transform: translateY(0.5rem)
|
||||||
|
scale(
|
||||||
|
calc(100% - var(--toast-index) * 5%),
|
||||||
|
calc(100% - var(--toast-index) * 2%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0)
|
||||||
|
scale(
|
||||||
|
calc(100% - var(--toast-index) * 5%),
|
||||||
|
calc(100% - var(--toast-index) * 2%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast[data-top] {
|
||||||
|
animation: slide-in 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-container:hover .toast[data-top],
|
||||||
|
.toast-container:focus-within .toast[data-top] {
|
||||||
|
animation: slide-in 0 ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(100%)
|
||||||
|
scale(
|
||||||
|
calc(110% - var(--toast-index) * 5%),
|
||||||
|
calc(110% - var(--toast-index) * 2%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0)
|
||||||
|
scale(
|
||||||
|
calc(100% - var(--toast-index) * 5%),
|
||||||
|
calc(100% - var(--toast-index) * 2%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-container:hover .toast,
|
||||||
|
.toast-container:focus-within .toast {
|
||||||
|
margin-top: var(--toast-padding);
|
||||||
|
filter: brightness(1);
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(calc(100%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast[data-type="success"] {
|
||||||
|
background-color: var(--primary-success-color);
|
||||||
|
color: var(--secondary-success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast[data-type="error"] {
|
||||||
|
background-color: var(--primary-error-color);
|
||||||
|
color: var(--contrast-error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast[data-type="warning"] {
|
||||||
|
background-color: var(--primary-warning-color);
|
||||||
|
color: var(--secondary-warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast[data-type="info"] {
|
||||||
|
background-color: var(--primary-info-color);
|
||||||
|
color: var(--secondary-info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
transition: filter 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-title {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--secondary-color-4);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-description {
|
||||||
|
color: var(--secondary-color-3);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--secondary-color-3);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close:hover {
|
||||||
|
color: var(--secondary-color-1);
|
||||||
|
}
|
||||||
401
src/landing.rs
401
src/landing.rs
@@ -1,86 +1,211 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use std::{collections::HashMap, thread::current};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use csv::StringRecord;
|
use csv::StringRecord;
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
use strum::{EnumCount, IntoEnumIterator};
|
||||||
struct GlobalStats {
|
|
||||||
count: usize,
|
use crate::components::select::*;
|
||||||
passed: usize,
|
|
||||||
failed: usize,
|
const PAGE_SIZE: usize = 100_usize;
|
||||||
skip: usize,
|
|
||||||
flake: usize,
|
#[derive(
|
||||||
crash: usize,
|
Debug,
|
||||||
|
Eq,
|
||||||
|
Hash,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
strum::EnumCount,
|
||||||
|
strum::EnumIter,
|
||||||
|
strum::Display,
|
||||||
|
strum::EnumString,
|
||||||
|
)]
|
||||||
|
enum TestStatus {
|
||||||
|
Pass,
|
||||||
|
Fail,
|
||||||
|
Skip,
|
||||||
|
Flake,
|
||||||
|
Crash,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn percentage(count: usize, total: usize) -> f32 {
|
impl TestStatus {
|
||||||
(count as f32 * 100.0) / total as f32
|
const fn emoji(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
TestStatus::Pass => "✅",
|
||||||
|
TestStatus::Fail => "❌",
|
||||||
|
TestStatus::Skip => "❎",
|
||||||
|
TestStatus::Flake => "⚠️",
|
||||||
|
TestStatus::Crash => "💥",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn color(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
TestStatus::Pass => "#22c55e",
|
||||||
|
TestStatus::Fail => "#ff6467",
|
||||||
|
TestStatus::Skip => "#ffdf20",
|
||||||
|
TestStatus::Flake => "#38bdf8",
|
||||||
|
TestStatus::Crash => "#e7000b",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn percentage(count: usize, total: f32) -> f32 {
|
||||||
|
(count as f32 * 100.0) / total
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Landing() -> Element {
|
pub fn Landing() -> Element {
|
||||||
let result = use_context::<Vec<StringRecord>>();
|
let result = use_context::<Signal<Vec<StringRecord>>>();
|
||||||
|
|
||||||
let global_stats = result.iter().fold(GlobalStats::default(), |acc, record| {
|
let global_stats = use_memo(move || result
|
||||||
let mut new_acc = acc;
|
.read()
|
||||||
match &record[1] {
|
.iter()
|
||||||
"Pass" => new_acc.passed += 1,
|
.fold(HashMap::from_iter(TestStatus::iter().map(|s| (s, 0))), |mut acc, record| {
|
||||||
"Skip" => new_acc.skip += 1,
|
if let Ok(status) = TestStatus::from_str(&record[1]) {
|
||||||
"Fail" => new_acc.failed += 1,
|
*acc.entry(status).or_insert(0) += 1;
|
||||||
"Flake" => new_acc.flake += 1,
|
}
|
||||||
"Crash" => new_acc.crash += 1,
|
acc
|
||||||
_ => {},
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let total = use_memo(move || global_stats.read().iter().fold(0.0_f32, |acc, (_, v)| acc + *v as f32));
|
||||||
|
|
||||||
|
let stats_cards = TestStatus::iter().map(|s| {
|
||||||
|
rsx! {
|
||||||
|
StatCard {
|
||||||
|
name: s.to_string(),
|
||||||
|
color: s.color().to_string(),
|
||||||
|
count: global_stats.read()[&s],
|
||||||
|
stat: percentage(global_stats.read()[&s], total()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut filter: Signal<Option<TestStatus>> = use_signal(|| None);
|
||||||
|
let mut filtered_count = use_signal(|| 0_usize);
|
||||||
|
|
||||||
|
let mut current_page = use_memo(move || 0_usize);
|
||||||
|
let mut page_count = use_memo(move || filtered_count().max(PAGE_SIZE - 1) / PAGE_SIZE);
|
||||||
|
|
||||||
|
let page = use_memo(move || {
|
||||||
|
if let Some(f) = filter() {
|
||||||
|
current_page.set(0);
|
||||||
|
let mut count = 0_usize;
|
||||||
|
filtered_count.set(0_usize);
|
||||||
|
let shift = (current_page() * PAGE_SIZE);
|
||||||
|
result
|
||||||
|
.read()
|
||||||
|
.iter()
|
||||||
|
.filter(|r| {
|
||||||
|
if let Ok(status) = TestStatus::from_str(&r[1]) && status == f {
|
||||||
|
filtered_count += 1;
|
||||||
|
if *filtered_count.peek() < shift {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
if count >= PAGE_SIZE {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
filtered_count.set(total() as usize);
|
||||||
|
let start = current_page() * PAGE_SIZE;
|
||||||
|
let end = result.read().len().min((current_page() * PAGE_SIZE) + PAGE_SIZE);
|
||||||
|
result.read()[start..end].to_vec()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let statuses = TestStatus::iter().enumerate().map(|(i, s)| {
|
||||||
|
rsx! {
|
||||||
|
SelectOption::<Option<TestStatus>> {
|
||||||
|
index: i,
|
||||||
|
value: s,
|
||||||
|
text_value: "{s}",
|
||||||
|
{format!("{} {s}", s.emoji())}
|
||||||
|
SelectItemIndicator {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
new_acc.count = new_acc.passed + new_acc.failed + new_acc.skip + new_acc.flake + new_acc.crash;
|
|
||||||
new_acc
|
|
||||||
});
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: "flex flex-col space-y-4 rounded-3xl p-4 w-full h-fit shadow-xl shadow-slate-950",
|
class: "flex flex-col space-y-4 rounded-3xl p-4 pt-8 w-full h-fit shadow-xl shadow-slate-950",
|
||||||
style: "background: linear-gradient(145deg, #020617 0, #02081f 60%, #020617 100%);",
|
style: "background: linear-gradient(145deg, #020617 0, #02081f 60%, #020617 100%);",
|
||||||
div { class: "border-1 border-slate-400 bg-slate-400/15 text-slate-400 w-fit rounded-3xl p-1 flex flex-row space-x-1 items-center",
|
div { class: "flex flex-row space-x-4",
|
||||||
div { class: "bg-[#22c55e] rounded-full size-3" }
|
div { class: "border-1 border-[#38bdf8] bg-[#38bdf8]/15 text-slate-400 w-fit rounded-3xl py-1 px-2 flex flex-row space-x-1 items-center",
|
||||||
p { class: "text-xs",
|
div { class: "bg-[#38bdf8] rounded-full size-3" }
|
||||||
"Count: {global_stats.count} tests"
|
p { class: "text-xs",
|
||||||
|
"Total: {total} tests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { class: "border-1 border-[#22c55e] bg-[#22c55e]/15 text-slate-400 w-fit rounded-3xl py-1 px-2 flex flex-row space-x-1 items-center",
|
||||||
|
div { class: "bg-[#22c55e] rounded-full size-3" }
|
||||||
|
p { class: "text-xs",
|
||||||
|
"Filtered: {filtered_count} tests"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div { class: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4",
|
div { class: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4",
|
||||||
StatCard {
|
{stats_cards}
|
||||||
name: "PASSED".to_string(),
|
|
||||||
color: "#22c55e".to_string(),
|
|
||||||
count: global_stats.passed,
|
|
||||||
stat: percentage(global_stats.passed, global_stats.count),
|
|
||||||
}
|
|
||||||
StatCard {
|
|
||||||
name: "FAILED".to_string(),
|
|
||||||
color: "#ff6467".to_string(),
|
|
||||||
count: global_stats.failed,
|
|
||||||
stat: percentage(global_stats.failed, global_stats.count),
|
|
||||||
}
|
|
||||||
StatCard {
|
|
||||||
name: "SKIPPED".to_string(),
|
|
||||||
color: "#ffdf20".to_string(),
|
|
||||||
count: global_stats.skip,
|
|
||||||
stat: percentage(global_stats.skip, global_stats.count),
|
|
||||||
}
|
|
||||||
StatCard {
|
|
||||||
name: "FLAKE".to_string(),
|
|
||||||
color: "#38bdf8".to_string(),
|
|
||||||
count: global_stats.flake,
|
|
||||||
stat: percentage(global_stats.flake, global_stats.count),
|
|
||||||
}
|
|
||||||
StatCard {
|
|
||||||
name: "CRASH".to_string(),
|
|
||||||
color: "#e7000b".to_string(),
|
|
||||||
count: global_stats.crash,
|
|
||||||
stat: percentage(global_stats.crash, global_stats.count),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
div { class: "mx-auto size-[200px]",
|
div { class: "mx-auto size-[200px]",
|
||||||
StatsPieChart { stats: global_stats }
|
StatsPieChart { stats: global_stats, total }
|
||||||
}
|
}
|
||||||
div { class: "mt-12 w-full bg-gray-900 overflow-hidden border-1 border-slate-700 rounded-lg",
|
div { class: "mt-12 w-full flex flex-row justify-between text-gray-400 text-sm",
|
||||||
|
p { "Page {current_page() + 1} of {page_count() + 1}" }
|
||||||
|
div { class: "flex flex-row space-x-2",
|
||||||
|
button { class: "pagination-button",
|
||||||
|
disabled: current_page() == 0,
|
||||||
|
onclick: move |_| current_page.set(0_usize),
|
||||||
|
"First"
|
||||||
|
}
|
||||||
|
button { class: "pagination-button",
|
||||||
|
disabled: current_page() == 0,
|
||||||
|
onclick: move |_| current_page -= 1,
|
||||||
|
"Prev"
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_page() > 2 {
|
||||||
|
p { "..." }
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in ((current_page() as i32 - 2).max(0) as usize)..=(current_page() + 2).min(page_count()) {
|
||||||
|
button { class: "pagination-button",
|
||||||
|
"data-active": i == current_page(),
|
||||||
|
onclick: move |_| current_page.set(i),
|
||||||
|
"{i + 1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_page() < (page_count() as i32 - 2).max(0) as usize {
|
||||||
|
p { "..." }
|
||||||
|
}
|
||||||
|
|
||||||
|
button { class: "pagination-button",
|
||||||
|
disabled: current_page() >= page_count(),
|
||||||
|
onclick: move |_| current_page += 1,
|
||||||
|
"Next"
|
||||||
|
}
|
||||||
|
button { class: "pagination-button",
|
||||||
|
disabled: current_page() >= page_count(),
|
||||||
|
onclick: move |_| current_page.set(page_count()),
|
||||||
|
"Last"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { class: "w-full bg-gray-900 overflow-hidden border-1 border-slate-700 rounded-lg text-gray-400",
|
||||||
table { class: "w-full border-collapse border-spacing-0",
|
table { class: "w-full border-collapse border-spacing-0",
|
||||||
tr {
|
tr {
|
||||||
class: "border-b-1 border-slate-700",
|
class: "border-b-1 border-slate-700",
|
||||||
@@ -88,17 +213,43 @@ pub fn Landing() -> Element {
|
|||||||
th { class: "text-left uppercase bold whitespace-nowrap py-2 px-3",
|
th { class: "text-left uppercase bold whitespace-nowrap py-2 px-3",
|
||||||
"Test name",
|
"Test name",
|
||||||
}
|
}
|
||||||
th { class: "text-left uppercase bold whitespace-nowrap py-2 px-3",
|
th { class: "uppercase bold whitespace-nowrap py-2 px-3",
|
||||||
"Status",
|
Select::<Option<TestStatus>> {
|
||||||
|
placeholder: "STATUS",
|
||||||
|
on_value_change: move |value: Option<Option<TestStatus>>| filter.set(value.unwrap_or(None)),
|
||||||
|
SelectTrigger {
|
||||||
|
class: "select-trigger mx-auto w-fit !bg-transparent !shadow-none !text-gray-400 cursor-pointer uppercase",
|
||||||
|
aria_label: "Select Trigger",
|
||||||
|
SelectValue { class: "!bg-transparent !shadow-none !text-gray-400" }
|
||||||
|
}
|
||||||
|
SelectList {
|
||||||
|
aria_label: "Select status",
|
||||||
|
SelectGroup {
|
||||||
|
{statuses}
|
||||||
|
SelectOption::<Option<TestStatus>> {
|
||||||
|
index: TestStatus::COUNT,
|
||||||
|
value: None,
|
||||||
|
text_value: "Status",
|
||||||
|
"🔄 None"
|
||||||
|
SelectItemIndicator {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for test in result[0..100].iter() {
|
for test in page.iter() {
|
||||||
tr { class: "text-sm hover:bg-[#38bef7]/5",
|
tr { class: "text-sm hover:bg-[#38bef7]/5",
|
||||||
td { class: "py-2 px-3",
|
td { class: "py-2 px-3",
|
||||||
"{&test[0]}"
|
"{&test[0]}"
|
||||||
}
|
}
|
||||||
td { class: "py-2 px-3",
|
td { class: "py-2 px-3",
|
||||||
"{&test[1]}"
|
StatusBadge {
|
||||||
|
status: match TestStatus::from_str(&test[1]) {
|
||||||
|
Ok(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,12 +260,7 @@ pub fn Landing() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn StatCard(
|
fn StatCard(name: String, color: String, count: usize, stat: f32) -> Element {
|
||||||
name: String,
|
|
||||||
color: String,
|
|
||||||
count: usize,
|
|
||||||
stat: f32,
|
|
||||||
) -> Element {
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "rounded-2xl p-4 border-1 border-slate-800 shadow-xl shadow-[#02081f] w-full h-fit bg-[#090f21] flex flex-col space-y-2",
|
div { class: "rounded-2xl p-4 border-1 border-slate-800 shadow-xl shadow-[#02081f] w-full h-fit bg-[#090f21] flex flex-col space-y-2",
|
||||||
div { class: "flex flex-row space-x-2 flex items-center",
|
div { class: "flex flex-row space-x-2 flex items-center",
|
||||||
@@ -138,53 +284,56 @@ fn StatCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Segment {
|
#[component]
|
||||||
percentage: f32,
|
fn StatusBadge(status: Option<TestStatus>) -> Element {
|
||||||
start: f32,
|
let color = match status {
|
||||||
end: f32,
|
Some(s) => s.color(),
|
||||||
color: String,
|
None => "#FFF",
|
||||||
|
};
|
||||||
|
let name = match status {
|
||||||
|
Some(s) => s.to_string(),
|
||||||
|
None => "Unrecognized".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "mx-auto border-1 py-1 px-3 rounded-3xl text-xs w-fit select-none",
|
||||||
|
style: format!(r#"
|
||||||
|
background-color: {color}0F;
|
||||||
|
color: {color};
|
||||||
|
border-color: {color};
|
||||||
|
"#),
|
||||||
|
"{name}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn StatsPieChart(stats: GlobalStats) -> Element {
|
fn StatsPieChart(stats: ReadSignal<HashMap<TestStatus, usize>>, total: ReadSignal<f32>) -> Element {
|
||||||
let total = stats.count as f32;
|
struct Segment {
|
||||||
|
percentage: f32,
|
||||||
if total == 0.0 {
|
start: f32,
|
||||||
return rsx!{};
|
end: f32,
|
||||||
|
color: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let passed = stats.passed as f32 / total;
|
if total() == 0.0 {
|
||||||
let failed = stats.failed as f32 / total;
|
return rsx! {};
|
||||||
let skip = stats.skip as f32 / total;
|
}
|
||||||
let flake = stats.flake as f32 / total;
|
|
||||||
let crash = stats.crash as f32 / total;
|
|
||||||
|
|
||||||
let colors = (
|
|
||||||
"#22c55e", // passed
|
|
||||||
"#ff6467", // failed
|
|
||||||
"#ffdf20", // skipped
|
|
||||||
"#38bdf8", // flake
|
|
||||||
"#e7000b", // crash
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut segments: Vec<Segment> = Vec::new();
|
let mut segments: Vec<Segment> = Vec::new();
|
||||||
let mut cumulative = 0.0_f32;
|
let mut cumulative = 0.0_f32;
|
||||||
|
|
||||||
for (pct, color) in [
|
for (key, val) in stats.read().iter() {
|
||||||
(passed, colors.0),
|
let stat = *val as f32 / total();
|
||||||
(failed, colors.1),
|
if stat > 0.0 {
|
||||||
(skip, colors.2),
|
|
||||||
(flake, colors.3),
|
|
||||||
(crash, colors.4),
|
|
||||||
] {
|
|
||||||
if pct > 0.0 {
|
|
||||||
segments.push(Segment {
|
segments.push(Segment {
|
||||||
percentage: pct * 100.0,
|
percentage: stat * 100.0,
|
||||||
start: cumulative,
|
start: cumulative,
|
||||||
end: cumulative + pct,
|
end: cumulative + stat,
|
||||||
color: color.to_string(),
|
color: key.color().to_string(),
|
||||||
});
|
});
|
||||||
cumulative += pct;
|
cumulative += stat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,35 +341,31 @@ fn StatsPieChart(stats: GlobalStats) -> Element {
|
|||||||
let cx: f32 = 100.0;
|
let cx: f32 = 100.0;
|
||||||
let cy: f32 = 100.0;
|
let cy: f32 = 100.0;
|
||||||
|
|
||||||
let paths = segments
|
let paths = segments.iter().enumerate().map(|(idx, seg)| {
|
||||||
.iter()
|
let start_angle = seg.start * 2.0 * PI;
|
||||||
.enumerate()
|
let end_angle = seg.end * 2.0 * PI;
|
||||||
.map(|(idx, seg)| {
|
|
||||||
let start_angle = seg.start * 2.0 * PI;
|
|
||||||
let end_angle = seg.end * 2.0 * PI;
|
|
||||||
|
|
||||||
let x1 = cx + radius * start_angle.cos();
|
let x1 = cx + radius * start_angle.cos();
|
||||||
let y1 = cy + radius * start_angle.sin();
|
let y1 = cy + radius * start_angle.sin();
|
||||||
let x2 = cx + radius * end_angle.cos();
|
let x2 = cx + radius * end_angle.cos();
|
||||||
let y2 = cy + radius * end_angle.sin();
|
let y2 = cy + radius * end_angle.sin();
|
||||||
|
|
||||||
let large_arc_flag = if (end_angle - start_angle) > PI { 1 } else { 0 };
|
let large_arc_flag = if (end_angle - start_angle) > PI { 1 } else { 0 };
|
||||||
|
|
||||||
let d = format!(
|
let d =
|
||||||
"M {cx} {cy} L {x1} {y1} A {radius} {radius} 0 {large_arc_flag} 1 {x2} {y2} Z"
|
format!("M {cx} {cy} L {x1} {y1} A {radius} {radius} 0 {large_arc_flag} 1 {x2} {y2} Z");
|
||||||
);
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
path {
|
path {
|
||||||
key: "{idx}",
|
key: "{idx}",
|
||||||
d: "{d}",
|
d: "{d}",
|
||||||
fill: "{seg.color}",
|
fill: "{seg.color}",
|
||||||
opacity: "0.9",
|
opacity: "0.9",
|
||||||
stroke: "rgba(255, 255, 255, 0.1)",
|
stroke: "rgba(255, 255, 255, 0.1)",
|
||||||
"stroke-width": "1",
|
"stroke-width": "1",
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
svg {
|
svg {
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
|
|
||||||
/// Default placeholder for loading state
|
|
||||||
#[component]
|
|
||||||
pub fn LoadingPlaceholder(message: String) -> Element {
|
|
||||||
rsx! {
|
|
||||||
div { class: "rounded-radius h-[50vh] w-1/2 bg-foreground/20 animate-pulse text-foreground flex justify-center items-center place-self-center mt-14 mx-auto",
|
|
||||||
h3 { class: "h3", {message} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
struct SuspenseContextPlaceholder {
|
|
||||||
element: Option<Element>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait to create a suspense with a default loading placeholder
|
|
||||||
pub trait Loader<T: 'static> {
|
|
||||||
fn load(
|
|
||||||
&self,
|
|
||||||
message: impl ToString,
|
|
||||||
) -> Result<MappedSignal<T, Signal<Option<T>> /* wtf Dioxus ??? */>, RenderError>;
|
|
||||||
fn load_with(
|
|
||||||
&self,
|
|
||||||
element: Element,
|
|
||||||
) -> Result<MappedSignal<T, Signal<Option<T>>>, RenderError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Loader<T> for Resource<T> {
|
|
||||||
fn load(
|
|
||||||
&self,
|
|
||||||
message: impl ToString,
|
|
||||||
) -> Result<MappedSignal<T, Signal<Option<T>>>, RenderError> {
|
|
||||||
let mut context = use_context::<Signal<SuspenseContextPlaceholder>>();
|
|
||||||
context.write().element = Some(rsx! {
|
|
||||||
LoadingPlaceholder { message: message.to_string() }
|
|
||||||
});
|
|
||||||
self.suspend()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_with(
|
|
||||||
&self,
|
|
||||||
element: Element,
|
|
||||||
) -> Result<MappedSignal<T, Signal<Option<T>>>, RenderError> {
|
|
||||||
let mut context = use_context::<Signal<SuspenseContextPlaceholder>>();
|
|
||||||
context.write().element = Some(element);
|
|
||||||
self.suspend()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Suspense(children: Element) -> Element {
|
|
||||||
let context = use_context_provider(|| Signal::new(SuspenseContextPlaceholder::default()));
|
|
||||||
|
|
||||||
rsx! {
|
|
||||||
SuspenseBoundary {
|
|
||||||
fallback: move |_| {
|
|
||||||
context.read().clone().element.unwrap_or_else(|| rsx! { "Loading..." })
|
|
||||||
},
|
|
||||||
{children}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
100
src/main.rs
100
src/main.rs
@@ -2,33 +2,110 @@ use dioxus::prelude::*;
|
|||||||
|
|
||||||
use csv::{ReaderBuilder, StringRecord};
|
use csv::{ReaderBuilder, StringRecord};
|
||||||
|
|
||||||
|
mod components;
|
||||||
mod landing;
|
mod landing;
|
||||||
mod navbar;
|
mod navbar;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod loader;
|
|
||||||
|
|
||||||
use crate::routes::Route;
|
use crate::routes::Route;
|
||||||
|
|
||||||
|
use dioxus_primitives::toast::{ToastOptions, ToastProvider, use_toast};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||||
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
||||||
|
const RESULT: Asset = asset!(
|
||||||
|
"/assets/results.csv",
|
||||||
|
AssetOptions::builder().with_hash_suffix(false)
|
||||||
|
);
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::launch(App);
|
dioxus::launch(AppWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn AppWrapper() -> Element {
|
||||||
|
rsx! {
|
||||||
|
ToastProvider { App{} }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn App() -> Element {
|
fn App() -> Element {
|
||||||
// TODO: get results from request
|
let toast = use_toast();
|
||||||
let result_data = include_str!("../assets/results.csv");
|
|
||||||
let mut reader = ReaderBuilder::new().from_reader(result_data.as_bytes());
|
|
||||||
|
|
||||||
let records = reader.into_records().collect::<Result<Vec<StringRecord>, csv::Error>>()?;
|
let resource = use_resource(move || get_results());
|
||||||
|
let mut result = use_context_provider(|| Signal::new(Vec::<StringRecord>::new()));
|
||||||
|
|
||||||
let result = use_context_provider(|| records);
|
use_effect(move || match &*resource.read() {
|
||||||
|
Some(Ok(results)) => {
|
||||||
|
let mut reader = ReaderBuilder::new().from_reader(results.as_bytes());
|
||||||
|
let records = match reader
|
||||||
|
.into_records()
|
||||||
|
.collect::<Result<Vec<StringRecord>, csv::Error>>()
|
||||||
|
{
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to parse results: {e}");
|
||||||
|
toast.error(
|
||||||
|
"Error".to_string(),
|
||||||
|
ToastOptions::new()
|
||||||
|
.description(format!("Failed to parse CTS results data: {e}"))
|
||||||
|
.duration(Duration::from_secs(2))
|
||||||
|
.permanent(false),
|
||||||
|
);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
result.set(records);
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
error!("Failed to fetch results: {e}");
|
||||||
|
toast.error(
|
||||||
|
"Error".to_string(),
|
||||||
|
ToastOptions::new()
|
||||||
|
.description(format!("Failed to fetch CTS results data: {e}"))
|
||||||
|
.duration(Duration::from_secs(2))
|
||||||
|
.permanent(false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
document::Link { rel: "icon", href: FAVICON }
|
document::Link { rel: "icon", href: FAVICON }
|
||||||
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
||||||
|
document::Style { "
|
||||||
|
:root {{
|
||||||
|
--primary-color: #000;
|
||||||
|
--primary-color-1: #020618;
|
||||||
|
--primary-color-2: #0a0a0a;
|
||||||
|
--primary-color-3: #020618;
|
||||||
|
--primary-color-4: #1a1a1a;
|
||||||
|
--primary-color-5: #02081e;
|
||||||
|
--primary-color-6: #232323;
|
||||||
|
--primary-color-7: #1e2939;
|
||||||
|
|
||||||
|
--secondary-color: #fff;
|
||||||
|
--secondary-color-1: #fafafa;
|
||||||
|
--secondary-color-2: #e6e6e6;
|
||||||
|
--secondary-color-3: #dcdcdc;
|
||||||
|
--secondary-color-4: #d4d4d4;
|
||||||
|
--secondary-color-5: #ddd;
|
||||||
|
--secondary-color-6: #5d5d5d;
|
||||||
|
|
||||||
|
--focused-border-color: #2b7fff;
|
||||||
|
--primary-success-color: #02271c;
|
||||||
|
--secondary-success-color: #b6fae3;
|
||||||
|
--primary-warning-color: #342203;
|
||||||
|
--secondary-warning-color: #feeac7;
|
||||||
|
--primary-error-color: #a22e2e;
|
||||||
|
--secondary-error-color: #9b1c1c;
|
||||||
|
--contrast-error-color: var(--secondary-color-3));
|
||||||
|
--primary-info-color: var(--primary-color-5));
|
||||||
|
--secondary-info-color: var(--primary-color-7));
|
||||||
|
}}
|
||||||
|
"}
|
||||||
div {
|
div {
|
||||||
class: "text-white min-h-screen",
|
class: "text-white min-h-screen",
|
||||||
style: "background: radial-gradient(circle at top, #1e293b 0, #020617 45%, #000 100%);",
|
style: "background: radial-gradient(circle at top, #1e293b 0, #020617 45%, #000 100%);",
|
||||||
@@ -36,3 +113,12 @@ fn App() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_results() -> Result<String> {
|
||||||
|
Ok(
|
||||||
|
reqwest::get(format!("{}/assets/results.csv", std::env!("URL")))
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::routes::Route;
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn Navbar() -> Element {
|
pub fn Navbar() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "w-screen mb-12 py-2 px-6 flex flex-row justify-between",
|
div { class: "mx-auto container mb-12 py-2 px-6 sm:px-0 flex flex-row justify-between",
|
||||||
Link { class: "flex flex-row h-16 text-4xl md:text-5xl select-none cursor-pointer",
|
Link { class: "flex flex-row h-16 text-4xl md:text-5xl select-none cursor-pointer",
|
||||||
to: Route::Landing {},
|
to: Route::Landing {},
|
||||||
VulkanVSvg {}
|
VulkanVSvg {}
|
||||||
@@ -21,8 +21,17 @@ pub fn Navbar() -> Element {
|
|||||||
main { class: "mx-auto container mb-24",
|
main { class: "mx-auto container mb-24",
|
||||||
Outlet::<Route> {}
|
Outlet::<Route> {}
|
||||||
}
|
}
|
||||||
footer { class: "w-screen flex flex-row justify-between px-6 text-sm text-gray-400",
|
footer { class: "w-screen h-11 flex flex-row justify-start space-x-1 px-6 text-sm text-gray-400",
|
||||||
"Made by kbz_8 with Dioxus"
|
p { "Made by"}
|
||||||
|
a { class: "hover:underline text-white",
|
||||||
|
href: "https://portfolio.kbz8.me/",
|
||||||
|
"kbz_8"
|
||||||
|
}
|
||||||
|
p { "with" }
|
||||||
|
a { class: "hover:underline text-white",
|
||||||
|
href: "https://dioxuslabs.com/",
|
||||||
|
"Dioxus"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{landing::Landing, navbar::Navbar};
|
||||||
landing::Landing,
|
|
||||||
navbar::Navbar,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Routable, PartialEq)]
|
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
|||||||
22
tailwind.css
22
tailwind.css
@@ -1,20 +1,10 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@layer "base" {
|
@layer base {
|
||||||
.skeleton {
|
.pagination-button {
|
||||||
border-radius: 0.375rem;
|
@apply py-1 px-3 border-1 border-gray-500 bg-gray-500/15 text-gray-500 rounded-3xl cursor-pointer;
|
||||||
animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
@apply hover:border-[#38bdf8] hover:bg-[#38bdf8]/15 hover:text-[#38bdf8];
|
||||||
background-color: #262626;
|
@apply data-[active=true]:border-[#38bdf8] data-[active=true]:bg-[#38bdf8]/15 data-[active=true]:text-[#38bdf8] data-[active=true]:cursor-default;
|
||||||
}
|
@apply disabled:border-dashed disabled:border-gray-700 disabled:bg-gray-800/15 disabled:text-gray-500 disabled:cursor-default;
|
||||||
|
|
||||||
@keyframes skeleton-pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
61.8% {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user