wip: frontend header & overall status

pull/5/head
lyc8503 2023-10-15 02:00:29 +08:00
parent cdd43685f2
commit f9212c88a6
13 changed files with 1002 additions and 403 deletions

45
components/Header.tsx Normal file
View File

@ -0,0 +1,45 @@
import { useState } from 'react';
import { Container, Group, Burger } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { MantineLogo } from '@mantine/ds';
import classes from '@/styles/Header.module.css';
const links = [
{ link: '/about', label: 'Features' },
{ link: '/pricing', label: 'Pricing' },
{ link: '/learn', label: 'Learn' },
{ link: '/community', label: 'Community' },
];
export default function Header() {
const [opened, { toggle }] = useDisclosure(false);
const [active, setActive] = useState(links[0].link);
const items = links.map((link) => (
<a
key={link.label}
href={link.link}
className={classes.link}
data-active={active === link.link || undefined}
onClick={(event) => {
event.preventDefault();
setActive(link.link);
}}
>
{link.label}
</a>
));
return (
<header className={classes.header}>
<Container size="md" className={classes.inner}>
<MantineLogo size={28} />
<Group gap={5} visibleFrom="xs">
{items}
</Group>
<Burger opened={opened} onClick={toggle} hiddenFrom="xs" size="sm" />
</Container>
</header>
);
}

View File

@ -0,0 +1,32 @@
import { Center, Space, Title } from '@mantine/core'
import { IconCircleCheck, IconAlertCircle } from '@tabler/icons-react'
export default function OverallStatus({ state }: { state: { overallUp: number, overallDown: number, lastUpdate: number }}) {
let statusString = ''
let icon = <IconAlertCircle style={{ width: 64, height: 64 }} />
if (state.overallUp === 0 && state.overallDown === 0) {
statusString = 'No data yet'
} else if (state.overallUp === 0) {
statusString = 'All down'
} else if (state.overallDown === 0) {
statusString = 'All up'
icon = <IconCircleCheck style={{ width: 64, height: 64 }} />
} else {
statusString = `${state.overallUp} up, ${state.overallDown} down`
}
return (<>
<Center>
{icon}
</Center>
<Space h="md"/>
<Center>
<Title order={1}>{statusString}</Title>
</Center>
<Center>
<Title order={5}>Last updated on: {new Date(state.lastUpdate * 1000).toLocaleString()}</Title>
</Center>
</>)
}

865
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,10 @@
}, },
"dependencies": { "dependencies": {
"@cloudflare/workers-types": "^4.20231010.0", "@cloudflare/workers-types": "^4.20231010.0",
"@mantine/core": "^7.1.3",
"@mantine/ds": "^7.1.3",
"@mantine/hooks": "^7.1.3",
"@tabler/icons-react": "^2.39.0",
"next": "13.5.4", "next": "13.5.4",
"react": "^18", "react": "^18",
"react-dom": "^18" "react-dom": "^18"
@ -20,6 +24,9 @@
"@types/react-dom": "^18", "@types/react-dom": "^18",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "13.5.4", "eslint-config-next": "13.5.4",
"postcss": "^8.4.31",
"postcss-preset-mantine": "^1.8.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "^5", "typescript": "^5",
"wrangler": "^3.13.1" "wrangler": "^3.13.1"
} }

View File

@ -1,6 +1,15 @@
import '@/styles/globals.css' import '@mantine/core/styles.css';
import type { AppProps } from 'next/app' import type { AppProps } from 'next/app'
import { MantineProvider, createTheme } from '@mantine/core';
const theme = createTheme({
/** Put your mantine theme override here */
});
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} /> return (
<MantineProvider theme={theme}>
<Component {...pageProps} />
</MantineProvider>
)
} }

View File

