mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-12 07:03:48 -05:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efea81ef39 | ||
|
|
d3cb5af225 | ||
|
|
5904c2d2e2 | ||
|
|
674bc3d614 | ||
|
|
e4a07f9810 | ||
|
|
4e0f3c3eb9 | ||
|
|
ba20dd3086 | ||
|
|
86d5b0465c | ||
|
|
9d358ca6b2 | ||
|
|
f886601185 | ||
|
|
806364f01d | ||
|
|
d35759ec66 | ||
|
|
ffefe1db38 | ||
|
|
2f9efe22d4 | ||
|
|
bbfb3c63ea | ||
|
|
6800f1ae27 | ||
|
|
402d72c85b | ||
|
|
4f54301467 | ||
|
|
be99438123 | ||
|
|
71164417a0 | ||
|
|
59ca170592 | ||
|
|
b37b7ed060 | ||
|
|
dd68c5097a | ||
|
|
5fbbba32f1 | ||
|
|
b10af38df4 | ||
|
|
77a07013a9 | ||
|
|
ccf825df24 |
1
.github/actions/build-image/Dockerfile
vendored
1
.github/actions/build-image/Dockerfile
vendored
@@ -9,4 +9,5 @@ RUN chmod +x cup
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /cup /cup
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT ["/cup"]
|
||||
1
.github/workflows/nightly.yml
vendored
1
.github/workflows/nightly.yml
vendored
@@ -78,6 +78,7 @@ jobs:
|
||||
nightly-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- get-tag
|
||||
- build-binaries
|
||||
- build-image
|
||||
steps:
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -355,7 +355,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cup"
|
||||
version = "3.2.0"
|
||||
version = "3.2.3"
|
||||
dependencies = [
|
||||
"bollard",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cup"
|
||||
version = "3.2.0"
|
||||
version = "3.2.3"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -39,4 +39,5 @@ FROM scratch
|
||||
# Copy binary
|
||||
COPY --from=build /cup/target/release/cup /cup
|
||||
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT ["/cup"]
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"refresh_interval": {
|
||||
"type": "string",
|
||||
"description": "The interval at which Cup should check for updates. Must be a valid cron expression. Reference: https://github.com/Hexagon/croner-rust#pattern",
|
||||
"description": "The interval at which Cup should check for updates. Must be a valid cron expression. Seconds are not optional. Reference: https://github.com/Hexagon/croner-rust#pattern",
|
||||
"minLength": 11
|
||||
},
|
||||
"registries": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"@tabler/icons-react": "^3.29.0",
|
||||
"geist": "^1.3.1",
|
||||
"next": "15.1.5",
|
||||
"next": "15.2.4",
|
||||
"nextra": "^4.1.0",
|
||||
"nextra-theme-docs": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@@ -45,7 +45,7 @@ export default async function RootLayout({
|
||||
navbar={navbar}
|
||||
pageMap={await getPageMap()}
|
||||
footer={footer}
|
||||
docsRepositoryBase="https://github.com/sergi0g/cup"
|
||||
docsRepositoryBase="https://github.com/sergi0g/cup/blob/main/docs"
|
||||
>
|
||||
<div>{children}</div>
|
||||
</Layout>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Callout } from "nextra/components";
|
||||
|
||||
# Docker Compose
|
||||
|
||||
Many users find it useful to run Cup with Docker Compose, as it enables them to have it constantly running in the background and easily control it. Cup's lightweight resource use makes it ideal for this use case.
|
||||
@@ -40,4 +42,8 @@ Cup can run with a non-root user, but needs to be in a docker group. Assuming us
|
||||
user: "1000:999"
|
||||
```
|
||||
|
||||
<Callout>
|
||||
You can use the command `getent group docker | cut -d: -f3` to find the group id for the docker group.
|
||||
</Callout>
|
||||
|
||||
The compose can be customized further of course, if you choose to use a different port, another config location, or would like to change something else. Have fun!
|
||||
@@ -4,9 +4,9 @@ Cup can automatically refresh the results when running in server mode. Simply ad
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"refresh_interval": "0 0,30 * * * *" // Check twice an hour
|
||||
"refresh_interval": "0 */30 * * * *", // Check twice an hour
|
||||
// Other options
|
||||
}
|
||||
```
|
||||
|
||||
You can use a cron expression to specify the refresh interval. The reference is [here](https://github.com/Hexagon/croner-rust#pattern)
|
||||
You can use a cron expression to specify the refresh interval. Note that seconds are not optional. The reference is [here](https://github.com/Hexagon/croner-rust#pattern)
|
||||
|
||||
19
src/check.rs
19
src/check.rs
@@ -26,7 +26,7 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
|
||||
let json_url = base_url.clone() + "json";
|
||||
if refresh {
|
||||
let refresh_url = base_url + "refresh";
|
||||
match client.get(&(&refresh_url), vec![], false).await {
|
||||
match client.get(&refresh_url, &[], false).await {
|
||||
Ok(response) => {
|
||||
if response.status() != 200 {
|
||||
ctx.logger.warn(format!("GET {}: Failed to refresh server. Server returned invalid response code: {}", refresh_url, response.status()));
|
||||
@@ -40,7 +40,7 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
|
||||
}
|
||||
|
||||
}
|
||||
match client.get(&json_url, vec![], false).await {
|
||||
match client.get(&json_url, &[], false).await {
|
||||
Ok(response) => {
|
||||
if response.status() != 200 {
|
||||
ctx.logger.warn(format!("GET {}: Failed to fetch updates from server. Server returned invalid response code: {}", json_url, response.status()));
|
||||
@@ -81,20 +81,29 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
|
||||
|
||||
/// Returns a list of updates for all images passed in.
|
||||
pub async fn get_updates(
|
||||
references: &Option<Vec<String>>,
|
||||
references: &Option<Vec<String>>, // If a user requested _specific_ references to be checked, this will have a value
|
||||
refresh: bool,
|
||||
ctx: &Context,
|
||||
) -> Vec<Update> {
|
||||
let client = Client::new(ctx);
|
||||
|
||||
// Merge references argument with references from config
|
||||
let all_references = match &references {
|
||||
Some(refs) => {
|
||||
refs.clone().extend_from_slice(&ctx.config.images.extra);
|
||||
refs
|
||||
}
|
||||
None => &ctx.config.images.extra,
|
||||
};
|
||||
|
||||
// Get local images
|
||||
ctx.logger.debug("Retrieving images to be checked");
|
||||
let mut images = get_images_from_docker_daemon(ctx, references).await;
|
||||
|
||||
// Add extra images from references
|
||||
if let Some(refs) = references {
|
||||
if !all_references.is_empty() {
|
||||
let image_refs: FxHashSet<&String> = images.iter().map(|image| &image.reference).collect();
|
||||
let extra = refs
|
||||
let extra = all_references
|
||||
.iter()
|
||||
.filter(|&reference| !image_refs.contains(reference))
|
||||
.map(|reference| Image::from_reference(reference))
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Client {
|
||||
&self,
|
||||
url: &str,
|
||||
method: RequestMethod,
|
||||
headers: Vec<(&str, Option<&str>)>,
|
||||
headers: &[(&str, Option<&str>)],
|
||||
ignore_401: bool,
|
||||
) -> Result<Response, String> {
|
||||
let mut request = match method {
|
||||
@@ -51,7 +51,7 @@ impl Client {
|
||||
};
|
||||
for (name, value) in headers {
|
||||
if let Some(v) = value {
|
||||
request = request.header(name, v)
|
||||
request = request.header(*name, *v)
|
||||
}
|
||||
}
|
||||
match request.send().await {
|
||||
@@ -114,7 +114,7 @@ impl Client {
|
||||
pub async fn get(
|
||||
&self,
|
||||
url: &str,
|
||||
headers: Vec<(&str, Option<&str>)>,
|
||||
headers: &[(&str, Option<&str>)],
|
||||
ignore_401: bool,
|
||||
) -> Result<Response, String> {
|
||||
self.request(url, RequestMethod::GET, headers, ignore_401)
|
||||
@@ -124,7 +124,7 @@ impl Client {
|
||||
pub async fn head(
|
||||
&self,
|
||||
url: &str,
|
||||
headers: Vec<(&str, Option<&str>)>,
|
||||
headers: &[(&str, Option<&str>)],
|
||||
) -> Result<Response, String> {
|
||||
self.request(url, RequestMethod::HEAD, headers, false).await
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::{
|
||||
pub async fn check_auth(registry: &str, ctx: &Context, client: &Client) -> Option<String> {
|
||||
let protocol = get_protocol(registry, &ctx.config.registries);
|
||||
let url = format!("{}://{}/v2/", protocol, registry);
|
||||
let response = client.get(&url, Vec::new(), true).await;
|
||||
let response = client.get(&url, &[], true).await;
|
||||
match response {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
@@ -57,9 +57,9 @@ pub async fn get_latest_digest(
|
||||
protocol, &image.parts.registry, &image.parts.repository, &image.parts.tag
|
||||
);
|
||||
let authorization = to_bearer_string(&token);
|
||||
let headers = vec![("Accept", Some("application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json")), ("Authorization", authorization.as_deref())];
|
||||
let headers = [("Accept", Some("application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json")), ("Authorization", authorization.as_deref())];
|
||||
|
||||
let response = client.head(&url, headers).await;
|
||||
let response = client.head(&url, &headers).await;
|
||||
let time = start.elapsed().unwrap().as_millis() as u32;
|
||||
ctx.logger.debug(format!(
|
||||
"Checked for digest update to {} in {}ms",
|
||||
@@ -95,7 +95,7 @@ pub async fn get_latest_digest(
|
||||
}
|
||||
|
||||
pub async fn get_token(
|
||||
images: &Vec<&Image>,
|
||||
images: &[&Image],
|
||||
auth_url: &str,
|
||||
credentials: &Option<String>,
|
||||
client: &Client,
|
||||
@@ -105,9 +105,9 @@ pub async fn get_token(
|
||||
url = format!("{}&scope=repository:{}:pull", url, image.parts.repository);
|
||||
}
|
||||
let authorization = credentials.as_ref().map(|creds| format!("Basic {}", creds));
|
||||
let headers = vec![("Authorization", authorization.as_deref())];
|
||||
let headers = [("Authorization", authorization.as_deref())];
|
||||
|
||||
let response = client.get(&url, headers, false).await;
|
||||
let response = client.get(&url, &headers, false).await;
|
||||
let response_json = match response {
|
||||
Ok(response) => parse_json(&get_response_body(response).await),
|
||||
Err(_) => error!("GET {}: Request failed!", url),
|
||||
@@ -131,7 +131,7 @@ pub async fn get_latest_tag(
|
||||
protocol, &image.parts.registry, &image.parts.repository,
|
||||
);
|
||||
let authorization = to_bearer_string(&token);
|
||||
let headers = vec![
|
||||
let headers = [
|
||||
("Accept", Some("application/json")),
|
||||
("Authorization", authorization.as_deref()),
|
||||
];
|
||||
@@ -147,7 +147,7 @@ pub async fn get_latest_tag(
|
||||
));
|
||||
let (new_tags, next) = match get_extra_tags(
|
||||
&next_url.unwrap(),
|
||||
headers.clone(),
|
||||
&headers,
|
||||
base,
|
||||
&image.version_info.as_ref().unwrap().format_str,
|
||||
client,
|
||||
@@ -182,10 +182,7 @@ pub async fn get_latest_tag(
|
||||
));
|
||||
get_latest_digest(
|
||||
&Image {
|
||||
version_info: Some(VersionInfo {
|
||||
latest_remote_tag: Some(t.clone()),
|
||||
..image.version_info.as_ref().unwrap().clone()
|
||||
}),
|
||||
version_info: None, // Overwrite previous version info, since it isn't useful anymore (equal tags means up to date and an image is truly up to date when its digests are up to date, and we'll be checking those anyway)
|
||||
time_ms: image.time_ms + elapsed(start),
|
||||
..image.clone()
|
||||
},
|
||||
@@ -205,18 +202,21 @@ pub async fn get_latest_tag(
|
||||
}
|
||||
}
|
||||
}
|
||||
None => error!("Image {} has no remote version tags! Local tag: {}", image.reference, image.parts.tag),
|
||||
None => error!(
|
||||
"Image {} has no remote version tags! Local tag: {}",
|
||||
image.reference, image.parts.tag
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_extra_tags(
|
||||
url: &str,
|
||||
headers: Vec<(&str, Option<&str>)>,
|
||||
headers: &[(&str, Option<&str>)],
|
||||
base: &Version,
|
||||
format_str: &str,
|
||||
client: &Client,
|
||||
) -> Result<(Vec<Version>, Option<String>), String> {
|
||||
let response = client.get(url, headers, false).await;
|
||||
let response = client.get(url, &headers, false).await;
|
||||
|
||||
match response {
|
||||
Ok(res) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ use tokio::sync::Mutex;
|
||||
use tokio_cron_scheduler::{Job, JobScheduler};
|
||||
use xitca_web::{
|
||||
body::ResponseBody,
|
||||
bytes::Bytes,
|
||||
error::Error,
|
||||
handler::{handler_service, path::PathRef, state::StateRef},
|
||||
http::{StatusCode, WebResponse},
|
||||
@@ -32,9 +33,9 @@ use crate::{
|
||||
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");
|
||||
const FAVICON_ICO: Bytes = Bytes::from_static(include_bytes!("static/favicon.ico"));
|
||||
const FAVICON_SVG: Bytes = Bytes::from_static(include_bytes!("static/favicon.svg"));
|
||||
const APPLE_TOUCH_ICON: Bytes = Bytes::from_static(include_bytes!("static/apple-touch-icon.png"));
|
||||
|
||||
const SORT_ORDER: [&str; 8] = [
|
||||
"monitored_images",
|
||||
|
||||
@@ -49,18 +49,24 @@ impl Version {
|
||||
positions.push((major.start(), major.end()));
|
||||
match major.as_str().parse() {
|
||||
Ok(m) => m,
|
||||
Err(_) => return None
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
None => return None,
|
||||
};
|
||||
let minor: Option<u32> = c.name("minor").map(|minor| {
|
||||
positions.push((minor.start(), minor.end()));
|
||||
minor.as_str().parse().unwrap_or_else(|_| panic!("Minor version invalid in tag {}", tag))
|
||||
minor
|
||||
.as_str()
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("Minor version invalid in tag {}", tag))
|
||||
});
|
||||
let patch: Option<u32> = c.name("patch").map(|patch| {
|
||||
positions.push((patch.start(), patch.end()));
|
||||
patch.as_str().parse().unwrap_or_else(|_| panic!("Patch version invalid in tag {}", tag))
|
||||
patch
|
||||
.as_str()
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("Patch version invalid in tag {}", tag))
|
||||
});
|
||||
let mut format_str = tag.to_string();
|
||||
positions.reverse();
|
||||
|
||||
@@ -8,15 +8,24 @@ pub fn split(reference: &str) -> (String, String, String) {
|
||||
0 => unreachable!(),
|
||||
1 => (DEFAULT_REGISTRY, reference.to_string()),
|
||||
_ => {
|
||||
// Check if the image is from Docker Hub
|
||||
if splits[0] == "docker.io" {
|
||||
(DEFAULT_REGISTRY, splits[1..].join("/"))
|
||||
// Check if we're looking at a domain
|
||||
if splits[0] == "localhost" || splits[0].contains('.') || splits[0].contains(':') {
|
||||
} else if splits[0] == "localhost" || splits[0].contains('.') || splits[0].contains(':')
|
||||
{
|
||||
(splits[0], splits[1..].join("/"))
|
||||
} else {
|
||||
(DEFAULT_REGISTRY, reference.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
let splits = repository_and_tag.split('@').next().unwrap().split(':').collect::<Vec<&str>>();
|
||||
let splits = repository_and_tag
|
||||
.split('@')
|
||||
.next()
|
||||
.unwrap()
|
||||
.split(':')
|
||||
.collect::<Vec<&str>>();
|
||||
let (repository, tag) = match splits.len() {
|
||||
1 | 2 => {
|
||||
let repository_components = splits[0].split('/').collect::<Vec<&str>>();
|
||||
@@ -38,7 +47,9 @@ pub fn split(reference: &str) -> (String, String, String) {
|
||||
};
|
||||
(repository, tag)
|
||||
}
|
||||
_ => {dbg!(splits); panic!()},
|
||||
_ => {
|
||||
panic!("Failed to parse reference! Splits: {:?}", splits)
|
||||
}
|
||||
};
|
||||
(registry.to_string(), repository, tag.to_string())
|
||||
}
|
||||
@@ -57,6 +68,7 @@ mod tests {
|
||||
assert_eq!(split("localhost:1234/test" ), (String::from("localhost:1234" ), String::from("test" ), String::from("latest")));
|
||||
assert_eq!(split("test:1234/idk" ), (String::from("test:1234" ), String::from("idk" ), String::from("latest")));
|
||||
assert_eq!(split("alpine:3.7" ), (String::from(DEFAULT_REGISTRY ), String::from("library/alpine" ), String::from("3.7" )));
|
||||
assert_eq!(split("docker.io/library/alpine" ), (String::from(DEFAULT_REGISTRY ), String::from("library/alpine" ), String::from("latest")));
|
||||
assert_eq!(split("docker.example.com/examplerepo/alpine:3.7" ), (String::from("docker.example.com" ), String::from("examplerepo/alpine" ), String::from("3.7" )));
|
||||
assert_eq!(split("docker.example.com/examplerepo/alpine/test2:3.7" ), (String::from("docker.example.com" ), String::from("examplerepo/alpine/test2" ), String::from("3.7" )));
|
||||
assert_eq!(split("docker.example.com/examplerepo/alpine/test2/test3:3.7"), (String::from("docker.example.com" ), String::from("examplerepo/alpine/test2/test3"), String::from("3.7" )));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
{% if theme == 'neutral' %}
|
||||
<meta
|
||||
name="theme-color"
|
||||
@@ -26,9 +27,9 @@
|
||||
content="#030712"
|
||||
>
|
||||
{% endif %}
|
||||
<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">
|
||||
<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">
|
||||
<title>Cup</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -24,6 +24,7 @@ const SORT_ORDER = [
|
||||
function App() {
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
if (!data) return <Loading onLoad={setData} />;
|
||||
return (
|
||||
<div
|
||||
@@ -63,13 +64,15 @@ function App() {
|
||||
<LastChecked datetime={data.last_updated} />
|
||||
<RefreshButton />
|
||||
</div>
|
||||
<div className="flex gap-2 px-6 text-black dark:text-white">
|
||||
<Search onChange={setSearchQuery} />
|
||||
</div>
|
||||
<ul>
|
||||
{Object.entries(
|
||||
data.images.reduce<Record<string, typeof data.images>>(
|
||||
(acc, image) => {
|
||||
const server = image.server ?? "";
|
||||
if (!acc[server]) acc[server] = [];
|
||||
if (!Object.hasOwn(acc, server)) acc[server] = [];
|
||||
acc[server].push(image);
|
||||
return acc;
|
||||
},
|
||||
|
||||
14
web/src/components/Badge.tsx
Normal file
14
web/src/components/Badge.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { theme } from "../theme";
|
||||
|
||||
export default function Badge({ from, to }: { from: string; to: string }) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full bg-${theme}-50 px-2 py-1 text-xs font-medium text-${theme}-700 ring-1 ring-inset ring-${theme}-700/10 dark:bg-${theme}-400/10 dark:text-${theme}-400 dark:ring-${theme}-400/30 break-keep`}
|
||||
>
|
||||
{from}
|
||||
<ArrowRight className="size-3" />
|
||||
{to}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
TriangleAlert,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import Badge from "./Badge";
|
||||
|
||||
const clickable_registries = [
|
||||
"registry-1.docker.io",
|
||||
@@ -60,12 +61,20 @@ export default function Image({ data }: { data: Image }) {
|
||||
>
|
||||
<Box className={`size-6 shrink-0 text-${theme}-500`} />
|
||||
<span className="font-mono">{data.reference}</span>
|
||||
<div className="ml-auto flex gap-2">
|
||||
{data.result.info?.type === "version" ? (
|
||||
<Badge
|
||||
from={data.result.info.current_version}
|
||||
to={data.result.info.new_version}
|
||||
/>
|
||||
) : null}
|
||||
<WithTooltip
|
||||
text={info.description}
|
||||
className={`ml-auto size-6 shrink-0 ${info.color}`}
|
||||
className={`size-6 shrink-0 ${info.color}`}
|
||||
>
|
||||
<info.icon />
|
||||
</WithTooltip>
|
||||
</div>
|
||||
</li>
|
||||
</button>
|
||||
<Dialog open={open} onClose={setOpen} className="relative z-10">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LoaderCircle } from "lucide-react";
|
||||
export default function Loading({ onLoad }: { onLoad: (data: Data) => void }) {
|
||||
fetch(
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/api/v3/json"
|
||||
? "./api/v3/json"
|
||||
: `http://${window.location.hostname}:8000/api/v3/json`,
|
||||
).then((response) =>
|
||||
response.json().then((data) => {
|
||||
@@ -26,9 +26,9 @@ export default function Loading({ onLoad }: { onLoad: (data: Data) => void }) {
|
||||
<Logo />
|
||||
</div>
|
||||
<div
|
||||
className={`flex flex-col h-full items-center justify-center gap-1 text-${theme}-500 dark:text-${theme}-400`}
|
||||
className={`flex h-full flex-col items-center justify-center gap-1 text-${theme}-500 dark:text-${theme}-400`}
|
||||
>
|
||||
<div className="flex gap-1 mb-8">
|
||||
<div className="mb-8 flex gap-1">
|
||||
Loading <LoaderCircle className="animate-spin" />
|
||||
</div>
|
||||
<p>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function RefreshButton() {
|
||||
request.open(
|
||||
"GET",
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/api/v3/refresh"
|
||||
? "./api/v3/refresh"
|
||||
: `http://${window.location.hostname}:8000/api/v3/refresh`,
|
||||
);
|
||||
request.send();
|
||||
|
||||
@@ -23,11 +23,12 @@ export default function Search({
|
||||
onChange("");
|
||||
};
|
||||
return (
|
||||
<div className={`w-full px-6 text-black dark:text-white`}>
|
||||
<div
|
||||
className={`flex w-full items-center rounded-md border border-${theme}-200 dark:border-${theme}-700 gap-1 px-2 bg-${theme}-100 dark:bg-${theme}-900 peer flex-nowrap`}
|
||||
className={`flex w-full items-center rounded-md border border-${theme}-200 dark:border-${theme}-700 gap-1 px-2 bg-${theme}-100 dark:bg-${theme}-900 group relative flex-nowrap`}
|
||||
>
|
||||
<SearchIcon className={`size-5 text-${theme}-600 dark:text-${theme}-400`} />
|
||||
<SearchIcon
|
||||
className={`size-5 text-${theme}-600 dark:text-${theme}-400`}
|
||||
/>
|
||||
<div className="w-full">
|
||||
<input
|
||||
className={`h-10 w-full text-sm text-${theme}-800 dark:text-${theme}-200 peer bg-transparent focus:outline-none placeholder:text-${theme}-600 placeholder:dark:text-${theme}-400`}
|
||||
@@ -44,9 +45,8 @@ export default function Search({
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="relative left-1/2 h-[8px] w-0 -translate-x-1/2 -translate-y-[8px] rounded-md border-b-2 border-b-blue-600 transition-all duration-200 peer-has-[:focus]:w-full"
|
||||
className="absolute -bottom-px left-1/2 h-full w-0 -translate-x-1/2 rounded-md border-b-2 border-b-blue-600 transition-all duration-200 group-has-[:focus]:w-[calc(100%+2px)]"
|
||||
style={{ clipPath: "inset(calc(100% - 2px) 0 0 0)" }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
variants: ["hover"],
|
||||
},
|
||||
{
|
||||
pattern: /bg-(gray|neutral)-(900|950)/,
|
||||
pattern: /bg-(gray|neutral)-(400|900|950)/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
@@ -27,24 +27,16 @@ export default {
|
||||
variants: ["before:dark", "after:dark", "dark", "hover:dark"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-(50|300|200)/,
|
||||
pattern: /text-(gray|neutral)-(50|300|200|400)/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-600/,
|
||||
variants: ["dark", "hover"],
|
||||
variants: ["*", "dark", "hover", "placeholder"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-400/,
|
||||
variants: ["dark", "dark:hover"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-600/,
|
||||
variants: ["placeholder"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-400/,
|
||||
variants: ["placeholder:dark"],
|
||||
variants: ["*:dark", "dark", "dark:hover", "placeholder:dark"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-700/,
|
||||
@@ -68,5 +60,12 @@ export default {
|
||||
pattern: /border-(gray|neutral)-(700|800|900)/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
pattern: /ring-(gray|neutral)-700/,
|
||||
},
|
||||
{
|
||||
pattern: /ring-(gray|neutral)-400/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import react from "@vitejs/plugin-react-swc";
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: "./",
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// https://stackoverflow.com/q/69614671/vite-without-hash-in-filename#75344943
|
||||
|
||||
Reference in New Issue
Block a user