1
0
Fork 0

switched from ant design to ionic framework

This commit is contained in:
Kim, Jimin 2021-09-26 17:47:37 +09:00
parent e9aaaa5ece
commit ed3b0e1458
38 changed files with 44852 additions and 12907 deletions

5
frontend/.gitignore vendored
View file

@ -15,7 +15,12 @@
.env.development.local
.env.test.local
.env.production.local
.vscode
.idea
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Optional eslint cache
.eslintcache

View file

@ -0,0 +1,10 @@
import { CapacitorConfig } from "@capacitor/cli"
const config: CapacitorConfig = {
appId: "io.ionic.starter",
appName: "llama-bot-web-interface",
webDir: "build",
bundledWebRuntime: false,
}
export default config

View file

@ -0,0 +1,7 @@
{
"name": "llama-bot-web-interface",
"integrations": {
"capacitor": {}
},
"type": "react"
}

43744
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -4,36 +4,59 @@
"private": true,
"scripts": {
"start": "react-scripts start",
"serve": "yarn build && firebase serve",
"serve": "npm run build && firebase serve",
"build": "react-scripts build",
"test": "react-scripts test",
"analyze": "source-map-explorer 'build/static/js/*.js'"
},
"dependencies": {
"@capacitor/app": "^1.0.3",
"@capacitor/cli": "^3.2.3",
"@capacitor/core": "^3.2.3",
"@capacitor/haptics": "^1.0.3",
"@capacitor/keyboard": "^1.0.3",
"@capacitor/status-bar": "^1.0.3",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15",
"antd": "^4.16.13",
"@ionic/react": "^5.8.1",
"@ionic/react-router": "^5.8.1",
"ionicons": "^5.5.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router": "^5.2.1",
"react-router-dom": "^5.3.0",
"react-spinners": "^0.11.0",
"styled-components": "^5.3.1",
"typescript": "^4.4.2"
"web-vitals": "^2.1.0",
"workbox-background-sync": "^6.3.0",
"workbox-broadcast-update": "^6.3.0",
"workbox-cacheable-response": "^6.3.0",
"workbox-core": "^6.3.0",
"workbox-expiration": "^6.3.0",
"workbox-google-analytics": "^6.3.0",
"workbox-navigation-preload": "^6.3.0",
"workbox-precaching": "^6.3.0",
"workbox-range-requests": "^6.3.0",
"workbox-routing": "^6.3.0",
"workbox-strategies": "^6.3.0",
"workbox-streams": "^6.3.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/react": "^12.1.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.10",
"@types/react": "^17.0.20",
"@types/jest": "^27.0.2",
"@types/node": "^16.9.6",
"@types/react": "^17.0.24",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8",
"@types/react-router": "^5.1.16",
"@types/react-router-dom": "^5.3.0",
"@types/styled-components": "^5.1.14",
"react-scripts": "^4.0.3",
"source-map-explorer": "^2.5.2"
"source-map-explorer": "^2.5.2",
"typescript": "^4.4.3"
},
"eslintConfig": {
"extends": [

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

View file

@ -0,0 +1 @@
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -2,18 +2,47 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/llama.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#5DADEC" />
<title>Llama Bot</title>
<meta name="description" content="Llama bot web interface" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/llama.png" />
<meta name="theme-color" content="#5DADEC" />
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR&family=Source+Code+Pro&display=swap"
rel="stylesheet"
rel="shortcut icon"
type="image/png"
href="%PUBLIC_URL%/assets/icon/llama.png"
/>
<link
rel="icon"
type="image/png"
href="%PUBLIC_URL%/assets/icon/llama.png"
/>
<title>Llama Bot</title>
<base href="/" />
<meta name="color-scheme" content="light dark" />
<meta
name="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR&family=Source+Code+Pro&display=swap"
/>
<!-- IOS stuff -->
<link
rel="apple-touch-icon"
href="%PUBLIC_URL%/assets/icon/llama.png"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Llama Bot" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
</head>
<body>
<noscript>

View file

@ -0,0 +1,21 @@
{
"short_name": "Ionic App",
"name": "My Ionic App",
"icons": [
{
"src": "assets/icon/favicon.png",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "assets/icon/icon.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#ffffff",
"background_color": "#ffffff"
}

BIN
frontend/resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

View file

@ -0,0 +1,8 @@
import React from "react"
import { render } from "@testing-library/react"
import App from "./App"
test("renders without crashing", () => {
const { baseElement } = render(<App />)
expect(baseElement).toBeDefined()
})

90
frontend/src/App.tsx Normal file
View file

@ -0,0 +1,90 @@
import { lazy, Suspense } from "react"
import { IonApp } from "@ionic/react"
import { IonReactRouter } from "@ionic/react-router"
import { Route } from "react-router-dom"
import styled from "styled-components"
import Loader from "react-spinners/CircleLoader"
import Navbar from "./components/Navbar"
import Footer from "./components/Footer"
/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css"
/* Basic CSS for apps built with Ionic */
import "@ionic/react/css/normalize.css"
import "@ionic/react/css/structure.css"
import "@ionic/react/css/typography.css"
/* Optional CSS utils that can be commented out */
import "@ionic/react/css/padding.css"
import "@ionic/react/css/float-elements.css"
import "@ionic/react/css/text-alignment.css"
import "@ionic/react/css/text-transformation.css"
import "@ionic/react/css/flex-utils.css"
import "@ionic/react/css/display.css"
/* Theme variables */
import "./theme/variables.css"
const Home = lazy(() => import("./pages/Home"))
const Servers = lazy(() => import("./pages/Servers"))
const Dashboard = lazy(() => import("./pages/Dashboard"))
const StyledSpinContainer = styled.div`
width: 100%;
opacity: 0;
/* center elements */
display: flex;
align-content: center;
justify-content: center;
/* fade in */
animation-delay: 0.2s;
animation-duration: 2s;
animation-name: fadein;
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`
const App: React.FC = () => {
return (
<IonApp>
<Suspense
fallback={
<StyledSpinContainer>
<Loader size={150} />
</StyledSpinContainer>
}
>
<IonReactRouter>
<Navbar />
<Route exact path="/">
<Home />
</Route>
<Route exact path="/servers">
<Servers />
</Route>
<Route exact path="/server/:server/dashboard">
<Dashboard />
</Route>
<Footer />
</IonReactRouter>
</Suspense>
</IonApp>
)
}
export default App

View file

@ -0,0 +1,214 @@
import { useLocation } from "react-router-dom"
import {
IonContent,
IonIcon,
IonItem,
IonLabel,
IonList,
IonMenu,
IonMenuToggle,
} from "@ionic/react"
import {
heartOutline,
heartSharp,
paperPlaneOutline,
paperPlaneSharp,
warningOutline,
warningSharp,
} from "ionicons/icons"
import styled from "styled-components"
interface AppPage {
url: string
iosIcon: string
mdIcon: string
title: string
}
const appPages: AppPage[] = [
{
title: "Favorites",
url: "/favorites",
iosIcon: heartOutline,
mdIcon: heartSharp,
},
{
title: "Modules",
url: "/modules",
iosIcon: heartOutline,
mdIcon: heartSharp,
},
{
title: "Logs",
url: "/logs",
iosIcon: paperPlaneOutline,
mdIcon: paperPlaneSharp,
},
{
title: "Incidents",
url: "/incidents",
iosIcon: warningOutline,
mdIcon: warningSharp,
},
]
const StyledIonMenu = styled(IonMenu)`
ion-menu ion-content {
--background: var(
--ion-item-background,
var(--ion-background-color, #fff)
);
}
ion-menu.md ion-content {
--padding-start: 8px;
--padding-end: 8px;
--padding-top: 20px;
--padding-bottom: 20px;
}
ion-menu.md ion-list {
padding: 20px 0;
}
ion-menu.md ion-note {
margin-bottom: 30px;
}
ion-menu.md ion-list-header,
ion-menu.md ion-note {
padding-left: 10px;
}
ion-menu.md ion-list#inbox-list {
border-bottom: 1px solid var(--ion-color-step-150, #d7d8da);
}
ion-menu.md ion-list#inbox-list ion-list-header {
font-size: 22px;
font-weight: 600;
min-height: 20px;
}
ion-menu.md ion-list#labels-list ion-list-header {
font-size: 16px;
margin-bottom: 18px;
color: #757575;
min-height: 26px;
}
ion-menu.md ion-item {
--padding-start: 10px;
--padding-end: 10px;
border-radius: 4px;
}
ion-menu.md ion-item.selected {
--background: rgba(var(--ion-color-primary-rgb), 0.14);
}
ion-menu.md ion-item.selected ion-icon {
color: var(--ion-color-primary);
}
ion-menu.md ion-item ion-icon {
color: #616e7e;
}
ion-menu.md ion-item ion-label {
font-weight: 500;
}
ion-menu.ios ion-content {
--padding-bottom: 20px;
}
ion-menu.ios ion-list {
padding: 20px 0 0 0;
}
ion-menu.ios ion-note {
line-height: 24px;
margin-bottom: 20px;
}
ion-menu.ios ion-item {
--padding-start: 16px;
--padding-end: 16px;
--min-height: 50px;
}
ion-menu.ios ion-item ion-icon {
font-size: 24px;
color: #73849a;
}
ion-menu.ios ion-item .selected ion-icon {
color: var(--ion-color-primary);
}
ion-menu.ios ion-list#labels-list ion-list-header {
margin-bottom: 8px;
}
ion-menu.ios ion-list-header,
ion-menu.ios ion-note {
padding-left: 16px;
padding-right: 16px;
}
ion-menu.ios ion-note {
margin-bottom: 8px;
}
ion-note {
display: inline-block;
font-size: 16px;
color: var(--ion-color-medium-shade);
}
ion-item.selected {
--color: var(--ion-color-primary);
}
`
const Menu: React.FC = () => {
// todo: collapse button
const location = useLocation()
return (
<StyledIonMenu contentId="main" type="overlay">
<IonContent>
<IonList id="inbox-list">
{appPages.map((appPage, index) => {
return (
<IonMenuToggle key={index} autoHide={false}>
<IonItem
className={
location.pathname === appPage.url
? "selected"
: ""
}
routerLink={location.pathname + appPage.url}
routerDirection="none"
lines="none"
detail={false}
>
<IonIcon
slot="start"
ios={appPage.iosIcon}
md={appPage.mdIcon}
/>
<IonLabel>{appPage.title}</IonLabel>
</IonItem>
</IonMenuToggle>
)
})}
</IonList>
</IonContent>
</StyledIonMenu>
)
}
export default Menu

View file

@ -0,0 +1,50 @@
import styled from "styled-components"
const StyledExploreContainer = styled.div`
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
strong {
font-size: 20px;
line-height: 26px;
}
p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
a {
text-decoration: none;
}
`
interface ContainerProps {
name: string
}
const ExploreContainer: React.FC<ContainerProps> = ({ name }) => {
return (
<StyledExploreContainer className="container">
<strong>{name}</strong>
<p>
Explore{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="https://ionicframework.com/docs/components"
>
UI Components
</a>
</p>
</StyledExploreContainer>
)
}
export default ExploreContainer

View file

@ -3,9 +3,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faGithub } from "@fortawesome/free-brands-svg-icons"
import styled from "styled-components"
import { Layout } from "antd"
import { IonFooter } from "@ionic/react"
const StyledFooter = styled(Layout.Footer)`
const StyledFooter = styled(IonFooter)`
display: flex;
align-items: center;
justify-content: center;
@ -36,22 +36,22 @@ const GithubLink = styled.a`
}
`
export default class Footer extends React.Component {
render() {
return (
<StyledFooter>
<StyledFooterContainer>
<div>
Created by <b>developomp</b>
</div>
<GithubLink
href="https://github.com/llama-bot/llama-bot-web-interface"
target="_blank"
>
<FontAwesomeIcon icon={faGithub} />
</GithubLink>
</StyledFooterContainer>
</StyledFooter>
)
}
const Footer: React.FC = () => {
return (
<StyledFooter>
<StyledFooterContainer>
<div>
Created by <b>developomp</b>
</div>
<GithubLink
href="https://github.com/llama-bot/llama-bot-web-interface"
target="_blank"
>
<FontAwesomeIcon icon={faGithub} />
</GithubLink>
</StyledFooterContainer>
</StyledFooter>
)
}
export default Footer

View file

@ -1,17 +1,16 @@
import { useEffect, useState } from "react"
import { IonHeader } from "@ionic/react"
import styled from "styled-components"
import { Layout } from "antd"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"
const { Header } = Layout
const StyledLlamaBotText = styled.b`
font-size: 1.5rem;
`
const StyledHeader = styled(Header)`
const StyledHeader = styled(IonHeader)`
height: 3.75rem;
color: white;
@ -46,7 +45,7 @@ const StyledHeader = styled(Header)`
}
`
const Navbar = () => {
const Navbar: React.FC = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [userName, setUserName] = useState("")
@ -65,7 +64,7 @@ const Navbar = () => {
<StyledHeader>
<div>
<img
src={`${process.env.PUBLIC_URL}/llama.png`}
src={"assets/icon/llama.png"}
alt="llama logo"
style={{ width: "60px" }}
/>

View file

@ -1,81 +0,0 @@
import { useContext } from "react"
import { Link } from "react-router-dom"
import { Layout, Menu } from "antd"
import { blue } from "@ant-design/colors"
import {
AppstoreAddOutlined,
ClockCircleOutlined,
CaretLeftFilled,
CaretRightFilled,
DashboardOutlined,
HomeOutlined,
ExclamationCircleOutlined,
} from "@ant-design/icons"
import { SidebarCollapsedContext } from "../contexts"
import styled from "styled-components"
const StyledMenuContainer = styled.div`
svg {
font-size: 1.25rem;
}
`
const SidebarCollapseButton = styled.div`
:hover {
background-color: ${blue[5]};
}
`
const Sidebar = () => {
const { isSidebarCollapsed, setSidebarCollapsed } = useContext(
SidebarCollapsedContext
)
return (
<Layout.Sider trigger={null} collapsible collapsed={isSidebarCollapsed}>
<StyledMenuContainer>
<Menu theme="dark" mode="inline">
<Menu.Item icon={<HomeOutlined />}>
<Link to="/">Home</Link>
</Menu.Item>
<Menu.Item icon={<DashboardOutlined />}>
<Link to="/dashboard">Dashboard</Link>
</Menu.Item>
<Menu.Item icon={<AppstoreAddOutlined />}>
<Link to="/modules">Modules</Link>
</Menu.Item>
<Menu.Item icon={<ClockCircleOutlined />}>
<Link to="/logs">Logs</Link>
</Menu.Item>
<Menu.Item icon={<ExclamationCircleOutlined />}>
<Link to="/incidents">Incidents</Link>
</Menu.Item>
<SidebarCollapseButton
style={{
fontSize: 25,
position: "absolute",
bottom: 0,
margin: 0,
width: "100%",
padding: "1rem 0 1rem 0",
textAlign: "center",
}}
onClick={() => {
setSidebarCollapsed((prev) => !prev)
}}
>
{isSidebarCollapsed ? (
<CaretRightFilled />
) : (
<CaretLeftFilled />
)}
</SidebarCollapseButton>
</Menu>
</StyledMenuContainer>
</Layout.Sider>
)
}
export default Sidebar

View file

@ -1,47 +0,0 @@
body {
margin: 0;
font-family: "Noto Sans KR", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: "Source Code Pro", monospace;
}
.site-layout-content {
min-height: 280px;
padding: 24px;
background: #fff;
}
#components-layout-demo-top .logo {
float: left;
width: 120px;
height: 31px;
margin: 16px 24px 16px 0;
background: rgba(255, 255, 255, 0.3);
}
.ant-row-rtl #components-layout-demo-top .logo {
float: right;
margin: 16px 0 16px 24px;
}
.trigger {
padding: 0 24px;
font-size: 18px;
line-height: 64px;
cursor: pointer;
transition: color 0.3s;
}
.trigger:hover {
color: #1890ff;
}
.logo {
height: 32px;
margin: 16px;
background: rgba(255, 255, 255, 0.3);
}

View file

@ -1,115 +1,8 @@
import React, { useState, Suspense, lazy } from "react"
import React from "react"
import ReactDOM from "react-dom"
import { BrowserRouter, Switch, Route } from "react-router-dom"
import Loader from "react-spinners/CircleLoader"
import { Layout } from "antd"
import Navbar from "./components/Navbar"
import Sidebar from "./components/Sidebar"
import Footer from "./components/Footer"
import { SidebarCollapsedContext } from "./contexts"
import "antd/dist/antd.min.css"
import "./index.css" // must be loaded last
import styled from "styled-components"
const Home = lazy(() => import("./routes/Home"))
const Dashboard = lazy(() => import("./routes/Dashboard"))
const Modules = lazy(() => import("./routes/Modules"))
const Logs = lazy(() => import("./routes/Logs"))
const Incidents = lazy(() => import("./routes/Incidents"))
const StyledSpinContainer = styled.div`
width: 100%;
opacity: 0;
/* center elements */
display: flex;
align-content: center;
justify-content: center;
/* fade in */
animation-delay: 0.2s;
animation-duration: 2s;
animation-name: fadein;
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`
const App = () => {
const [isSidebarCollapsed, setSidebarCollapsed] = useState(false)
return (
<>
<SidebarCollapsedContext.Provider
value={{ isSidebarCollapsed, setSidebarCollapsed }}
>
<BrowserRouter basename="/">
<Layout style={{ minHeight: "100vh" }}>
<Layout className="site-layout">
<Navbar />
<Layout>
<Sidebar />
<Layout.Content
style={{
padding: "2.5rem",
display: "flex",
}}
>
<div
className="site-layout-background site-layout-content"
style={{
height: "auto",
width: "100%",
borderRadius: "0.5rem",
}}
>
<Suspense
fallback={
<StyledSpinContainer>
<Loader size={150} />
</StyledSpinContainer>
}
>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route exact path="/dashboard">
<Dashboard />
</Route>
<Route exact path="/modules">
<Modules />
</Route>
<Route exact path="/logs">
<Logs />
</Route>
<Route exact path="/incidents">
<Incidents />
</Route>
</Switch>
</Suspense>
</div>
</Layout.Content>
</Layout>
<Footer />
</Layout>
</Layout>
</BrowserRouter>
</SidebarCollapsedContext.Provider>
</>
)
}
import App from "./App"
import * as serviceWorkerRegistration from "./serviceWorkerRegistration"
import reportWebVitals from "./reportWebVitals"
ReactDOM.render(
<React.StrictMode>
@ -117,3 +10,13 @@ ReactDOM.render(
</React.StrictMode>,
document.getElementById("root")
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.unregister()
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

View file

@ -0,0 +1,39 @@
import {
IonButtons,
IonContent,
IonHeader,
IonMenuButton,
IonPage,
IonTitle,
IonToolbar,
} from "@ionic/react"
import { useParams } from "react-router"
import ExploreContainer from "../components/ExploreContainer"
const Page: React.FC = () => {
const { name } = useParams<{ name: string }>()
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonTitle>{name}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">{name}</IonTitle>
</IonToolbar>
</IonHeader>
<ExploreContainer name={name} />
</IonContent>
</IonPage>
)
}
export default Page

View file

@ -0,0 +1,16 @@
import styled from "styled-components"
const StyledHome = styled.div`
text-align: center;
`
const Home: React.FC = () => {
return (
<StyledHome>
<h3>Home</h3>
<h4>Log in to list servers</h4>
</StyledHome>
)
}
export default Home

View file

@ -0,0 +1,11 @@
import { IonHeader } from "@ionic/react"
const Incidents: React.FC = () => {
return (
<>
<IonHeader>Incidents</IonHeader>
</>
)
}
export default Incidents

View file

@ -0,0 +1,11 @@
import { IonHeader } from "@ionic/react"
const Logs: React.FC = () => {
return (
<>
<IonHeader>Logs</IonHeader>
</>
)
}
export default Logs

View file

@ -0,0 +1,7 @@
import { IonHeader } from "@ionic/react"
const Modules: React.FC = () => {
return <IonHeader>Modules</IonHeader>
}
export default Modules

View file

@ -0,0 +1,16 @@
import { IonHeader } from "@ionic/react"
import styled from "styled-components"
const StyledServers = styled.div`
text-align: center;
`
const Servers: React.FC = () => {
return (
<StyledServers>
<IonHeader>Available servers</IonHeader>
</StyledServers>
)
}
export default Servers

View file

@ -0,0 +1,17 @@
import { ReportHandler } from "web-vitals"
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import("web-vitals").then(
({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(onPerfEntry)
getTTFB(onPerfEntry)
}
)
}
}
export default reportWebVitals

View file

@ -1,7 +0,0 @@
export default function Dashboard() {
return (
<>
<h2>Dashboard</h2>
</>
)
}

View file

@ -1,11 +0,0 @@
import { Component } from "react"
export default class Home extends Component {
render() {
return (
<>
<h2>Home</h2>
</>
)
}
}

View file

@ -1,11 +0,0 @@
import { Component } from "react"
export default class Incidents extends Component {
render() {
return (
<>
<h2>Incidents</h2>
</>
)
}
}

View file

@ -1,11 +0,0 @@
import { Component } from "react"
export default class Logs extends Component {
render() {
return (
<>
<h2>Logs</h2>
</>
)
}
}

View file

@ -1,11 +0,0 @@
import { Component } from "react"
export default class Modules extends Component {
render() {
return (
<>
<h2>Modules</h2>
</>
)
}
}

View file

@ -0,0 +1,81 @@
/// <reference lib="webworker" />
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules
// for the list of available Workbox modules, or add any other
// code you'd like.
// You can also remove this file if you'd prefer not to use a
// service worker, and the Workbox build step will be skipped.
import { clientsClaim } from "workbox-core"
import { ExpirationPlugin } from "workbox-expiration"
import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching"
import { registerRoute } from "workbox-routing"
import { StaleWhileRevalidate } from "workbox-strategies"
declare const self: ServiceWorkerGlobalScope
clientsClaim()
// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
precacheAndRoute(self.__WB_MANIFEST)
// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$")
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }: { request: Request; url: URL }) => {
// If this isn't a navigation, skip.
if (request.mode !== "navigate") {
return false
}
// If this is a URL that starts with /_, skip.
if (url.pathname.startsWith("/_")) {
return false
}
// If this looks like a URL for a resource, because it contains
// a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false
}
// Return true to signal that we want to use the handler.
return true
},
createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html")
)
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) =>
url.origin === self.location.origin && url.pathname.endsWith(".png"),
// Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: "images",
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
)
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting()
}
})
// Any other custom service worker logic can go here.

