m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-08 05:03:49 -05:00

Added tooltips, centralized theme declaration, fixed some eslint errors
Some checks failed
CI / build-binary (push) Has been cancelled
CI / build-image (push) Has been cancelled
Deploy github pages / build (push) Has been cancelled
Deploy github pages / deploy (push) Has been cancelled

This commit is contained in:
Sergio
2024-09-07 18:57:57 +03:00
parent 2c4f2a1e05
commit 572ca8858a
13 changed files with 141 additions and 51 deletions

Binary file not shown.

View File

@@ -11,10 +11,14 @@
"fmt": "prettier --write ."
},
"dependencies": {
"@radix-ui/react-tooltip": "^1.1.2",
"@tabler/icons-react": "^3.14.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.9.0",

View File

@@ -1,33 +1,16 @@
import { MouseEvent, useState } from "react";
import { useState } from "react";
import Logo from "./components/Logo";
import Statistic from "./components/Statistic";
import Image from "./components/Image";
import { LastChecked } from "./components/LastChecked";
import Loading from "./components/Loading";
import { Data } from "./types";
import { theme } from "./theme";
import RefreshButton from "./components/RefreshButton";
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 = () => {
if (request.status === 200) {
window.location.reload();
}
};
request.open(
"GET",
process.env.NODE_ENV === "production"
? "/refresh"
: `http://${window.location.hostname}:8000/refresh`
);
request.send();
};
return (
<div
className={`flex justify-center min-h-screen bg-${theme}-50 dark:bg-${theme}-950`}
@@ -56,24 +39,7 @@ function App() {
className={`flex justify-between items-center px-6 py-4 text-${theme}-500`}
>
<LastChecked datetime={data.last_updated} />
<button className="group" onClick={refresh}>
<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"
className="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>
<RefreshButton />
</div>
<ul
className={`*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-${theme}-800 divide-y dark:text-white`}

View File

@@ -4,6 +4,7 @@ import {
IconCube,
IconHelpCircleFilled,
} from "@tabler/icons-react";
import { WithTooltip } from "./Tooltip";
export default function Image({
name,
@@ -17,13 +18,28 @@ export default function Image({
<IconCube className="size-6 shrink-0" />
{name}
{status == false && (
<IconCircleCheckFilled className="text-green-500 ml-auto size-6 shrink-0" />
<WithTooltip
text="Up to date"
className="text-green-500 ml-auto size-6 shrink-0"
>
<IconCircleCheckFilled />
</WithTooltip>
)}
{status == true && (
<IconCircleArrowUpFilled className="text-blue-500 ml-auto size-6 shrink-0" />
<WithTooltip
text="Update available"
className="text-blue-500 ml-auto size-6 shrink-0"
>
<IconCircleArrowUpFilled />
</WithTooltip>
)}
{status == null && (
<IconHelpCircleFilled className="text-gray-500 ml-auto size-6 shrink-0" />
<WithTooltip
text="Unknown"
className="text-gray-500 ml-auto size-6 shrink-0"
>
<IconHelpCircleFilled />
</WithTooltip>
)}
</li>
);

View File

@@ -1,14 +1,14 @@
import { IconLoader2 } from "@tabler/icons-react";
import { Data } from "../types";
import Logo from "./Logo";
import { theme } from "../theme";
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)));
).then((response) => response.json().then((data) => {onLoad(data as Data)}));
return (
<div
className={`flex justify-center min-h-screen bg-${theme}-50 dark:bg-${theme}-950`}

View File

@@ -0,0 +1,45 @@
import { MouseEvent } from "react";
import { WithTooltip } from "./Tooltip";
export default function RefreshButton() {
const refresh = (event: MouseEvent) => {
const btn = event.currentTarget as HTMLButtonElement;
btn.disabled = true;
const request = new XMLHttpRequest();
request.onload = () => {
if (request.status === 200) {
window.location.reload();
}
};
request.open(
"GET",
process.env.NODE_ENV === "production"
? "/refresh"
: `http://${window.location.hostname}:8000/refresh`,
);
request.send();
};
return (
<WithTooltip text="Reload">
<button className="group" onClick={refresh}>
<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"
className="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>
</WithTooltip>
);
}

View File

@@ -4,6 +4,7 @@ import {
IconEyeFilled,
IconHelpCircleFilled,
} from "@tabler/icons-react";
import { theme } from "../theme";
export default function Statistic({
name,
@@ -12,7 +13,6 @@ export default function Statistic({
name: string;
value: number;
}) {
const theme = "neutral";
name = name.replaceAll("_", " ");
name = name.slice(0, 1).toUpperCase() + name.slice(1); // Capitalize name
return (

View File

@@ -0,0 +1,46 @@
import { Provider, Root, Trigger, Content } from "@radix-ui/react-tooltip";
import { cn } from "../utils";
import { forwardRef, ReactNode } from "react";
import { theme } from "../theme";
const TooltipContent = forwardRef<
React.ElementRef<typeof Content>,
React.ComponentPropsWithoutRef<typeof Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<Content
ref={ref}
sideOffset={sideOffset}
className={cn(
`z-50 overflow-hidden rounded-md border border-${theme}-200 bg-white px-3 py-1.5 text-sm text-${theme}-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-${theme}-800 dark:bg-${theme}-950 dark:text-${theme}-50`,
className,
)}
{...props}
/>
));
TooltipContent.displayName = Content.displayName;
const WithTooltip = ({
children,
text,
className,
}: {
children: ReactNode;
text: string;
className?: string;
}) => {
return (
<Provider>
<Root>
<Trigger className={className} asChild>
{children}
</Trigger>
<TooltipContent>
<p className="text-black dark:text-white">{text}</p>
</TooltipContent>
</Root>
</Provider>
);
};
export { WithTooltip };

1
web/src/theme.ts Normal file
View File

@@ -0,0 +1 @@
export const theme = "neutral"; // Will be modified by server at runtime

View File

@@ -1,12 +1,10 @@
export type Data = {
export interface Data {
metrics: {
monitored_images: number;
up_to_date: number;
update_available: number;
unknown: number;
};
images: {
[key: string]: boolean | null;
};
images: Record<string, boolean | null>;
last_updated: string;
};

6
web/src/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -4,7 +4,7 @@ export default {
theme: {
extend: {},
},
plugins: [],
plugins: [require("tailwindcss-animate")],
safelist: [
// Generate minimum extra CSS
{
@@ -33,5 +33,12 @@ export default {
pattern: /divide-(gray|neutral)-800/,
variants: ["dark"],
},
{
pattern: /border-(gray|neutral)-200/,
},
{
pattern: /border-(gray|neutral)-800/,
variants: ["dark"],
},
],
};

View File

@@ -5,7 +5,8 @@ import react from "@vitejs/plugin-react-swc";
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: { // https://stackoverflow.com/q/69614671/vite-without-hash-in-filename#75344943
rollupOptions: {
// https://stackoverflow.com/q/69614671/vite-without-hash-in-filename#75344943
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,