Merge branch 'feature/themes' into 'main'

Merge styled-components

Closes #17, #15, and #21

See merge request neshura-websites/www!7
This commit is contained in:
Neshura 2022-12-16 22:01:04 +00:00
commit 998fcc9514
23 changed files with 2279 additions and 538 deletions

View file

@ -1,6 +1,6 @@
## INIT STEP ## INIT STEP
# Install dependencies only when needed # Install dependencies only when needed
FROM node:16-alpine AS deps FROM node:18-alpine AS deps
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
@ -11,7 +11,7 @@ RUN yarn install --frozen-lockfile
## BUILD STEP ## BUILD STEP
# Rebuild the source code only when needed # Rebuild the source code only when needed
FROM node:16-alpine AS builder FROM node:18-alpine AS builder
WORKDIR /app WORKDIR /app
@ -23,7 +23,7 @@ COPY . .
RUN yarn build RUN yarn build
## RUN STEP ## RUN STEP
FROM node:16-alpine AS runner FROM node:18-alpine AS runner
LABEL author="neshura@proton.me" LABEL author="neshura@proton.me"
WORKDIR /usr/src/app WORKDIR /usr/src/app

View file

@ -1,11 +1,11 @@
import styles from '/styles/Home.module.css' import { Footer } from "../components/styles/generic"
const Footer = () => { const PageFooter = () => {
return ( return (
<footer className={styles.footer}> <Footer>
Built using Next.js Built using Next.js
</footer> </Footer>
); );
} }
export default Footer; export default PageFooter;

View file

@ -1,11 +1,12 @@
import Footer from './footer' import PageFooter from './footer'
import Navbar from './navbar' import PageNavbar from './navbar'
import styles from '/styles/Home.module.css'
import Script from 'next/script' import Script from 'next/script'
import { Page, Main } from './styles/generic'
const Layout = ({ children }: { children: React.ReactNode }) => { const Layout = ({ children }: { children: React.ReactNode }) => {
return ( return (
<div className={styles.page}> <Page>
<Script id="matomo_analytics"> <Script id="matomo_analytics">
{` {`
var _paq = window._paq = window._paq || []; var _paq = window._paq = window._paq || [];
@ -25,12 +26,12 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
`} `}
</Script> </Script>
<Navbar /> <PageNavbar />
<main className={styles.main}> <Main>
{children} {children}
</main> </Main>
<Footer /> <PageFooter />
</div> </Page>
); );
} }

View file

