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:
commit
998fcc9514
23 changed files with 2279 additions and 538 deletions
|
@ -1,6 +1,6 @@
|
|||
## INIT STEP
|
||||
# Install dependencies only when needed
|
||||
FROM node:16-alpine AS deps
|
||||
FROM node:18-alpine AS deps
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
@ -11,7 +11,7 @@ RUN yarn install --frozen-lockfile
|
|||
|
||||
## BUILD STEP
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:16-alpine AS builder
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@ -23,7 +23,7 @@ COPY . .
|
|||
RUN yarn build
|
||||
|
||||
## RUN STEP
|
||||
FROM node:16-alpine AS runner
|
||||
FROM node:18-alpine AS runner
|
||||
|
||||
LABEL author="neshura@proton.me"
|
||||
WORKDIR /usr/src/app
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import styles from '/styles/Home.module.css'
|
||||
import { Footer } from "../components/styles/generic"
|
||||
|
||||
const Footer = () => {
|
||||
const PageFooter = () => {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<Footer>
|
||||
Built using Next.js
|
||||
</footer>
|
||||
</Footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
export default PageFooter;
|
|
@ -1,36 +1,37 @@
|
|||
import Footer from './footer'
|
||||
import Navbar from './navbar'
|
||||
import styles from '/styles/Home.module.css'
|
||||
import PageFooter from './footer'
|
||||
import PageNavbar from './navbar'
|
||||
import Script from 'next/script'
|
||||
import { Page, Main } from './styles/generic'
|
||||
|
||||
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<Script id="matomo_analytics">
|
||||
{`
|
||||
var _paq = window._paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
|
||||
_paq.push(["setCookieDomain", "www.neshweb.net"]);
|
||||
_paq.push(["disableCookies"]);
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="//tracking.neshweb.net/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '2']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
`}
|
||||
<Page>
|
||||
<Script id="matomo_analytics">
|
||||
{`
|
||||
var _paq = window._paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
|
||||
_paq.push(["setCookieDomain", "www.neshweb.net"]);
|
||||
_paq.push(["disableCookies"]);
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="//tracking.neshweb.net/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '2']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
`}
|
||||
</Script>
|
||||
|
||||
<Navbar />
|
||||
<main className={styles.main}>
|
||||
|
||||
<PageNavbar />
|
||||
<Main>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</Main>
|
||||
<PageFooter />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import styles from '/styles/Home.module.css'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { NavBar, NavLink, NavWrap } from './styles/navbar';
|
||||
import { StyleSelector, StyleSelectorPlaceholder } from './themeselector';
|
||||
|
||||
const navLinks = [
|
||||
{ name: "Home", href: "/" },
|
||||
|
@ -9,22 +9,25 @@ const navLinks = [
|
|||
{ name: "Services", href: "/services" }
|
||||
]
|
||||
|
||||
const Navbar = () => {
|
||||
const router = useRouter();
|
||||
const PageNavbar = () => {
|
||||
const path = usePathname();
|
||||
|
||||
return (
|
||||
<nav className={styles.navbar}>
|
||||
{navLinks.map((item, index) => (
|
||||
<Link key={item.name} href={item.href}>
|
||||
<a className={router.pathname == item.href ? styles.navelem_active : styles.navelem}>{item.name}</a>
|
||||
</Link>
|
||||
))}
|
||||
<Link key="Mastodon_Verify" href="https://mastodon.neshweb.net/@neshura">
|
||||
<a className={styles.navelem} rel="me" href="https://mastodon.neshweb.net/@neshura">Mastodon</a>
|
||||
</Link>
|
||||
|
||||
</nav>
|
||||
<NavWrap>
|
||||
<StyleSelector></StyleSelector>
|
||||
<NavBar>
|
||||
{navLinks.map((item) => (
|
||||
<NavLink active={path == item.href ? true : false} key={item.name} href={item.href}>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
))}
|
||||
<NavLink key="Mastodon_Verify" rel="me" href="https://mastodon.neshweb.net/@neshura">
|
||||
Mastodon
|
||||
</NavLink>
|
||||
</NavBar>
|
||||
<StyleSelectorPlaceholder></StyleSelectorPlaceholder>
|
||||
</NavWrap>
|
||||
);
|
||||
}
|
||||
|
||||
export default Navbar;
|
||||
export default PageNavbar;
|
154
components/styles/content.tsx
Normal file
154
components/styles/content.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
42
components/styles/generic.tsx
Normal file
42
components/styles/generic.tsx
Normal 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;
|
||||
} */
|
40
components/styles/navbar.tsx
Normal file
40
components/styles/navbar.tsx
Normal 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};
|
||||
}
|
||||
|
||||
`
|
53
components/styles/themedropdown.tsx
Normal file
53
components/styles/themedropdown.tsx
Normal 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
72
components/themes.tsx
Normal 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',
|
||||
},
|
||||
}
|
99
components/themeselector.tsx
Normal file
99
components/themeselector.tsx
Normal 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;
|
|
@ -3,6 +3,9 @@
|
|||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
compiler: {
|
||||
styledComponents: true,
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = nextConfig
|
12
package.json
12
package.json
|
@ -11,14 +11,20 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"dockerode": "^3.3.4",
|
||||
"next": "^12.3.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"eslint-config": "^0.3.0",
|
||||
"next": "^13.0.6",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/dockerode": "^3.3.14",
|
||||
"@types/react": "^18.0.14",
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-next": "12.2.0",
|
||||
"typescript": "^4.7.4"
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import '/styles/globals.css'
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
import { Fragment, ReactElement, ReactNode } from 'react'
|
||||
import Layout from '../components/layout'
|
||||
import type { NextPage } from 'next'
|
||||
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> & {
|
||||
getLayout?: (page: ReactElement) => ReactNode
|
||||
|
@ -13,10 +16,42 @@ export type AppPropsWithLayout = AppProps & {
|
|||
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) {
|
||||
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
|
||||
const getLayout = Component.getLayout ?? ((page) => (
|
||||
<Layout>{page}</Layout>))
|
||||
|
||||
return getLayout(<Component {...pageProps} />)
|
||||
return (
|
||||
<Fragment>
|
||||
<GlobalStyle />
|
||||
<ThemeProvider theme={selectedTheme}>
|
||||
<ThemeUpdateContext.Provider value={setselectedTheme}>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</ThemeUpdateContext.Provider>
|
||||
</ThemeProvider>
|
||||
</Fragment>
|
||||
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
import { StyledBody } from '../components/styles/generic'
|
||||
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang='en'>
|
||||
<Head />
|
||||
<body>
|
||||
<StyledBody>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</StyledBody>
|
||||
</Html>
|
||||
)
|
||||
}
|
|
@ -1,20 +1,21 @@
|
|||
import Head from 'next/head'
|
||||
import styles from '/styles/Home.module.css'
|
||||
import { PageDescription, PageTitle } from '../components/styles/content'
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Neshura Servers</title>
|
||||
<title>Neshweb - About</title>
|
||||
<meta charSet='utf-8' />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<h1 className={styles.title}>
|
||||
|
||||
<PageTitle>
|
||||
About
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
</PageTitle>
|
||||
<PageDescription>
|
||||
This website is primarily for managing my game servers in one spot
|
||||
</p>
|
||||
</PageDescription>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,55 +1,49 @@
|
|||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
import styles from '/styles/Home.module.css'
|
||||
import fsPromises from 'fs/promises'
|
||||
import path from 'path'
|
||||
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) {
|
||||
const serverList = props.games
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Neshura Servers</title>
|
||||
<title>Neshweb - Games</title>
|
||||
<meta charSet='utf-8' />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<h1 className={styles.title}>
|
||||
<PageTitle>
|
||||
Server List
|
||||
</h1>
|
||||
</PageTitle>
|
||||
|
||||
<p className={styles.description}>
|
||||
<PageDescription>
|
||||
Lists all available Services, probably up-to-date
|
||||
</p>
|
||||
<div className={styles.grid}>
|
||||
</PageDescription>
|
||||
|
||||
<PageContentBox>
|
||||
{Object.values(serverList).map((item: CustomLink) => {
|
||||
if (item.href != null) {
|
||||
return (
|
||||
<Link key={item.name} href={item.href}>
|
||||
<a className={styles.contentcard}>
|
||||
<div className={styles.contenttitle}><h2>{item.name }</h2></div>
|
||||
<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>
|
||||
<PageCard key={item.name}>
|
||||
<Link href={item.href}>
|
||||
<CardContentGame content={item} />
|
||||
</Link>
|
||||
</PageCard>
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<a key={item.name} className={styles.contentcardstatic}>
|
||||
<div className={styles.contenttitle}><h2>{item.name }</h2></div>
|
||||
<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>
|
||||
<PageCard key={item.name}>
|
||||
<CardContentGame content={item} />
|
||||
</PageCard>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
)}
|
||||
</div>
|
||||
</PageContentBox>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,46 +1,44 @@
|
|||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
import styles from '/styles/Home.module.css'
|
||||
import Link from 'next/link';
|
||||
import { PageTitle, PageDescription, PageContentBox, PageCard } from '../components/styles/content';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Neshura Servers</title>
|
||||
<title>Neshweb - Home</title>
|
||||
<meta charSet='utf-8' />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<h1 className={styles.title}>
|
||||
<PageTitle>
|
||||
Welcome to my Servers Webpage
|
||||
</h1>
|
||||
</PageTitle>
|
||||
|
||||
<p className={styles.description}>
|
||||
<PageDescription>
|
||||
Feel free to look around
|
||||
</p>
|
||||
<div className={styles.grid}>
|
||||
<Link key="about" href="/about">
|
||||
<a className={styles.card}>
|
||||
</PageDescription>
|
||||
<PageContentBox>
|
||||
<PageCard key="about">
|
||||
<Link href="/about">
|
||||
<h2>About →</h2>
|
||||
<p>Useless Info, don't bother</p>
|
||||
</a>
|
||||
</Link>
|
||||
</Link>
|
||||
</PageCard>
|
||||
|
||||
<Link key="servers" href="/games">
|
||||
<a className={styles.card}>
|
||||
<PageCard key="servers">
|
||||
<Link href="/games">
|
||||
<h2>Games →</h2>
|
||||
<p>List of all available Servers</p>
|
||||
</a>
|
||||
</Link>
|
||||
</Link>
|
||||
</PageCard>
|
||||
|
||||
<Link key="services" href="/services">
|
||||
<a className={styles.card}>
|
||||
<PageCard key="services">
|
||||
<Link href="/services">
|
||||
<h2>Services →</h2>
|
||||
<p>List of available Services</p>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
</Link>
|
||||
</PageCard>
|
||||
</PageContentBox>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
import styles from '/styles/Home.module.css'
|
||||
import { Service, ServiceStatus, ServiceType, ServiceLocation } from '../interfaces/Services';
|
||||
import Dockerode from 'dockerode';
|
||||
import { ReactElement } from 'react'
|
||||
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())
|
||||
|
||||
|
@ -18,55 +17,28 @@ function Services() {
|
|||
else if (loadingInitial) { content = <div>Loading</div> }
|
||||
else if (loadingFull) {
|
||||
content =
|
||||
<div className={styles.grid}>
|
||||
<PageContentBox>
|
||||
{initialData?.map((item: Service) => (
|
||||
<Link key={item.name} href={item.href}>
|
||||
<a className={styles.contentcard}>
|
||||
<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>
|
||||
<PageCard key={item.name}>
|
||||
<Link key={item.name} href={item.href}>
|
||||
<CardContentService content={item} />
|
||||
</Link>
|
||||
</PageCard>
|
||||
))}
|
||||
</div>
|
||||
</PageContentBox>
|
||||
|
||||
}
|
||||
else if (fullData) {
|
||||
content =
|
||||
<div className={styles.grid}>
|
||||
<PageContentBox>
|
||||
{fullData.map((item: Service) => (
|
||||
<Link key={item.name} href={item.href}>
|
||||
<a className={styles.contentcard}>
|
||||
<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>
|
||||
<PageCard key={item.name}>
|
||||
<Link key={item.name} href={item.href}>
|
||||
<CardContentService content={item} />
|
||||
</Link>
|
||||
</PageCard>
|
||||
))}
|
||||
</div>
|
||||
</PageContentBox>
|
||||
}
|
||||
else {
|
||||
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" />
|
||||
</Head>
|
||||
|
||||
<h1 className={styles.title}>
|
||||
<PageTitle>
|
||||
Service List
|
||||
</h1>
|
||||
</PageTitle>
|
||||
|
||||
<p className={styles.description}>
|
||||
<PageDescription>
|
||||
Lists all available Services, most likely up-to-date
|
||||
</p>
|
||||
</PageDescription>
|
||||
|
||||
{content}
|
||||
</>
|
||||
|
|
16
styled.d.ts
vendored
Normal file
16
styled.d.ts
vendored
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "esnext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
|
@ -17,12 +17,18 @@
|
|||
"moduleResolution": "nodenext",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
"jsx": "preserve",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
|
|
Loading…
Reference in a new issue