@ -1,9 +1,12 @@
import { Html, Head, Main, NextScript } from 'next/document' import { Html, Head, Main, NextScript } from 'next/document'
import { ColorSchemeScript } from '@mantine/core';
export default function Document() { export default function Document() {
return ( return (
<Html lang="en"> <Html lang="en">
<Head /> <Head>
<ColorSchemeScript defaultColorScheme="auto" />
</Head>
<body> <body>
<Main /> <Main />
<NextScript /> <NextScript />

View File

@ -1,46 +1,16 @@
import Head from 'next/head' import Head from 'next/head'
import { Inter } from 'next/font/google' import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
import { MonitorState } from '@/uptime.types' import { MonitorState } from '@/uptime.types'
import { KVNamespace } from '@cloudflare/workers-types' import { KVNamespace } from '@cloudflare/workers-types'
import config from '@/uptime.config' import config from '@/uptime.config'
import OverallStatus from '@/components/OverallStatus'
import Header from '@/components/Header'
export const runtime = 'experimental-edge' export const runtime = 'experimental-edge'
const inter = Inter({ subsets: ['latin'] })
export default function Home({ state }: { state: MonitorState }) { export default function Home({ state }: { state: MonitorState }) {
// Calculate overall status
// TODO: Move this to a component
let downCount = 0
let upCount = 0
for (const monitor of config.monitors) {
if (state.history[monitor.id]?.slice(-1)[0].up) {
upCount += 1
} else {
downCount += 1
}
}
let overallStatus = ""
if (downCount === 0 && upCount === 0) {
overallStatus = "No monitors configured"
}
if (downCount === 0) {
overallStatus = "All systems operational"
}
if (upCount === 0) {
overallStatus = "All systems not operational"
}
if (upCount > 0 && downCount > 0) {
overallStatus = "Some systems not operational"
}
return ( return (
<> <>
<Head> <Head>
@ -48,11 +18,10 @@ export default function Home({ state }: { state: MonitorState }) {
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<main> <main className={inter.className}>
<h1>Uptime Flare</h1>
<h2>Current Status: {overallStatus}</h2> <Header />
<h2>Last updated on: {state.lastUpdate.toString()}</h2> <OverallStatus state={state} />
{ {
config.monitors.map(monitor => ( config.monitors.map(monitor => (

14
postcss.config.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
}

33
styles/Header.module.css Normal file
View File

@ -0,0 +1,33 @@
.header {
height: rem(56px);
margin-bottom: rem(120px);
background-color: var(--mantine-color-body);
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.inner {
height: rem(56px);
display: flex;
justify-content: space-between;
align-items: center;
}
.link {
display: block;
line-height: 1;
padding: rem(8px) rem(12px);
border-radius: var(--mantine-radius-sm);
text-decoration: none;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
font-size: var(--mantine-font-size-sm);
font-weight: 500;
@mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}
[data-mantine-color-scheme] &[data-active] {
background-color: var(--mantine-color-blue-filled);
color: var(--mantine-color-white);
}
}

View File

@ -1,229 +0,0 @@
.main {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 6rem;
min-height: 100vh;
}
.description {
display: inherit;
justify-content: inherit;
align-items: inherit;
font-size: 0.85rem;
max-width: var(--max-width);
width: 100%;
z-index: 2;
font-family: var(--font-mono);
}
.description a {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
}
.description p {
position: relative;
margin: 0;
padding: 1rem;
background-color: rgba(var(--callout-rgb), 0.5);
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
border-radius: var(--border-radius);
}
.code {
font-weight: 700;
font-family: var(--font-mono);
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(25%, auto));
max-width: 100%;
width: var(--max-width);
}
.card {
padding: 1rem 1.2rem;
border-radius: var(--border-radius);
background: rgba(var(--card-rgb), 0);
border: 1px solid rgba(var(--card-border-rgb), 0);
transition: background 200ms, border 200ms;
}
.card span {
display: inline-block;
transition: transform 200ms;
}
.card h2 {
font-weight: 600;
margin-bottom: 0.7rem;
}
.card p {
margin: 0;
opacity: 0.6;
font-size: 0.9rem;
line-height: 1.5;
max-width: 30ch;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 4rem 0;
}
.center::before {
background: var(--secondary-glow);
border-radius: 50%;
width: 480px;
height: 360px;
margin-left: -400px;
}
.center::after {
background: var(--primary-glow);
width: 240px;
height: 180px;
z-index: -1;
}
.center::before,
.center::after {
content: '';
left: 50%;
position: absolute;
filter: blur(45px);
transform: translateZ(0);
}
.logo {
position: relative;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: rgba(var(--card-rgb), 0.1);
border: 1px solid rgba(var(--card-border-rgb), 0.15);
}
.card:hover span {
transform: translateX(4px);
}
}
@media (prefers-reduced-motion) {
.card:hover span {
transform: none;
}
}
/* Mobile */
@media (max-width: 700px) {
.content {
padding: 4rem;
}
.grid {
grid-template-columns: 1fr;
margin-bottom: 120px;
max-width: 320px;
text-align: center;
}
.card {
padding: 1rem 2.5rem;
}
.card h2 {
margin-bottom: 0.5rem;
}
.center {
padding: 8rem 0 6rem;
}
.center::before {
transform: none;
height: 300px;
}
.description {
font-size: 0.8rem;
}
.description a {
padding: 1rem;
}
.description p,
.description div {
display: flex;
justify-content: center;
position: fixed;
width: 100%;
}
.description p {
align-items: center;
inset: 0 0 auto;
padding: 2rem 1rem 1.4rem;
border-radius: 0;
border: none;
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
background: linear-gradient(
to bottom,
rgba(var(--background-start-rgb), 1),
rgba(var(--callout-rgb), 0.5)
);
background-clip: padding-box;
backdrop-filter: blur(24px);
}
.description div {
align-items: flex-end;
pointer-events: none;
inset: auto 0 0;
padding: 2rem;
height: 200px;
background: linear-gradient(
to bottom,
transparent 0%,
rgb(var(--background-end-rgb)) 40%
);
z-index: 1;
}
}
/* Tablet and Smaller Desktop */
@media (min-width: 701px) and (max-width: 1120px) {
.grid {
grid-template-columns: repeat(2, 50%);
}
}
@media (prefers-color-scheme: dark) {
.vercelLogo {
filter: invert(1);
}
.logo {
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
}
}
@keyframes rotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}

View File

@ -1,107 +0,0 @@
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}

View File

@ -28,7 +28,7 @@ const config = {
method: 'GET', method: 'GET',
target: 'https://cloudflare.com', target: 'https://cloudflare.com',
expectedCode: [200], expectedCode: [200],
timeout: 10, timeout: 5,
headers: { headers: {
"User-Agent": "Uptimeflare" "User-Agent": "Uptimeflare"
}, },
@ -39,7 +39,7 @@ const config = {
name: 'Ping 1.1.1.1', name: 'Ping 1.1.1.1',
method: 'TCP_PING', method: 'TCP_PING',
target: '1.1.1.1:1234' target: '1.1.1.1:1234'
} },
] ]
} }

View File

@ -19,6 +19,7 @@ async function getStatus(monitor: MonitorTarget): Promise<{ ping: number; up: bo
if (monitor.method === "TCP_PING") { if (monitor.method === "TCP_PING") {
// TCP port endpoint monitor // TCP port endpoint monitor
// TODO: TCP timeout
try { try {
const [hostname, port] = monitor.target.split(":") const [hostname, port] = monitor.target.split(":")
@ -90,6 +91,9 @@ export default {
state.overallUp = 0 state.overallUp = 0
// Check each monitor // Check each monitor
// TODO: callback exception handler
// TODO: advanced status check
// TODO: concurrent status check
for (const monitor of config.monitors) { for (const monitor of config.monitors) {
console.log(`[${workerLocation}] Checking ${monitor.name}...`) console.log(`[${workerLocation}] Checking ${monitor.name}...`)