@ -1,6 +1,6 @@
import styles from '/styles/Home.module.css' import { usePathname } from 'next/navigation'
import Link from 'next/link' import { NavBar, NavLink, NavWrap } from './styles/navbar';
import { useRouter } from 'next/router' import { StyleSelector, StyleSelectorPlaceholder } from './themeselector';
const navLinks = [ const navLinks = [
{ name: "Home", href: "/" }, { name: "Home", href: "/" },
@ -9,22 +9,25 @@ const navLinks = [
{ name: "Services", href: "/services" } { name: "Services", href: "/services" }
] ]
const Navbar = () => { const PageNavbar = () => {
const router = useRouter(); const path = usePathname();
return ( return (
<nav className={styles.navbar}> <NavWrap>
{navLinks.map((item, index) => ( <StyleSelector></StyleSelector>
<Link key={item.name} href={item.href}> <NavBar>
<a className={router.pathname == item.href ? styles.navelem_active : styles.navelem}>{item.name}</a> {navLinks.map((item) => (
</Link> <NavLink active={path == item.href ? true : false} key={item.name} href={item.href}>
{item.name}
</NavLink>
))} ))}
<Link key="Mastodon_Verify" href="https://mastodon.neshweb.net/@neshura"> <NavLink key="Mastodon_Verify" rel="me" href="https://mastodon.neshweb.net/@neshura">
<a className={styles.navelem} rel="me" href="https://mastodon.neshweb.net/@neshura">Mastodon</a> Mastodon
</Link> </NavLink>
</NavBar>
</nav> <StyleSelectorPlaceholder></StyleSelectorPlaceholder>
</NavWrap>
); );
} }
export default Navbar; export default PageNavbar;

View file

@ -0,0 +1,154 @@
import Link from 'next/link';
import Image from 'next/image';
import styled, { DefaultTheme } from 'styled-components';
import { CustomLink } from '../../interfaces/LinkTypes';
import { Service } from '../../interfaces/Services';
// needed for Online Status checks
// TODO: migrate to shared Status type for Games and Services
interface OnlinePropType {
status: string;
}
// replaces .title
export const PageTitle = styled.h1`
margin: 0;
line-height: 1.15;
font-size: 4rem;
text-align: center;
`
// replaces .description
export const PageDescription = styled.p`
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
text-align: center;
`
// replaces .grid
export const PageContentBox = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 80%;
`
// replaces .card & .contentcard
export const PageCard = styled.div`
margin: 1rem;
padding: 1rem;
text-align: center;
color: ${({ theme }) => theme.colors.primary};
text-decoration: none;
border: 1px solid;
border-radius: 10px;
border-color: ${({ theme }) => theme.colors.primary};
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
p {
margin: 0;
font-size: 1.2rem;
line-height: 1.5;
}
&:focus,:active,:hover {
color: ${({ theme }) => theme.colors.secondary};
border-color: ${({ theme }) => theme.colors.secondary};
}
`
// replaces the three status classes
const OnlineStatus = styled.p<OnlinePropType>`
color: ${props => {
let ret;
switch (props.status) {
case "Online":
ret = ({ theme }: { theme: DefaultTheme }) => theme.colors.online;
break;
case "Loading":
ret = ({ theme }: { theme: DefaultTheme }) => theme.colors.loading;
break;
case "Offline":
ret = ({ theme }: { theme: DefaultTheme }) => theme.colors.offline;
break;
default:
ret = ({ theme }: { theme: DefaultTheme }) => theme.colors.offline;
}
return ret;
}};
`
// replaces .cardwarn
const CardContentWarning = styled.p`
color: ${({ theme }) => theme.colors.secondary};
`
// replaces .contentIcon
const CardContentTitleIcon = styled.div`
object-fit: "contain";
margin-right: 0.4rem;
position: relative;
aspect-ratio: 1;
height: 1.5rem;
`
// replaces .contentTitle
const CardContentTitleWrap = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-bottom: 1rem;
h2 {
margin: 0;
white-space: nowrap;
}
`
const CardContentTitle = ({ content }: { content: Service | CustomLink }) => {
return (
<CardContentTitleWrap>
{
content.icon ? (
<CardContentTitleIcon>
<Image alt="icon" src={content.icon} fill></Image>
</CardContentTitleIcon>
) : (<></>)
}
<h2>{content.name}</h2>
</CardContentTitleWrap>
)
}
// Card Content Component for Games Page
export const CardContentGame = ({ content }: { content: CustomLink }) => {
return (
<>
<CardContentTitle content={content} />
<p>{content.desc}</p>
<p>{content.ip}</p>
<OnlineStatus status={content.status}>{content.status}</OnlineStatus>
</>
)
}
// Card Content Component for Services Page
export const CardContentService = ({ content }: { content: Service }) => {
return (
<>
<CardContentTitle content={content} />
<OnlineStatus status={content.status}>{content.status}</OnlineStatus>
<p>{content.desc}</p>
<CardContentWarning>{content.warn}</CardContentWarning>
</>
)
}

View file

@ -0,0 +1,42 @@
import styled from 'styled-components'
export const StyledBody = styled.body`
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
`
export const Page = styled.div`
width: 100%;
background-color: ${({ theme }) => theme.colors.background};
`
export const Main = styled.main`
color: ${({ theme }) => theme.colors.primary };
min-height: 100vh;
padding: 1rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
`
export const Footer = styled.footer`
color: ${({ theme }) => theme.colors.primary };
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid ${({ theme }) => theme.colors.primary };
justify-content: center;
align-items: center;
`
// TODO
/* .footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
} */

View file

@ -0,0 +1,40 @@
import styled from 'styled-components'
import Link from 'next/link';
interface ActivePropType {
active?: false | true;
}
export const NavWrap = styled.div`
display: flex;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
border-bottom: 1px solid ${({ theme }) => theme.colors.primary};
`
export const NavBar = styled.nav`
margin-right: 1%;
display: flex;
flex: 1;
padding: 2rem 0;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
`
export const NavLink = styled(Link)<ActivePropType>`
color: ${props => props.active ? ({ theme }) => theme.colors.secondary : ({ theme }) => theme.colors.primary};
margin: 0.2rem;
border: 1px solid;
padding: 0.2rem 0.5rem;
display: flex;
justify-content: center;
align-items: center;
transition: color 0.15s ease, border-color 0.15s ease;
&:hover {
color: ${({ theme }) => theme.colors.secondary};
}
`

View file

@ -0,0 +1,53 @@
import styled from 'styled-components';
interface DisplayPropType {
show?: false | true;
}
interface ActivePropType {
active?: false | true;
}
export const ThemeDropDown = styled.div`
margin-left: 1%;
min-width: 180px;
color: ${({ theme }) => theme.colors.primary}
display: flex;
flex-direction: column;
`
export const ThemeDropDownButton = styled.button`
width: 90%;
border: 1px solid;
background-color: ${({ theme }) => theme.colors.background};
padding: 0.2rem 0.5rem;
cursor: pointer;
color: ${({ theme }) => theme.colors.primary};
transition: color 0.15s ease;
&:focus,:hover {
color: ${({ theme }) => theme.colors.secondary};
}
`
export const ThemeDropDownOptions = styled.div<DisplayPropType>`
position: absolute;
color: ${({ theme }) => theme.colors.primary};
background-color: ${({ theme }) => theme.colors.background};
display: ${ props => props.show ? "flex" : "none" };
flex-direction: column;
min-width: 160px;
z-index: 1;
`
export const ThemeDropDownOption = styled.button<ActivePropType>`
color: ${ props => props.active ? ({ theme }) => theme.colors.secondary : ({ theme }) => theme.colors.primary };
background-color: ${({ theme }) => theme.colors.background};
border: 1px solid;
padding: 0.2rem 0.5rem;
text-decoration: none;
&:hover {
color: ${({ theme }) => theme.colors.secondary};
}
`

72
components/themes.tsx Normal file
View file

@ -0,0 +1,72 @@
// Probably a good idea to spread this out into multiple files under a folder once it gets bigger
import { createGlobalStyle, DefaultTheme } from 'styled-components'
export const GlobalStyle = createGlobalStyle`
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
`
export const lightTheme: DefaultTheme = {
themeName: "Light Theme",
themeId: 0,
colors: {
background: '#ffffff',
primary: '#00aaff',
secondary:'#ff5300',
online: '#00ff00',
loading: '#ff5300',
offline: '#ff0000',
},
}
export const darkTheme: DefaultTheme = {
themeName: "Dark Theme",
themeId: 1,
colors: {
background: '#1f1f1f',
primary: '#00aaff',
secondary:'#ff5300',
online: '#00ff00',
loading: '#ff5300',
offline: '#ff0000',
},
}
export const amoledTheme: DefaultTheme = {
themeName: "AMOLED Theme",
themeId: 2,
colors: {
background: '#000000',
primary: '#00aaff',
secondary:'#ff5300',
online: '#00ff00',
loading: '#ff5300',
offline: '#ff0000',
},
}
export const amoled2Theme: DefaultTheme = {
themeName: "AMOLED Theme 2",
themeId: 3,
colors: {
background: '#000000',
primary: '#00ffaa',
secondary:'#aa00ff',
online: '#00ff00',
loading: '#ff5300',
offline: '#ff0000',
},
}

View file

@ -0,0 +1,99 @@
import { useUpdateTheme } from "../pages/_app";
import { Fragment, useContext, useState } from 'react';
import { ThemeContext, DefaultTheme } from "styled-components";
import { darkTheme, amoledTheme, lightTheme, amoled2Theme } from './themes';
import { ThemeDropDown, ThemeDropDownButton, ThemeDropDownOption, ThemeDropDownOptions } from "./styles/themedropdown";
const themes = [
lightTheme,
darkTheme,
amoledTheme,
amoled2Theme,
]
export const StyleSelector = () => {
const updateTheme = useUpdateTheme();
const currentTheme = useContext(ThemeContext);
const [selectedTheme, setSelectedTheme] = useState(themes[currentTheme.themeId]);
if(currentTheme !== selectedTheme) {
setSelectedTheme(currentTheme);
}
const updateThemeWithStorage = (newTheme: DefaultTheme) => {
if (newTheme.themeId === lightTheme.themeId) {
updateLightTheme(newTheme);
}
else {
setSelectedTheme(newTheme);
localStorage.setItem("theme", newTheme.themeId.toString());
updateTheme(newTheme);
}
}
const updateLightTheme = (newTheme: DefaultTheme) => {
if (confirm("Really switch to Light Mode?")) {
setSelectedTheme(newTheme);
localStorage.setItem("theme", newTheme.themeId.toString());
updateTheme(newTheme)
}
else {
}
}
const [test, setTest] = useState(false);
function handleBlur(event:any) {
console.log(event)
if (!event.currentTarget.contains(event.relatedTarget)) {
setTest(false);
}
}
return (
<ThemeDropDown onBlur={(event) => handleBlur(event)}>
<ThemeDropDownButton onClick={() => setTest(test => !test)}>{selectedTheme.themeName}</ThemeDropDownButton>
<ThemeDropDownOptions id="themesDropdown" show={test}>
{themes.map((theme) => (
<ThemeDropDownOption active={theme === selectedTheme} key={theme.themeId} onClick={() => updateThemeWithStorage(theme)}>
{theme.themeName}
</ThemeDropDownOption>
))}
</ThemeDropDownOptions>
</ThemeDropDown>
);
}
export const StyleSelectorPlaceholder = () => {
return (
<ThemeDropDown></ThemeDropDown>
)
}
export function getTheme(themeId: number): DefaultTheme {
let theme: DefaultTheme;
switch (themeId) {
case 0:
theme = lightTheme;
break;
case 1:
theme = darkTheme;
break;
case 2:
theme = amoledTheme;
break;
case 3:
theme = amoled2Theme;
break;
default:
theme = darkTheme
}
return theme;
}
export default StyleSelector;

View file

@ -3,6 +3,9 @@
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
output: 'standalone', output: 'standalone',
compiler: {
styledComponents: true,
}
}; };
module.exports = nextConfig module.exports = nextConfig

