switched from ant design to ionic framework
5
frontend/.gitignore
vendored
|
@ -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
|
||||
|
|
10
frontend/capacitor.config.ts
Normal 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
|
7
frontend/ionic.config.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "llama-bot-web-interface",
|
||||
"integrations": {
|
||||
"capacitor": {}
|
||||
},
|
||||
"type": "react"
|
||||
}
|
43744
frontend/package-lock.json
generated
Normal 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": [
|
||||
|
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
1
frontend/public/assets/shapes.svg
Normal 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 |
|
@ -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>
|
||||
|
|
21
frontend/public/manifest.json
Normal 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
After Width: | Height: | Size: 59 KiB |
BIN
frontend/resources/splash.png
Normal file
After Width: | Height: | Size: 167 KiB |
8
frontend/src/App.test.tsx
Normal 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
|
@ -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
|
214
frontend/src/components/DashboardSidebar.tsx
Normal 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
|
50
frontend/src/components/ExploreContainer.tsx
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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" }}
|
||||
/>
|
||||
|
|
|
@ -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
|
|
@ -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);
|
||||
}
|
|
@ -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()
|
||||
|
|
39
frontend/src/pages/Dashboard.tsx
Normal 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
|
16
frontend/src/pages/Home.tsx
Normal 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
|
11
frontend/src/pages/Incidents.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { IonHeader } from "@ionic/react"
|
||||
|
||||
const Incidents: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<IonHeader>Incidents</IonHeader>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Incidents
|
11
frontend/src/pages/Logs.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { IonHeader } from "@ionic/react"
|
||||
|
||||
const Logs: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<IonHeader>Logs</IonHeader>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Logs
|
7
frontend/src/pages/Modules.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { IonHeader } from "@ionic/react"
|
||||
|
||||
const Modules: React.FC = () => {
|
||||
return <IonHeader>Modules</IonHeader>
|
||||
}
|
||||
|
||||
export default Modules
|
16
frontend/src/pages/Servers.tsx
Normal 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
|
17
frontend/src/reportWebVitals.ts
Normal 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
|
|
@ -1,7 +0,0 @@
|
|||
export default function Dashboard() {
|
||||
return (
|
||||
<>
|
||||
<h2>Dashboard</h2>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { Component } from "react"
|
||||
|
||||
export default class Home extends Component {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<h2>Home</h2>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { Component } from "react"
|
||||
|
||||
export default class Incidents extends Component {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<h2>Incidents</h2>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { Component } from "react"
|
||||
|
||||
export default class Logs extends Component {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<h2>Logs</h2>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { Component } from "react"
|
||||
|
||||
export default class Modules extends Component {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<h2>Modules</h2>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
81
frontend/src/service-worker.ts
Normal 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.
|
147
frontend/src/serviceWorkerRegistration.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 () {},
|
||||
}
|
||||
}
|
||||
|
|
234
frontend/src/theme/variables.css
Normal 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;
|
||||
}
|
||||
}
|