View file

@ -0,0 +1,147 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://cra.link/PWA
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
)
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void
onUpdate?: (registration: ServiceWorkerRegistration) => void
}
export function register(config?: Config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker. To learn more, visit https://cra.link/PWA"
)
})
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config)
}
})
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing
if (installingWorker == null) {
return
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
"New content is available and will be used when all " +
"tabs for this page are closed. See https://cra.link/PWA."
)
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration)
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log("Content is cached for offline use.")
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration)
}
}
}
}
}
})
.catch((error) => {
console.error("Error during service worker registration:", error)
})
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { "Service-Worker": "script" },
})
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get("content-type")
if (
response.status === 404 ||
(contentType != null &&
contentType.indexOf("javascript") === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload()
})
})
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config)
}
})
.catch(() => {
console.log(
"No internet connection found. App is running in offline mode."
)
})
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister()
})
.catch((error) => {
console.error(error.message)
})
}
}

View file

@ -1 +1,16 @@
import "@testing-library/jest-dom"
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom/extend-expect"
// Mock matchmedia
window.matchMedia =
window.matchMedia ||
function () {
return {
matches: false,
addListener: function () {},
removeListener: function () {},
}
}

View file

@ -0,0 +1,234 @@
/* Ionic Variables and Theming. For more info, please see:
http://ionicframework.com/docs/theming/ */
:root {
/** primary **/
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
/** secondary **/
--ion-color-secondary: #3dc2ff;
--ion-color-secondary-rgb: 61, 194, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #36abe0;
--ion-color-secondary-tint: #50c8ff;
/** tertiary **/
--ion-color-tertiary: #5260ff;
--ion-color-tertiary-rgb: 82, 96, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #4854e0;
--ion-color-tertiary-tint: #6370ff;
/** success **/
--ion-color-success: #2dd36f;
--ion-color-success-rgb: 45, 211, 111;
--ion-color-success-contrast: #ffffff;
--ion-color-success-contrast-rgb: 255, 255, 255;
--ion-color-success-shade: #28ba62;
--ion-color-success-tint: #42d77d;
/** warning **/
--ion-color-warning: #ffc409;
--ion-color-warning-rgb: 255, 196, 9;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0, 0, 0;
--ion-color-warning-shade: #e0ac08;
--ion-color-warning-tint: #ffca22;
/** danger **/
--ion-color-danger: #eb445a;
--ion-color-danger-rgb: 235, 68, 90;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #cf3c4f;
--ion-color-danger-tint: #ed576b;
/** dark **/
--ion-color-dark: #222428;
--ion-color-dark-rgb: 34, 36, 40;
--ion-color-dark-contrast: #ffffff;
--ion-color-dark-contrast-rgb: 255, 255, 255;
--ion-color-dark-shade: #1e2023;
--ion-color-dark-tint: #383a3e;
/** medium **/
--ion-color-medium: #92949c;
--ion-color-medium-rgb: 146, 148, 156;
--ion-color-medium-contrast: #ffffff;
--ion-color-medium-contrast-rgb: 255, 255, 255;
--ion-color-medium-shade: #808289;
--ion-color-medium-tint: #9d9fa6;
/** light **/
--ion-color-light: #f4f5f8;
--ion-color-light-rgb: 244, 245, 248;
--ion-color-light-contrast: #000000;
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #d7d8da;
--ion-color-light-tint: #f5f6f9;
}
@media (prefers-color-scheme: dark) {
/*
* Dark Colors
* -------------------------------------------
*/
body {
--ion-color-primary: #afccff;
--ion-color-primary-rgb: 175, 204, 255;
--ion-color-primary-contrast: #000000;
--ion-color-primary-contrast-rgb: 0, 0, 0;
--ion-color-primary-shade: #9ab4e0;
--ion-color-primary-tint: #b7d1ff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80, 200, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106, 100, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47, 223, 117;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0, 0, 0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255, 213, 52;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0, 0, 0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255, 73, 97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244, 245, 248;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0, 0, 0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152, 154, 162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0, 0, 0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #222428;
--ion-color-light-rgb: 34, 36, 40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255, 255, 255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
}
/*
* iOS Dark Theme
* -------------------------------------------
*/
.ios body {
--ion-background-color: #000000;
--ion-background-color-rgb: 0, 0, 0;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255, 255, 255;
--ion-color-step-50: #0d0d0d;
--ion-color-step-100: #1a1a1a;
--ion-color-step-150: #262626;
--ion-color-step-200: #333333;
--ion-color-step-250: #404040;
--ion-color-step-300: #4d4d4d;
--ion-color-step-350: #595959;
--ion-color-step-400: #666666;
--ion-color-step-450: #737373;
--ion-color-step-500: #808080;
--ion-color-step-550: #8c8c8c;
--ion-color-step-600: #999999;
--ion-color-step-650: #a6a6a6;
--ion-color-step-700: #b3b3b3;
--ion-color-step-750: #bfbfbf;
--ion-color-step-800: #cccccc;
--ion-color-step-850: #d9d9d9;
--ion-color-step-900: #e6e6e6;
--ion-color-step-950: #f2f2f2;
--ion-item-background: #000000;
--ion-card-background: #1c1c1d;
}
.ios ion-modal {
--ion-background-color: var(--ion-color-step-100);
--ion-toolbar-background: var(--ion-color-step-150);
--ion-toolbar-border-color: var(--ion-color-step-250);
}
/*
* Material Design Dark Theme
* -------------------------------------------
*/
.md body {
--ion-background-color: #121212;
--ion-background-color-rgb: 18, 18, 18;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255, 255, 255;
--ion-border-color: #222222;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
--ion-color-step-150: #363636;
--ion-color-step-200: #414141;
--ion-color-step-250: #4d4d4d;
--ion-color-step-300: #595959;
--ion-color-step-350: #656565;
--ion-color-step-400: #717171;
--ion-color-step-450: #7d7d7d;
--ion-color-step-500: #898989;
--ion-color-step-550: #949494;
--ion-color-step-600: #a0a0a0;
--ion-color-step-650: #acacac;
--ion-color-step-700: #b8b8b8;
--ion-color-step-750: #c4c4c4;
--ion-color-step-800: #d0d0d0;
--ion-color-step-850: #dbdbdb;
--ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3;
--ion-item-background: #1e1e1e;
--ion-toolbar-background: #1f1f1f;
--ion-tab-bar-background: #1f1f1f;
--ion-card-background: #1e1e1e;
}
}

File diff suppressed because it is too large Load diff