View file

@ -11,14 +11,20 @@
}, },
"dependencies": { "dependencies": {
"dockerode": "^3.3.4", "dockerode": "^3.3.4",
"next": "^12.3.0", "eslint-config": "^0.3.0",
"react": "18.2.0", "next": "^13.0.6",
"react-dom": "18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0",
"react-is": "^18.2.0",
"sharp": "^0.31.2",
"styled-components": "^5.3.6",
"swr": "^1.3.0" "swr": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cookie": "^0.5.1",
"@types/dockerode": "^3.3.14", "@types/dockerode": "^3.3.14",
"@types/react": "^18.0.14", "@types/react": "^18.0.14",
"@types/styled-components": "^5.1.26",
"eslint": "^8.23.1", "eslint": "^8.23.1",
"eslint-config-next": "12.2.0", "eslint-config-next": "12.2.0",
"typescript": "^4.7.4" "typescript": "^4.7.4"

View file

@ -1,9 +1,12 @@
import '/styles/globals.css' import '/styles/globals.css'
import type { ReactElement, ReactNode } from 'react' import { Fragment, ReactElement, ReactNode } from 'react'
import Layout from '../components/layout' import Layout from '../components/layout'
import type { NextPage } from 'next' import type { NextPage } from 'next'
import { AppProps } from 'next/app'; import { AppProps } from 'next/app';
import { DefaultTheme, ThemeProvider } from 'styled-components';
import { createContext, useContext, useEffect, useState } from 'react'
import { darkTheme, GlobalStyle } from '../components/themes';
import { getTheme } from '../components/themeselector';
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & { export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode getLayout?: (page: ReactElement) => ReactNode
@ -13,10 +16,42 @@ export type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout Component: NextPageWithLayout
} }
const ThemeUpdateContext = createContext(
(theme: DefaultTheme) => console.error("attempted to set theme outside of a ThemeUpdateContext.Provider")
)
export const useUpdateTheme = () => useContext(ThemeUpdateContext);
export default function Website({ Component, pageProps }: AppPropsWithLayout) { export default function Website({ Component, pageProps }: AppPropsWithLayout) {
const [selectedTheme, setselectedTheme] = useState(darkTheme);
useEffect(() => {
const storedThemeIdTemp = localStorage.getItem("theme");
// get stored theme data
// if theme data differs set it
// if not just exit
if (storedThemeIdTemp && parseInt(storedThemeIdTemp) !== selectedTheme.themeId) {
setselectedTheme(getTheme(parseInt(storedThemeIdTemp)))
}
}, [selectedTheme])
// Use the layout defined at the page level, if available // Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => ( const getLayout = Component.getLayout ?? ((page) => (
<Layout>{page}</Layout>)) <Layout>{page}</Layout>))
return getLayout(<Component {...pageProps} />) return (
<Fragment>
<GlobalStyle />
<ThemeProvider theme={selectedTheme}>
<ThemeUpdateContext.Provider value={setselectedTheme}>
{getLayout(<Component {...pageProps} />)}
</ThemeUpdateContext.Provider>
</ThemeProvider>
</Fragment>
)
} }

