diff --git a/components/Footer.tsx b/components/Footer.tsx new file mode 100644 index 0000000..9153254 --- /dev/null +++ b/components/Footer.tsx @@ -0,0 +1,31 @@ +import { Divider, Text } from '@mantine/core' + +export default function Footer() { + return ( + <> + + + Open-source monitoring and status page powered by{' '} + + Uptimeflare + {' '} + and{' '} + + Cloudflare + + , made with ❤ by{' '} + + lyc8503 + + . + + + ) +} diff --git a/components/Header.tsx b/components/Header.tsx index 2e6a149..8729577 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -4,12 +4,12 @@ import { pageConfig } from '@/uptime.config' import { PageConfigLink } from '@/types/config' export default function Header() { - const linkToElement = (link: PageConfigLink) => { + const linkToElement = (link: PageConfigLink, i: number) => { return ( @@ -18,11 +18,16 @@ export default function Header() { ) } + const links = [{ label: 'Incident History', link: '/incidents' }, ...(pageConfig.links || [])] + return (
- + 🕒 @@ -39,11 +44,11 @@ export default function Header() {
- {pageConfig.links?.map(linkToElement)} + {links?.map(linkToElement)} - {pageConfig.links?.filter((link) => link.highlight).map(linkToElement)} + {links?.filter((link) => (link.highlight || link.link.startsWith('/'))).map(linkToElement)}
diff --git a/components/MaintenanceAlert.tsx b/components/MaintenanceAlert.tsx index 40f9d20..da7b2e3 100644 --- a/components/MaintenanceAlert.tsx +++ b/components/MaintenanceAlert.tsx @@ -1,4 +1,5 @@ -import { Alert, List, Text } from '@mantine/core' +import { Alert, List, Text, useMantineTheme } from '@mantine/core' +import { useMediaQuery } from '@mantine/hooks' import { IconAlertTriangle } from '@tabler/icons-react' import { MaintenanceConfig, MonitorTarget } from '@/types/config' @@ -9,24 +10,42 @@ export default function MaintenanceAlert({ maintenance: Omit & { monitors?: MonitorTarget[] } style?: React.CSSProperties }) { + const theme = useMantineTheme() + const isDesktop = useMediaQuery(`(min-width: ${theme.breakpoints.sm})`) + return ( } - title={maintenance.title || 'Scheduled Maintenance'} + title={ + + {maintenance.title || 'Scheduled Maintenance'} + + } color={maintenance.color || 'yellow'} withCloseButton={false} style={{ position: 'relative', margin: '16px auto 0 auto', ...style }} > - {/* Date range in top right */} + {/* Date range in top right (desktop) or inline (mobile) */}
From: {new Date(maintenance.start).toLocaleString()} @@ -35,7 +54,7 @@ export default function MaintenanceAlert({ {maintenance.end ? new Date(maintenance.end).toLocaleString() : 'Until further notice'}
- {maintenance.body} + {maintenance.body} {maintenance.monitors && maintenance.monitors.length > 0 && ( <> diff --git a/components/NoIncidents.tsx b/components/NoIncidents.tsx new file mode 100644 index 0000000..f3b8066 --- /dev/null +++ b/components/NoIncidents.tsx @@ -0,0 +1,29 @@ +import { Alert, Text } from '@mantine/core' +import { IconInfoCircle } from '@tabler/icons-react' + +export default function NoIncidentsAlert({ style }: { style?: React.CSSProperties }) { + return ( + } + title={ + + {'No incidents in this month'} + + } + color="gray" + withCloseButton={false} + style={{ + position: 'relative', + margin: '16px auto 0 auto', + ...style, + }} + > + There are no incidents for this month. + + ) +} diff --git a/pages/incidents.tsx b/pages/incidents.tsx new file mode 100644 index 0000000..f25451a --- /dev/null +++ b/pages/incidents.tsx @@ -0,0 +1,130 @@ +import Head from 'next/head' + +import { Inter } from 'next/font/google' +import { MaintenanceConfig, MonitorTarget } from '@/types/config' +import { maintenances, pageConfig, workerConfig } from '@/uptime.config' +import Header from '@/components/Header' +import { Box, Button, Center, Container, Group, Select } from '@mantine/core' +import Footer from '@/components/Footer' +import { useEffect, useState } from 'react' +import MaintenanceAlert from '@/components/MaintenanceAlert' +import NoIncidentsAlert from '@/components/NoIncidents' + +export const runtime = 'experimental-edge' +const inter = Inter({ subsets: ['latin'] }) + +function getSelectedMonth() { + const hash = window.location.hash.replace('#', '') + if (!hash) { + const now = new Date() + return now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + } + return hash.split('-').splice(0, 2).join('-') +} + +function filterIncidentsByMonth( + incidents: MaintenanceConfig[], + monthStr: string +): (Omit & { monitors: MonitorTarget[] })[] { + return incidents + .filter((incident) => { + const d = new Date(incident.start) + const incidentMonth = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + return incidentMonth === monthStr + }) + .map((e) => ({ + ...e, + monitors: (e.monitors || []).map((e) => workerConfig.monitors.find((mon) => mon.id === e)!), + })) + .sort((a, b) => (new Date(a.start) > new Date(b.start) ? -1 : 1)) +} + +function getPrevNextMonth(monthStr: string) { + const [year, month] = monthStr.split('-').map(Number) + const date = new Date(year, month - 1) + const prev = new Date(date) + prev.setMonth(prev.getMonth() - 1) + const next = new Date(date) + next.setMonth(next.getMonth() + 1) + return { + prev: prev.getFullYear() + '-' + String(prev.getMonth() + 1).padStart(2, '0'), + next: next.getFullYear() + '-' + String(next.getMonth() + 1).padStart(2, '0'), + } +} + +export default function IncidentsPage() { + const [selectedMonitor, setSelectedMonitor] = useState('') + const [selectedMonth, setSelectedMonth] = useState(getSelectedMonth()) + + useEffect(() => { + const onHashChange = () => setSelectedMonth(getSelectedMonth()) + window.addEventListener('hashchange', onHashChange) + return () => window.removeEventListener('hashchange', onHashChange) + }, []) + + const filteredIncidents = filterIncidentsByMonth(maintenances, selectedMonth) + const monitorFilteredIncidents = selectedMonitor + ? filteredIncidents.filter((i) => i.monitors.find((e) => e.id === selectedMonitor)) + : filteredIncidents + + const { prev, next } = getPrevNextMonth(selectedMonth) + + const monitorOptions = [ + { value: '', label: 'All' }, + ...workerConfig.monitors.map((monitor) => ({ + value: monitor.id, + label: monitor.name, + })), + ] + + return ( + <> + + {pageConfig.title} + + + +
+
+
+ + +