├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── degraded-performance.md │ ├── major-outage.md │ ├── partial-outage.md │ └── scheduled-maintenance.md └── dependabot.yml ├── .gitignore ├── README.md ├── demo-all.png ├── demo.png ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── src ├── api │ ├── client.tsx │ └── types.ts ├── app │ ├── ThemeProvider.tsx │ ├── components │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── Stack.tsx │ │ ├── components │ │ │ ├── Badge.tsx │ │ │ ├── Component.tsx │ │ │ ├── Components.tsx │ │ │ ├── Skeleton.tsx │ │ │ └── Status.tsx │ │ └── incidents │ │ │ ├── HistoricalIncidents.tsx │ │ │ ├── Incident.tsx │ │ │ ├── Incidents.tsx │ │ │ └── Skeleton.tsx │ ├── layout.tsx │ ├── page.tsx │ └── themes.ts ├── lib │ └── registry.tsx └── providers │ ├── github.test.ts │ ├── github.ts │ ├── static.test.ts │ └── static.ts ├── styled.d.ts ├── tsconfig.json └── vitest.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tadhglewis 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/degraded-performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Degraded performance 3 | about: Degraded performance 4 | title: " degraded performance" 5 | labels: incident, degraded performance 6 | --- 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/major-outage.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Major outage 3 | about: Major outage 4 | title: " major outage" 5 | labels: incident, major outage 6 | --- 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/partial-outage.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Partial outage 3 | about: Partial outage 4 | title: " partial outage" 5 | labels: incident, partial outage 6 | --- 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/scheduled-maintenance.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Scheduled maintenance 3 | about: Scheduled maintenance 4 | title: " scheduled maintenance" 5 | labels: incident, maintenance 6 | --- 7 | 8 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overhaul in progress! [You can still use version 1.1.2](https://github.com/tadhglewis/issue-status/tree/d8bc206c84f59be3feaca09a04467119895939de) or alternatively [view all previously tagged releases](https://github.com/tadhglewis/issue-status/releases) 2 | 3 | # issue-status 4 | 5 | A flexible, modern and blazingly fast ☄️ status page 6 | 7 | ![Issue Status](./demo-all.png) 8 | 9 | ## Features 10 | 11 | 💗 System health 12 | 13 | 📝 Incident history, scheduled maintenance and postmortems 14 | 15 | ⌨️ Pre-built templates 16 | 17 | 🌓 Dark mode (theming) 18 | 19 | 🛜 Hosted on GitHub Pages and more 20 | 21 | ✍️ Markdown support 22 | 23 | 🔴 Live updates 24 | 25 | ## Demo 26 | 27 | [**View demo now!**](https://tadhglewis.github.io/issue-status) 28 | 29 | This demo is hosted on GitHub Pages and using the GitHub [provider](#providers). 30 | 31 | ## Templates 32 | 33 | Pre-built incident templates are included to quickly provide updates on an incident. These templates are available when creating a GitHub Issue. 34 | 35 | You may [modify templates](./.github/ISSUE_TEMPLATE/) to suit your needs. 36 | 37 | ## Providers 38 | 39 | The data fetching layer is separated into so called _Providers_. This allows you to swap out the underlying data source that powers the frontend. 40 | 41 | Currently, only the following providers are supported: 42 | 43 | - GitHub - uses the GitHub API and GitHub Issues as a source. 44 | - Static - a testing provider with static data. 45 | 46 | **Contributions:** If you have created a custom provider which may have value to others, please feel free to reach out to discuss including it in this project. 47 | 48 | ## Theming 49 | 50 | Currently, there are two available themes which will automatically be applied based on the users system preferences: 51 | 52 | - `light` 53 | - `dark` 54 | 55 | Theming tokens are available for editing in the [themes](./src/app/themes.ts) file. 56 | -------------------------------------------------------------------------------- /demo-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tadhglewis/issue-status/522cf1064c156360bd5eb63e8e863bdf14ade73f/demo-all.png -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tadhglewis/issue-status/522cf1064c156360bd5eb63e8e863bdf14ade73f/demo.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | output: "export", 4 | compiler: { 5 | styledComponents: true, 6 | }, 7 | }; 8 | 9 | export default nextConfig; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "issue-status", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "vitest" 11 | 12 | }, 13 | "dependencies": { 14 | "@octokit/rest": "21.0.2", 15 | "dayjs": "1.11.13", 16 | "next": "15.0.4", 17 | "react": "18", 18 | "react-dom": "18", 19 | "react-markdown": "9.0.1", 20 | "remark-remove-comments": "1.0.1", 21 | "styled-components": "6.1.13" 22 | }, 23 | "devDependencies": { 24 | "@testing-library/react": "16.0.1", 25 | "@types/node": "22", 26 | "@types/react": "18", 27 | "@types/react-dom": "18", 28 | "@vitejs/plugin-react": "4.3.2", 29 | "eslint": "8", 30 | "jsdom": "25.0.1", 31 | "vitest": "2.1.8", 32 | "eslint-config-next": "15.0.4", 33 | "typescript": "5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/api/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Data } from "./types"; 4 | import { 5 | ReactNode, 6 | createContext, 7 | useContext, 8 | useEffect, 9 | useState, 10 | } from "react"; 11 | import { staticProvider } from "@/providers/static"; 12 | 13 | const DataContext = createContext(undefined); 14 | 15 | const createApiClient = () => staticProvider; 16 | 17 | export const DataProvider: React.FC<{ 18 | children: ReactNode | ReactNode[] | null; 19 | }> = ({ children }) => { 20 | const api = createApiClient(); 21 | 22 | const [state, setState] = useState({ 23 | loading: true, 24 | components: undefined, 25 | incidents: undefined, 26 | historicalIncidents: undefined, 27 | }); 28 | 29 | useEffect(() => { 30 | (async () => { 31 | setState({ 32 | loading: false, 33 | components: await api.getComponents(), 34 | incidents: await api.getIncidents(), 35 | historicalIncidents: await api.getHistoricalIncidents(), 36 | }); 37 | })(); 38 | }, [api]); 39 | 40 | return {children}; 41 | }; 42 | 43 | export const useData = () => { 44 | const data = useContext(DataContext); 45 | 46 | if (!data) { 47 | throw new Error("DataProvider was not provided"); 48 | } 49 | 50 | return data; 51 | }; 52 | -------------------------------------------------------------------------------- /src/api/types.ts: -------------------------------------------------------------------------------- 1 | type BaseComponentType = { 2 | id: string; 3 | name: string; 4 | status: 5 | | "operational" 6 | | "degradedPerformance" 7 | | "partialOutage" 8 | | "majorOutage" 9 | | "unknown"; 10 | }; 11 | 12 | export type ComponentType = BaseComponentType & { 13 | children?: BaseComponentType[]; 14 | }; 15 | 16 | export type IncidentType = { 17 | id: string; 18 | title: string; 19 | description: string; 20 | active: boolean; 21 | scheduled: boolean; 22 | createdAt: string; 23 | }; 24 | 25 | export type Data = 26 | | { 27 | loading: true; 28 | components: undefined; 29 | incidents: undefined; 30 | historicalIncidents: undefined; 31 | } 32 | | { 33 | components: ComponentType[]; 34 | incidents: IncidentType[]; 35 | historicalIncidents: IncidentType[]; 36 | loading: false; 37 | }; 38 | 39 | export type Provider = { 40 | getComponents: () => Promise | ComponentType[]; 41 | getIncidents: () => Promise | IncidentType[]; 42 | getHistoricalIncidents: () => Promise | IncidentType[]; 43 | }; 44 | -------------------------------------------------------------------------------- /src/app/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider as ScThemeProvider } from "styled-components"; 2 | import { ReactNode, useEffect, useState } from "react"; 3 | import { themes } from "./themes"; 4 | 5 | /** 6 | * Handle system theme preference 7 | * 8 | * This will prefer dark mode over light if the theme is unknown 9 | */ 10 | export const ThemeProvider: React.FC<{ 11 | children: ReactNode | ReactNode[] | null; 12 | }> = ({ children }) => { 13 | const [theme, setTheme] = useState( 14 | typeof window !== "undefined" && 15 | window.matchMedia("(prefers-color-scheme: light)").matches 16 | ? "light" 17 | : "dark" 18 | ); 19 | 20 | useEffect(() => { 21 | const onChange = (event: MediaQueryListEvent) => { 22 | setTheme(event.matches ? "light" : "dark"); 23 | }; 24 | 25 | window 26 | .matchMedia("(prefers-color-scheme: light)") 27 | .addEventListener("change", onChange); 28 | 29 | return () => { 30 | window 31 | .matchMedia("(prefers-color-scheme: light)") 32 | .removeEventListener("change", onChange); 33 | }; 34 | }); 35 | 36 | return {children}; 37 | }; 38 | -------------------------------------------------------------------------------- /src/app/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Box = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | 8 | const Link = styled.a` 9 | color: ${(props) => props.theme.colors.hintText}; 10 | font-size: 13px; 11 | text-decoration: none; 12 | transition: 0.3s; 13 | display: block; 14 | align-self: end; 15 | 16 | &:hover { 17 | opacity: 0.9; 18 | } 19 | `; 20 | 21 | export const Footer = () => ( 22 | 23 | 28 | Powered by Issue Status 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /src/app/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Title = styled.h1` 4 | color: ${(props) => props.theme.colors.text}; 5 | `; 6 | 7 | export const Header = () => 💗 Issue Status; 8 | -------------------------------------------------------------------------------- /src/app/components/Stack.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | type Space = "small" | "medium" | "large"; 4 | 5 | const spaceMap: Record = { 6 | large: 32, 7 | medium: 16, 8 | small: 8, 9 | }; 10 | 11 | export const Stack = styled.div<{ $space?: Space }>` 12 | display: flex; 13 | flex-direction: column; 14 | row-gap: ${(props) => spaceMap[props.$space ?? "small"]}px; 15 | justify-content: space-between; 16 | `; 17 | -------------------------------------------------------------------------------- /src/app/components/components/Badge.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentType } from "@/api/types"; 2 | import styled from "styled-components"; 3 | 4 | export const BaseBadge = styled.div` 5 | padding: 5px 12px; 6 | border-radius: 16px; 7 | font-size: 13px; 8 | transition: 0.3s; 9 | `; 10 | 11 | const OperationalBadge = styled(BaseBadge)` 12 | color: ${(props) => props.theme.colors.operational.text}; 13 | background-color: ${(props) => props.theme.colors.operational.background}; 14 | `; 15 | 16 | const DegradedPerformanceBadge = styled(BaseBadge)` 17 | color: ${(props) => props.theme.colors.degradedPerformance.text}; 18 | background-color: ${(props) => 19 | props.theme.colors.degradedPerformance.background}; 20 | `; 21 | 22 | const PartialOutageBadge = styled(BaseBadge)` 23 | color: ${(props) => props.theme.colors.partialOutage.text}; 24 | background-color: ${(props) => props.theme.colors.partialOutage.background}; 25 | `; 26 | 27 | const MajorOutageBadge = styled(BaseBadge)` 28 | color: ${(props) => props.theme.colors.majorOutage.text}; 29 | background-color: ${(props) => props.theme.colors.majorOutage.background}; 30 | `; 31 | 32 | const UnknownBadge = styled(BaseBadge)` 33 | color: ${(props) => props.theme.colors.unknown.text}; 34 | background-color: ${(props) => props.theme.colors.unknown.background}; 35 | `; 36 | 37 | export const Badge = ({ status }: { status: ComponentType["status"] }) => { 38 | const badges: Record = { 39 | operational: Operational, 40 | degradedPerformance: ( 41 | Degraded Performance 42 | ), 43 | partialOutage: Partial Outage, 44 | majorOutage: Major Outage, 45 | unknown: Unknown, 46 | }; 47 | 48 | return badges[status]; 49 | }; 50 | -------------------------------------------------------------------------------- /src/app/components/components/Component.tsx: -------------------------------------------------------------------------------- 1 | // TODO remove 2 | "use client"; 3 | 4 | import { ComponentType } from "@/api/types"; 5 | import styled from "styled-components"; 6 | import { Badge } from "./Badge"; 7 | import { useState } from "react"; 8 | 9 | const Box = styled.div<{ $clickable?: boolean; child?: boolean }>` 10 | background-color: ${(props) => props.theme.colors.body}; 11 | padding: 8px 16px; 12 | border-radius: 3px; 13 | justify-content: space-between; 14 | align-items: center; 15 | display: flex; 16 | color: ${(props) => props.theme.colors.text}; 17 | cursor: ${(props) => (props.$clickable ? "pointer" : "unset")}; 18 | `; 19 | 20 | export const Component = ({ name, status, children }: ComponentType) => { 21 | const [showChildren, setShowChildren] = useState(false); 22 | 23 | const chevron = showChildren ? "▾" : "▸"; 24 | 25 | return ( 26 | <> 27 | setShowChildren(!showChildren)} 29 | $clickable={Boolean(children?.length)} 30 | > 31 | {children ? chevron : null} {name} 32 | 33 | {showChildren 34 | ? children?.map((child) => ( 35 | 36 | {child.name} 37 | 38 | )) 39 | : null} 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/app/components/components/Components.tsx: -------------------------------------------------------------------------------- 1 | // TODO remove 2 | "use client"; 3 | 4 | import { useData } from "@/api/client"; 5 | import { Component } from "./Component"; 6 | import styled from "styled-components"; 7 | import { Status } from "./Status"; 8 | import { Skeleton } from "./Skeleton"; 9 | import { Stack } from "../Stack"; 10 | 11 | const Card = styled.div` 12 | box-shadow: 0px 0px 33px -32px rgba(0, 0, 0, 0.75); 13 | border-radius: 3px; 14 | background-color: ${(props) => props.theme.colors.content}; 15 | padding: 16px; 16 | `; 17 | 18 | export const Components = () => { 19 | const { components, loading } = useData(); 20 | 21 | return ( 22 | 23 | 24 | 25 | {loading ? ( 26 | <> 27 | 28 | 29 | 30 | 31 | ) : ( 32 | components.map((component) => ( 33 | 34 | )) 35 | )} 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/app/components/components/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Skeleton = styled.div` 4 | width: 100%; 5 | height: 44px; 6 | border-radius: 3px; 7 | background-color: ${(props) => props.theme.colors.body}; 8 | `; 9 | -------------------------------------------------------------------------------- /src/app/components/components/Status.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Background = styled.div` 4 | background-color: rgb(61, 167, 81); //#b1b1b1; 5 | color: white; 6 | padding: 16px; 7 | border-radius: 3px; 8 | margin-bottom: 32px; 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | flex-wrap: wrap; 13 | transition: 0.3s; 14 | `; 15 | 16 | const Title = styled.h2` 17 | font-size: 20px; 18 | margin: 0; 19 | font-weight: normal; 20 | `; 21 | 22 | export const Status = () => ( 23 | 24 | All Systems Operational 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /src/app/components/incidents/HistoricalIncidents.tsx: -------------------------------------------------------------------------------- 1 | // TODO remove 2 | "use client"; 3 | 4 | import { useData } from "@/api/client"; 5 | import styled from "styled-components"; 6 | import { Incident } from "./Incident"; 7 | import { Stack } from "../Stack"; 8 | import { Skeleton } from "./Skeleton"; 9 | 10 | const Heading = styled.div` 11 | padding: 0 16px; 12 | font-size: 20px; 13 | color: ${(props) => props.theme.colors.text}; 14 | `; 15 | 16 | export const HistoricalIncidents = () => { 17 | const { historicalIncidents, loading } = useData(); 18 | 19 | return ( 20 | 21 | Past Incidents 22 | 23 | {loading ? ( 24 | <> 25 | 26 | 27 | 28 | ) : ( 29 | historicalIncidents.map((incident) => ( 30 | 31 | )) 32 | )} 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/app/components/incidents/Incident.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore-next 2 | import remarkRemoveComments from "remark-remove-comments"; 3 | import React from "react"; 4 | import styled from "styled-components"; 5 | import ReactMarkdown from "react-markdown"; 6 | import { IncidentType } from "@/api/types"; 7 | import { BaseBadge } from "../components/Badge"; 8 | import dayjs from "dayjs"; 9 | 10 | const Card = styled.div<{ $active: boolean }>` 11 | transition: 0.3s; 12 | border-left: 16px solid 13 | ${(props) => 14 | props.$active ? "rgba(73, 144, 226, 0.2)" : "rgba(177, 177, 177,0.2)"}; 15 | background-color: ${(props) => props.theme.colors.content}; 16 | border-radius: 3px; 17 | padding: 16px; 18 | box-shadow: 0px 0px 33px -32px rgba(0, 0, 0, 0.75); 19 | `; 20 | 21 | const Details = styled.div` 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: center; 25 | margin-bottom: 3px; 26 | `; 27 | 28 | const Title = styled.div` 29 | margin-right: 16px; 30 | font-weight: bold; 31 | color: ${(props) => props.theme.colors.text}; 32 | `; 33 | 34 | const Description = styled.div` 35 | color: ${(props) => props.theme.colors.text}; 36 | `; 37 | 38 | const Status = styled(BaseBadge)<{ $active: boolean }>` 39 | color: ${(props) => 40 | props.$active 41 | ? props.theme.colors.status.active.text 42 | : props.theme.colors.status.closed.text}; 43 | background-color: ${(props) => 44 | props.$active 45 | ? props.theme.colors.status.active.background 46 | : props.theme.colors.status.closed.background}; 47 | `; 48 | 49 | const Created = styled.div` 50 | font-size: 13px; 51 | color: #6e6b6b; 52 | font-weight: bold; 53 | `; 54 | 55 | export const Incident = ({ 56 | title, 57 | active, 58 | description, 59 | createdAt, 60 | scheduled, 61 | }: IncidentType) => ( 62 | 63 |
64 | {dayjs(createdAt).format("MMMM D, YYYY h:mm A")} 65 | {scheduled ? ( 66 | Scheduled 67 | ) : ( 68 | {active ? "Active" : "Closed"} 69 | )} 70 |
71 | {title} 72 | 73 | 74 | {description} 75 | 76 | 77 |
78 | ); 79 | -------------------------------------------------------------------------------- /src/app/components/incidents/Incidents.tsx: -------------------------------------------------------------------------------- 1 | // TODO remove 2 | "use client"; 3 | 4 | import { useData } from "@/api/client"; 5 | import { Incident } from "./Incident"; 6 | import { Stack } from "../Stack"; 7 | 8 | export const Incidents = () => { 9 | const { incidents, loading } = useData(); 10 | 11 | return ( 12 | 13 | {loading 14 | ? null 15 | : incidents.map((incident) => ( 16 | 17 | ))} 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/components/incidents/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Skeleton = styled.div` 4 | width: 100%; 5 | height: 84px; 6 | border-radius: 3px; 7 | background-color: ${(props) => props.theme.colors.content}; 8 | box-shadow: 0px 0px 33px -32px rgba(0, 0, 0, 0.75); 9 | `; 10 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | // TODO remove 2 | "use client"; 3 | 4 | import { DataProvider } from "@/api/client"; 5 | import { StyledComponentsRegistry } from "@/lib/registry"; 6 | import styled from "styled-components"; 7 | import { Roboto } from "next/font/google"; 8 | import { themes } from "./themes"; 9 | import { ThemeProvider } from "./ThemeProvider"; 10 | 11 | const inter = Roboto({ weight: ["400", "500"], subsets: ["latin"] }); 12 | 13 | // TODO ?? 14 | // export const metadata: Metadata = { 15 | // title: "Issue Status", 16 | // description: "", 17 | // }; 18 | 19 | const Box = styled.div` 20 | max-width: 600px; 21 | padding: 16px; 22 | margin: 16px auto; 23 | `; 24 | 25 | const Body = styled.body` 26 | background-color: ${(props) => props.theme.colors.body}; 27 | margin: 0; 28 | `; 29 | 30 | export default function RootLayout({ 31 | children, 32 | }: Readonly<{ 33 | children: React.ReactNode; 34 | }>) { 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | {children} 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | // TODO remove 2 | "use client"; 3 | 4 | import { Components } from "./components/components/Components"; 5 | import { HistoricalIncidents } from "./components/incidents/HistoricalIncidents"; 6 | import { Header } from "./components/Header"; 7 | import { Footer } from "./components/Footer"; 8 | import { Stack } from "./components/Stack"; 9 | import { Incidents } from "./components/incidents/Incidents"; 10 | 11 | export default function Home() { 12 | return ( 13 | 14 |
15 | 16 | 17 | 18 |