View file

@ -1,13 +1,15 @@
import { Html, Head, Main, NextScript } from 'next/document' import { Html, Head, Main, NextScript } from 'next/document'
import { StyledBody } from '../components/styles/generic'
export default function Document() { export default function Document() {
return ( return (
<Html lang='en'> <Html lang='en'>
<Head /> <Head />
<body> <StyledBody>
<Main /> <Main />
<NextScript /> <NextScript />
</body> </StyledBody>
</Html> </Html>
) )
} }

View file

@ -1,20 +1,21 @@
import Head from 'next/head' import Head from 'next/head'
import styles from '/styles/Home.module.css' import { PageDescription, PageTitle } from '../components/styles/content'
export default function About() { export default function About() {
return ( return (
<> <>
<Head> <Head>
<title>Neshura Servers</title> <title>Neshweb - About</title>
<meta charSet='utf-8' /> <meta charSet='utf-8' />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<h1 className={styles.title}>
<PageTitle>
About About
</h1> </PageTitle>
<p className={styles.description}> <PageDescription>
This website is primarily for managing my game servers in one spot This website is primarily for managing my game servers in one spot
</p> </PageDescription>
</> </>
) )
} }

View file

@ -1,55 +1,49 @@
import Head from 'next/head' import Head from 'next/head'
import Link from 'next/link'
import styles from '/styles/Home.module.css'
import fsPromises from 'fs/promises' import fsPromises from 'fs/promises'
import path from 'path' import path from 'path'
import type { CustomLink, EntryList } from '../interfaces/LinkTypes' import type { CustomLink, EntryList } from '../interfaces/LinkTypes'
import { PageContentBox, PageCard, PageDescription, PageTitle, CardContentGame } from '../components/styles/content'
import Link from 'next/link'
function Servers(props: EntryList) { function Servers(props: EntryList) {
const serverList = props.games const serverList = props.games
return ( return (
<> <>
<Head> <Head>
<title>Neshura Servers</title> <title>Neshweb - Games</title>
<meta charSet='utf-8' /> <meta charSet='utf-8' />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<h1 className={styles.title}> <PageTitle>
Server List Server List
</h1> </PageTitle>
<p className={styles.description}> <PageDescription>
Lists all available Services, probably up-to-date Lists all available Services, probably up-to-date
</p> </PageDescription>
<div className={styles.grid}>
<PageContentBox>
{Object.values(serverList).map((item: CustomLink) => { {Object.values(serverList).map((item: CustomLink) => {
if (item.href != null) { if (item.href != null) {
return ( return (
<Link key={item.name} href={item.href}> <PageCard key={item.name}>
<a className={styles.contentcard}> <Link href={item.href}>
<div className={styles.contenttitle}><h2>{item.name }</h2></div> <CardContentGame content={item} />
<div><p>{item.desc}</p></div>
<div><p>{item.ip }</p></div>
<div className={item.status == "Online" ? styles.contentonline : styles.contentoffline}><p>{item.status}</p></div>
</a>
</Link> </Link>
</PageCard>
) )
} }
else { else {
return ( return (
<a key={item.name} className={styles.contentcardstatic}> <PageCard key={item.name}>
<div className={styles.contenttitle}><h2>{item.name }</h2></div> <CardContentGame content={item} />
<div><p>{item.desc}</p></div> </PageCard>
<div><p>{item.ip}</p></div>
<div className={item.status == "Online" ? styles.contentonline : styles.contentoffline}><p>{item.status}</p></div>
</a>
) )
} }
} }
)} )}
</div> </PageContentBox>
</> </>
) )
} }

