yes
This commit is contained in:
@@ -7,6 +7,7 @@ edition = "2024"
|
||||
[dependencies]
|
||||
csv = "1.4.0"
|
||||
dioxus = { version = "0.7.2", features = ["router"] }
|
||||
dioxus-sdk-time = "0.7.0"
|
||||
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"] }
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 18rem;
|
||||
height: 4rem;
|
||||
height: 5rem;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -178,3 +178,171 @@
|
||||
.toast-close:hover {
|
||||
color: var(--secondary-color-1);
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
border-radius: 0.375rem;
|
||||
animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
background-color: var(--primary-color-7);
|
||||
}
|
||||
|
||||
@keyframes skeleton-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
61.8% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.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,3 +1,4 @@
|
||||
// AUTOGENERTED Components module
|
||||
pub mod select;
|
||||
pub mod toast;
|
||||
pub mod skeleton;
|
||||
|
||||
@@ -7,7 +7,6 @@ use dioxus_primitives::select::{
|
||||
#[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,
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
8
src/components/skeleton/component.rs
git.filemode.normal_file
8
src/components/skeleton/component.rs
git.filemode.normal_file
@@ -0,0 +1,8 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn Skeleton(#[props(extends=GlobalAttributes)] attributes: Vec<Attribute>) -> Element {
|
||||
rsx! {
|
||||
div { class: "skeleton", ..attributes }
|
||||
}
|
||||
}
|
||||
2
src/components/skeleton/mod.rs
git.filemode.normal_file
2
src/components/skeleton/mod.rs
git.filemode.normal_file
@@ -0,0 +1,2 @@
|
||||
mod component;
|
||||
pub use component::*;
|
||||
@@ -4,7 +4,6 @@ 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,
|
||||
|
||||
595
src/landing.rs
595
src/landing.rs
@@ -1,17 +1,40 @@
|
||||
use crate::components::{select::*, skeleton::*};
|
||||
use crate::loader::{Loader, Suspense};
|
||||
use csv::{ReaderBuilder, StringRecord};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use std::{collections::HashMap, thread::current};
|
||||
use std::str::FromStr;
|
||||
|
||||
use csv::StringRecord;
|
||||
use dioxus_primitives::toast::{ToastOptions, use_toast};
|
||||
use dioxus_sdk_time::*;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashMap, thread::current};
|
||||
use strum::{EnumCount, IntoEnumIterator};
|
||||
|
||||
use crate::components::select::*;
|
||||
|
||||
const PAGE_SIZE: usize = 100_usize;
|
||||
|
||||
// Wrapper for displaying a duration in h:m:s (integer seconds, rounded down)
|
||||
struct HMSDuration(Duration);
|
||||
impl fmt::Display for HMSDuration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut ms = self.0.as_millis();
|
||||
let hours = ms / 3_600_000;
|
||||
ms %= 3_600_000;
|
||||
let mins = ms / 60_000;
|
||||
ms %= 60_000;
|
||||
let secs = ms / 1000;
|
||||
ms %= 1000;
|
||||
|
||||
if hours > 0 {
|
||||
write!(f, "{}:{:02}:{:02}.{}", hours, mins, secs, ms)
|
||||
} else if mins > 0 {
|
||||
write!(f, "0:{}:{:02}.{}", mins, secs, ms)
|
||||
} else {
|
||||
write!(f, "0:0:{}.{}", secs, ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Eq,
|
||||
@@ -58,9 +81,162 @@ fn percentage(count: usize, total: f32) -> f32 {
|
||||
(count as f32 * 100.0) / total
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn LandingPlaceholder() -> Element {
|
||||
let stats_cards = TestStatus::iter().map(|s| {
|
||||
rsx! {
|
||||
StatCardPlaceholder {
|
||||
name: s.to_string(),
|
||||
color: s.color().to_string(),
|
||||
count: 0,
|
||||
stat: 0.0_f32,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let statuses = TestStatus::iter().enumerate().map(|(i, s)| {
|
||||
rsx! {
|
||||
SelectOption::<Option<TestStatus>> { index: i, value: s, text_value: "{s}",
|
||||
{format!("{} {s}", s.emoji())}
|
||||
SelectItemIndicator {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
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%);",
|
||||
div { class: "flex flex-row space-x-4",
|
||||
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",
|
||||
div { class: "bg-[#38bdf8] rounded-full size-3" }
|
||||
p { class: "text-xs", "Total: 0 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: 0 tests" }
|
||||
}
|
||||
}
|
||||
div { class: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4",
|
||||
{stats_cards}
|
||||
}
|
||||
div { class: "mx-auto size-[200px]",
|
||||
Skeleton { class: "skeleton size-[200px] !rounded-full" }
|
||||
}
|
||||
div { class: "mt-12 w-full flex flex-col md:flex-row justify-between text-gray-400 text-sm gap-4 items-center",
|
||||
Skeleton { class: "hidden lg:block skeleton w-full h-9" }
|
||||
p { class: "my-auto w-fit text-nowrap", "Page 1 of 1" }
|
||||
div { class: "hidden lg:block",
|
||||
PaginationPlaceholder { small: false }
|
||||
}
|
||||
div { class: "block lg:hidden",
|
||||
PaginationPlaceholder { small: true }
|
||||
}
|
||||
}
|
||||
Skeleton { class: "block lg:hidden skeleton w-full h-9" }
|
||||
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",
|
||||
tr {
|
||||
class: "border-b-1 border-slate-700",
|
||||
style: "background: radial-gradient(circle at top, rgba(56, 189, 248, 0.1), rgba(15, 23, 42, 1));",
|
||||
th { class: "text-left uppercase bold whitespace-nowrap py-2 px-3",
|
||||
"Test name"
|
||||
}
|
||||
th { class: "text-left uppercase bold whitespace-nowrap py-2 px-3",
|
||||
"Duration (H:M:S.MS)"
|
||||
}
|
||||
th { class: "uppercase bold whitespace-nowrap py-2 px-3",
|
||||
Select::<Option<TestStatus>> { placeholder: "STATUS",
|
||||
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 _ in 0..12 {
|
||||
tr { class: "text-sm hover:bg-[#38bef7]/5",
|
||||
td { class: "py-2 px-3 w-full",
|
||||
Skeleton { class: "skeleton w-[60%] h-6" }
|
||||
}
|
||||
td { class: "py-2",
|
||||
Skeleton { class: "skeleton w-24 h-6 mx-auto" }
|
||||
}
|
||||
td { class: "py-2",
|
||||
Skeleton { class: "skeleton w-16 h-9 !rounded-3xl mx-auto" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Landing() -> Element {
|
||||
let result = use_context::<Signal<Vec<StringRecord>>>();
|
||||
let toast = use_toast();
|
||||
|
||||
let resource = use_resource(move || {
|
||||
toast.info(
|
||||
"Loading...".to_string(),
|
||||
ToastOptions::new()
|
||||
.description("Loading CTS results")
|
||||
.duration(Duration::from_secs(12))
|
||||
);
|
||||
get_results()
|
||||
}).load_with(rsx!{
|
||||
LandingPlaceholder {}
|
||||
})?;
|
||||
let mut result = use_signal(Vec::<StringRecord>::new);
|
||||
|
||||
use_effect(move || match &*resource.read() {
|
||||
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("Failed to parse CTS results")
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
result.set(records);
|
||||
toast.success(
|
||||
"Success".to_string(),
|
||||
ToastOptions::new()
|
||||
.description("Successfully loaded CTS results")
|
||||
);
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to fetch results: {e}");
|
||||
toast.error(
|
||||
"Error".to_string(),
|
||||
ToastOptions::new()
|
||||
.description("Failed to fetch CTS results")
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let global_stats = use_memo(move || result
|
||||
.read()
|
||||
@@ -86,59 +262,113 @@ pub fn Landing() -> Element {
|
||||
}
|
||||
});
|
||||
|
||||
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 mut search_input: Signal<Option<String>> = use_signal(|| None);
|
||||
let mut search_name: Signal<Option<String>> = use_signal(|| None);
|
||||
let mut filter: Signal<Option<TestStatus>> = use_signal(|| None);
|
||||
let filtered_count = use_memo(move || {
|
||||
let f = filter();
|
||||
let search = search_name();
|
||||
let rows = result.read();
|
||||
|
||||
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()
|
||||
current_page.set(0_usize);
|
||||
|
||||
let mut total = 0_usize;
|
||||
|
||||
for r in rows.iter() {
|
||||
let name = &r[0];
|
||||
|
||||
if let Some(ref s) = search {
|
||||
if !name.contains(s) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(wanted) = f {
|
||||
let Ok(status) = TestStatus::from_str(&r[1]) else { continue };
|
||||
if status != wanted {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
total += 1;
|
||||
}
|
||||
|
||||
total
|
||||
});
|
||||
let mut page_count = use_memo(move || filtered_count().max(PAGE_SIZE - 1) / PAGE_SIZE);
|
||||
let page = use_memo(move || {
|
||||
let _ = *filtered_count.read();
|
||||
|
||||
let f = filter();
|
||||
let search = search_name();
|
||||
let shift = current_page() * PAGE_SIZE;
|
||||
|
||||
let rows = result.read();
|
||||
|
||||
let mut idx = 0_usize;
|
||||
let mut out = Vec::with_capacity(PAGE_SIZE);
|
||||
|
||||
for r in rows.iter() {
|
||||
let name = &r[0];
|
||||
|
||||
if let Some(ref s) = search {
|
||||
if !name.contains(s) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(wanted) = f {
|
||||
let Ok(status) = TestStatus::from_str(&r[1]) else { continue };
|
||||
if status != wanted {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if idx >= shift && idx < shift + PAGE_SIZE {
|
||||
out.push(r.clone());
|
||||
if out.len() == PAGE_SIZE {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
out
|
||||
});
|
||||
|
||||
let statuses = TestStatus::iter().enumerate().map(|(i, s)| {
|
||||
rsx! {
|
||||
SelectOption::<Option<TestStatus>> {
|
||||
index: i,
|
||||
value: s,
|
||||
text_value: "{s}",
|
||||
SelectOption::<Option<TestStatus>> { index: i, value: s, text_value: "{s}",
|
||||
{format!("{} {s}", s.emoji())}
|
||||
SelectItemIndicator {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut search_timeout: Signal<Option<TimeoutHandle>> = use_signal(|| None);
|
||||
let timeout = use_timeout(Duration::from_secs(1), move |()| {
|
||||
search_timeout.set(None);
|
||||
search_name.set(search_input());
|
||||
});
|
||||
|
||||
let onsearch_input = move |event: FormEvent| {
|
||||
if event.value().is_empty() {
|
||||
search_input.set(None);
|
||||
} else {
|
||||
search_input.set(Some(event.value()));
|
||||
}
|
||||
};
|
||||
|
||||
let onsearch_keyup = move |_| {
|
||||
if let Some(handle) = *search_timeout.read() {
|
||||
handle.cancel();
|
||||
}
|
||||
let handle = timeout.action(());
|
||||
search_timeout.set(Some(handle));
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "flex flex-col space-y-4 rounded-3xl p-4 pt-8 w-full h-fit shadow-xl shadow-slate-950",
|
||||
@@ -146,15 +376,11 @@ pub fn Landing() -> Element {
|
||||
div { class: "flex flex-row space-x-4",
|
||||
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",
|
||||
div { class: "bg-[#38bdf8] rounded-full size-3" }
|
||||
p { class: "text-xs",
|
||||
"Total: {total} 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"
|
||||
}
|
||||
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",
|
||||
@@ -163,55 +389,43 @@ pub fn Landing() -> Element {
|
||||
div { class: "mx-auto size-[200px]",
|
||||
StatsPieChart { stats: global_stats, total }
|
||||
}
|
||||
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: "mt-12 w-full flex flex-col md:flex-row justify-between text-gray-400 text-sm gap-4 items-center",
|
||||
input {
|
||||
class: "hidden lg:block w-full border-1 border-gray-700 px-3 py-1 rounded-lg text-sm",
|
||||
style: "background: radial-gradient(circle at top, rgba(56, 189, 248, 0.1), rgba(15, 23, 42, 1));",
|
||||
r#type: "search",
|
||||
placeholder: "Search tests...",
|
||||
oninput: onsearch_input,
|
||||
onkeyup: onsearch_keyup
|
||||
}
|
||||
p { class: "my-auto w-fit text-nowrap",
|
||||
"Page {current_page() + 1} of {page_count() + 1}"
|
||||
}
|
||||
div { class: "hidden lg:block",
|
||||
Pagination { current_page, page_count, small: false }
|
||||
}
|
||||
div { class: "block lg:hidden",
|
||||
Pagination { current_page, page_count, small: true }
|
||||
}
|
||||
}
|
||||
div { class: "w-full bg-gray-900 overflow-hidden border-1 border-slate-700 rounded-lg text-gray-400",
|
||||
input {
|
||||
class: "block lg:hidden w-full border-1 border-gray-700 px-3 py-1 rounded-lg text-sm",
|
||||
style: "background: radial-gradient(circle at top, rgba(56, 189, 248, 0.1), rgba(15, 23, 42, 1));",
|
||||
r#type: "search",
|
||||
placeholder: "Search tests...",
|
||||
oninput: onsearch_input,
|
||||
onkeyup: onsearch_keyup
|
||||
}
|
||||
div { class: "w-full bg-gray-900 overflow-auto border-1 border-slate-700 rounded-lg text-gray-400",
|
||||
table { class: "w-full border-collapse border-spacing-0",
|
||||
tr {
|
||||
class: "border-b-1 border-slate-700",
|
||||
style: "background: radial-gradient(circle at top, rgba(56, 189, 248, 0.1), rgba(15, 23, 42, 1));",
|
||||
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",
|
||||
"Duration (H:M:S.MS)"
|
||||
}
|
||||
th { class: "uppercase bold whitespace-nowrap py-2 px-3",
|
||||
Select::<Option<TestStatus>> {
|
||||
@@ -222,8 +436,7 @@ pub fn Landing() -> Element {
|
||||
aria_label: "Select Trigger",
|
||||
SelectValue { class: "!bg-transparent !shadow-none !text-gray-400" }
|
||||
}
|
||||
SelectList {
|
||||
aria_label: "Select status",
|
||||
SelectList { aria_label: "Select status",
|
||||
SelectGroup {
|
||||
{statuses}
|
||||
SelectOption::<Option<TestStatus>> {
|
||||
@@ -240,8 +453,15 @@ pub fn Landing() -> Element {
|
||||
}
|
||||
for test in page.iter() {
|
||||
tr { class: "text-sm hover:bg-[#38bef7]/5",
|
||||
td { class: "py-2 px-3", "{&test[0]}" }
|
||||
td { class: "py-2 px-3",
|
||||
"{&test[0]}"
|
||||
p { class: "mx-auto w-fit",
|
||||
if let Ok(duration) = test[2].parse::<f32>() {
|
||||
"{HMSDuration(Duration::from_secs_f32(duration))}"
|
||||
} else {
|
||||
"Invalid data"
|
||||
}
|
||||
}
|
||||
}
|
||||
td { class: "py-2 px-3",
|
||||
StatusBadge {
|
||||
@@ -259,6 +479,23 @@ pub fn Landing() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn StatCardPlaceholder(name: String, color: String, count: usize, stat: f32) -> Element {
|
||||
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: "flex flex-row space-x-2 flex items-center",
|
||||
div {
|
||||
class: "rounded-full size-4",
|
||||
style: format!("background-color: {color};"),
|
||||
}
|
||||
h3 { class: "text-sm text-gray-300", "{name}" }
|
||||
}
|
||||
Skeleton { class: "skeleton w-32 h-7" }
|
||||
Skeleton { class: "skeleton w-24 h-4" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn StatCard(name: String, color: String, count: usize, stat: f32) -> Element {
|
||||
rsx! {
|
||||
@@ -268,18 +505,14 @@ fn StatCard(name: String, color: String, count: usize, stat: f32) -> Element {
|
||||
class: "rounded-full size-4",
|
||||
style: format!("background-color: {color};"),
|
||||
}
|
||||
h3 { class: "text-sm text-gray-300",
|
||||
"{name}"
|
||||
}
|
||||
h3 { class: "text-sm text-gray-300", "{name}" }
|
||||
}
|
||||
h2 {
|
||||
class: "text-2xl font-bold",
|
||||
style: format!("color: {color};"),
|
||||
"{count}",
|
||||
}
|
||||
p { class: "text-xs text-gray-400",
|
||||
"{stat:.1}% of total"
|
||||
"{count}"
|
||||
}
|
||||
p { class: "text-xs text-gray-400", "{stat:.1}% of total" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,11 +531,13 @@ fn StatusBadge(status: Option<TestStatus>) -> Element {
|
||||
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};
|
||||
"#),
|
||||
style: format!(
|
||||
r#"
|
||||
background-color: {color}0F;
|
||||
color: {color};
|
||||
border-color: {color};
|
||||
"#,
|
||||
),
|
||||
"{name}"
|
||||
}
|
||||
}
|
||||
@@ -368,10 +603,138 @@ fn StatsPieChart(stats: ReadSignal<HashMap<TestStatus, usize>>, total: ReadSigna
|
||||
});
|
||||
|
||||
rsx! {
|
||||
svg {
|
||||
class: "max-w-[200px] h-auto",
|
||||
view_box: "0 0 200 200",
|
||||
{paths}
|
||||
svg { class: "max-w-[200px] h-auto", view_box: "0 0 200 200", {paths} }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Pagination(
|
||||
current_page: Memo<usize>,
|
||||
page_count: ReadSignal<usize>,
|
||||
small: bool,
|
||||
) -> Element {
|
||||
let range = if small { 1_usize } else { 2_usize };
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "flex flex-row data-[is-small=true]:flow-col gap-2 max-w-screen overflow-x-auto",
|
||||
"data-is-small": small,
|
||||
div { class: "flex flex-row gap-2",
|
||||
button {
|
||||
class: "pagination-button",
|
||||
disabled: current_page() == 0,
|
||||
onclick: move |_| current_page.set(0_usize),
|
||||
if small {
|
||||
"<<"
|
||||
} else {
|
||||
"First"
|
||||
}
|
||||
}
|
||||
if !small {
|
||||
button {
|
||||
class: "pagination-button",
|
||||
disabled: current_page() == 0,
|
||||
onclick: move |_| current_page -= 1,
|
||||
"Prev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "flex flex-row gap-2",
|
||||
if current_page() > range {
|
||||
p { "..." }
|
||||
}
|
||||
|
||||
for i in ((current_page() as i32 - range as i32).max(0)
|
||||
as usize)..=(current_page() + range).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 - range as i32).max(0) as usize {
|
||||
p { "..." }
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "flex flex-row gap-2",
|
||||
if !small {
|
||||
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()),
|
||||
if small {
|
||||
">>"
|
||||
} else {
|
||||
"Last"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn PaginationPlaceholder(small: bool) -> Element {
|
||||
let range = if small { 1_usize } else { 2_usize };
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "flex flex-row data-[is-small=true]:flow-col gap-2 max-w-screen overflow-x-auto",
|
||||
"data-is-small": small,
|
||||
div { class: "flex flex-row gap-2",
|
||||
button { class: "pagination-button", disabled: true,
|
||||
if small {
|
||||
"<<"
|
||||
} else {
|
||||
"First"
|
||||
}
|
||||
}
|
||||
if !small {
|
||||
button { class: "pagination-button", disabled: true, "Prev" }
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "flex flex-row gap-2",
|
||||
for i in 1..(if small { 3 } else { 4 }) {
|
||||
button { class: "pagination-button", disabled: true, "{i}" }
|
||||
}
|
||||
|
||||
p { "..." }
|
||||
}
|
||||
|
||||
div { class: "flex flex-row gap-2",
|
||||
if !small {
|
||||
button { class: "pagination-button", disabled: true, "Next" }
|
||||
}
|
||||
button { class: "pagination-button", disabled: true,
|
||||
if small {
|
||||
">>"
|
||||
} else {
|
||||
"Last"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_results() -> Result<String> {
|
||||
Ok(
|
||||
reqwest::get(format!("{}/assets/results.csv", std::env!("URL")))
|
||||
.await?
|
||||
.text()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
61
src/loader.rs
git.filemode.normal_file
61
src/loader.rs
git.filemode.normal_file
@@ -0,0 +1,61 @@
|
||||
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/main.rs
95
src/main.rs
@@ -1,81 +1,38 @@
|
||||
use dioxus::prelude::*;
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use csv::{ReaderBuilder, StringRecord};
|
||||
use dioxus::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
mod components;
|
||||
mod landing;
|
||||
mod loader;
|
||||
mod navbar;
|
||||
mod routes;
|
||||
|
||||
use crate::routes::Route;
|
||||
|
||||
use dioxus_primitives::toast::{ToastOptions, ToastProvider, use_toast};
|
||||
use std::time::Duration;
|
||||
|
||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
||||
const COMPONENTS_CSS: Asset = asset!("/assets/dx-components-theme.css");
|
||||
const RESULT: Asset = asset!(
|
||||
"/assets/results.csv",
|
||||
AssetOptions::builder().with_hash_suffix(false)
|
||||
);
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(AppWrapper);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn AppWrapper() -> Element {
|
||||
rsx! {
|
||||
ToastProvider { App{} }
|
||||
}
|
||||
dioxus::launch(App);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
let toast = use_toast();
|
||||
|
||||
let resource = use_resource(move || get_results());
|
||||
let mut result = use_context_provider(|| Signal::new(Vec::<StringRecord>::new()));
|
||||
|
||||
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! {
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
||||
document::Style { "
|
||||
document::Link { rel: "stylesheet", href: COMPONENTS_CSS }
|
||||
document::Style {
|
||||
"
|
||||
:root {{
|
||||
--primary-color: #000;
|
||||
--primary-color-1: #020618;
|
||||
@@ -95,30 +52,28 @@ fn App() -> Element {
|
||||
--secondary-color-6: #5d5d5d;
|
||||
|
||||
--focused-border-color: #2b7fff;
|
||||
--primary-success-color: #02271c;
|
||||
--primary-success-color: #1A7D35;
|
||||
--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));
|
||||
--contrast-error-color: var(--secondary-color-3);
|
||||
--primary-info-color: var(--primary-color-5);
|
||||
--secondary-info-color: var(--primary-color-7);
|
||||
}}
|
||||
"}
|
||||
div {
|
||||
class: "text-white min-h-screen",
|
||||
style: "background: radial-gradient(circle at top, #1e293b 0, #020617 45%, #000 100%);",
|
||||
Router::<Route> {}
|
||||
|
||||
body {{
|
||||
background-color: #000;
|
||||
}}
|
||||
"
|
||||
}
|
||||
ToastProvider { default_duration: Duration::from_secs(4), max_toasts: 1_usize,
|
||||
div {
|
||||
class: "text-white min-h-screen",
|
||||
style: "background: radial-gradient(circle at top, #1e293b 0, #020617 45%, #000 100%);",
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_results() -> Result<String> {
|
||||
Ok(
|
||||
reqwest::get(format!("{}/assets/results.csv", std::env!("URL")))
|
||||
.await?
|
||||
.text()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::routes::Route;
|
||||
use crate::{loader::Suspense, routes::Route};
|
||||
|
||||
#[component]
|
||||
pub fn Navbar() -> Element {
|
||||
rsx! {
|
||||
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 {},
|
||||
VulkanVSvg {}
|
||||
p { class: "hidden md:block mt-auto font-bold -ml-3.5 text-[#9d1b1f]",
|
||||
"ulkan"
|
||||
}
|
||||
p { class: "mt-auto ml-2 font-bold text-gray-300",
|
||||
"CTS Report"
|
||||
}
|
||||
p { class: "mt-auto ml-2 font-bold text-gray-300", "CTS Report" }
|
||||
}
|
||||
}
|
||||
|
||||
main { class: "mx-auto container mb-24",
|
||||
Outlet::<Route> {}
|
||||
Suspense { Outlet::<Route> {} }
|
||||
}
|
||||
footer { class: "w-screen h-11 flex flex-row justify-start space-x-1 px-6 text-sm text-gray-400",
|
||||
p { "Made by"}
|
||||
a { class: "hover:underline text-white",
|
||||
p { "Made by" }
|
||||
a {
|
||||
class: "hover:underline text-white",
|
||||
href: "https://portfolio.kbz8.me/",
|
||||
"kbz_8"
|
||||
}
|
||||
p { "with" }
|
||||
a { class: "hover:underline text-white",
|
||||
a {
|
||||
class: "hover:underline text-white",
|
||||
href: "https://dioxuslabs.com/",
|
||||
"Dioxus"
|
||||
}
|
||||
@@ -39,24 +40,15 @@ pub fn Navbar() -> Element {
|
||||
#[component]
|
||||
fn VulkanVSvg() -> Element {
|
||||
rsx! {
|
||||
svg {
|
||||
view_box: "0 0 192 192",
|
||||
svg { view_box: "0 0 192 192",
|
||||
g {
|
||||
transform: "translate(0.0, 192.0) scale(0.1, -0.1)",
|
||||
fill: "#9d1b1f",
|
||||
stroke: "none",
|
||||
path {
|
||||
d: "M320 1703 c1 -10 18 -70 38 -133 35 -106 40 -115 63 -114 41 2 311 33 315 36 1 2 -8 35 -21 73 -14 39 -29 89 -35 113 l-11 42 -174 0 c-160 0 -175 -1 -175 -17z",
|
||||
}
|
||||
path {
|
||||
d: "M1336 1678 c-38 -104 -77 -240 -73 -251 3 -7 31 -21 63 -31 33 -10 97 -34 143 -52 46 -19 88 -33 93 -32 8 3 138 380 138 401 0 4 -78 7 -174 7 l-174 0 -16 -42z",
|
||||
}
|
||||
path {
|
||||
d: "M550 1369 c-235 -26 -412 -107 -509 -232 l-41 -54 0 -91 c0 -89 1 -92 39 -149 50 -75 117 -142 206 -205 124 -87 339 -197 350 -178 3 6 -35 50 -84 98 -160 157 -189 276 -96 402 35 47 116 105 183 130 285 108 789 43 1208 -155 107 -50 114 -52 113 -32 0 14 -26 44 -74 86 -218 191 -465 307 -765 361 -134 24 -401 34 -530 19z",
|
||||
}
|
||||
path {
|
||||
d: "M739 1025 c-75 -15 -166 -51 -172 -68 -4 -13 221 -717 242 -755 12 -22 14 -23 192 -20 98 1 184 5 189 8 9 6 81 213 205 589 31 95 53 177 48 182 -10 10 -294 67 -308 62 -10 -3 -83 -220 -106 -312 -9 -38 -12 -42 -25 -29 -8 8 -39 91 -68 184 l-53 169 -39 2 c-21 1 -69 -4 -105 -12z",
|
||||
}
|
||||
path { d: "M320 1703 c1 -10 18 -70 38 -133 35 -106 40 -115 63 -114 41 2 311 33 315 36 1 2 -8 35 -21 73 -14 39 -29 89 -35 113 l-11 42 -174 0 c-160 0 -175 -1 -175 -17z" }
|
||||
path { d: "M1336 1678 c-38 -104 -77 -240 -73 -251 3 -7 31 -21 63 -31 33 -10 97 -34 143 -52 46 -19 88 -33 93 -32 8 3 138 380 138 401 0 4 -78 7 -174 7 l-174 0 -16 -42z" }
|
||||
path { d: "M550 1369 c-235 -26 -412 -107 -509 -232 l-41 -54 0 -91 c0 -89 1 -92 39 -149 50 -75 117 -142 206 -205 124 -87 339 -197 350 -178 3 6 -35 50 -84 98 -160 157 -189 276 -96 402 35 47 116 105 183 130 285 108 789 43 1208 -155 107 -50 114 -52 113 -32 0 14 -26 44 -74 86 -218 191 -465 307 -765 361 -134 24 -401 34 -530 19z" }
|
||||
path { d: "M739 1025 c-75 -15 -166 -51 -172 -68 -4 -13 221 -717 242 -755 12 -22 14 -23 192 -20 98 1 184 5 189 8 9 6 81 213 205 589 31 95 53 177 48 182 -10 10 -294 67 -308 62 -10 -3 -83 -220 -106 -312 -9 -38 -12 -42 -25 -29 -8 8 -39 91 -68 184 l-53 169 -39 2 c-21 1 -69 -4 -105 -12z" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
152
src/routes.rs
152
src/routes.rs
@@ -17,7 +17,157 @@ pub enum Route {
|
||||
|
||||
#[component]
|
||||
fn PageNotFound(route: Vec<String>) -> Element {
|
||||
let nav = use_navigator();
|
||||
|
||||
rsx! {
|
||||
"test"
|
||||
div { class: "w-full flex justify-center my-32",
|
||||
div { class: "flex bg-slate-800 flex-col md:flex-row justify-center items-center gap-2 border-1 border-gray-700 rounded-xl p-4 lg:p-8 w-fit shadow-xl shadow-slate-950",
|
||||
PageNotFoundSVG { size_class: "size-64 lg:size-[300px] xl:size-96" }
|
||||
div { class: "flex flex-col gap-8 items-center",
|
||||
h1 { class: "h1 text-6xl text-gray-400", "Oops!" }
|
||||
p { class: "p text-gray-500 text-2xl whitespace-pre-line text-center",
|
||||
"We couldn't find the page
|
||||
you were looking for"
|
||||
}
|
||||
Link {
|
||||
class: "mx-auto cursor-pointer hover:underline",
|
||||
to: Route::Landing {},
|
||||
"Go back home"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn PageNotFoundSVG(size_class: String) -> Element {
|
||||
rsx! {
|
||||
svg {
|
||||
class: size_class,
|
||||
"version": "1.1",
|
||||
"xmlns:svg": "http://www.w3.org/2000/svg",
|
||||
"xml:space": "preserve",
|
||||
"viewBox": "0 0 64 64",
|
||||
"fill": "#000000",
|
||||
"xmlns": "http://www.w3.org/2000/svg",
|
||||
id: "svg5",
|
||||
g { "stroke-width": "0", id: "SVGRepo_bgCarrier" }
|
||||
g {
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round",
|
||||
id: "SVGRepo_tracerCarrier",
|
||||
}
|
||||
g { id: "SVGRepo_iconCarrier",
|
||||
defs { id: "defs2" }
|
||||
g { "transform": "translate(-384,-96)", id: "layer1",
|
||||
path {
|
||||
"d": "m 393.99999,105 h 49 v 6 h -49 z",
|
||||
style: "fill:#333333;fill-opacity:1;fill-rule:evenodd;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
|
||||
id: "path27804",
|
||||
}
|
||||
path {
|
||||
"d": "m 393.99999,111 h 49 v 40 h -49 z",
|
||||
style: "fill:#acbec2;fill-opacity:1;fill-rule:evenodd;stroke-width:2.00001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
|
||||
id: "path27806",
|
||||
}
|
||||
path {
|
||||
"d": "m 393.99999,111 v 40 h 29.76954 a 28.484051,41.392605 35.599482 0 0 18.625,-40 z",
|
||||
style: "fill:#e8edee;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.00002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
|
||||
id: "path27808",
|
||||
}
|
||||
path {
|
||||
"d": "m 395.99999,104 c -1.64501,0 -3,1.355 -3,3 v 40 c 0,0.55229 0.44772,1 1,1 0.55229,0 1,-0.44771 1,-1 v -40 c 0,-0.56413 0.43587,-1 1,-1 h 45 c 0.56414,0 1,0.43587 1,1 v 3 h -42 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 h 42 v 37 c 0,0.56413 -0.43586,1 -1,1 h -49 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 h 49 c 1.64501,0 3,-1.35499 3,-3 0,-14 0,-28 0,-42 0,-1.645 -1.35499,-3 -3,-3 z",
|
||||
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
|
||||
id: "path27810",
|
||||
}
|
||||
path {
|
||||
style: "color:#000000;fill:#ed7161;fill-opacity:1;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
|
||||
"d": "m 438.99999,107 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 0.55229,0 1,-0.44771 1,-1 0,-0.55228 -0.44771,-1 -1,-1 z",
|
||||
id: "path27812",
|
||||
}
|
||||
path {
|
||||
"d": "m 434.99999,107 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 0.55229,0 1,-0.44771 1,-1 0,-0.55228 -0.44771,-1 -1,-1 z",
|
||||
style: "color:#000000;fill:#ecba16;fill-opacity:1;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
|
||||
id: "path27814",
|
||||
}
|
||||
path {
|
||||
"d": "m 430.99999,107 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 0.55229,0 1,-0.44771 1,-1 0,-0.55228 -0.44771,-1 -1,-1 z",
|
||||
style: "color:#000000;fill:#42b05c;fill-opacity:1;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
|
||||
id: "path27816",
|
||||
}
|
||||
path {
|
||||
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
|
||||
"d": "m 388.99999,150 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z",
|
||||
id: "path27818",
|
||||
}
|
||||
path {
|
||||
"d": "m 396.99999,110 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 0.55229,0 1,-0.44771 1,-1 0,-0.55228 -0.44771,-1 -1,-1 z",
|
||||
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
|
||||
id: "path27820",
|
||||
}
|
||||
rect {
|
||||
"y": "120",
|
||||
"rx": "2",
|
||||
width: "29",
|
||||
style: "fill:#256ada;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
|
||||
height: "22",
|
||||
"ry": "2",
|
||||
"x": "404",
|
||||
id: "rect4427",
|
||||
}
|
||||
path {
|
||||
"d": "m 406,120 c -1.108,0 -2,0.892 -2,2 v 18 c 0,1.108 0.892,2 2,2 h 19.58398 A 19.317461,16.374676 0 0 0 430.2207,131.36719 19.317461,16.374676 0 0 0 424.80273,120 Z",
|
||||
style: "fill:#6b9ae6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
|
||||
id: "path27648",
|
||||
}
|
||||
rect {
|
||||
height: "6",
|
||||
style: "fill:#50a824;fill-opacity:1;fill-rule:evenodd;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
|
||||
width: "29",
|
||||
"y": "120",
|
||||
"x": "404",
|
||||
id: "rect8552",
|
||||
}
|
||||
path {
|
||||
"d": "m 404,120 v 6 h 24.58984 a 14,8.5 0 0 0 0.10938,-1 14,8.5 0 0 0 -2.67969,-5 z",
|
||||
style: "fill:#83db57;fill-opacity:1;fill-rule:evenodd;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
|
||||
id: "path8626",
|
||||
}
|
||||
g { "transform": "translate(0,-4)", id: "path4429",
|
||||
path {
|
||||
"d": "m 404,130 h 29",
|
||||
style: "color:#000000;fill:#918383;fill-rule:evenodd;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
|
||||
id: "path7162",
|
||||
}
|
||||
path {
|
||||
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
|
||||
"d": "m 406,123 c -1.6447,0 -3,1.3553 -3,3 0,1.97201 0,3.94401 0,5.91602 0,0.55228 0.44772,1 1,1 0.55228,0 1,-0.44772 1,-1 V 131 h 27 v 6 c 0,0.55228 0.44772,1 1,1 0.55228,0 1,-0.44772 1,-1 0,-3.66667 0,-7.33333 0,-11 0,-1.6447 -1.3553,-3 -3,-3 z m 0,2 h 25 c 0.5713,0 1,0.4287 1,1 v 3 h -27 v -3 c 0,-0.5713 0.4287,-1 1,-1 z m -2,10 c -0.55228,0 -1,0.44772 -1,1 v 8 c 0,1.6447 1.3553,3 3,3 h 25 c 1.6447,0 3,-1.3553 3,-3 v -3 c 0,-0.55228 -0.44772,-1 -1,-1 -0.55228,0 -1,0.44772 -1,1 v 3 c 0,0.5713 -0.4287,1 -1,1 h -25 c -0.5713,0 -1,-0.4287 -1,-1 v -8 c 0,-0.55228 -0.44772,-1 -1,-1 z",
|
||||
id: "path7164",
|
||||
}
|
||||
}
|
||||
path {
|
||||
"d": "m 409.93555,129.00195 c -0.45187,0.0293 -0.82765,0.35863 -0.91602,0.80274 l -1,5 C 407.89645,135.42313 408.36944,135.99975 409,136 h 3 v 2 c 0,0.55228 0.44772,1 1,1 0.55228,0 1,-0.44772 1,-1 0,-1.66667 0,-3.33333 0,-5 0,-0.55228 -0.44772,-1 -1,-1 -0.55228,0 -1,0.44772 -1,1 v 1 h -1.78125 l 0.76172,-3.80469 c 0.10771,-0.54147 -0.24375,-1.06778 -0.78516,-1.17578 -0.0854,-0.0172 -0.17278,-0.0231 -0.25976,-0.0176 z",
|
||||
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
|
||||
id: "path8873",
|
||||
}
|
||||
path {
|
||||
style: "fill:#ffc343;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
|
||||
"d": "m 418.99999,130 c 1.10801,0 2.00002,0.89201 2.00002,2.00002 v 2.99996 c 0,1.10801 -0.89201,2.00002 -2.00002,2.00002 -1.10801,0 -2.00002,-0.89201 -2.00002,-2.00002 v -2.99996 c 0,-1.10801 0.89201,-2.00002 2.00002,-2.00002 z",
|
||||
id: "rect5745",
|
||||
}
|
||||
path {
|
||||
"d": "m 419,129 c -1.64471,0 -3,1.35529 -3,3 v 3 c 0,1.64471 1.35529,3 3,3 1.64471,0 3,-1.35529 3,-3 v -3 a 1,1 0 0 0 -1,-1 1,1 0 0 0 -1,1 v 3 c 0,0.57131 -0.42869,1 -1,1 -0.57131,0 -1,-0.42869 -1,-1 v -3 c 0,-0.57131 0.42869,-1 1,-1 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z",
|
||||
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
|
||||
id: "path7169",
|
||||
}
|
||||
path {
|
||||
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
|
||||
"d": "m 425.93555,129.00195 c -0.45187,0.0293 -0.82765,0.35863 -0.91602,0.80274 l -1,5 C 423.89645,135.42313 424.36944,135.99975 425,136 h 3 v 2 c 0,0.55228 0.44772,1 1,1 0.55228,0 1,-0.44772 1,-1 0,-1.66667 0,-3.33333 0,-5 0,-0.55228 -0.44772,-1 -1,-1 -0.55228,0 -1,0.44772 -1,1 v 1 h -1.78125 l 0.76172,-3.80469 c 0.10771,-0.54147 -0.24375,-1.06778 -0.78516,-1.17578 -0.0854,-0.0172 -0.17278,-0.0231 -0.25976,-0.0176 z",
|
||||
id: "path69785",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user