mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-08 05:03:49 -05:00
Changed frontend from Liquid to React, fixed bug where server would check for updates twice
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
/docs/.next
|
||||
/docs/node_modules
|
||||
/docs/out
|
||||
/src/static
|
||||
|
||||
# In case I accidentally commit mine...
|
||||
cup.json
|
||||
152
Cargo.lock
generated
152
Cargo.lock
generated
@@ -90,12 +90,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anymap2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
@@ -358,7 +352,6 @@ dependencies = [
|
||||
"http-auth",
|
||||
"indicatif",
|
||||
"json",
|
||||
"liquid",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"regex",
|
||||
@@ -388,12 +381,6 @@ dependencies = [
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "doc-comment"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
@@ -768,15 +755,6 @@ version = "1.70.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
@@ -798,16 +776,6 @@ version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd"
|
||||
|
||||
[[package]]
|
||||
name = "kstring"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -820,63 +788,6 @@ version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "liquid"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10929f201279ba14da3297b957dcda1e0bf7a6f3bb5115688be684aa8864e9cc"
|
||||
dependencies = [
|
||||
"doc-comment",
|
||||
"liquid-core",
|
||||
"liquid-derive",
|
||||
"liquid-lib",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "liquid-core"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3aef4b2160791f456eb880c990a97746f693746f92302ef5f1d06111cf14b768"
|
||||
dependencies = [
|
||||
"anymap2",
|
||||
"itertools",
|
||||
"kstring",
|
||||
"liquid-derive",
|
||||
"num-traits",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"regex",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "liquid-derive"
|
||||
version = "0.26.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915f6d0a2963a27cd5205c1902f32ddfe3bc035816afd268cf88c0fc0f8d287e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "liquid-lib"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f48fc446873f74d869582f5c4b8cbf3248c93395e410a67af5809b3731e44a"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"liquid-core",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"regex",
|
||||
"time",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
@@ -987,51 +898,6 @@ version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"pest",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.5"
|
||||
@@ -1369,12 +1235,6 @@ version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -1623,12 +1483,6 @@ version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.15"
|
||||
@@ -1650,12 +1504,6 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.13"
|
||||
|
||||
@@ -10,7 +10,6 @@ tokio = {version = "1.38.0", features = ["rt", "rt-multi-thread", "macros"]}
|
||||
ureq = { version = "2.9.7", features = ["tls"] }
|
||||
rayon = "1.10.0"
|
||||
xitca-web = { version = "0.5.0", optional = true, features = ["logger"] }
|
||||
liquid = { version = "0.26.6", optional = true }
|
||||
bollard = "0.16.1"
|
||||
once_cell = "1.19.0"
|
||||
http-auth = { version = "0.1.9", features = [] }
|
||||
@@ -21,7 +20,7 @@ json = "0.12.4"
|
||||
|
||||
[features]
|
||||
default = ["server", "cli"]
|
||||
server = ["dep:xitca-web", "dep:liquid", "dep:chrono"]
|
||||
server = ["dep:xitca-web", "dep:chrono"]
|
||||
cli = ["dep:indicatif", "dep:termsize"]
|
||||
|
||||
[profile.release]
|
||||
|
||||
7
build.sh
Executable file
7
build.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
rm -rf src/static
|
||||
cd web/
|
||||
bun run build
|
||||
cp -r dist/ ../src/static
|
||||
cargo build $@
|
||||
@@ -2,11 +2,10 @@ use std::sync::Arc;
|
||||
|
||||
use chrono::Local;
|
||||
use json::JsonValue;
|
||||
use liquid::{object, Object};
|
||||
use tokio::sync::Mutex;
|
||||
use xitca_web::{
|
||||
body::ResponseBody,
|
||||
handler::{handler_service, state::StateRef},
|
||||
handler::{handler_service, path::PathRef, state::StateRef},
|
||||
http::WebResponse,
|
||||
middleware::Logger,
|
||||
route::get,
|
||||
@@ -19,23 +18,22 @@ use crate::{
|
||||
utils::{sort_update_vec, to_json},
|
||||
};
|
||||
|
||||
const RAW_TEMPLATE: &str = include_str!("static/template.liquid");
|
||||
const STYLE: &str = include_str!("static/index.css");
|
||||
const HTML: &str = include_str!("static/index.html");
|
||||
const JS: &str = include_str!("static/assets/index.js");
|
||||
const CSS: &str = include_str!("static/assets/index.css");
|
||||
const FAVICON_ICO: &[u8] = include_bytes!("static/favicon.ico");
|
||||
const FAVICON_SVG: &[u8] = include_bytes!("static/favicon.svg");
|
||||
const APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png");
|
||||
|
||||
pub async fn serve(port: &u16, socket: Option<String>, config: JsonValue) -> std::io::Result<()> {
|
||||
let mut data = ServerData::new(socket, config).await;
|
||||
data.refresh().await;
|
||||
println!("Starting server, please wait...");
|
||||
let data = ServerData::new(socket, config).await;
|
||||
App::new()
|
||||
.with_state(Arc::new(Mutex::new(data)))
|
||||
.at("/", get(handler_service(home)))
|
||||
.at("/", get(handler_service(_static)))
|
||||
.at("/json", get(handler_service(json)))
|
||||
.at("/refresh", get(handler_service(refresh)))
|
||||
.at("/favicon.ico", handler_service(favicon_ico)) // These aren't pretty but this is xitca-web...
|
||||
.at("/favicon.svg", handler_service(favicon_svg))
|
||||
.at("/apple-touch-icon.png", handler_service(apple_touch_icon))
|
||||
.at("/*", get(handler_service(_static)))
|
||||
.enclosed(Logger::new())
|
||||
.serve()
|
||||
.bind(format!("0.0.0.0:{}", port))?
|
||||
@@ -43,8 +41,16 @@ pub async fn serve(port: &u16, socket: Option<String>, config: JsonValue) -> std
|
||||
.wait()
|
||||
}
|
||||
|
||||
async fn home(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from(data.lock().await.template.clone()))
|
||||
async fn _static(data: StateRef<'_, Arc<Mutex<ServerData>>>, path: PathRef<'_>) -> WebResponse {
|
||||
match path.0 {
|
||||
"/" => WebResponse::builder().header("Content-Type", "text/html").body(ResponseBody::from(HTML)).unwrap(),
|
||||
"/assets/index.js" => WebResponse::builder().header("Content-Type", "text/javascript").body(ResponseBody::from(JS.replace("=\"neutral\"", &format!("=\"{}\"", data.lock().await.theme)))).unwrap(),
|
||||
"/assets/index.css" => WebResponse::builder().header("Content-Type", "text/css").body(ResponseBody::from(CSS)).unwrap(),
|
||||
"/favicon.ico" => WebResponse::builder().header("Content-Type", "image/vnd.microsoft.icon").body(ResponseBody::from(FAVICON_ICO)).unwrap(),
|
||||
"/favicon.svg" => WebResponse::builder().header("Content-Type", "image/svg+xml").body(ResponseBody::from(FAVICON_SVG)).unwrap(),
|
||||
"/apple-touch-icon.png" => WebResponse::builder().header("Content-Type", "image/png").body(ResponseBody::from(APPLE_TOUCH_ICON)).unwrap(),
|
||||
_ => WebResponse::builder().status(404).body(ResponseBody::from("Not found")).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
async fn json(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||
@@ -58,37 +64,25 @@ async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from("OK"))
|
||||
}
|
||||
|
||||
async fn favicon_ico() -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from(FAVICON_ICO))
|
||||
}
|
||||
|
||||
async fn favicon_svg() -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from(FAVICON_SVG))
|
||||
}
|
||||
|
||||
async fn apple_touch_icon() -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from(APPLE_TOUCH_ICON))
|
||||
}
|
||||
|
||||
struct ServerData {
|
||||
template: String,
|
||||
raw_updates: Vec<(String, Option<bool>)>,
|
||||
json: JsonValue,
|
||||
socket: Option<String>,
|
||||
config: JsonValue,
|
||||
theme: &'static str
|
||||
}
|
||||
|
||||
impl ServerData {
|
||||
async fn new(socket: Option<String>, config: JsonValue) -> Self {
|
||||
let mut s = Self {
|
||||
socket,
|
||||
template: String::new(),
|
||||
json: json::object! {
|
||||
metrics: json::object! {},
|
||||
images: json::object! {},
|
||||
},
|
||||
raw_updates: Vec::new(),
|
||||
config,
|
||||
theme: "neutral"
|
||||
};
|
||||
s.refresh().await;
|
||||
s
|
||||
@@ -96,22 +90,10 @@ impl ServerData {
|
||||
async fn refresh(&mut self) {
|
||||
let updates = sort_update_vec(&get_all_updates(self.socket.clone(), &self.config["authentication"]).await);
|
||||
self.raw_updates = updates;
|
||||
let template = liquid::ParserBuilder::with_stdlib()
|
||||
.build()
|
||||
.unwrap()
|
||||
.parse(RAW_TEMPLATE)
|
||||
.unwrap();
|
||||
let images = self
|
||||
.raw_updates
|
||||
.iter()
|
||||
.map(|(name, has_update)| match has_update {
|
||||
Some(v) => object!({"name": name, "has_update": v.to_string()}), // Liquid kinda thinks false == nil, so we'll be comparing strings from now on
|
||||
None => object!({"name": name, "has_update": "null"}),
|
||||
})
|
||||
.collect::<Vec<Object>>();
|
||||
self.json = to_json(&self.raw_updates);
|
||||
let last_updated = Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
let theme = match &self.config["theme"].as_str() {
|
||||
let last_updated = Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
self.json["last_updated"] = last_updated.to_string().into();
|
||||
self.theme = match &self.config["theme"].as_str() {
|
||||
Some(t) => match *t {
|
||||
"default" => "neutral",
|
||||
"blue" => "gray",
|
||||
@@ -122,13 +104,5 @@ impl ServerData {
|
||||
},
|
||||
None => "neutral",
|
||||
};
|
||||
let globals = object!({
|
||||
"metrics": [{"name": "Monitored images", "value": self.json["metrics"]["monitored_images"].as_usize()}, {"name": "Up to date", "value": self.json["metrics"]["up_to_date"].as_usize()}, {"name": "Updates available", "value": self.json["metrics"]["update_available"].as_usize()}, {"name": "Unknown", "value": self.json["metrics"]["unknown"].as_usize()}],
|
||||
"images": images,
|
||||
"style": STYLE,
|
||||
"last_updated": last_updated.to_string(),
|
||||
"theme": theme
|
||||
});
|
||||
self.template = template.render(&globals).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,294 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Cup</title>
|
||||
<style>
|
||||
{{ style }}
|
||||
</style>
|
||||
<style>
|
||||
/* Kinda hacky, but thank you https://geary.co/internal-borders-css-grid/ */
|
||||
.gi {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gi::before,
|
||||
.gi::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
{% if theme == "neutral" %}
|
||||
background-color: #e5e5e5;
|
||||
{% elsif theme == "gray" %}
|
||||
background-color: #e5e7eb
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.gi::before,
|
||||
.gi::after {
|
||||
{% if theme == "neutral" %}
|
||||
background-color: #262626;
|
||||
{% elsif theme == "gray" %}
|
||||
background-color: #1f2937
|
||||
{% endif %}
|
||||
}
|
||||
}
|
||||
|
||||
.gi::before {
|
||||
inline-size: 1px;
|
||||
block-size: 100vh;
|
||||
inset-inline-start: -0.125rem;
|
||||
}
|
||||
|
||||
.gi::after {
|
||||
inline-size: 100vw;
|
||||
block-size: 1px;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: -0.12rem;
|
||||
}
|
||||
|
||||
@supports (scrollbar-color: auto) {
|
||||
html {
|
||||
scrollbar-color: #707070 #343840;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
html::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-track {
|
||||
background: #343840;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb {
|
||||
background: #707070;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb:hover {
|
||||
background: #b5b5b5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function refresh(event) {
|
||||
var button = event.currentTarget;
|
||||
button.disabled = true;
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
request.onload = function () {
|
||||
if (request.status === 200) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
request.open('GET', `${window.location.origin}/refresh`);
|
||||
request.send();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex justify-center items-center min-h-screen bg-{{ theme }}-50 dark:bg-{{ theme }}-950">
|
||||
<div class="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full">
|
||||
<div class="max-w-[48rem] mx-auto h-full my-8">
|
||||
<div class="flex items-center gap-1">
|
||||
<h1 class="text-5xl lg:text-6xl font-bold dark:text-white">Cup</h1>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 128 128"
|
||||
style="enable-background:new 0 0 128 128;"
|
||||
xml:space="preserve"
|
||||
class="size-16"
|
||||
>
|
||||
<path style="fill:#A6CFD6;" d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
|
||||
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
|
||||
C97.82,30.66,94.2,18.43,65.12,17.55z"/>
|
||||
<path style="fill:#DCEDF6;" d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
|
||||
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"/>
|
||||
<path style="fill:#6CA4AE;" d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
|
||||
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"/>
|
||||
<path style="fill:#DC0D27;" d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
|
||||
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
|
||||
C110.47,3.47,86.08,11.74,84.85,13.1z"/>
|
||||
<path style="fill:#8A1F0F;" d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
|
||||
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"/>
|
||||
<g>
|
||||
<path style="fill:#8A1F0F;" d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
|
||||
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#8A1F0F;" d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
|
||||
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#8A1F0F;" d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
|
||||
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8">
|
||||
<dl class="lg:grid-cols-4 grid-cols-2 gap-1 grid overflow-hidden *:relative">
|
||||
{% for metric in metrics %}
|
||||
<div class="gi">
|
||||
<div class="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full">
|
||||
<dt class="text-{{ theme }}-500 dark:text-{{ theme }}-400 leading-6 font-medium">
|
||||
{{ metric.name }}
|
||||
</dt>
|
||||
<div class="flex gap-1 justify-between items-center">
|
||||
<dd class="text-black dark:text-white tracking-tight leading-10 font-medium text-3xl w-full">
|
||||
{{ metric.value }}
|
||||
</dd>
|
||||
{% if metric.name == 'Monitored images' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-black dark:text-white shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 4c4.29 0 7.863 2.429 10.665 7.154l.22 .379l.045 .1l.03 .083l.014 .055l.014 .082l.011 .1v.11l-.014 .111a.992 .992 0 0 1 -.026 .11l-.039 .108l-.036 .075l-.016 .03c-2.764 4.836 -6.3 7.38 -10.555 7.499l-.313 .004c-4.396 0 -8.037 -2.549 -10.868 -7.504a1 1 0 0 1 0 -.992c2.831 -4.955 6.472 -7.504 10.868 -7.504zm0 5a3 3 0 1 0 0 6a3 3 0 0 0 0 -6z" />
|
||||
</svg>
|
||||
{% elsif metric.name == 'Up to date' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-green-500 shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" />
|
||||
</svg>
|
||||
{% elsif metric.name == 'Updates available' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-blue-500 shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z" />
|
||||
</svg>
|
||||
{% elsif metric.name == 'Unknown' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-{{ theme }}-500 shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
||||
<div class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8">
|
||||
<div class="flex justify-between items-center px-6 py-4 text-{{ theme }}-500">
|
||||
<h3>Last checked: {{ last_updated }}</h3>
|
||||
<button class="group" onclick="refresh(event)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="group-disabled:animate-spin"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" /><path d="M20,13A8.1,8.1 0 0 1 4.5,15M4,19v-4h4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-{{ theme }}-800 divide-y dark:text-white">
|
||||
{% for image in images %}
|
||||
<li>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M21 16.008v-8.018a1.98 1.98 0 0 0 -1 -1.717l-7 -4.008a2.016 2.016 0 0 0 -2 0l-7 4.008c-.619 .355 -1 1.01 -1 1.718v8.018c0 .709 .381 1.363 1 1.717l7 4.008a2.016 2.016 0 0 0 2 0l7 -4.008c.619 -.355 1 -1.01 1 -1.718z" />
|
||||
<path d="M12 22v-10" />
|
||||
<path d="M12 12l8.73 -5.04" />
|
||||
<path d="M3.27 6.96l8.73 5.04" />
|
||||
</svg>
|
||||
{{ image.name }}
|
||||
{% if image.has_update == 'false' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-green-500 ml-auto"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" />
|
||||
</svg>
|
||||
{% elsif image.has_update == 'true' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-blue-500 ml-auto"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z" />
|
||||
</svg>
|
||||
{% elsif image.has_update == 'null' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-{{ theme }}-500 ml-auto"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,17 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"src/static/template.liquid"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
safelist: [
|
||||
{
|
||||
pattern: /(bg|text|divide)-(gray|neutral)-.+/,
|
||||
variants: ["dark"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
24
web/.gitignore
vendored
Normal file
24
web/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
1
web/.tool-versions
Normal file
1
web/.tool-versions
Normal file
@@ -0,0 +1 @@
|
||||
nodejs 21.6.2
|
||||
9
web/README.md
Normal file
9
web/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Cup web frontend
|
||||
|
||||
This is the Cup web frontend, built with Vite and React. Once it's built, Cup modifies a few things (notably the theme) and sends the result to the client.
|
||||
|
||||
# Development
|
||||
|
||||
Requirements: Bun, Node.js 20+
|
||||
|
||||
Install dependencies with `bun install` and start the development server with `bun dev`.
|
||||
BIN
web/bun.lockb
Executable file
BIN
web/bun.lockb
Executable file
Binary file not shown.
36
web/eslint.config.js
Normal file
36
web/eslint.config.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
16
web/index.html
Normal file
16
web/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Cup</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
37
web/package.json
Normal file
37
web/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"fmt": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/icons-react": "^3.14.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@types/node": "^22.5.1",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.42",
|
||||
"prettier": "^3.3.3",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
6
web/postcss.config.js
Normal file
6
web/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
web/public/apple-touch-icon.png
Normal file
BIN
web/public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
web/public/favicon.ico
Normal file
BIN
web/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
29
web/public/favicon.svg
Normal file
29
web/public/favicon.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
|
||||
<path style="fill:#A6CFD6;" d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
|
||||
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
|
||||
C97.82,30.66,94.2,18.43,65.12,17.55z"/>
|
||||
<path style="fill:#DCEDF6;" d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
|
||||
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"/>
|
||||
<path style="fill:#6CA4AE;" d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
|
||||
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"/>
|
||||
<path style="fill:#DC0D27;" d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
|
||||
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
|
||||
C110.47,3.47,86.08,11.74,84.85,13.1z"/>
|
||||
<path style="fill:#8A1F0F;" d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
|
||||
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"/>
|
||||
<g>
|
||||
<path style="fill:#8A1F0F;" d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
|
||||
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#8A1F0F;" d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
|
||||
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#8A1F0F;" d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
|
||||
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
78
web/src/App.tsx
Normal file
78
web/src/App.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { MouseEvent, useState } from "react";
|
||||
import Logo from "./components/Logo";
|
||||
import Statistic from "./components/Statistic";
|
||||
import Image from "./components/Image";
|
||||
import { IconRefresh } from "@tabler/icons-react";
|
||||
import { LastChecked } from "./components/LastChecked";
|
||||
import Loading from "./components/Loading";
|
||||
import { Data } from "./types";
|
||||
|
||||
function App() {
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const theme = "neutral"; // Stupid, I know but I want both the dev and prod to work easily.
|
||||
if (!data) return <Loading onLoad={setData} />;
|
||||
const refresh = (event: MouseEvent) => {
|
||||
const btn = event.currentTarget as HTMLButtonElement;
|
||||
btn.disabled = true;
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
request.onload = function () {
|
||||
if (request.status === 200) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
request.open(
|
||||
"GET",
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/json"
|
||||
: `http://${window.location.hostname}:8000/json`,
|
||||
);
|
||||
request.send();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-center min-h-screen bg-${theme}-50 dark:bg-${theme}-950`}
|
||||
>
|
||||
<div className="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full">
|
||||
<div className="flex flex-col max-w-[48rem] mx-auto h-full my-8">
|
||||
<div className="flex items-center gap-1">
|
||||
<h1 className="text-5xl lg:text-6xl font-bold dark:text-white">
|
||||
Cup
|
||||
</h1>
|
||||
<Logo />
|
||||
</div>
|
||||
<div
|
||||
className={`shadow-sm bg-white dark:bg-${theme}-900 rounded-md my-8`}
|
||||
>
|
||||
<dl className="lg:grid-cols-4 md:grid-cols-2 gap-1 grid-cols-1 grid overflow-hidden *:relative">
|
||||
{Object.entries(data.metrics).map(([name, value]) => (
|
||||
<Statistic name={name} value={value} key={name} />
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
<div
|
||||
className={`shadow-sm bg-white dark:bg-${theme}-900 rounded-md my-8`}
|
||||
>
|
||||
<div
|
||||
className={`flex justify-between items-center px-6 py-4 text-${theme}-500`}
|
||||
>
|
||||
<LastChecked datetime={data.last_updated} />
|
||||
<button className="group" onClick={refresh}>
|
||||
<IconRefresh className="-scale-x-100 group-disabled:animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
<ul
|
||||
className={`*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-${theme}-800 divide-y dark:text-white`}
|
||||
>
|
||||
{Object.entries(data.images).map(([name, status]) => (
|
||||
<Image name={name} status={status} key={name} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
30
web/src/components/Image.tsx
Normal file
30
web/src/components/Image.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
IconCircleArrowUpFilled,
|
||||
IconCircleCheckFilled,
|
||||
IconCube,
|
||||
IconHelpCircleFilled,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
export default function Image({
|
||||
name,
|
||||
status,
|
||||
}: {
|
||||
name: string;
|
||||
status: boolean | null;
|
||||
}) {
|
||||
return (
|
||||
<li className="break-all">
|
||||
<IconCube className="size-6 shrink-0" />
|
||||
{name}
|
||||
{status == false && (
|
||||
<IconCircleCheckFilled className="text-green-500 ml-auto size-6 shrink-0" />
|
||||
)}
|
||||
{status == true && (
|
||||
<IconCircleArrowUpFilled className="text-blue-500 ml-auto size-6 shrink-0" />
|
||||
)}
|
||||
{status == null && (
|
||||
<IconHelpCircleFilled className="text-gray-500 ml-auto size-6 shrink-0" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
6
web/src/components/LastChecked.tsx
Normal file
6
web/src/components/LastChecked.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { intlFormatDistance } from "date-fns/intlFormatDistance";
|
||||
|
||||
export function LastChecked({ datetime }: { datetime: string }) {
|
||||
const date = intlFormatDistance(new Date(datetime), new Date());
|
||||
return <h3>Last checked: {date}</h3>;
|
||||
}
|
||||
34
web/src/components/Loading.tsx
Normal file
34
web/src/components/Loading.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { IconLoader2 } from "@tabler/icons-react";
|
||||
import { Data } from "../types";
|
||||
import Logo from "./Logo";
|
||||
|
||||
export default function Loading({ onLoad }: { onLoad: (data: Data) => void }) {
|
||||
const theme = "neutral";
|
||||
fetch(
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/json"
|
||||
: `http://${window.location.hostname}:8000/json`,
|
||||
).then((response) => response.json().then((data) => onLoad(data)));
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-center min-h-screen bg-${theme}-50 dark:bg-${theme}-950`}
|
||||
>
|
||||
<div className="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full absolute overflow-hidden">
|
||||
<div className="flex flex-col max-w-[48rem] mx-auto h-full my-8">
|
||||
<div className="flex items-center gap-1">
|
||||
<h1 className="text-5xl lg:text-6xl font-bold dark:text-white">
|
||||
Cup
|
||||
</h1>
|
||||
<Logo />
|
||||
</div>
|
||||
<div
|
||||
className={`h-full flex justify-center
|
||||
items-center gap-1 text-${theme}-500 dark:text-${theme}-400`}
|
||||
>
|
||||
Loading <IconLoader2 className="animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
web/src/components/Logo.tsx
Normal file
54
web/src/components/Logo.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
export default function Logo() {
|
||||
return (
|
||||
<svg viewBox="0 0 128 128" className="size-14 lg:size-16">
|
||||
<path
|
||||
style={{ fill: "#A6CFD6" }}
|
||||
d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
|
||||
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
|
||||
C97.82,30.66,94.2,18.43,65.12,17.55z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#DCEDF6" }}
|
||||
d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
|
||||
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#6CA4AE" }}
|
||||
d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
|
||||
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#DC0D27" }}
|
||||
d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
|
||||
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
|
||||
C110.47,3.47,86.08,11.74,84.85,13.1z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#8A1F0F" }}
|
||||
d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
|
||||
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: "#8A1F0F" }}
|
||||
d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
|
||||
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: "#8A1F0F" }}
|
||||
d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
|
||||
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: "#8A1F0F" }}
|
||||
d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
|
||||
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
48
web/src/components/Statistic.tsx
Normal file
48
web/src/components/Statistic.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
IconCircleArrowUpFilled,
|
||||
IconCircleCheckFilled,
|
||||
IconEyeFilled,
|
||||
IconHelpCircleFilled,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
export default function Statistic({
|
||||
name,
|
||||
value,
|
||||
}: {
|
||||
name: string;
|
||||
value: number;
|
||||
}) {
|
||||
const theme = "neutral";
|
||||
name = name.replaceAll("_", " ");
|
||||
name = name.slice(0, 1).toUpperCase() + name.slice(1); // Capitalize name
|
||||
return (
|
||||
<div
|
||||
className={`before:bg-${theme}-200 before:dark:bg-${theme}-800 after:bg-${theme}-200 after:dark:bg-${theme}-800 gi`}
|
||||
>
|
||||
<div className="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full">
|
||||
<dt
|
||||
className={`text-${theme}-500 dark:text-${theme}-400 leading-6 font-medium`}
|
||||
>
|
||||
{name}
|
||||
</dt>
|
||||
<div className="flex gap-1 justify-between items-center">
|
||||
<dd className="text-black dark:text-white tracking-tight leading-10 font-medium text-3xl w-full">
|
||||
{value}
|
||||
</dd>
|
||||
{name == "Monitored images" && (
|
||||
<IconEyeFilled className="size-6 text-black dark:text-white shrink-0" />
|
||||
)}
|
||||
{name == "Up to date" && (
|
||||
<IconCircleCheckFilled className="size-6 text-green-500 shrink-0" />
|
||||
)}
|
||||
{name == "Update available" && (
|
||||
<IconCircleArrowUpFilled className="size-6 text-blue-500 shrink-0" />
|
||||
)}
|
||||
{name == "Unknown" && (
|
||||
<IconHelpCircleFilled className="size-6 text-gray-500 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
web/src/index.css
Normal file
54
web/src/index.css
Normal file
@@ -0,0 +1,54 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Kinda hacky, but thank you https://geary.co/internal-borders-css-grid/ */
|
||||
.gi {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gi::before,
|
||||
.gi::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.gi::before {
|
||||
inline-size: 1px;
|
||||
block-size: 100vh;
|
||||
inset-inline-start: -0.125rem;
|
||||
}
|
||||
|
||||
.gi::after {
|
||||
inline-size: 100vw;
|
||||
block-size: 1px;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: -0.12rem;
|
||||
}
|
||||
|
||||
@supports (scrollbar-color: auto) {
|
||||
html {
|
||||
scrollbar-color: #707070 #343840;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
html::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-track {
|
||||
background: #343840;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb {
|
||||
background: #707070;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb:hover {
|
||||
background: #b5b5b5;
|
||||
}
|
||||
}
|
||||
10
web/src/main.tsx
Normal file
10
web/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
12
web/src/types.ts
Normal file
12
web/src/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type Data = {
|
||||
metrics: {
|
||||
monitored_images: number;
|
||||
up_to_date: number;
|
||||
update_available: number;
|
||||
unknown: number;
|
||||
};
|
||||
images: {
|
||||
[key: string]: boolean | null;
|
||||
};
|
||||
last_updated: string;
|
||||
};
|
||||
1
web/src/vite-env.d.ts
vendored
Normal file
1
web/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
37
web/tailwind.config.js
Normal file
37
web/tailwind.config.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/App.tsx", "./src/components/*.tsx"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
safelist: [
|
||||
// Generate minimum extra CSS
|
||||
{
|
||||
pattern: /bg-(gray|neutral)-50/,
|
||||
},
|
||||
{
|
||||
pattern: /bg-(gray|neutral)-(900|950)/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
pattern: /bg-(gray|neutral)-200/,
|
||||
variants: ["before", "after"],
|
||||
},
|
||||
{
|
||||
pattern: /bg-(gray|neutral)-800/,
|
||||
variants: ["before:dark", "after:dark"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-400/,
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-500/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
pattern: /divide-(gray|neutral)-800/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
],
|
||||
};
|
||||
24
web/tsconfig.app.json
Normal file
24
web/tsconfig.app.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
web/tsconfig.json
Normal file
7
web/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
22
web/tsconfig.node.json
Normal file
22
web/tsconfig.node.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
16
web/vite.config.ts
Normal file
16
web/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
rollupOptions: { // https://stackoverflow.com/q/69614671/vite-without-hash-in-filename#75344943
|
||||
output: {
|
||||
entryFileNames: `assets/[name].js`,
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
assetFileNames: `assets/[name].[ext]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user