View file

@ -1,46 +1,44 @@
import Head from 'next/head' import Head from 'next/head'
import Link from 'next/link' import Link from 'next/link';
import styles from '/styles/Home.module.css' import { PageTitle, PageDescription, PageContentBox, PageCard } from '../components/styles/content';
export default function Home() { export default function Home() {
return ( return (
<> <>
<Head> <Head>
<title>Neshura Servers</title> <title>Neshweb - Home</title>
<meta charSet='utf-8' /> <meta charSet='utf-8' />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<PageTitle>
<h1 className={styles.title}>
Welcome to my Servers Webpage Welcome to my Servers Webpage
</h1> </PageTitle>
<p className={styles.description}> <PageDescription>
Feel free to look around Feel free to look around
</p> </PageDescription>
<div className={styles.grid}> <PageContentBox>
<Link key="about" href="/about"> <PageCard key="about">
<a className={styles.card}> <Link href="/about">
<h2>About &rarr;</h2> <h2>About &rarr;</h2>
<p>Useless Info, don&apos;t bother</p> <p>Useless Info, don&apos;t bother</p>
</a>
</Link> </Link>
</PageCard>
<Link key="servers" href="/games"> <PageCard key="servers">
<a className={styles.card}> <Link href="/games">
<h2>Games &rarr;</h2> <h2>Games &rarr;</h2>
<p>List of all available Servers</p> <p>List of all available Servers</p>
</a>
</Link> </Link>
</PageCard>
<Link key="services" href="/services"> <PageCard key="services">
<a className={styles.card}> <Link href="/services">
<h2>Services &rarr;</h2> <h2>Services &rarr;</h2>
<p>List of available Services</p> <p>List of available Services</p>
</a>
</Link> </Link>
</PageCard>
</div> </PageContentBox>
</> </>
) )
} }

View file

@ -1,11 +1,10 @@
import Head from 'next/head' import Head from 'next/head'
import Link from 'next/link' import Link from 'next/link'
import styles from '/styles/Home.module.css'
import { Service, ServiceStatus, ServiceType, ServiceLocation } from '../interfaces/Services'; import { Service, ServiceStatus, ServiceType, ServiceLocation } from '../interfaces/Services';
import Dockerode from 'dockerode'; import Dockerode from 'dockerode';
import { ReactElement } from 'react' import { ReactElement } from 'react'
import useSWR from 'swr'; import useSWR from 'swr';
import Image from 'next/image'; import { PageCard, CardContentService, PageContentBox, PageDescription, PageTitle } from '../components/styles/content';
const fetcher = (url: string) => fetch(url).then((res) => res.json()) const fetcher = (url: string) => fetch(url).then((res) => res.json())
@ -18,55 +17,28 @@ function Services() {
else if (loadingInitial) { content = <div>Loading</div> } else if (loadingInitial) { content = <div>Loading</div> }
else if (loadingFull) { else if (loadingFull) {
content = content =
<div className={styles.grid}> <PageContentBox>
{initialData?.map((item: Service) => ( {initialData?.map((item: Service) => (
<PageCard key={item.name}>
<Link key={item.name} href={item.href}> <Link key={item.name} href={item.href}>
<a className={styles.contentcard}> <CardContentService content={item} />
<div className={styles.contentTitle}>
{
item.icon ? (
<div className={styles.contentIcon}>
<Image alt="icon" src={item.icon} layout="fill" objectFit="contain"></Image>
</div>
) : (<></>)
}
<h2>{item.name}</h2>
</div>
<div className={item.status === ServiceStatus.online ? styles.statusOnline : item.status === ServiceStatus.offline ? styles.statusOffline : styles.statusLoading}>
{item.status}
</div>
<div><p>{item.desc}</p></div>
<div className={styles.cardwarn}><p>{item.warn}</p></div>
</a>
</Link> </Link>
</PageCard>
))} ))}
</div> </PageContentBox>
} }
else if (fullData) { else if (fullData) {
content = content =
<div className={styles.grid}> <PageContentBox>
{fullData.map((item: Service) => ( {fullData.map((item: Service) => (
<PageCard key={item.name}>
<Link key={item.name} href={item.href}> <Link key={item.name} href={item.href}>
<a className={styles.contentcard}> <CardContentService content={item} />
<div className={styles.contentTitle}>
{
item.icon ? (
<div className={styles.contentIcon}>
<Image alt="icon" src={item.icon} layout="fill" objectFit="contain"></Image>
</div>
) : (<></>)
}
<h2>{item.name}</h2>
</div>
<div className={item.status === ServiceStatus.online ? styles.statusOnline : item.status === ServiceStatus.offline ? styles.statusOffline : styles.statusLoading}>
{item.status}
</div>
<div><p>{item.desc}</p></div>
<div className={styles.cardwarn}><p>{item.warn}</p></div>
</a>
</Link> </Link>
</PageCard>
))} ))}
</div> </PageContentBox>
} }
else { else {
content = <div>Error loading data</div> content = <div>Error loading data</div>
@ -81,13 +53,13 @@ function Services() {
<meta name="description" content="Lists all available Services, most likely up-to-date" /> <meta name="description" content="Lists all available Services, most likely up-to-date" />
</Head> </Head>
<h1 className={styles.title}> <PageTitle>
Service List Service List
</h1> </PageTitle>
<p className={styles.description}> <PageDescription>
Lists all available Services, most likely up-to-date Lists all available Services, most likely up-to-date
</p> </PageDescription>
{content} {content}
</> </>

16
styled.d.ts vendored Normal file
View file

@ -0,0 +1,16 @@
import 'styled-components'
declare module 'styled-components' {
export interface DefaultTheme {
themeName: string,
themeId: number,
colors: {
background: string,
primary: string,
secondary: string,
online: string,
loading: string,
offline: string,
}
}
}

View file

@ -1,229 +1 @@
.page {
width: 100%;
background-color: var(--black-0f);
}
.container {
padding: 0 2rem;
}
.main {
color: var(--def-blue);
min-height: 100vh;
padding: 1rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
.navbar {
display: flex;
flex: 1;
padding: 2rem 0;
border-bottom: 1px solid var(--def-blue);
flex-wrap: nowrap;
justify-content: center;
align-items: center;
}
.navelem {
width: auto;
color: var(--def-blue);
margin: 0.2rem;
border: 1px solid;
display: flex;
justify-content: center;
align-items: center;
flex-grow: 0.05;
}
.navelem:hover {
color: var(--def-orange);
background-color: var(--black-1e);
}
.navelem_active {
width: auto;
color: var(--def-orange);
margin: 0.2rem;
border: 1px solid;
display: flex;
justify-content: center;
align-items: center;
flex-grow: 0.05;
}
.footer {
color: var(--def-blue);
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid var(--def-blue);
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: var(--def-orange);
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: var(--black-1e);
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 80%;
}
.statusOffline {
color: var(--def-red);
}
.statusOnline {
color: var(--def-green);
}
.statusLoading {
color: var(--def-orange);
}
.contentcard {
margin: 1rem;
padding: 1rem;
text-align: center;
color: inherit;
text-decoration: none;
border: 1px solid var(--black-2d);
border-radius: 10px;
border-color: var(--def-blue);
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.contentcardstatic {
margin: 1rem;
padding: 1rem;
text-align: center;
color: inherit;
text-decoration: none;
border: 1px solid var(--black-2d);
border-radius: 10px;
border-color: var(--def-blue);
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.contentcard:hover,
.contentcard:focus,
.contentcard:active {
color: var(--def-orange);
border-color: var(--def-orange);
}
.contentTitle {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.contentTitle h2 {
white-space: nowrap;
}
.contentIcon {
margin-right: 0.4rem;
position: relative;
aspect-ratio: 1;
height: 1.5rem;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid var(--black-2d);
border-radius: 10px;
border-color: var(--def-blue);
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: var(--def-orange);
border-color: var(--def-orange);
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.cardwarn {
color: var(--def-orange);
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

View file

@ -1,27 +1 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
:root {
--black-0f: #0f0f0f;
--black-1e: #1e1e1e;
--black-2d: #2d2d2d;
--def-blue: #00aaff;
--def-orange: #ff5300;
--def-red: #ff0000;
--def-green: #00ff00;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

View file

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "esnext",
"lib": [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",
@ -17,12 +17,18 @@
"moduleResolution": "nodenext", "moduleResolution": "nodenext",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve" "jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
}, },
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",
"**/*.ts", "**/*.ts",
"**/*.tsx" "**/*.tsx",
".next/types/**/*.ts"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"

1736
yarn.lock

File diff suppressed because it is too large Load diff