5.5.1833
This commit is contained in:
parent
f686d573da
commit
44b9780057
|
@ -39,20 +39,25 @@ Database:
|
|||
# -----------------------------------------------------
|
||||
Webserver:
|
||||
Port: 8804
|
||||
Alternative_IP: false
|
||||
Alternative_IP:
|
||||
Enabled: false
|
||||
# %port% is replaced automatically with Webserver.Port
|
||||
Address: your.domain.here:%port%
|
||||
# InternalIP usually does not need to be changed, only change it if you know what you're doing!
|
||||
# 0.0.0.0 allocates Internal (local) IP automatically for the WebServer.
|
||||
Internal_IP: 0.0.0.0
|
||||
Cache:
|
||||
Reduced_refresh_barrier: 15
|
||||
Reduced_refresh_barrier:
|
||||
Time: 15
|
||||
Unit: SECONDS
|
||||
Invalidate_query_results_on_disk_after: 7
|
||||
Invalidate_query_results_on_disk_after:
|
||||
Time: 7
|
||||
Unit: DAYS
|
||||
Invalidate_disk_cache_after: 2
|
||||
Invalidate_disk_cache_after:
|
||||
Time: 2
|
||||
Unit: DAYS
|
||||
Invalidate_memory_cache_after: 5
|
||||
Invalidate_memory_cache_after:
|
||||
Time: 5
|
||||
Unit: MINUTES
|
||||
Security:
|
||||
SSL_certificate:
|
||||
|
@ -69,12 +74,17 @@ Webserver:
|
|||
# Allows using the whitelist & brute-force shield with a reverse-proxy.
|
||||
# ! Make sure non-proxy access is not possible, it would allow IP spoofing !
|
||||
Use_X-Forwarded-For_Header: false
|
||||
IP_whitelist: false
|
||||
Access_log:
|
||||
Print_to_console: false
|
||||
Remove_logs_after_days: 30
|
||||
IP_whitelist:
|
||||
Enabled: false
|
||||
Whitelist:
|
||||
- "192.168.0.0"
|
||||
- "0:0:0:0:0:0:0:1"
|
||||
# Does not affect existing cookies
|
||||
Cookies_expire_after: 2
|
||||
Cookies_expire_after:
|
||||
Time: 2
|
||||
Unit: HOURS
|
||||
Disable_Webserver: false
|
||||
External_Webserver_address: "https://www.example.address"
|
||||
|
@ -91,37 +101,49 @@ Data_gathering:
|
|||
# -----------------------------------------------------
|
||||
Time:
|
||||
Delays:
|
||||
Ping_server_enable_delay: 300
|
||||
Ping_server_enable_delay:
|
||||
Time: 300
|
||||
Unit: SECONDS
|
||||
Ping_player_join_delay: 30
|
||||
Ping_player_join_delay:
|
||||
Time: 30
|
||||
Unit: SECONDS
|
||||
Wait_for_DB_Transactions_on_disable: 20
|
||||
Wait_for_DB_Transactions_on_disable:
|
||||
Time: 20
|
||||
Unit: SECONDS
|
||||
Thresholds:
|
||||
# How long player needs to be idle until Plan considers them AFK
|
||||
AFK_threshold: 3
|
||||
AFK_threshold:
|
||||
Time: 3
|
||||
Unit: MINUTES
|
||||
# Activity Index considers last 3 weeks and uses these thresholds in the calculation
|
||||
# The index is a number from 0 to 5.
|
||||
# These numbers were calibrated with data of 250 players (Small sample size).
|
||||
Activity_index:
|
||||
Playtime_threshold: 30
|
||||
Playtime_threshold:
|
||||
Time: 30
|
||||
Unit: MINUTES
|
||||
Remove_inactive_player_data_after: 180
|
||||
Remove_inactive_player_data_after:
|
||||
Time: 3650
|
||||
Unit: DAYS
|
||||
# Includes players online, tps and performance time series
|
||||
Remove_time_series_data_after: 90
|
||||
Remove_time_series_data_after:
|
||||
Time: 90
|
||||
Unit: DAYS
|
||||
Remove_ping_data_after: 14
|
||||
Remove_ping_data_after:
|
||||
Time: 14
|
||||
Unit: DAYS
|
||||
Remove_disabled_extension_data_after: 2
|
||||
Remove_disabled_extension_data_after:
|
||||
Time: 2
|
||||
Unit: DAYS
|
||||
Periodic_tasks:
|
||||
Extension_data_refresh_every: 1
|
||||
Extension_data_refresh_every:
|
||||
Time: 1
|
||||
Unit: HOURS
|
||||
Check_DB_for_server_config_files_every: 1
|
||||
Check_DB_for_server_config_files_every:
|
||||
Time: 1
|
||||
Unit: MINUTES
|
||||
Clean_Database_every: 1
|
||||
Clean_Database_every:
|
||||
Time: 1
|
||||
Unit: HOURS
|
||||
# -----------------------------------------------------
|
||||
Display_options:
|
||||
|
@ -171,8 +193,8 @@ Formatting:
|
|||
Dates:
|
||||
# Show_recent_day_names replaces day number with Today, Yesterday, Wednesday etc.
|
||||
Show_recent_day_names: true
|
||||
# Non-regex pattern to replace
|
||||
DatePattern: 'MMM d YYYY'
|
||||
# Non-regex pattern to replace
|
||||
Show_recent_day_names_date_pattern: 'MMM d YYYY'
|
||||
Full: 'MMM d YYYY, HH:mm:ss'
|
||||
NoSeconds: 'MMM d YYYY, HH:mm'
|
||||
JustClock: 'HH:mm:ss'
|
||||
|
@ -209,7 +231,8 @@ Export:
|
|||
Export_player_on_login_and_logout: false
|
||||
# If there are multiple servers the period is divided evenly to avoid export of all servers at once
|
||||
# Also affects Players page export
|
||||
Server_refresh_period: 20
|
||||
Server_refresh_period:
|
||||
Time: 20
|
||||
Unit: MINUTES
|
||||
# -----------------------------------------------------
|
||||
# These settings affect Plugin data integration.
|
||||
|
|
|
@ -41,20 +41,25 @@ Database:
|
|||
# -----------------------------------------------------
|
||||
Webserver:
|
||||
Port: 8804
|
||||
Alternative_IP: false
|
||||
Alternative_IP:
|
||||
Enabled: false
|
||||
# %port% is replaced automatically with Webserver.Port
|
||||
Address: your.domain.here:%port%
|
||||
# InternalIP usually does not need to be changed, only change it if you know what you're doing!
|
||||
# 0.0.0.0 allocates Internal (local) IP automatically for the WebServer.
|
||||
Internal_IP: 0.0.0.0
|
||||
Cache:
|
||||
Reduced_refresh_barrier: 15
|
||||
Reduced_refresh_barrier:
|
||||
Time: 15
|
||||
Unit: SECONDS
|
||||
Invalidate_query_results_on_disk_after: 7
|
||||
Invalidate_query_results_on_disk_after:
|
||||
Time: 7
|
||||
Unit: DAYS
|
||||
Invalidate_disk_cache_after: 2
|
||||
Invalidate_disk_cache_after:
|
||||
Time: 2
|
||||
Unit: DAYS
|
||||
Invalidate_memory_cache_after: 5
|
||||
Invalidate_memory_cache_after:
|
||||
Time: 5
|
||||
Unit: MINUTES
|
||||
Security:
|
||||
SSL_certificate:
|
||||
|
@ -71,12 +76,17 @@ Webserver:
|
|||
# Allows using the whitelist with a reverse-proxy.
|
||||
# ! Make sure non-proxy access is not possible, it would allow IP spoofing !
|
||||
Use_X-Forwarded-For_Header: false
|
||||
IP_whitelist: false
|
||||
Access_log:
|
||||
Print_to_console: false
|
||||
Remove_logs_after_days: 30
|
||||
IP_whitelist:
|
||||
Enabled: false
|
||||
Whitelist:
|
||||
- "192.168.0.0"
|
||||
- "0:0:0:0:0:0:0:1"
|
||||
# Does not affect existing cookies
|
||||
Cookies_expire_after: 2
|
||||
Cookies_expire_after:
|
||||
Time: 2
|
||||
Unit: HOURS
|
||||
Disable_Webserver: false
|
||||
External_Webserver_address: https://www.example.address
|
||||
|
@ -96,37 +106,49 @@ Data_gathering:
|
|||
# -----------------------------------------------------
|
||||
Time:
|
||||
Delays:
|
||||
Ping_server_enable_delay: 300
|
||||
Ping_server_enable_delay:
|
||||
Time: 300
|
||||
Unit: SECONDS
|
||||
Ping_player_join_delay: 30
|
||||
Ping_player_join_delay:
|
||||
Time: 30
|
||||
Unit: SECONDS
|
||||
Wait_for_DB_Transactions_on_disable: 20
|
||||
Wait_for_DB_Transactions_on_disable:
|
||||
Time: 20
|
||||
Unit: SECONDS
|
||||
Thresholds:
|
||||
# How long player needs to be idle until Plan considers them AFK
|
||||
AFK_threshold: 3
|
||||
AFK_threshold:
|
||||
Time: 3
|
||||
Unit: MINUTES
|
||||
# Activity Index considers last 3 weeks and uses these thresholds in the calculation
|
||||
# The index is a number from 0 to 5.
|
||||
# These numbers were calibrated with data of 250 players (Small sample size).
|
||||
Activity_index:
|
||||
Playtime_threshold: 30
|
||||
Playtime_threshold:
|
||||
Time: 30
|
||||
Unit: MINUTES
|
||||
Remove_inactive_player_data_after: 180
|
||||
Remove_inactive_player_data_after:
|
||||
Time: 3650
|
||||
Unit: DAYS
|
||||
# Includes players online, tps and performance time series
|
||||
Remove_time_series_data_after: 90
|
||||
Remove_time_series_data_after:
|
||||
Time: 90
|
||||
Unit: DAYS
|
||||
Remove_ping_data_after: 14
|
||||
Remove_ping_data_after:
|
||||
Time: 14
|
||||
Unit: DAYS
|
||||
Remove_disabled_extension_data_after: 2
|
||||
Remove_disabled_extension_data_after:
|
||||
Time: 2
|
||||
Unit: DAYS
|
||||
Periodic_tasks:
|
||||
Extension_data_refresh_every: 1
|
||||
Extension_data_refresh_every:
|
||||
Time: 1
|
||||
Unit: HOURS
|
||||
Check_DB_for_server_config_files_every: 1
|
||||
Check_DB_for_server_config_files_every:
|
||||
Time: 1
|
||||
Unit: MINUTES
|
||||
Clean_Database_every: 1
|
||||
Clean_Database_every:
|
||||
Time: 1
|
||||
Unit: HOURS
|
||||
# -----------------------------------------------------
|
||||
Display_options:
|
||||
|
@ -176,8 +198,8 @@ Formatting:
|
|||
Dates:
|
||||
# Show_recent_day_names replaces day number with Today, Yesterday, Wednesday etc.
|
||||
Show_recent_day_names: true
|
||||
# Non-regex pattern to replace
|
||||
DatePattern: 'MMM d YYYY'
|
||||
# Non-regex pattern to replace
|
||||
Show_recent_day_names_date_pattern: 'MMM d YYYY'
|
||||
Full: 'MMM d YYYY, HH:mm:ss'
|
||||
NoSeconds: 'MMM d YYYY, HH:mm'
|
||||
JustClock: HH:mm:ss
|
||||
|
@ -215,7 +237,8 @@ Export:
|
|||
Export_player_on_login_and_logout: false
|
||||
# If there are multiple servers the period is divided evenly to avoid export of all servers at once
|
||||
# Also affects Players page export
|
||||
Server_refresh_period: 20
|
||||
Server_refresh_period:
|
||||
Time: 20
|
||||
Unit: MINUTES
|
||||
# -----------------------------------------------------
|
||||
# These settings affect Plugin data integration.
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"name": "dashboard",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"proxy": "https://localhost:8804",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.1.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@fullcalendar/bootstrap": "^5.11.2",
|
||||
"@fullcalendar/daygrid": "^5.11.2",
|
||||
"@fullcalendar/react": "^5.11.2",
|
||||
"@highcharts/map-collection": "^2.0.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@testing-library/user-event": "^14.3.0",
|
||||
"axios": "^0.27.2",
|
||||
"bootstrap": "^5.2.0",
|
||||
"datatables.net": "^1.12.1",
|
||||
"datatables.net-bs5": "^1.12.1",
|
||||
"datatables.net-responsive-bs5": "^2.3.0",
|
||||
"highcharts": "^9.3.2",
|
||||
"i18next": "^21.8.16",
|
||||
"i18next-chained-backend": "^3.0.2",
|
||||
"i18next-http-backend": "^1.4.1",
|
||||
"i18next-localstorage-backend": "^3.1.3",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap-v5": "^1.4.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.18.3",
|
||||
"react-router-dom": "6",
|
||||
"react-scripts": "5.0.1",
|
||||
"sass": "^1.54.2",
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"swagger-ui": "^4.13.2",
|
||||
"web-vitals": "^2.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
|
||||
<meta content="Player Analytics, player page that displays more insights about a specific player"
|
||||
name="description">
|
||||
<meta content="AuroraLS3" name="author">
|
||||
<meta content="noindex, nofollow" name="robots">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link href="%PUBLIC_URL%/manifest.json" rel="manifest"/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Plan | Player Analytics</title>
|
||||
|
||||
|
||||
<link crossorigin="anonymous"
|
||||
href="https://fonts.googleapis.com/css?family=Nunito:400,700,800,900&display=swap&subset=latin-ext"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"short_name": "Plan",
|
||||
"name": "Player Analytics",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
|
@ -0,0 +1,38 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
import './style/main.sass';
|
||||
import './style/sb-admin-2.css'
|
||||
import './style/style.css';
|
||||
|
||||
import {BrowserRouter, Navigate, Route, Routes} from "react-router-dom";
|
||||
import React from "react";
|
||||
import PlayerPage from "./views/layout/PlayerPage";
|
||||
import PlayerOverview from "./views/player/PlayerOverview";
|
||||
import PlayerSessions from "./views/player/PlayerSessions";
|
||||
import PlayerPvpPve from "./views/player/PlayerPvpPve";
|
||||
import PlayerServers from "./views/player/PlayerServers";
|
||||
import PlayerPluginData from "./views/player/PlayerPluginData";
|
||||
import {ThemeContextProvider} from "./hooks/themeHook";
|
||||
import axios from "axios";
|
||||
import ErrorView from "./views/ErrorView";
|
||||
import {faMapSigns} from "@fortawesome/free-solid-svg-icons";
|
||||
import {MetadataContextProvider} from "./hooks/metadataHook";
|
||||
import {AuthenticationContextProvider} from "./hooks/authenticationHook";
|
||||
import {NavigationContextProvider} from "./hooks/navigationHook";
|
||||
import ServerPage from "./views/layout/ServerPage";
|
||||
import ServerOverview from "./views/server/ServerOverview";
|
||||
import MainPageRedirect from "./components/navigation/MainPageRedirect";
|
||||
import OnlineActivity from "./views/server/OnlineActivity";
|
||||
import ServerSessions from "./views/server/ServerSessions";
|
||||
import ServerPvpPve from "./views/server/ServerPvpPve";
|
||||
import PlayerbaseOverview from "./views/server/PlayerbaseOverview";
|
||||
import ServerPlayers from "./views/server/ServerPlayers";
|
||||
import PlayersPage from "./views/layout/PlayersPage";
|
||||
import AllPlayers from "./views/players/AllPlayers";
|
||||
import ServerGeolocations from "./views/server/ServerGeolocations";
|
||||
|
||||
const SwaggerView = React.lazy(() => import("./views/SwaggerView"));
|
||||
|
||||
const OverviewRedirect = () => {
|
||||
return (<Navigate to={"overview"} replace={true}/>)
|
||||
}
|
||||
|
||||
const ContextProviders = ({children}) => (
|
||||
<AuthenticationContextProvider>
|
||||
<MetadataContextProvider>
|
||||
<ThemeContextProvider>
|
||||
<NavigationContextProvider>
|
||||
{children}
|
||||
</NavigationContextProvider>
|
||||
</ThemeContextProvider>
|
||||
</MetadataContextProvider>
|
||||
</AuthenticationContextProvider>
|
||||
)
|
||||
|
||||
function App() {
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<ContextProviders>
|
||||
<div id="wrapper">
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="" element={<MainPageRedirect/>}/>
|
||||
<Route path="/player/:identifier" element={<PlayerPage/>}>
|
||||
<Route path="" element={<OverviewRedirect/>}/>
|
||||
<Route path="overview" element={<PlayerOverview/>}/>
|
||||
<Route path="sessions" element={<PlayerSessions/>}/>
|
||||
<Route path="pvppve" element={<PlayerPvpPve/>}/>
|
||||
<Route path="servers" element={<PlayerServers/>}/>
|
||||
<Route path="plugins/:serverName" element={<PlayerPluginData/>}/>
|
||||
<Route path="*" element={<ErrorView error={{
|
||||
message: 'Unknown tab address, please correct the address',
|
||||
title: 'No such tab',
|
||||
icon: faMapSigns
|
||||
}}/>}/>
|
||||
</Route>
|
||||
<Route path="/players" element={<PlayersPage/>}>
|
||||
<Route path="" element={<AllPlayers/>}/>
|
||||
<Route path="*" element={<AllPlayers/>}/>
|
||||
</Route>
|
||||
<Route path="/server/:identifier" element={<ServerPage/>}>
|
||||
<Route path="" element={<OverviewRedirect/>}/>
|
||||
<Route path="overview" element={<ServerOverview/>}/>
|
||||
<Route path="online-activity" element={<OnlineActivity/>}/>
|
||||
<Route path="sessions" element={<ServerSessions/>}/>
|
||||
<Route path="pvppve" element={<ServerPvpPve/>}/>
|
||||
<Route path="playerbase" element={<PlayerbaseOverview/>}/>
|
||||
<Route path="retention" element={<></>}/>
|
||||
<Route path="players" element={<ServerPlayers/>}/>
|
||||
<Route path="geolocations" element={<ServerGeolocations/>}/>
|
||||
<Route path="performance" element={<></>}/>
|
||||
<Route path="plugins-overview" element={<></>}/>
|
||||
</Route>
|
||||
<Route path="docs" element={<React.Suspense fallback={<></>}>
|
||||
<SwaggerView/>
|
||||
</React.Suspense>}/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
</ContextProviders>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,8 @@
|
|||
import {render, screen} from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App/>);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
|
@ -0,0 +1,51 @@
|
|||
import React, {useEffect, useState} from "react";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
|
||||
const TabButton = ({name, href, icon, color, active}) => {
|
||||
return (
|
||||
<li className="nav-item">
|
||||
<a className={"nav-link col-black" + (active ? ' active' : '')} aria-selected={active} role="tab"
|
||||
href={'#' + href}>
|
||||
<Fa icon={icon} className={'col-' + color}/> {name}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const TabButtons = ({tabs, selectedTab}) => {
|
||||
return (
|
||||
<ul className="nav nav-tabs" role="tablist">
|
||||
{tabs.map((tab, i) => (
|
||||
<TabButton
|
||||
key={i}
|
||||
name={tab.name}
|
||||
href={tab.href}
|
||||
icon={tab.icon}
|
||||
color={tab.color}
|
||||
active={tab.href === selectedTab}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
const CardTabs = ({tabs}) => {
|
||||
const {hash} = useLocation();
|
||||
const firstTab = tabs ? tabs[0].href : undefined;
|
||||
const [selectedTab, setSelectedTab] = useState(firstTab);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTab(hash && tabs ? tabs.find(t => t.href === hash.substring(1)).href : firstTab)
|
||||
}, [hash, tabs, firstTab])
|
||||
|
||||
const tabContent = tabs.find(t => t.href === selectedTab).element;
|
||||
return (
|
||||
<>
|
||||
<TabButtons tabs={tabs} selectedTab={selectedTab}/>
|
||||
{tabContent}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CardTabs
|
|
@ -0,0 +1,17 @@
|
|||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import End from "./layout/End";
|
||||
|
||||
const Datapoint = ({icon, color, name, value, valueLabel, bold, boldTitle, title, trend}) => {
|
||||
const displayedValue = bold ? <b>{value}</b> : value;
|
||||
const extraLabel = typeof valueLabel === 'string' ? ` (${valueLabel})` : '';
|
||||
const colorClass = color && color.startsWith("col-") ? color : "col-" + color;
|
||||
return (
|
||||
<p title={title ? title : name + " is " + value}>
|
||||
{icon && <Fa icon={icon} className={colorClass}/>} {boldTitle ? <b>{name}</b> : name}
|
||||
{value !== undefined ? <End>{displayedValue} {extraLabel}{trend}</End> : ''}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export default Datapoint
|
|
@ -0,0 +1,9 @@
|
|||
import React from "react";
|
||||
|
||||
const Scrollable = ({children}) => (
|
||||
<div className="scrollbar">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Scrollable;
|
|
@ -0,0 +1,71 @@
|
|||
import React, {useState} from "react";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
|
||||
const SliceHeader = ({i, open, onClick, slice}) => {
|
||||
let style = 'bg-' + slice.color + (slice.outline ? '-outline' : '');
|
||||
return (
|
||||
<tr id={"slice_h_" + i} aria-controls={"slice_t_" + i} aria-expanded={open ? "true" : "false"}
|
||||
className={"clickable collapsed " + style} data-bs-target={"#slice_t_" + i} data-bs-toggle="collapse"
|
||||
onClick={onClick}
|
||||
>
|
||||
{slice.header}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const SliceBody = ({i, open, slice, width}) => {
|
||||
if (!open) return <tr className={open ? 'open' : 'closed'}/>
|
||||
|
||||
return (
|
||||
<tr className={"collapse" + (open ? ' show' : '')} data-bs-parent="#tableAccordion" id={"slice_t_" + i}>
|
||||
<td colSpan={width}>
|
||||
{slice.body}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const Slice = ({i, slice, open, onClick, width}) => (
|
||||
<>
|
||||
<SliceHeader i={i} open={open} onClick={onClick} slice={slice}/>
|
||||
<SliceBody i={i} open={open} slice={slice} width={width}/>
|
||||
</>
|
||||
)
|
||||
|
||||
const NoDataRow = ({width}) => {
|
||||
const nLengthArray = Array.from(Array(width - 1).keys());
|
||||
return (<tr>
|
||||
<td>No Data</td>
|
||||
{nLengthArray.map(i => <td key={i}>-</td>)}
|
||||
</tr>);
|
||||
}
|
||||
|
||||
const Accordion = ({headers, slices, open}) => {
|
||||
const [openSlice, setOpenSlice] = useState(open ? 0 : -1);
|
||||
const {nightModeEnabled} = useTheme();
|
||||
|
||||
const toggleSlice = (i) => {
|
||||
setOpenSlice(openSlice === i ? -1 : i);
|
||||
}
|
||||
|
||||
const width = headers.length;
|
||||
|
||||
return (
|
||||
<table className={"table accordion-striped" + (nightModeEnabled ? " table-dark" : '')} id="tableAccordion">
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, i) => <th key={i}>{header}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{slices.length ? slices.map((slice, i) => (
|
||||
<Slice key={'slice-' + i} i={i} slice={slice} width={width}
|
||||
open={openSlice === i} onClick={() => toggleSlice(i)}
|
||||
/>
|
||||
)) : <NoDataRow width={width}/>}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
export default Accordion;
|
|
@ -0,0 +1,111 @@
|
|||
import React from "react";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCrosshairs, faGavel, faLocationArrow, faServer, faSkull} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faCalendarCheck, faCalendarPlus, faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
import Datapoint from "../Datapoint";
|
||||
import {Col, Row} from "react-bootstrap-v5";
|
||||
import WorldPie from "../graphs/WorldPie";
|
||||
import Accordion from "./Accordion";
|
||||
import {faSuperpowers} from "@fortawesome/free-brands-svg-icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {baseAddress} from "../../service/backendConfiguration";
|
||||
import {useAuth} from "../../hooks/authenticationHook";
|
||||
|
||||
const ServerHeader = ({server}) => {
|
||||
return (
|
||||
<>
|
||||
<td>{server.server_name}
|
||||
{server.operator ? <Fa icon={faSuperpowers} title="Operator"/> : ''}
|
||||
{server.banned ? <Fa icon={faGavel} title="Banned"/> : ''}
|
||||
</td>
|
||||
<td>{server.playtime}</td>
|
||||
<td>{server.registered}</td>
|
||||
<td>{server.last_seen}</td>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ServerBody = ({i, server}) => {
|
||||
const {t} = useTranslation();
|
||||
const {hasPermission} = useAuth();
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col lg={6}>
|
||||
{server.operator ? <Datapoint icon={faSuperpowers} color="blue" name={t('html.label.operator')}/> : ''}
|
||||
{server.banned ? <Datapoint icon={faGavel} color="red" name={t('html.label.banned')}/> : ''}
|
||||
{server.operator || server.banned ? <br/> : ''}
|
||||
<Datapoint
|
||||
icon={faCalendarCheck} color={"teal"}
|
||||
name={t('html.label.sessions')} value={server.session_count} bold
|
||||
/>
|
||||
<Datapoint
|
||||
icon={faClock} color={"green"}
|
||||
name={t('html.label.playtime')} value={server.playtime} bold
|
||||
/>
|
||||
<Datapoint
|
||||
icon={faClock} color={"grey"}
|
||||
name={t('html.label.afkTime')} value={server.afk_time} bold
|
||||
/>
|
||||
<Datapoint
|
||||
icon={faClock} color={"teal"}
|
||||
name={t('html.label.longestSession')} value={server.longest_session_length} bold
|
||||
/>
|
||||
<Datapoint
|
||||
icon={faClock} color={"teal"}
|
||||
name={t('html.label.sessionMedian')} value={server.session_median} bold
|
||||
/>
|
||||
<br/>
|
||||
<Datapoint
|
||||
icon={faLocationArrow} color={"amber"}
|
||||
name={t('html.label.joinAddress')} value={server.join_address}
|
||||
/>
|
||||
<br/>
|
||||
<Datapoint
|
||||
icon={faCrosshairs} color="red"
|
||||
name={t('html.label.playerKills')} value={server.player_kills} bold
|
||||
/>
|
||||
<Datapoint
|
||||
icon={faCrosshairs} color="green"
|
||||
name={t('html.label.mobKills')} value={server.mob_kills} bold
|
||||
/>
|
||||
<Datapoint
|
||||
icon={faSkull} color="black"
|
||||
name={t('html.label.deaths')} value={server.deaths} bold
|
||||
/>
|
||||
<hr/>
|
||||
</Col>
|
||||
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-6">
|
||||
<WorldPie id={"worldpie_server_" + i}
|
||||
worldSeries={server.world_pie_series}
|
||||
gmSeries={server.gm_series}/>
|
||||
|
||||
{hasPermission('page.server') && <a href={`${baseAddress}/server/${server.server_uuid}`}
|
||||
className="float-end btn bg-light-green me-2">
|
||||
<Fa icon={faServer}/> {t('html.label.serverPage')}
|
||||
</a>}
|
||||
</div>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
const ServerAccordion = ({servers}) => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Accordion headers={[
|
||||
<><Fa icon={faServer}/> {t('html.label.server')}</>,
|
||||
<><Fa icon={faClock}/> {t('html.label.playtime')}</>,
|
||||
<><Fa icon={faCalendarPlus}/> {t('html.label.registered')}</>,
|
||||
<><Fa icon={faCalendarCheck}/> {t('html.label.lastSeen')}</>
|
||||
]} slices={servers.map(server => {
|
||||
return {
|
||||
body: <ServerBody server={server}/>,
|
||||
header: <ServerHeader server={server}/>,
|
||||
color: 'light-green',
|
||||
outline: true
|
||||
}
|
||||
})}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerAccordion
|
|
@ -0,0 +1,111 @@
|
|||
import React from "react";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCrosshairs, faServer, faSignal, faSkull, faUser} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faCalendarPlus, faClock, faMap} from "@fortawesome/free-regular-svg-icons";
|
||||
import Datapoint from "../Datapoint";
|
||||
import {Col, Row} from "react-bootstrap-v5";
|
||||
import WorldPie from "../graphs/WorldPie";
|
||||
import KillsTable from "../table/KillsTable";
|
||||
import Accordion from "./Accordion";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {baseAddress} from "../../service/backendConfiguration";
|
||||
|
||||
const SessionHeader = ({session}) => {
|
||||
return (
|
||||
<>
|
||||
<td>{session.name}{session.first_session ?
|
||||
<Fa icon={faCalendarPlus} title="Registered (First session)"/> : ''}</td>
|
||||
<td>{session.start}</td>
|
||||
<td>{session.length}</td>
|
||||
<td>{session.network_server ? session.network_server : session.most_used_world}</td>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SessionBody = ({i, session}) => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Row>
|
||||
<Col lg={6}>
|
||||
<Datapoint
|
||||
icon={faClock} color={"teal"}
|
||||
name={t('html.label.sessionEnded')} value={session.end} bold
|
||||
/>
|
||||
<Datapoint
|
||||
icon={faClock} color={"teal"}
|
||||
name={t('html.label.length')} value={session.length} bold
|
||||
/>
|
||||
<Datapoint
|
||||
icon={faClock} color={"grey"}
|
||||
name={t('html.label.afkTime')} value={session.afk_time} bold
|
||||
/>
|
||||
<Datapoint
|
||||
icon={faServer} color={"green"}
|
||||
name={t('html.label.server')} value={session.server_name} bold
|
||||
/>
|
||||
{session.avg_ping ? <Datapoint
|
||||
icon={faSignal} color={"amber"}
|
||||
name={t('html.label.averagePing')} value={session.avg_ping} bold
|
||||
/> : ''}
|
||||
<br/>
|
||||
<Datapoint
|
||||
icon={faCrosshairs} color="red"
|
||||
name={t('html.label.playerKills')} value={session.player_kills.length} bold
|
||||
/>
|
||||
<Datapoint
|
||||
icon={faCrosshairs} color="green"
|
||||
name={t('html.label.mobKills')} value={session.mob_kills} bold
|
||||
/>
|
||||
<Datapoint
|
||||
icon={faSkull} color="black"
|
||||
name={t('html.label.deaths')} value={session.deaths} bold
|
||||
/>
|
||||
<hr/>
|
||||
<KillsTable kills={session.player_kills}/>
|
||||
</Col>
|
||||
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-6">
|
||||
<WorldPie id={"worldpie_" + i}
|
||||
worldSeries={session.world_series}
|
||||
gmSeries={session.gm_series}/>
|
||||
<a href={`${baseAddress}/player/${session.player_uuid}`}
|
||||
className="float-end btn bg-blue">
|
||||
<Fa icon={faUser}/> {t('html.label.playerPage')}
|
||||
</a>
|
||||
{session.network_server ? <a href={`${baseAddress}/server/${session.server_uuid}`}
|
||||
className="float-end btn bg-light-green me-2">
|
||||
<Fa icon={faServer}/> {t('html.label.serverPage')}
|
||||
</a> : ''}
|
||||
</div>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
const SessionAccordion = (
|
||||
{
|
||||
sessions,
|
||||
isPlayer
|
||||
}
|
||||
) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const firstColumn = isPlayer ? (<><Fa icon={faUser}/> {t('html.label.player')}</>)
|
||||
: (<><Fa icon={faServer}/> {t('html.label.server')}</>)
|
||||
|
||||
return (
|
||||
<Accordion headers={[
|
||||
firstColumn,
|
||||
<><Fa icon={faClock}/> {t('html.label.sessionStart')}</>,
|
||||
<><Fa icon={faClock}/> {t('html.label.length')}</>,
|
||||
<><Fa icon={faMap}/> {t('html.label.mostPlayedWorld')}</>
|
||||
]} slices={sessions.map(session => {
|
||||
return {
|
||||
body: <SessionBody session={session}/>,
|
||||
header: <SessionHeader session={session}/>,
|
||||
color: 'teal',
|
||||
outline: !session.start.includes("Online")
|
||||
}
|
||||
})}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SessionAccordion
|
|
@ -0,0 +1,28 @@
|
|||
import React from "react";
|
||||
import FullCalendar from '@fullcalendar/react'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
|
||||
const PlayerSessionCalendar = ({series, firstDay}) => {
|
||||
return (
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin]}
|
||||
timeZone="UTC"
|
||||
themeSystem='bootstrap'
|
||||
eventColor='#009688'
|
||||
dayMaxEventRows={4}
|
||||
firstDay={firstDay}
|
||||
initialView='dayGridMonth'
|
||||
navLinks={true}
|
||||
height={560}
|
||||
contentHeight={560}
|
||||
headerToolbar={{
|
||||
left: 'title',
|
||||
center: '',
|
||||
right: 'dayGridMonth dayGridWeek dayGridDay today prev next'
|
||||
}}
|
||||
events={(_fetchInfo, successCallback) => successCallback(series)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerSessionCalendar
|
|
@ -0,0 +1,28 @@
|
|||
import React from "react";
|
||||
import FullCalendar from '@fullcalendar/react'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
|
||||
const ServerCalendar = ({series, firstDay}) => {
|
||||
return (
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin]}
|
||||
timeZone="UTC"
|
||||
themeSystem='bootstrap'
|
||||
eventColor='#2196F3'
|
||||
// dayMaxEventRows={4}
|
||||
firstDay={firstDay}
|
||||
initialView='dayGridMonth'
|
||||
navLinks={true}
|
||||
height={800}
|
||||
contentHeight={800}
|
||||
headerToolbar={{
|
||||
left: 'title',
|
||||
center: '',
|
||||
right: 'dayGridMonth dayGridWeek dayGridDay today prev next'
|
||||
}}
|
||||
events={(_fetchInfo, successCallback) => successCallback(series)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerCalendar
|
|
@ -0,0 +1,9 @@
|
|||
.fc-button
|
||||
background-color: #368F17 !important
|
||||
|
||||
.fc-header-toolbar
|
||||
margin-bottom: 0 !important
|
||||
padding: 0.5rem
|
||||
|
||||
.fc-button
|
||||
border-color: transparent !important
|
|
@ -0,0 +1,42 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card, Col, Row} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import {faExclamationTriangle, faGlobe} from "@fortawesome/free-solid-svg-icons";
|
||||
import GeolocationBarGraph from "../../graphs/GeolocationBarGraph";
|
||||
import GeolocationWorldMap from "../../graphs/GeolocationWorldMap";
|
||||
|
||||
const GeolocationsCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
if (!data?.geolocations_enabled) {
|
||||
return (
|
||||
<div className="alert alert-warning mb-0" id="geolocation-warning">
|
||||
<Fa icon={faExclamationTriangle}/>{' '}
|
||||
{t('html.description.noGeolocations')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faGlobe} className="col-green"/> {t('html.label.geolocations')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<Card.Body className="chart-area" style={{height: "100%"}}>
|
||||
<Row>
|
||||
<Col md={3}>
|
||||
<GeolocationBarGraph series={data.geolocation_bar_series} color={data.colors.bars}/>
|
||||
</Col>
|
||||
<Col md={9}>
|
||||
<GeolocationWorldMap series={data.geolocation_series} colors={data.colors}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default GeolocationsCard;
|
|
@ -0,0 +1,23 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import {faLifeRing} from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
const InsightsFor30DaysCard = ({children}) => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faLifeRing} className="col-red"/> {t('html.label.insights30days')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
{children}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default InsightsFor30DaysCard;
|
|
@ -0,0 +1,41 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import DataTablesTable from "../../table/DataTablesTable";
|
||||
import {CardLoader} from "../../navigation/Loader";
|
||||
|
||||
const PlayerListCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
const [options, setOptions] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
for (const row of data.data) {
|
||||
row.name = row.name.replace('../player/', '../../player/');
|
||||
}
|
||||
|
||||
setOptions({
|
||||
responsive: true,
|
||||
deferRender: true,
|
||||
columns: data.columns,
|
||||
data: data.data,
|
||||
order: [[5, "desc"]]
|
||||
});
|
||||
}, [data])
|
||||
|
||||
if (!options) return <CardLoader/>
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faUsers} className="col-black"/> {t('html.label.playerList')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<DataTablesTable id={"players-table"} options={options}/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerListCard;
|
|
@ -0,0 +1,25 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCrosshairs} from "@fortawesome/free-solid-svg-icons";
|
||||
import KillsTable from "../../table/KillsTable";
|
||||
import React from "react";
|
||||
|
||||
const PvpKillsTableCard = ({player_kills}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
if (!player_kills) return <></>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faCrosshairs} className="col-red"/> {t('html.label.recentPvpKills')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<KillsTable kills={player_kills}/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default PvpKillsTableCard;
|
|
@ -0,0 +1,28 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCalendar, faHandPointer} from "@fortawesome/free-regular-svg-icons";
|
||||
import Scrollable from "../../Scrollable";
|
||||
import SessionAccordion from "../../accordion/SessionAccordion";
|
||||
import React from "react";
|
||||
|
||||
const RecentSessionsCard = ({sessions, isPlayer}) => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black" style={{width: '100%'}}>
|
||||
<Fa icon={faCalendar} className="col-teal"/> {t('html.label.recentSessions')}
|
||||
<span className="float-end">
|
||||
<Fa icon={faHandPointer}/> <small>{t('html.text.clickToExpand')}</small>
|
||||
</span>
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<Scrollable>
|
||||
<SessionAccordion sessions={sessions} isPlayer={isPlayer}/>
|
||||
</Scrollable>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecentSessionsCard;
|
|
@ -0,0 +1,21 @@
|
|||
import {Card} from "react-bootstrap-v5";
|
||||
import Datapoint from "../../Datapoint";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faLongArrowAltRight} from "@fortawesome/free-solid-svg-icons";
|
||||
import BigTrend from "../../trend/BigTrend";
|
||||
import React from "react";
|
||||
|
||||
const TrendCard = ({icon, color, name, previous, next, trend}) => {
|
||||
return <Card>
|
||||
<Card.Body>
|
||||
<Datapoint icon={icon} name={name} color={color}
|
||||
value={
|
||||
<>
|
||||
{previous} <Fa icon={faLongArrowAltRight}/> {next} <BigTrend trend={trend}/>
|
||||
</>
|
||||
}/>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
}
|
||||
|
||||
export default TrendCard;
|
|
@ -0,0 +1,26 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
import WorldPie from "../../graphs/WorldPie";
|
||||
import React from "react";
|
||||
|
||||
const WorldPieCard = ({worldSeries, gmSeries}) => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black" style={{width: '100%'}}>
|
||||
<Fa icon={faClock} className="col-teal"/> {t('html.label.worldPlaytime')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<WorldPie
|
||||
id="world-pie"
|
||||
worldSeries={worldSeries}
|
||||
gmSeries={gmSeries}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorldPieCard;
|
|
@ -0,0 +1,42 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {useTheme} from "../../../hooks/themeHook";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faServer, faSignature} from "@fortawesome/free-solid-svg-icons";
|
||||
import Scrollable from "../../Scrollable";
|
||||
import {faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
import React from "react";
|
||||
|
||||
const NicknamesCard = ({player}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled} = useTheme();
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faSignature}/> {t('html.label.seenNicknames')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<Scrollable>
|
||||
<table className={"table table-striped mb-0" + (nightModeEnabled ? " table-dark" : '')}>
|
||||
<thead className="bg-purple">
|
||||
<tr>
|
||||
<th><Fa icon={faSignature}/> {t('html.label.nickname')}</th>
|
||||
<th><Fa icon={faServer}/> {t('html.label.server')}</th>
|
||||
<th><Fa icon={faClock}/> {t('html.label.lastSeen')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{player.nicknames.map((nickname, i) => (<tr key={'nick-' + i}>
|
||||
<td dangerouslySetInnerHTML={{__html: nickname.nickname}}/>
|
||||
<td>{nickname.server}</td>
|
||||
<td>{nickname.date}</td>
|
||||
</tr>))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Scrollable>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default NicknamesCard;
|
|
@ -0,0 +1,10 @@
|
|||
import React from "react";
|
||||
import RecentSessionsCard from "../common/RecentSessionsCard";
|
||||
|
||||
const PlayerRecentSessionsCard = ({player}) => {
|
||||
return (
|
||||
<RecentSessionsCard sessions={player.sessions}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerRecentSessionsCard;
|
|
@ -0,0 +1,13 @@
|
|||
import React from "react";
|
||||
import WorldPieCard from "../common/WorldPieCard";
|
||||
|
||||
const PlayerWorldPieCard = ({player}) => {
|
||||
return (
|
||||
<WorldPieCard
|
||||
worldSeries={player.world_pie_series}
|
||||
gmSeries={player.gm_series}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerWorldPieCard;
|
|
@ -0,0 +1,22 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCampground} from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import PlayerPvpPveAsNumbersTable from "../../table/PlayerPvpPveAsNumbersTable";
|
||||
|
||||
const PvpPveAsNumbersCard = ({player}) => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faCampground} className="col-red"/> {t('html.label.pvpPveAsNumbers')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<PlayerPvpPveAsNumbersTable killData={player.kill_data}/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default PvpPveAsNumbersCard;
|
|
@ -0,0 +1,33 @@
|
|||
import React from "react";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||
import {fetchPlayerbaseDevelopmentGraph} from "../../../../service/serverService";
|
||||
import {ErrorViewBody} from "../../../../views/ErrorView";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import PlayerbasePie from "../../../graphs/PlayerbasePie";
|
||||
|
||||
const CurrentPlayerbaseCard = () => {
|
||||
const {t} = useTranslation();
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchPlayerbaseDevelopmentGraph, [identifier]);
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <></>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black" style={{width: '100%'}}>
|
||||
<Fa icon={faUsers} className="col-amber"/> {t('html.label.currentPlayerbase')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<PlayerbasePie series={data.activity_pie_series}/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default CurrentPlayerbaseCard;
|
|
@ -0,0 +1,36 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||
import {fetchPlayersOnlineGraph} from "../../../../service/serverService";
|
||||
import {ErrorViewCard} from "../../../../views/ErrorView";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faChartArea} from "@fortawesome/free-solid-svg-icons";
|
||||
import PlayersOnlineGraph from "../../../graphs/PlayersOnlineGraph";
|
||||
import React from "react";
|
||||
import {CardLoader} from "../../../navigation/Loader";
|
||||
|
||||
const OnlineActivityCard = () => {
|
||||
const {t} = useTranslation();
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(
|
||||
fetchPlayersOnlineGraph,
|
||||
[identifier])
|
||||
|
||||
if (loadingError) return <ErrorViewCard error={loadingError}/>
|
||||
if (!data) return <CardLoader/>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa className="col-blue" icon={faChartArea}/> {t('html.label.onlineActivity')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<PlayersOnlineGraph data={data}/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnlineActivityCard;
|
|
@ -0,0 +1,86 @@
|
|||
import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||
import {
|
||||
fetchDayByDayGraph,
|
||||
fetchHourByHourGraph,
|
||||
fetchPunchCardGraph,
|
||||
fetchServerCalendarGraph
|
||||
} from "../../../../service/serverService";
|
||||
import {ErrorViewBody} from "../../../../views/ErrorView";
|
||||
import PunchCard from "../../../graphs/PunchCard";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import CardTabs from "../../../CardTabs";
|
||||
import {faBraille, faChartArea} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faCalendar} from "@fortawesome/free-regular-svg-icons";
|
||||
import React from "react";
|
||||
import TimeByTimeGraph from "../../../graphs/TimeByTimeGraph";
|
||||
import ServerCalendar from "../../../calendar/ServerCalendar";
|
||||
import {ChartLoader} from "../../../navigation/Loader";
|
||||
|
||||
const DayByDayTab = () => {
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchDayByDayGraph, [identifier])
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return <TimeByTimeGraph data={data}/>
|
||||
}
|
||||
|
||||
const HourByHourTab = () => {
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchHourByHourGraph, [identifier])
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return <TimeByTimeGraph data={data}/>
|
||||
}
|
||||
|
||||
const ServerCalendarTab = () => {
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchServerCalendarGraph, [identifier])
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return <ServerCalendar series={data.data} firstDay={data.firstDay}/>
|
||||
}
|
||||
|
||||
const PunchCardTab = () => {
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchPunchCardGraph, [identifier])
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return <PunchCard series={data.punchCard}/>
|
||||
}
|
||||
|
||||
const OnlineActivityGraphsCard = () => {
|
||||
const {t} = useTranslation();
|
||||
return <Card>
|
||||
<CardTabs tabs={[
|
||||
{
|
||||
name: t('html.label.dayByDay'), icon: faChartArea, color: 'blue', href: 'day-by-day',
|
||||
element: <DayByDayTab/>
|
||||
}, {
|
||||
name: t('html.label.hourByHour'), icon: faChartArea, color: 'blue', href: 'hour-by-hour',
|
||||
element: <HourByHourTab/>
|
||||
}, {
|
||||
name: t('html.label.serverCalendar'), icon: faCalendar, color: 'teal', href: 'server-calendar',
|
||||
element: <ServerCalendarTab/>
|
||||
}, {
|
||||
name: t('html.label.punchcard30days'), icon: faBraille, color: 'black', href: 'punchcard',
|
||||
element: <PunchCardTab/>
|
||||
},
|
||||
]}/>
|
||||
</Card>
|
||||
}
|
||||
|
||||
export default OnlineActivityGraphsCard;
|
|
@ -0,0 +1,35 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||
import {fetchPlayerbaseDevelopmentGraph} from "../../../../service/serverService";
|
||||
import {ErrorViewCard} from "../../../../views/ErrorView";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faChartLine} from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import PlayerbaseGraph from "../../../graphs/PlayerbaseGraph";
|
||||
|
||||
const PlayerbaseDevelopmentCard = () => {
|
||||
const {t} = useTranslation();
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(
|
||||
fetchPlayerbaseDevelopmentGraph,
|
||||
[identifier])
|
||||
|
||||
if (!data) return <></>;
|
||||
if (loadingError) return <ErrorViewCard error={loadingError}/>
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa className="col-amber" icon={faChartLine}/> {t('html.label.playerbaseDevelopment')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<PlayerbaseGraph data={data}/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerbaseDevelopmentCard;
|
|
@ -0,0 +1,25 @@
|
|||
import React from "react";
|
||||
import WorldPieCard from "../../common/WorldPieCard";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||
import {fetchWorldPie} from "../../../../service/serverService";
|
||||
import {ErrorViewBody} from "../../../../views/ErrorView";
|
||||
import {CardLoader} from "../../../navigation/Loader";
|
||||
|
||||
const ServerWorldPieCard = () => {
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchWorldPie, [identifier]);
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <CardLoader/>;
|
||||
|
||||
return (
|
||||
<WorldPieCard
|
||||
worldSeries={data.world_series}
|
||||
gmSeries={data.gm_series}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerWorldPieCard;
|
|
@ -0,0 +1,39 @@
|
|||
import React from "react";
|
||||
import InsightsFor30DaysCard from "../../common/InsightsFor30DaysCard";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import Datapoint from "../../../Datapoint";
|
||||
import {faUserClock, faUserGroup} from "@fortawesome/free-solid-svg-icons";
|
||||
import SmallTrend from "../../../trend/SmallTrend";
|
||||
import {faCalendar, faCalendarPlus} from "@fortawesome/free-regular-svg-icons";
|
||||
import ComparingLabel from "../../../trend/ComparingLabel";
|
||||
import End from "../../../layout/End";
|
||||
|
||||
const OnlineActivityInsightsCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
if (!data) return <></>
|
||||
|
||||
return (
|
||||
<InsightsFor30DaysCard>
|
||||
<Datapoint name={t('html.label.onlineOnFirstJoin')} icon={faUserGroup} color="light-green"
|
||||
value={data.players_first_join_avg}
|
||||
trend={<SmallTrend trend={data.players_first_join_avg_trend}/>}/>
|
||||
<Datapoint name={t('html.label.firstSessionLength.average')} icon={faUserClock} color="light-green"
|
||||
value={data.first_session_length_avg}
|
||||
trend={<SmallTrend trend={data.first_session_length_avg_trend}/>}/>
|
||||
<Datapoint name={t('html.label.firstSessionLength.median')} icon={faUserClock} color="light-green"
|
||||
value={data.first_session_length_median}
|
||||
trend={<SmallTrend trend={data.first_session_length_median_trend}/>}/>
|
||||
<Datapoint name={t('html.label.loneJoins')} icon={faCalendar} color="teal"
|
||||
value={data.lone_joins}
|
||||
trend={<SmallTrend trend={data.lone_joins_trend}/>}/>
|
||||
<Datapoint name={t('html.label.loneNewbieJoins')} icon={faCalendarPlus} color="teal"
|
||||
value={data.lone_new_joins}
|
||||
trend={<SmallTrend trend={data.lone_new_joins_trend}/>}/>
|
||||
<End>
|
||||
<ComparingLabel>{t('html.text.comparing15days')}</ComparingLabel>
|
||||
</End>
|
||||
</InsightsFor30DaysCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnlineActivityInsightsCard;
|
|
@ -0,0 +1,45 @@
|
|||
import React from "react";
|
||||
import InsightsFor30DaysCard from "../../common/InsightsFor30DaysCard";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import Datapoint from "../../../Datapoint";
|
||||
import {faLongArrowAltRight, faUser} from "@fortawesome/free-solid-svg-icons";
|
||||
import SmallTrend from "../../../trend/SmallTrend";
|
||||
import End from "../../../layout/End";
|
||||
import ComparingLabel from "../../../trend/ComparingLabel";
|
||||
|
||||
const TwoPlayerChange = ({colorBefore, labelBefore, colorAfter, labelAfter}) => {
|
||||
return (
|
||||
<>
|
||||
<Fa icon={faUser} className={`col-${colorBefore}`}/>{' '}{labelBefore}
|
||||
{' '}<Fa icon={faLongArrowAltRight}/>{' '}
|
||||
<Fa icon={faUser} className={`col-${colorAfter}`}/>{' '}{labelAfter}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const PlayerbaseInsightsCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
if (!data) return <></>;
|
||||
return (
|
||||
<InsightsFor30DaysCard>
|
||||
<Datapoint name={<TwoPlayerChange colorBefore={'light-green'}
|
||||
labelBefore={t('html.label.new')}
|
||||
colorAfter={'lime'}
|
||||
labelAfter={t('html.label.regular')}/>}
|
||||
value={data.new_to_regular}
|
||||
trend={<SmallTrend trend={data.new_to_regular_trend}/>}
|
||||
/>
|
||||
<Datapoint name={<TwoPlayerChange colorBefore={'lime'}
|
||||
labelBefore={t('html.label.regular')}
|
||||
colorAfter={'bluegray'}
|
||||
labelAfter={t('html.label.inactive')}/>}
|
||||
value={data.regular_to_inactive}
|
||||
trend={<SmallTrend trend={data.regular_to_inactive_trend}/>}
|
||||
/>
|
||||
<End><ComparingLabel>{t('html.text.comparing30daysAgo')}</ComparingLabel></End>
|
||||
</InsightsFor30DaysCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerbaseInsightsCard;
|
|
@ -0,0 +1,22 @@
|
|||
import React from "react";
|
||||
import InsightsFor30DaysCard from "../../common/InsightsFor30DaysCard";
|
||||
import Datapoint from "../../../Datapoint";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {faKhanda} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const PvpPveInsightsCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<InsightsFor30DaysCard>
|
||||
<Datapoint name={t('html.label.deadliestWeapon')} icon={faKhanda} color="amber"
|
||||
value={data.weapon_1st}/>
|
||||
<Datapoint name={t('html.label.secondDeadliestWeapon')} icon={faKhanda} color="gray"
|
||||
value={data.weapon_2nd}/>
|
||||
<Datapoint name={t('html.label.thirdDeadliestWeapon')} icon={faKhanda} color="brown"
|
||||
value={data.weapon_3rd}/>
|
||||
</InsightsFor30DaysCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default PvpPveInsightsCard;
|
|
@ -0,0 +1,39 @@
|
|||
import React from "react";
|
||||
import InsightsFor30DaysCard from "../../common/InsightsFor30DaysCard";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||
import {fetchSessionOverview} from "../../../../service/serverService";
|
||||
import ErrorView from "../../../../views/ErrorView";
|
||||
import Datapoint from "../../../Datapoint";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {faGamepad, faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
const SessionInsightsCard = () => {
|
||||
const {t} = useTranslation();
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchSessionOverview, [identifier]);
|
||||
|
||||
if (!data) return <></>;
|
||||
if (loadingError) return <ErrorView error={loadingError}/>
|
||||
|
||||
return (
|
||||
<InsightsFor30DaysCard>
|
||||
<Datapoint name={t('html.label.mostActiveGamemode')} icon={faGamepad} color="teal" bold
|
||||
value={data.insights.most_active_gamemode} valueLabel={data.insights.most_active_gamemode_perc}
|
||||
/>
|
||||
<Datapoint name={t('html.label.serverOccupied')} icon={faUsers} color="teal"
|
||||
value={'~' + data.insights.server_occupied} valueLabel={data.insights.server_occupied_perc}
|
||||
/>
|
||||
<Datapoint name={t('html.label.playtime')} icon={faClock} color="green"
|
||||
value={data.insights.total_playtime}
|
||||
/>
|
||||
<Datapoint name={t('html.label.afkTime')} icon={faClock} color="grey"
|
||||
value={data.insights.afk_time} valueLabel={data.insights.afk_time_perc}
|
||||
/>
|
||||
</InsightsFor30DaysCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default SessionInsightsCard;
|
|
@ -0,0 +1,24 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faBookOpen} from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import OnlineActivityAsNumbersTable from "../../../table/OnlineActivityAsNumbersTable";
|
||||
|
||||
const OnlineActivityAsNumbersCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
if (!data) return <></>
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faBookOpen} className="col-light-blue"/> {t('html.label.onlineActivityAsNumbers')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<OnlineActivityAsNumbersTable data={data}/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnlineActivityAsNumbersCard;
|
|
@ -0,0 +1,52 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faExchangeAlt, faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import ComparisonTable from "../../../table/ComparisonTable";
|
||||
import BigTrend from "../../../trend/BigTrend";
|
||||
import React from "react";
|
||||
import {faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
import {TableRow} from "../../../table/TableRow";
|
||||
|
||||
const PlayerbaseTrendsCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
if (!data) return <></>;
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faExchangeAlt} className="col-amber"/> {t('html.label.trends30days')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<ComparisonTable comparisonHeader={t('html.text.comparing30daysAgo')}
|
||||
headers={[t('html.label.thirtyDaysAgo'), t('html.label.now'), t('html.label.trend')]}>
|
||||
<TableRow icon={faUsers} color="black" text={t('html.label.totalPlayers')}
|
||||
values={[data.total_players_now, data.total_players_then,
|
||||
<BigTrend trend={data.total_players_trend}/>]}/>
|
||||
<TableRow icon={faUsers} color="lime" text={t('html.label.regularPlayers')}
|
||||
values={[data.regular_players_now, data.regular_players_then,
|
||||
<BigTrend trend={data.regular_players_trend}/>]}/>
|
||||
<TableRow icon={faClock} color="green"
|
||||
text={t('html.label.averagePlaytime') + ' ' + t('html.label.perPlayer')}
|
||||
values={[data.playtime_avg_now, data.playtime_avg_then,
|
||||
<BigTrend trend={data.playtime_avg_trend}/>]}/>
|
||||
<TableRow icon={faClock} color="gray" text={t('html.label.afk') + ' ' + t('html.label.perPlayer')}
|
||||
values={[data.afk_now, data.afk_then, <BigTrend trend={data.afk_trend}/>]}/>
|
||||
<TableRow icon={faClock} color="green"
|
||||
text={t('html.label.averagePlaytime') + ' ' + t('html.label.perRegularPlayer')}
|
||||
values={[data.regular_playtime_avg_now, data.regular_playtime_avg_then,
|
||||
<BigTrend trend={data.regular_playtime_avg_trend}/>]}/>
|
||||
<TableRow icon={faClock} color="teal"
|
||||
text={t('html.label.averageSessionLength') + ' ' + t('html.label.perRegularPlayer')}
|
||||
values={[data.regular_session_avg_now, data.regular_session_avg_then,
|
||||
<BigTrend trend={data.regular_session_avg_trend}/>]}/>
|
||||
<TableRow icon={faClock} color="gray"
|
||||
text={t('html.label.afk') + ' ' + t('html.label.perRegularPlayer')}
|
||||
values={[data.regular_afk_avg_now, data.regular_afk_avg_then,
|
||||
<BigTrend trend={data.regular_afk_avg_trend}/>]}/>
|
||||
</ComparisonTable>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerbaseTrendsCard
|
|
@ -0,0 +1,22 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCampground} from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import ServerPvpPveAsNumbersTable from "../../../table/ServerPvpPveAsNumbersTable";
|
||||
|
||||
const PvpPveAsNumbersCard = ({kill_data}) => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faCampground} className="col-red"/> {t('html.label.pvpPveAsNumbers')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<ServerPvpPveAsNumbersTable killData={kill_data}/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default PvpPveAsNumbersCard;
|
|
@ -0,0 +1,22 @@
|
|||
import React from "react";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||
import {fetchSessions} from "../../../../service/serverService";
|
||||
import {ErrorViewBody} from "../../../../views/ErrorView";
|
||||
import RecentSessionsCard from "../../common/RecentSessionsCard";
|
||||
|
||||
const ServerRecentSessionsCard = () => {
|
||||
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchSessions, [identifier])
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <></>;
|
||||
|
||||
return (
|
||||
<RecentSessionsCard sessions={data.sessions} isPlayer={true}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerRecentSessionsCard;
|
|
@ -0,0 +1,49 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCrosshairs, faExchangeAlt, faSkull, faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import ComparisonTable from "../../../table/ComparisonTable";
|
||||
import BigTrend from "../../../trend/BigTrend";
|
||||
import {faCalendarCheck, faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
import React from "react";
|
||||
import {TableRow} from "../../../table/TableRow";
|
||||
|
||||
const ServerWeekComparisonCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
if (!data) return <></>;
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faExchangeAlt}/> {t('html.label.weekComparison')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<ComparisonTable comparisonHeader={t('html.label.comparing7days')}
|
||||
headers={[data.start + ' - ' + data.midpoint, data.midpoint + ' - ' + data.end, t('html.label.trend')]}>
|
||||
<TableRow icon={faUsers} color="blue" text={t('html.label.uniquePlayers')}
|
||||
values={[data.unique_before, data.unique_after, <BigTrend trend={data.unique_trend}/>]}/>
|
||||
<TableRow icon={faUsers} color="light-green" text={t('html.label.newPlayers')}
|
||||
values={[data.new_before, data.new_after, <BigTrend trend={data.new_trend}/>]}/>
|
||||
<TableRow icon={faUsers} color="lime" text={t('html.label.regularPlayers')}
|
||||
values={[data.regular_before, data.regular_after, <BigTrend trend={data.regular_trend}/>]}/>
|
||||
<TableRow icon={faClock} color="green"
|
||||
text={t('html.label.averagePlaytime') + ' ' + t('html.label.perPlayer')}
|
||||
values={[data.average_playtime_before, data.average_playtime_after,
|
||||
<BigTrend trend={data.average_playtime_trend}/>]}/>
|
||||
<TableRow icon={faCalendarCheck} color="teal" text={t('html.label.sessions')}
|
||||
values={[data.sessions_before, data.sessions_after,
|
||||
<BigTrend trend={data.sessions_trend}/>]}/>
|
||||
<TableRow icon={faCrosshairs} color="red" text={t('html.label.playerKills')}
|
||||
values={[data.player_kills_before, data.player_kills_after,
|
||||
<BigTrend trend={data.player_kills_trend}/>]}/>
|
||||
<TableRow icon={faCrosshairs} color="green" text={t('html.label.mobKills')}
|
||||
values={[data.mob_kills_before, data.mob_kills_after,
|
||||
<BigTrend trend={data.mob_kills_trend}/>]}/>
|
||||
<TableRow icon={faSkull} color="black" text={t('html.label.deaths')}
|
||||
values={[data.deaths_before, data.deaths_after, <BigTrend trend={data.deaths_trend}/>]}/>
|
||||
</ComparisonTable>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerWeekComparisonCard
|
|
@ -0,0 +1,75 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faBookOpen,
|
||||
faChartLine,
|
||||
faCrosshairs,
|
||||
faPowerOff,
|
||||
faSkull,
|
||||
faUser,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import Datapoint from "../../../Datapoint";
|
||||
import {faCalendarCheck, faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
import React from "react";
|
||||
|
||||
const ServerAsNumbersCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
if (!data) return <></>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faBookOpen}/> {t('html.label.serverAsNumberse')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Datapoint name={t('html.label.currentUptime')}
|
||||
color={'light-green'} icon={faPowerOff}
|
||||
value={data.current_uptime}/>
|
||||
<hr/>
|
||||
<Datapoint name={t('html.label.totalPlayers')}
|
||||
color={'black'} icon={faUsers}
|
||||
value={data.total_players} bold/>
|
||||
<Datapoint name={t('html.label.regularPlayers')}
|
||||
color={'lime'} icon={faUsers}
|
||||
value={data.regular_players} bold/>
|
||||
<Datapoint name={t('html.label.playersOnline')}
|
||||
color={'blue'} icon={faUser}
|
||||
value={data.online_players} bold/>
|
||||
<hr/>
|
||||
<Datapoint name={t('html.label.lastPeak') + ' (' + data.last_peak_date + ')'}
|
||||
color={'blue'} icon={faChartLine}
|
||||
value={data.last_peak_players} valueLabel={t('html.unit.players')} bold/>
|
||||
<Datapoint name={t('html.label.bestPeak') + ' (' + data.best_peak_date + ')'}
|
||||
color={'light-green'} icon={faChartLine}
|
||||
value={data.best_peak_players} valueLabel={t('html.unit.players')} bold/>
|
||||
<hr/>
|
||||
<Datapoint name={t('html.label.totalPlaytime')}
|
||||
color={'green'} icon={faClock}
|
||||
value={data.playtime}/>
|
||||
<Datapoint name={t('html.label.averagePlaytime') + ' ' + t('html.label.perPlayer')}
|
||||
color={'green'} icon={faClock}
|
||||
value={data.player_playtime}/>
|
||||
<Datapoint name={t('html.label.sessions')}
|
||||
color={'teal'} icon={faCalendarCheck}
|
||||
value={data.sessions} bold/>
|
||||
<hr/>
|
||||
<Datapoint name={t('html.label.playerKills')}
|
||||
color={'red'} icon={faCrosshairs}
|
||||
value={data.player_kills} bold/>
|
||||
<Datapoint name={t('html.label.mobKills')}
|
||||
color={'green'} icon={faCrosshairs}
|
||||
value={data.mob_kills} bold/>
|
||||
<Datapoint name={t('html.label.deaths')}
|
||||
color={'black'} icon={faSkull}
|
||||
value={data.deaths} bold/>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerAsNumbersCard;
|
|
@ -0,0 +1,126 @@
|
|||
import React, {useEffect, useState} from "react";
|
||||
import {Card, Col} from "react-bootstrap-v5";
|
||||
import ExtensionIcon from "./ExtensionIcon";
|
||||
import Datapoint from "../Datapoint";
|
||||
import Masonry from 'masonry-layout'
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
|
||||
export const ExtensionCardWrapper = ({extension, children}) => {
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
|
||||
const onResize = () => {
|
||||
setWindowWidth(window.innerWidth);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", onResize);
|
||||
}
|
||||
}, [])
|
||||
|
||||
const wide = extension.wide;
|
||||
|
||||
const baseWidth = windowWidth < 1000 ? 6 : 4;
|
||||
const width = (wide ? 2 : 1) * baseWidth;
|
||||
|
||||
return <Col lg={width} md={width} className="extension-wrapper">{children}</Col>
|
||||
}
|
||||
|
||||
const ExtensionTab = ({tab}) => {
|
||||
return (<>
|
||||
{tab.tabInformation.elementOrder.map((type, i) => {
|
||||
switch (type) {
|
||||
case "VALUES":
|
||||
return <ExtensionValues key={i} tab={tab}/>
|
||||
case "TABLE":
|
||||
return <ExtensionTables key={i} tab={tab}/>
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})}
|
||||
</>);
|
||||
}
|
||||
|
||||
const ExtensionValue = ({data}) => {
|
||||
return (<Datapoint name={data.description.text}
|
||||
title={data.description.description}
|
||||
icon={[data.description.icon.familyClass, data.description.icon.iconName]}
|
||||
color={data.description.icon.colorClass}
|
||||
value={data.value}
|
||||
/>);
|
||||
}
|
||||
|
||||
const ExtensionValues = ({tab}) => (
|
||||
<Card.Body>
|
||||
{tab.values.map((data, i) => {
|
||||
return (<ExtensionValue key={i} data={data}/>);
|
||||
}
|
||||
)}
|
||||
</Card.Body>
|
||||
)
|
||||
|
||||
const ExtensionTable = ({table}) => {
|
||||
const {nightModeEnabled} = useTheme();
|
||||
return (
|
||||
<table className={"table table-striped" + (nightModeEnabled ? " table-dark" : '')}>
|
||||
<thead className={table.tableColorClass}>
|
||||
<tr>
|
||||
{table.table.columns.map((column, i) => <th key={i}><ExtensionIcon
|
||||
icon={table.table.icons[i]}/> {column}
|
||||
</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.table.rows.map((row, i) => <tr key={i}>{row.map((value, j) => <td
|
||||
key={i + '' + j}>{value}</td>)}</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
const ExtensionTables = ({tab}) => {
|
||||
return (<>
|
||||
{tab.tableData.map((table, i) => (
|
||||
<ExtensionTable key={i} table={table}/>
|
||||
))}
|
||||
</>);
|
||||
}
|
||||
|
||||
const ExtensionCard = ({extension}) => {
|
||||
const [openTabIndex, setOpenTabIndex] = useState(0);
|
||||
|
||||
const toggleTabIndex = index => {
|
||||
setOpenTabIndex(index);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const masonryRow = document.getElementById('extension-masonry-row');
|
||||
const masonry = Masonry.data(masonryRow);
|
||||
if (masonry) {
|
||||
masonry.layout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return <Card>
|
||||
<Card.Header>
|
||||
<h6>
|
||||
<ExtensionIcon
|
||||
icon={extension.extensionInformation.icon}/> {extension.extensionInformation.pluginName}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<ul className="nav nav-tabs tab-nav-right" role="tablist">
|
||||
{extension.onlyGenericTab ? '' :
|
||||
extension.tabs.map((tab, i) => <li key={i} role="presentation" className="nav-item col-black">
|
||||
<button className={"nav-link col-black"
|
||||
+ (openTabIndex === i ? ' active' : '')} onClick={() => toggleTabIndex(i)}>
|
||||
<ExtensionIcon icon={tab.tabInformation.icon}/> {tab.tabInformation.tabName}
|
||||
</button>
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
{extension.tabs.map((tab, i) => openTabIndex === i ? <ExtensionTab tab={tab} key={i}/> : '')}
|
||||
</Card>
|
||||
}
|
||||
|
||||
export default ExtensionCard
|
|
@ -0,0 +1,14 @@
|
|||
import {FontAwesomeIcon as Fa} from '@fortawesome/react-fontawesome'
|
||||
import {iconTypeToFontAwesomeClass} from "../../util/icons";
|
||||
import React from "react";
|
||||
|
||||
const ExtensionIcon = ({icon}) => (
|
||||
<Fa icon={[
|
||||
iconTypeToFontAwesomeClass(icon.family),
|
||||
icon.iconName
|
||||
]}
|
||||
className={icon.colorClass}
|
||||
/>
|
||||
)
|
||||
|
||||
export default ExtensionIcon;
|
|
@ -0,0 +1,46 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
import Highcharts from "highcharts";
|
||||
|
||||
const GeolocationBarGraph = ({series, color}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const bars = series.map(bar => bar.value);
|
||||
const categories = series.map(bar => bar.label);
|
||||
const geolocationBarSeries = {
|
||||
color: nightModeEnabled ? withReducedSaturation(color) : color,
|
||||
name: t('html.label.players'),
|
||||
data: bars
|
||||
};
|
||||
|
||||
Highcharts.setOptions(graphTheming);
|
||||
Highcharts.chart("countryBarChart", {
|
||||
chart: {type: 'bar'},
|
||||
title: {text: ''},
|
||||
xAxis: {
|
||||
categories: categories,
|
||||
title: {text: ''}
|
||||
},
|
||||
yAxis: {
|
||||
min: 0,
|
||||
title: {text: t('html.label.players'), align: 'high'},
|
||||
labels: {overflow: 'justify'}
|
||||
},
|
||||
legend: {enabled: false},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
dataLabels: {enabled: true}
|
||||
}
|
||||
},
|
||||
series: [geolocationBarSeries]
|
||||
})
|
||||
}, [color, series, graphTheming, nightModeEnabled, t]);
|
||||
|
||||
return (<div id="countryBarChart"/>);
|
||||
};
|
||||
|
||||
export default GeolocationBarGraph
|
|
@ -0,0 +1,46 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
import Highcharts from 'highcharts/highmaps.js';
|
||||
import map from '@highcharts/map-collection/custom/world.geo.json';
|
||||
|
||||
const GeolocationWorldMap = ({series, colors}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const mapSeries = {
|
||||
name: t('html.label.players'),
|
||||
type: 'map',
|
||||
mapData: map,
|
||||
data: series,
|
||||
joinBy: ['iso-a3', 'code']
|
||||
};
|
||||
|
||||
Highcharts.setOptions(graphTheming);
|
||||
Highcharts.mapChart('countryWorldMap', {
|
||||
chart: {
|
||||
animation: true
|
||||
},
|
||||
title: {text: ''},
|
||||
|
||||
mapNavigation: {
|
||||
enabled: true,
|
||||
enableDoubleClickZoomTo: true
|
||||
},
|
||||
|
||||
colorAxis: {
|
||||
min: 1,
|
||||
type: 'logarithmic',
|
||||
minColor: nightModeEnabled ? withReducedSaturation(colors.low) : colors.low,
|
||||
maxColor: nightModeEnabled ? withReducedSaturation(colors.high) : colors.high
|
||||
},
|
||||
series: [mapSeries]
|
||||
})
|
||||
}, [colors, series, graphTheming, nightModeEnabled, t]);
|
||||
|
||||
return (<div id="countryWorldMap"/>);
|
||||
};
|
||||
|
||||
export default GeolocationWorldMap
|
|
@ -0,0 +1,42 @@
|
|||
import {useTheme} from "../../hooks/themeHook";
|
||||
import React, {useEffect} from "react";
|
||||
import {linegraphButtons} from "../../util/graphs";
|
||||
import Highcharts from "highcharts/highstock";
|
||||
import NoDataDisplay from "highcharts/modules/no-data-to-display"
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const LineGraph = ({id, series}) => {
|
||||
const {t} = useTranslation()
|
||||
const {graphTheming} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
NoDataDisplay(Highcharts);
|
||||
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
|
||||
Highcharts.setOptions(graphTheming);
|
||||
Highcharts.stockChart(id, {
|
||||
rangeSelector: {
|
||||
selected: 2,
|
||||
buttons: linegraphButtons
|
||||
},
|
||||
yAxis: {
|
||||
softMax: 2,
|
||||
softMin: 0
|
||||
},
|
||||
title: {text: ''},
|
||||
plotOptions: {
|
||||
areaspline: {
|
||||
fillOpacity: 0.4
|
||||
}
|
||||
},
|
||||
series: series
|
||||
})
|
||||
}, [series, graphTheming, id, t])
|
||||
|
||||
return (
|
||||
<div className="chart-area" id={id}>
|
||||
<span className="loader"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LineGraph
|
|
@ -0,0 +1,40 @@
|
|||
import React, {useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {tooltip} from "../../util/graphs";
|
||||
import LineGraph from "./LineGraph";
|
||||
|
||||
const PingGraph = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
const [series, setSeries] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const avgPingSeries = {
|
||||
name: t('html.label.averagePing'),
|
||||
type: 'spline',
|
||||
tooltip: tooltip.twoDecimals,
|
||||
data: data.avg_ping_series,
|
||||
color: data.colors.avg
|
||||
}
|
||||
const maxPingSeries = {
|
||||
name: t('html.label.worstPing'),
|
||||
type: 'spline',
|
||||
tooltip: tooltip.twoDecimals,
|
||||
data: data.max_ping_series,
|
||||
color: data.colors.max
|
||||
}
|
||||
const minPingSeries = {
|
||||
name: t('html.label.bestPing'),
|
||||
type: 'spline',
|
||||
tooltip: tooltip.twoDecimals,
|
||||
data: data.min_ping_series,
|
||||
color: data.colors.min
|
||||
}
|
||||
setSeries([avgPingSeries, maxPingSeries, minPingSeries]);
|
||||
}, [data, t])
|
||||
|
||||
return (
|
||||
<LineGraph id="ping-graph" series={series}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default PingGraph
|
|
@ -0,0 +1,60 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import React, {useEffect} from "react";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import NoDataDisplay from "highcharts/modules/no-data-to-display";
|
||||
import Highcharts from "highcharts/highstock";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
|
||||
const PlayerbaseGraph = ({data}) => {
|
||||
const {t} = useTranslation()
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
const id = 'playerbase-graph';
|
||||
|
||||
useEffect(() => {
|
||||
const reduceColors = (lines) => lines.map(line => {
|
||||
return {...line, color: withReducedSaturation(line.color)}
|
||||
});
|
||||
|
||||
NoDataDisplay(Highcharts);
|
||||
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
|
||||
Highcharts.setOptions(graphTheming);
|
||||
|
||||
const labels = data?.activity_labels;
|
||||
const series = data?.activity_series;
|
||||
|
||||
Highcharts.chart(id, {
|
||||
chart: {
|
||||
type: "area"
|
||||
},
|
||||
xAxis: {
|
||||
categories: labels,
|
||||
tickmarkPlacement: 'on',
|
||||
title: {
|
||||
enabled: false
|
||||
},
|
||||
ordinal: false
|
||||
},
|
||||
yAxis: {
|
||||
softMax: 2,
|
||||
softMin: 0
|
||||
},
|
||||
title: {text: ''},
|
||||
plotOptions: {
|
||||
area: {
|
||||
stacking: 'normal',
|
||||
lineWidth: 1
|
||||
}
|
||||
},
|
||||
series: nightModeEnabled ? reduceColors(series) : series
|
||||
})
|
||||
}, [data, graphTheming, id, t, nightModeEnabled])
|
||||
|
||||
return (
|
||||
<div className="chart-area" id={id}>
|
||||
<span className="loader"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerbaseGraph;
|
|
@ -0,0 +1,48 @@
|
|||
import React, {useEffect} from "react";
|
||||
import Highcharts from 'highcharts';
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const PlayerbasePie = ({series}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const reduceColors = (slices) => slices.map(slice => {
|
||||
return {...slice, color: withReducedSaturation(slice.color)}
|
||||
});
|
||||
|
||||
const pieSeries = {
|
||||
name: t('html.label.players'),
|
||||
colorByPoint: true,
|
||||
data: nightModeEnabled ? reduceColors(series) : series
|
||||
};
|
||||
|
||||
Highcharts.setOptions(graphTheming);
|
||||
Highcharts.chart('playerbase-pie', {
|
||||
chart: {
|
||||
backgroundColor: 'transparent',
|
||||
plotBorderWidth: null,
|
||||
plotShadow: false,
|
||||
type: 'pie'
|
||||
},
|
||||
title: {text: ''},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
allowPointSelect: true,
|
||||
cursor: 'pointer',
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
showInLegend: true
|
||||
}
|
||||
},
|
||||
series: [pieSeries]
|
||||
});
|
||||
}, [series, graphTheming, nightModeEnabled, t]);
|
||||
|
||||
return (<div className="chart-area" id="playerbase-pie"/>);
|
||||
}
|
||||
|
||||
export default PlayerbasePie;
|
|
@ -0,0 +1,30 @@
|
|||
import React, {useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {tooltip} from "../../util/graphs";
|
||||
import LineGraph from "./LineGraph";
|
||||
import {ChartLoader} from "../navigation/Loader";
|
||||
|
||||
const PlayersOnlineGraph = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
const [series, setSeries] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const playersOnlineSeries = {
|
||||
name: t('html.label.playersOnline'),
|
||||
type: 'areaspline',
|
||||
tooltip: tooltip.zeroDecimals,
|
||||
data: data.playersOnline,
|
||||
color: data.color,
|
||||
yAxis: 0
|
||||
}
|
||||
setSeries([playersOnlineSeries]);
|
||||
}, [data, t])
|
||||
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return (
|
||||
<LineGraph id="players-online-graph" series={series}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayersOnlineGraph
|
|
@ -0,0 +1,56 @@
|
|||
import React, {useEffect} from "react";
|
||||
import Highcharts from 'highcharts';
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const PunchCard = ({series}) => {
|
||||
const {t} = useTranslation();
|
||||
const {graphTheming} = useTheme();
|
||||
useEffect(() => {
|
||||
const punchCard = {
|
||||
name: t('html.label.relativeJoinActivity'),
|
||||
color: '#222',
|
||||
data: series
|
||||
};
|
||||
Highcharts.setOptions(graphTheming);
|
||||
setTimeout(() => Highcharts.chart('punchcard', {
|
||||
chart: {
|
||||
backgroundColor: 'transparent',
|
||||
plotBackgroundColor: 'transparent',
|
||||
defaultSeriesType: 'scatter'
|
||||
},
|
||||
title: {text: ''},
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
dateTimeLabelFormats: {
|
||||
// https://www.php.net/manual/en/function.strftime.php
|
||||
hour: '%I %P',
|
||||
day: '%I %P'
|
||||
},
|
||||
tickInterval: 3600000
|
||||
},
|
||||
time: {
|
||||
timezoneOffset: 0
|
||||
},
|
||||
yAxis: {
|
||||
title: {
|
||||
text: t('html.label.dayOfweek')
|
||||
},
|
||||
reversed: true,
|
||||
categories: t('html.label.weekdays').replaceAll("'", '').split(', ')
|
||||
},
|
||||
tooltip: {
|
||||
pointFormat: t('html.label.active') + ': {point.z}'
|
||||
},
|
||||
series: [punchCard]
|
||||
}), 25)
|
||||
}, [series, graphTheming, t])
|
||||
|
||||
return (
|
||||
<div className="chart-area" id="punchcard">
|
||||
<span className="loader"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PunchCard
|
|
@ -0,0 +1,54 @@
|
|||
import React, {useEffect} from "react";
|
||||
import Highcharts from 'highcharts';
|
||||
|
||||
import {formatTimeAmount} from '../../util/formatters'
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const ServerPie = ({colors, series}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const reduceColors = (colorsToReduce) => colorsToReduce.map(color => withReducedSaturation(color));
|
||||
|
||||
const pieSeries = {
|
||||
name: t('html.label.serverPlaytime'),
|
||||
colorByPoint: true,
|
||||
colors: nightModeEnabled ? reduceColors(colors) : colors,
|
||||
data: series
|
||||
};
|
||||
|
||||
Highcharts.setOptions(graphTheming);
|
||||
Highcharts.chart('server-pie', {
|
||||
chart: {
|
||||
backgroundColor: 'transparent',
|
||||
plotBorderWidth: null,
|
||||
plotShadow: false,
|
||||
type: 'pie'
|
||||
},
|
||||
title: {text: ''},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
allowPointSelect: true,
|
||||
cursor: 'pointer',
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
showInLegend: true
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
formatter: function () {
|
||||
return '<b>' + this.point.name + ':</b> ' + formatTimeAmount(this.y) + ' (' + this.percentage.toFixed(2) + '%)';
|
||||
}
|
||||
},
|
||||
series: [pieSeries]
|
||||
});
|
||||
}, [colors, series, graphTheming, nightModeEnabled, t]);
|
||||
|
||||
return (<div className="chart-pie" id="server-pie"/>);
|
||||
}
|
||||
|
||||
export default ServerPie;
|
|
@ -0,0 +1,33 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {tooltip} from "../../util/graphs";
|
||||
import LineGraph from "./LineGraph";
|
||||
|
||||
const TimeByTimeGraph = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
const [series, setSeries] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const uniquePlayers = {
|
||||
name: t('html.label.uniquePlayers'),
|
||||
type: 'spline',
|
||||
tooltip: tooltip.zeroDecimals,
|
||||
data: data.uniquePlayers,
|
||||
color: data.colors.playersOnline
|
||||
};
|
||||
const newPlayers = {
|
||||
name: t('html.label.newPlayers'),
|
||||
type: 'spline',
|
||||
tooltip: tooltip.zeroDecimals,
|
||||
data: data.newPlayers,
|
||||
color: data.colors.newPlayers
|
||||
};
|
||||
setSeries([uniquePlayers, newPlayers]);
|
||||
}, [data, t])
|
||||
|
||||
return (
|
||||
<LineGraph id="day-by-day-graph" series={series}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimeByTimeGraph
|
|
@ -0,0 +1,86 @@
|
|||
import React, {useEffect} from "react";
|
||||
import Highcharts from 'highcharts';
|
||||
import factory from 'highcharts/modules/drilldown';
|
||||
|
||||
import {formatTimeAmount} from '../../util/formatters'
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
import {useMetadata} from "../../hooks/metadataHook";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const WorldPie = ({id, worldSeries, gmSeries}) => {
|
||||
const {t} = useTranslation();
|
||||
const {gmPieColors} = useMetadata();
|
||||
|
||||
useEffect(() => {
|
||||
factory(Highcharts)
|
||||
}, []);
|
||||
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const reduceColors = (series) => {
|
||||
return series.map(slice => {
|
||||
return {...slice, color: withReducedSaturation(slice.color)};
|
||||
})
|
||||
}
|
||||
|
||||
const pieSeries = {
|
||||
name: t('html.label.worldPlaytime'),
|
||||
colorByPoint: true,
|
||||
data: nightModeEnabled ? reduceColors(worldSeries) : worldSeries
|
||||
};
|
||||
|
||||
const defaultTitle = '';
|
||||
const defaultSubtitle = t('html.text.clickToExpand');
|
||||
Highcharts.setOptions(graphTheming);
|
||||
setTimeout(() => {
|
||||
const chart = Highcharts.chart(id, {
|
||||
chart: {
|
||||
backgroundColor: 'transparent',
|
||||
plotBackgroundColor: 'transparent',
|
||||
plotBorderWidth: null,
|
||||
plotShadow: false,
|
||||
type: 'pie',
|
||||
events: {
|
||||
drilldown: function (e) {
|
||||
chart.setTitle({text: '' + e.point.name}, {text: ''});
|
||||
},
|
||||
drillup: function () {
|
||||
chart.setTitle({text: defaultTitle}, {text: defaultSubtitle});
|
||||
}
|
||||
}
|
||||
},
|
||||
title: {text: defaultTitle},
|
||||
subtitle: {
|
||||
text: defaultSubtitle
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
allowPointSelect: true,
|
||||
cursor: 'pointer',
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
showInLegend: true
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
formatter: function () {
|
||||
return '<b>' + this.point.name + ':</b> ' + formatTimeAmount(this.y) + ' (' + this.percentage.toFixed(2) + '%)';
|
||||
}
|
||||
},
|
||||
series: [pieSeries],
|
||||
drilldown: {
|
||||
series: gmSeries.map(function (d) {
|
||||
return {name: d.name, id: d.id, colors: gmPieColors, data: d.data}
|
||||
})
|
||||
}
|
||||
});
|
||||
}, 25)
|
||||
}, [worldSeries, gmSeries, graphTheming, nightModeEnabled, id, gmPieColors, t]);
|
||||
|
||||
return (<div className="chart-pie" id={id}/>)
|
||||
}
|
||||
|
||||
export default WorldPie;
|
|
@ -0,0 +1,7 @@
|
|||
import React from "react";
|
||||
|
||||
const End = ({children}) => (
|
||||
<span className="float-end">{children}</span>
|
||||
)
|
||||
|
||||
export default End;
|
|
@ -0,0 +1,52 @@
|
|||
import React from "react";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCloudMoon, faPalette} from "@fortawesome/free-solid-svg-icons";
|
||||
import {colorEnumToBgClass} from "../../util/colors";
|
||||
import {Modal} from "react-bootstrap-v5";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const ColorSelectorButton = ({color, setColor, disabled}) =>
|
||||
<button className={"btn color-chooser " + colorEnumToBgClass(color)}
|
||||
id={"choose-" + color}
|
||||
disabled={disabled}
|
||||
onClick={() => setColor(color)}
|
||||
>
|
||||
<Fa icon={faPalette}/>
|
||||
</button>
|
||||
|
||||
const ColorSelectorModal = () => {
|
||||
const {t} = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Modal id="colorChooserModal"
|
||||
aria-labelledby="colorChooserModalLabel"
|
||||
show={theme.colorChooserOpen}
|
||||
onHide={theme.toggleColorChooser}>
|
||||
<Modal.Header>
|
||||
<Modal.Title id="colorChooserModalLabel">
|
||||
<Fa icon={faPalette}/> {t('html.label.themeSelect')}
|
||||
</Modal.Title>
|
||||
<button aria-label="Close" className="btn-close" onClick={theme.toggleColorChooser}/>
|
||||
</Modal.Header>
|
||||
<Modal.Body style={{padding: "0.5rem 0 0.5rem 0.5rem"}}>
|
||||
{theme.themeColors.map((color, i) =>
|
||||
<ColorSelectorButton
|
||||
key={i}
|
||||
color={color.name}
|
||||
setColor={theme.setColor}
|
||||
disabled={theme.nightModeEnabled}
|
||||
/>)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button className="btn" id="night-mode-toggle" type="button" onClick={theme.toggleNightMode}>
|
||||
<Fa icon={faCloudMoon}/> {t('html.button.nightMode')}
|
||||
</button>
|
||||
<button className="btn bg-plan" type="button" onClick={theme.toggleColorChooser}>OK</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ColorSelectorModal;
|
|
@ -0,0 +1,147 @@
|
|||
import React from "react";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {Modal} from "react-bootstrap-v5";
|
||||
import {
|
||||
faBug,
|
||||
faChartArea,
|
||||
faCode,
|
||||
faGraduationCap,
|
||||
faLanguage,
|
||||
faQuestionCircle,
|
||||
faStar
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faDiscord} from "@fortawesome/free-brands-svg-icons";
|
||||
import {useMetadata} from "../../hooks/metadataHook";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const LicenseSection = () => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<p>{t('html.modal.info.license')}{' '}
|
||||
<a href="https://opensource.org/licenses/LGPL-3.0"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank">
|
||||
Lesser General Public License v3.0
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
const Links = () => {
|
||||
const {t} = useTranslation();
|
||||
return (<>
|
||||
<a className="btn col-plan" href="https://github.com/plan-player-analytics/Plan/wiki"
|
||||
rel="noopener noreferrer" target="_blank">
|
||||
<Fa icon={faGraduationCap}/> {t('html.modal.info.wiki')}
|
||||
</a>
|
||||
<a className="btn col-plan" href="https://github.com/plan-player-analytics/Plan/issues"
|
||||
rel="noopener noreferrer" target="_blank">
|
||||
<Fa icon={faBug}/> {t('html.modal.info.bugs')}</a>
|
||||
<a className="btn col-plan" href="https://discord.gg/yXKmjzT" rel="noopener noreferrer"
|
||||
target="_blank">
|
||||
<Fa icon={faDiscord}/> {t('html.modal.info.discord')}
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const getContributionIcon = (type) => {
|
||||
switch (type) {
|
||||
case "LANG":
|
||||
return 'language';
|
||||
case "CODE":
|
||||
return 'code';
|
||||
default:
|
||||
return "exclamation-triangle";
|
||||
}
|
||||
}
|
||||
|
||||
const Contributor = ({contributor}) => {
|
||||
const icons = contributor.contributed.map(
|
||||
(type, i) => <Fa key={i} icon={["fa", getContributionIcon(type)]}/>);
|
||||
return (
|
||||
<li className="col-4">{contributor.name} {icons} </li>
|
||||
)
|
||||
}
|
||||
|
||||
const Contributions = () => {
|
||||
const {t} = useTranslation();
|
||||
const metadata = useMetadata();
|
||||
const contributors = metadata.contributors ? metadata.contributors : [{
|
||||
name: '(Error getting contributors)',
|
||||
contributed: ['exclamation-triangle']
|
||||
}];
|
||||
|
||||
// TODO Translate
|
||||
return (<>
|
||||
<p>Player Analytics {t('html.modal.info.developer')} AuroraLS3.</p>
|
||||
<p>In addition following <span className="col-plan">awesome people</span> have
|
||||
contributed:</p>
|
||||
<ul className="row contributors">
|
||||
{contributors.map((contributor, i) => <Contributor key={i} contributor={contributor}/>)}
|
||||
<li>{t('html.modal.info.contributors.bugreporters')}</li>
|
||||
</ul>
|
||||
<small>
|
||||
<Fa icon={faCode}/> {t('html.modal.info.contributors.code')} <Fa
|
||||
icon={faLanguage}/> {t('html.modal.info.contributors.translator')}
|
||||
</small>
|
||||
<hr/>
|
||||
<p className="col-plan">
|
||||
{t('html.modal.info.contributors.donate')}
|
||||
<Fa icon={faStar} className={"col-amber"}/>
|
||||
</p>
|
||||
</>)
|
||||
}
|
||||
|
||||
const MetricsLinks = () => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<h6>{t('html.modal.info.metrics')}</h6>
|
||||
<a className="btn col-plan" href="https://bstats.org/plugin/bukkit/Plan"
|
||||
rel="noopener noreferrer" target="_blank">
|
||||
<Fa icon={faChartArea}/> Bukkit
|
||||
</a>
|
||||
<a className="btn col-plan" href="https://bstats.org/plugin/bungeecord/Plan"
|
||||
rel="noopener noreferrer" target="_blank">
|
||||
<Fa icon={faChartArea}/> BungeeCord
|
||||
</a>
|
||||
<a className="btn col-plan" href="https://bstats.org/plugin/sponge/plan"
|
||||
rel="noopener noreferrer" target="_blank">
|
||||
<Fa icon={faChartArea}/> Sponge
|
||||
</a>
|
||||
<a className="btn col-plan" href="https://bstats.org/plugin/velocity/Plan/10326"
|
||||
rel="noopener noreferrer" target="_blank">
|
||||
<Fa icon={faChartArea}/> Velocity
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const PluginInformationModal = ({open, toggle}) => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Modal id="informationModal" aria-labelledby="informationModalLabel" show={open} onHide={toggle} size="lg">
|
||||
<Modal.Header>
|
||||
<Modal.Title id="informationModalLabel">
|
||||
<Fa icon={faQuestionCircle}/> {t('html.modal.info.text')}
|
||||
</Modal.Title>
|
||||
<button aria-label="Close" className="btn-close" type="button" onClick={toggle}/>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<LicenseSection/>
|
||||
<hr/>
|
||||
<Links/>
|
||||
<hr/>
|
||||
<Contributions/>
|
||||
<hr/>
|
||||
<MetricsLinks/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button className="btn bg-plan" onClick={toggle}>OK</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default PluginInformationModal;
|
|
@ -0,0 +1,62 @@
|
|||
import React from "react";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {Modal} from "react-bootstrap-v5";
|
||||
import {faCheckCircle, faDownload} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
// TODO translate
|
||||
|
||||
const UpdateAvailableModal = ({open, toggle, versionInfo}) => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Modal id="versionModal" aria-labelledby="versionModalLabel" show={open} onHide={toggle}>
|
||||
<Modal.Header>
|
||||
<Modal.Title id="versionModalLabel">
|
||||
<Fa icon={faDownload}/> {t('html.modal.version.title')} {versionInfo.newVersion} {t('html.modal.version.available')}
|
||||
</Modal.Title>
|
||||
<button aria-label="Close" className="btn-close" type="button" onClick={toggle}/>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>You have version {versionInfo.currentVersion}.</p>
|
||||
<p>New
|
||||
release: {versionInfo.newVersion}{versionInfo.isRelease ? '' : " (" + t('html.modal.version.dev') + ")"}</p>
|
||||
<a className="btn col-plan" href={versionInfo.changelogUrl} rel="noopener noreferrer" target="_blank">
|
||||
{t('html.modal.version.changelog')}
|
||||
</a>
|
||||
<a className="btn col-plan" href={versionInfo.downloadUrl} rel="noopener noreferrer" target="_blank">
|
||||
{t('html.modal.version.download')} Plan-{versionInfo.newVersion}.jar
|
||||
</a>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button className="btn bg-plan" onClick={toggle}>OK</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const NewestVersionModal = ({open, toggle, versionInfo}) => {
|
||||
return (
|
||||
<Modal id="versionModal" aria-labelledby="versionModalLabel" show={open} onHide={toggle}>
|
||||
<Modal.Header>
|
||||
<Modal.Title id="versionModalLabel">
|
||||
<Fa icon={faCheckCircle}/> You have version {versionInfo.currentVersion}.
|
||||
</Modal.Title>
|
||||
<button aria-label="Close" className="btn-close" type="button" onClick={toggle}/>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
You're using the latest version {versionInfo.currentVersion}. (No updates available)
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button className="btn bg-plan" onClick={toggle}>OK</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const VersionInformationModal = ({open, toggle, versionInfo}) => {
|
||||
return versionInfo.updateAvailable
|
||||
? <UpdateAvailableModal open={open} toggle={toggle} versionInfo={versionInfo}/>
|
||||
: <NewestVersionModal open={open} toggle={toggle} versionInfo={versionInfo}/>
|
||||
}
|
||||
|
||||
export default VersionInformationModal;
|
|
@ -0,0 +1,96 @@
|
|||
import {useMetadata} from "../../hooks/metadataHook";
|
||||
import {useAuth} from "../../hooks/authenticationHook";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faBars, faCog, faDoorOpen, faPalette, faSyncAlt} from "@fortawesome/free-solid-svg-icons";
|
||||
import DropdownMenu from "react-bootstrap-v5/lib/esm/DropdownMenu";
|
||||
import DropdownItem from "react-bootstrap-v5/lib/esm/DropdownItem";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {Dropdown} from "react-bootstrap-v5";
|
||||
import DropdownToggle from "react-bootstrap-v5/lib/esm/DropdownToggle";
|
||||
import {localeService} from "../../service/localeService";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useNavigation} from "../../hooks/navigationHook";
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const languages = localeService.getLanguages();
|
||||
|
||||
const onSelect = ({target}) => {
|
||||
localeService.loadLocale(target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<select onChange={onSelect}
|
||||
aria-label="Language selector"
|
||||
className="form-select form-select-sm"
|
||||
id="langSelector"
|
||||
defaultValue={localeService.clientLocale}>
|
||||
{languages.map((lang, i) =>
|
||||
<option key={i} value={lang.name}>{lang.displayName}</option>)}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({page, tab}) => {
|
||||
const {authRequired, user} = useAuth();
|
||||
const {toggleColorChooser} = useTheme();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const {requestUpdate, updating, lastUpdate, toggleSidebar} = useNavigation();
|
||||
|
||||
const {getPlayerHeadImageUrl} = useMetadata();
|
||||
const headImageUrl = user ? getPlayerHeadImageUrl(user.playerName, user.linkedToUuid) : undefined
|
||||
// <!-- <li><a className="dropdown-item" href="#"><i className="fas fa-users-cog"></i> Web users</a></li>-->
|
||||
// <!-- <li><a className="dropdown-item" href="#"><i className="fas fa-code"></i> API access</a></li>-->
|
||||
// <!-- <li>-->
|
||||
// <!-- <hr className="dropdown-divider">-->
|
||||
// <!-- </li>-->
|
||||
return (
|
||||
<nav className="nav mt-3 align-items-center justify-content-between container-fluid">
|
||||
<div className="d-sm-flex">
|
||||
<h1 className="h3 mb-0 text-gray-800">
|
||||
<button onClick={toggleSidebar}>
|
||||
<Fa icon={faBars} className={"sidebar-toggler"}/>
|
||||
</button>
|
||||
{page}
|
||||
{tab ? <>{' '}· {t(tab)}</> : ''}</h1>
|
||||
</div>
|
||||
|
||||
<span className="topbar-divider"/>
|
||||
<div className="refresh-element">
|
||||
<button onClick={requestUpdate}>
|
||||
<Fa icon={faSyncAlt} spin={updating}/>
|
||||
</button>
|
||||
{' '}
|
||||
<span className="refresh-time">{lastUpdate.formatted}</span>
|
||||
</div>
|
||||
|
||||
<div className="ms-auto">
|
||||
<LanguageSelector/>
|
||||
</div>
|
||||
|
||||
<div className="topbar-divider"/>
|
||||
|
||||
<Dropdown className="nav-item">
|
||||
<DropdownToggle variant=''>
|
||||
{authRequired && user ? <>
|
||||
<span className="me-1 login-username">{user.username} </span>
|
||||
<img alt="user img" className="rounded-circle" src={headImageUrl} style={{height: "2rem"}}/>
|
||||
</> : <>
|
||||
<Fa icon={faCog} className="me-2"/>
|
||||
</>}
|
||||
</DropdownToggle>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownItem onClick={toggleColorChooser}>
|
||||
<Fa icon={faPalette}/> {t('html.label.themeSelect')}
|
||||
</DropdownItem>
|
||||
{authRequired ? <DropdownItem href="./auth/logout">
|
||||
<Fa icon={faDoorOpen}/> {t('html.login.logout')}
|
||||
</DropdownItem> : ''}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header;
|
|
@ -0,0 +1,29 @@
|
|||
import React from "react";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
|
||||
export const CardLoader = () => {
|
||||
return (
|
||||
<Card className="loading">
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
...
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<ChartLoader/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export const ChartLoader = () => {
|
||||
return <div className="chart-area loading">
|
||||
<Loader/>
|
||||
</div>
|
||||
}
|
||||
|
||||
const Loader = () => {
|
||||
return (
|
||||
<span className="loader"/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loader;
|
|
@ -0,0 +1,76 @@
|
|||
import {useAuth} from "../../hooks/authenticationHook";
|
||||
import {useMetadata} from "../../hooks/metadataHook";
|
||||
import {Navigate} from "react-router-dom";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
const RedirectPlaceholder = () => {
|
||||
const [redirectStart] = useState(Date.now())
|
||||
const [dateDiff, setDateDiff] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (dateDiff <= 50) {
|
||||
setDateDiff(Date.now() - redirectStart);
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, [redirectStart, dateDiff])
|
||||
|
||||
if (dateDiff > 50) {
|
||||
return <>
|
||||
<p className="m-4">Redirecting..</p>
|
||||
<div style={{maxWidth: "500px"}}>
|
||||
<p className="m-4">
|
||||
This is taking longer than expected.
|
||||
</p>
|
||||
<p className="m-4">
|
||||
Make sure the Plan webserver is enabled.<br/>(This page can show up if the Plan webserver goes
|
||||
offline.)
|
||||
</p>
|
||||
<p className="m-4">
|
||||
If you are trying to set up a development environment,
|
||||
change package.json "proxy" to your Plan webserver address.
|
||||
</p>
|
||||
<p className="m-4">
|
||||
<button className="btn bg-plan" onClick={() => window.location.reload()}>Click to Refresh the
|
||||
page & try again.
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
} else {
|
||||
return <p className="m-4">Redirecting..</p>
|
||||
}
|
||||
}
|
||||
|
||||
const MainPageRedirect = () => {
|
||||
const {authLoaded, authRequired, loggedIn, user} = useAuth();
|
||||
const {isProxy, serverName} = useMetadata();
|
||||
|
||||
console.log(authLoaded, authRequired, loggedIn, user)
|
||||
|
||||
if (!authLoaded || !serverName) {
|
||||
return <RedirectPlaceholder/>
|
||||
}
|
||||
|
||||
if (authRequired && !loggedIn) {
|
||||
return (<Navigate to={"login"} replace={true}/>)
|
||||
} else if (authRequired && loggedIn) {
|
||||
if (isProxy && user.permissions.includes('page.network')) {
|
||||
return (<Navigate to={"network/overview"} replace={true}/>)
|
||||
} else if (user.permissions.includes('page.server')) {
|
||||
return (<Navigate to={"server/overview"} replace={true}/>)
|
||||
} else if (user.permissions.includes('page.player.other')) {
|
||||
return (<Navigate to={"players"} replace={true}/>)
|
||||
} else if (user.permissions.includes('page.player.self')) {
|
||||
return (<Navigate to={"player/" + user.linkedToUuid} replace={true}/>)
|
||||
}
|
||||
} else {
|
||||
return (<Navigate to={isProxy ? "network/overview" : "server/" + encodeURIComponent(serverName) + "/overview"}
|
||||
replace={true}/>)
|
||||
}
|
||||
}
|
||||
|
||||
export default MainPageRedirect
|
|
@ -0,0 +1,240 @@
|
|||
import React, {useCallback, useEffect, useState} from "react";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import logo from '../../Flaticon_circle.png';
|
||||
import {faArrowLeft, faDoorOpen, faDownload, faPalette, faQuestionCircle} from "@fortawesome/free-solid-svg-icons";
|
||||
import {NavLink, useLocation} from "react-router-dom";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import PluginInformationModal from "../modal/PluginInformationModal";
|
||||
import VersionInformationModal from "../modal/VersionInformationModal";
|
||||
import {fetchPlanVersion} from "../../service/metadataService";
|
||||
import {useAuth} from "../../hooks/authenticationHook";
|
||||
import {useNavigation} from "../../hooks/navigationHook";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Collapse} from "react-bootstrap-v5";
|
||||
import {baseAddress} from "../../service/backendConfiguration";
|
||||
|
||||
const Logo = () => (
|
||||
<a className="sidebar-brand d-flex align-items-center justify-content-center" href="/">
|
||||
<img alt="logo" className="w-22" src={logo}/>
|
||||
</a>
|
||||
)
|
||||
|
||||
const Divider = ({showMargin}) => (
|
||||
<hr className={"sidebar-divider" + (showMargin ? '' : " my-0")}/>
|
||||
)
|
||||
|
||||
const InnerItem = ({href, icon, name, nameShort}) => {
|
||||
if (href.startsWith('/')) {
|
||||
return (
|
||||
<a href={href} className="collapse-item nav-button">
|
||||
<Fa icon={icon}/> <span>{nameShort ? nameShort : name}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return <NavLink to={href} className={({isActive}) => {
|
||||
return isActive ? "collapse-item nav-button active" : "collapse-item nav-button"
|
||||
}}>
|
||||
<Fa icon={icon}/> <span>{nameShort ? nameShort : name}</span>
|
||||
</NavLink>
|
||||
}
|
||||
|
||||
const Item = ({href, icon, name, nameShort, inner}) => {
|
||||
const {setCurrentTab} = useNavigation();
|
||||
const {pathname} = useLocation();
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if ('/' !== href && pathname.includes(href)) setCurrentTab(name);
|
||||
}, [pathname, href, setCurrentTab, name])
|
||||
|
||||
if (inner) {
|
||||
return (<InnerItem href={href} icon={icon} name={t(name)} nameShort={t(nameShort)}/>)
|
||||
}
|
||||
|
||||
if (href.startsWith('/')) {
|
||||
return (
|
||||
<li className={"nav-item nav-button"}>
|
||||
<a href={baseAddress + href} className="nav-link">
|
||||
<Fa icon={icon}/> <span>{t(nameShort ? nameShort : name)}</span>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={"nav-item nav-button"}>
|
||||
<NavLink to={href} className={({isActive}) => {
|
||||
return isActive ? "nav-link active" : "nav-link"
|
||||
}}>
|
||||
<Fa icon={icon}/> <span>{t(name)}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const VersionButton = ({toggleVersionModal, versionInfo}) => {
|
||||
if (versionInfo.updateAvailable) {
|
||||
return <button className="btn bg-white col-plan" onClick={toggleVersionModal}>
|
||||
<Fa icon={faDownload}/> Update Available!
|
||||
</button>;
|
||||
}
|
||||
|
||||
return <button className="btn bg-transparent-light" onClick={toggleVersionModal}>
|
||||
{versionInfo.currentVersion}
|
||||
</button>;
|
||||
}
|
||||
|
||||
const FooterButtons = () => {
|
||||
const {t} = useTranslation();
|
||||
const {toggleColorChooser} = useTheme();
|
||||
const {authRequired} = useAuth();
|
||||
|
||||
const [infoModalOpen, setInfoModalOpen] = useState(false);
|
||||
const toggleInfoModal = () => setInfoModalOpen(!infoModalOpen);
|
||||
|
||||
const [versionModalOpen, setVersionModalOpen] = useState(false);
|
||||
const toggleVersionModal = () => setVersionModalOpen(!versionModalOpen);
|
||||
|
||||
const [versionInfo, setVersionInfo] = useState({currentVersion: 'Loading..', updateAvailable: false});
|
||||
|
||||
const loadVersion = async () => {
|
||||
const {data, error} = await fetchPlanVersion();
|
||||
if (data) {
|
||||
setVersionInfo(data);
|
||||
} else if (error) {
|
||||
setVersionInfo({currentVersion: "Error getting version", updateAvailable: false})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadVersion();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-2 ms-md-3 text-center text-md-start">
|
||||
<button className="btn bg-transparent-light" onClick={toggleColorChooser}
|
||||
title={t('html.label.themeSelect')}>
|
||||
<Fa icon={faPalette}/>
|
||||
</button>
|
||||
<button className="btn bg-transparent-light" onClick={toggleInfoModal}
|
||||
title={t('html.modal.info.text')}>
|
||||
<Fa icon={faQuestionCircle}/>
|
||||
</button>
|
||||
{authRequired ?
|
||||
<a className="btn bg-transparent-light" href={baseAddress + "/auth/logout"} id="logout-button">
|
||||
<Fa icon={faDoorOpen}/> Logout
|
||||
</a> : ''}
|
||||
</div>
|
||||
<div className="ms-md-3 text-center text-md-start">
|
||||
<VersionButton toggleVersionModal={toggleVersionModal} versionInfo={versionInfo}/>
|
||||
</div>
|
||||
<PluginInformationModal open={infoModalOpen} toggle={toggleInfoModal}/>
|
||||
<VersionInformationModal open={versionModalOpen} toggle={toggleVersionModal} versionInfo={versionInfo}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarCollapse = ({item, open, setOpen}) => {
|
||||
const {t} = useTranslation();
|
||||
const toggle = event => {
|
||||
event.preventDefault();
|
||||
setOpen(!open);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="nav-item">
|
||||
<button className="nav-link"
|
||||
onClick={toggle}
|
||||
aria-controls={item.name + "-collapse"}
|
||||
aria-expanded={open}
|
||||
data-bs-toggle="collapse"
|
||||
>
|
||||
<Fa icon={item.icon}/> <span>{t(item.name)}</span>
|
||||
</button>
|
||||
<Collapse in={open}>
|
||||
<div id={item.name + "-collapse"}>
|
||||
<div className="bg-white py-2 collapse-inner rounded">
|
||||
{item.contents.map((content, i) =>
|
||||
<Item key={i}
|
||||
inner
|
||||
active={false}
|
||||
href={content.href}
|
||||
icon={content.icon}
|
||||
name={content.name}
|
||||
nameShort={content.nameShort}
|
||||
/>)}
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const renderItem = (item, i, openCollapse, setOpenCollapse, t) => {
|
||||
if (item.contents) {
|
||||
return <SidebarCollapse key={i}
|
||||
item={item}
|
||||
open={openCollapse && openCollapse === i}
|
||||
setOpen={() => setOpenCollapse(i)}/>
|
||||
}
|
||||
|
||||
if (item.href !== undefined) {
|
||||
return <Item key={i}
|
||||
active={false}
|
||||
href={item.href}
|
||||
icon={item.icon}
|
||||
name={item.name}
|
||||
nameShort={item.nameShort}
|
||||
/>
|
||||
}
|
||||
|
||||
if (item.name) {
|
||||
return <div key={i} className="sidebar-heading">{t(item.name)}</div>
|
||||
}
|
||||
|
||||
return <hr key={i} className="sidebar-divider"/>
|
||||
}
|
||||
|
||||
const Sidebar = ({items, showBackButton}) => {
|
||||
const {t} = useTranslation();
|
||||
const {color} = useTheme();
|
||||
const {currentTab, sidebarExpanded, setSidebarExpanded} = useNavigation();
|
||||
|
||||
const [openCollapse, setOpenCollapse] = useState(undefined);
|
||||
const toggleCollapse = collapse => {
|
||||
setOpenCollapse(openCollapse === collapse ? undefined : collapse);
|
||||
}
|
||||
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
const updateWidth = useCallback(() => setWindowWidth(window.innerWidth), []);
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', updateWidth);
|
||||
return () => window.removeEventListener('resize', updateWidth);
|
||||
}, [updateWidth]);
|
||||
|
||||
const collapseSidebar = () => setSidebarExpanded(windowWidth > 1350);
|
||||
useEffect(collapseSidebar, [windowWidth, currentTab, setSidebarExpanded]);
|
||||
|
||||
if (!items.length) return <></>
|
||||
|
||||
return (
|
||||
<>
|
||||
{sidebarExpanded &&
|
||||
<ul className={"navbar-nav sidebar sidebar-dark accordion bg-" + color} id="accordionSidebar">
|
||||
<Logo/>
|
||||
<Divider/>
|
||||
{showBackButton && <>
|
||||
<Item active={false} href="/" icon={faArrowLeft} name={t('html.label.toMainPage')}/>
|
||||
<Divider showMargin={!items[0].contents && items[0].href === undefined}/>
|
||||
</>}
|
||||
{items.map((item, i) => renderItem(item, i, openCollapse, toggleCollapse, t))}
|
||||
<Divider/>
|
||||
<FooterButtons/>
|
||||
</ul>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar
|
|
@ -0,0 +1,20 @@
|
|||
import React from "react";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
|
||||
const AsNumbersTable = ({headers, children}) => {
|
||||
const {nightModeEnabled} = useTheme();
|
||||
|
||||
return <table className={"table table-striped" + (nightModeEnabled ? " table-dark" : '')}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th/>
|
||||
{headers.map((header, i) => <th key={i}>{header}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{children}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
export default AsNumbersTable
|
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import ComparingLabel from "../trend/ComparingLabel";
|
||||
|
||||
const ComparisonTable = ({headers, children, comparisonHeader}) => {
|
||||
const {nightModeEnabled} = useTheme();
|
||||
|
||||
return <table className={"table table-striped" + (nightModeEnabled ? " table-dark" : '')}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<ComparingLabel>{comparisonHeader}</ComparingLabel>
|
||||
</th>
|
||||
{headers.map((header, i) => <th key={i}>{header}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{children}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
export default ComparisonTable
|
|
@ -0,0 +1,31 @@
|
|||
import React, {useEffect, useRef} from 'react';
|
||||
import DataTable from 'datatables.net'
|
||||
import 'datatables.net-bs5'
|
||||
import 'datatables.net-responsive-bs5'
|
||||
import 'datatables.net-bs5/css/dataTables.bootstrap5.min.css';
|
||||
import 'datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css';
|
||||
|
||||
const DataTablesTable = ({id, options}) => {
|
||||
const dataTableRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const idSelector = `#${id}`;
|
||||
if (dataTableRef.current && DataTable.isDataTable(idSelector)) {
|
||||
dataTableRef.current.destroy();
|
||||
}
|
||||
|
||||
dataTableRef.current = new DataTable(idSelector, options);
|
||||
|
||||
return () => {
|
||||
if (dataTableRef.current) {
|
||||
dataTableRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, [id, options, dataTableRef]);
|
||||
|
||||
return (
|
||||
<table id={id} className="table table-bordered table-striped" style={{width: "100%"}}/>
|
||||
)
|
||||
};
|
||||
|
||||
export default DataTablesTable
|
|
@ -0,0 +1,39 @@
|
|||
import React from "react";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faAngleRight, faSkullCrossbones} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const KillRow = ({kill}) => {
|
||||
const killSeparator = <Fa
|
||||
icon={kill.killerUUID === kill.victimUUID ? faSkullCrossbones : faAngleRight}
|
||||
className={"col-red"}/>;
|
||||
return (
|
||||
<tr>
|
||||
<td>{kill.date}</td>
|
||||
<td>{kill.killerName} {killSeparator} {kill.victimName}</td>
|
||||
<td>{kill.weapon}</td>
|
||||
<td>{kill.serverName}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
const KillsTable = ({kills}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled} = useTheme();
|
||||
|
||||
return (
|
||||
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
|
||||
<tbody>
|
||||
{kills.length ? kills.map((kill, i) => <KillRow key={i} kill={kill}/>) : <tr>
|
||||
<td>{t('html.generic.none')}</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
};
|
||||
|
||||
export default KillsTable;
|
|
@ -0,0 +1,86 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {faUser, faUserCircle, faUserPlus, faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import {TableRow} from "./TableRow";
|
||||
import ComparisonTable from "./ComparisonTable";
|
||||
import SmallTrend from "../trend/SmallTrend";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCalendarCheck, faClock, faEye} from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
const OnlineActivityAsNumbersTable = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
if (!data) return <></>;
|
||||
|
||||
return (
|
||||
<ComparisonTable
|
||||
headers={[t('html.label.last30days'), t('html.label.last7days'), t('html.label.last24hours')]}
|
||||
comparisonHeader={t('html.text.comparing15days')}
|
||||
>
|
||||
<TableRow icon={faUsers} color="light-blue" text={t('html.label.uniquePlayers')}
|
||||
values={[
|
||||
<>{data.unique_players_30d}{' '}<SmallTrend trend={data.unique_players_30d_trend}/></>,
|
||||
data.unique_players_7d,
|
||||
data.unique_players_24h
|
||||
]}/>
|
||||
<TableRow icon={faUser} color="light-blue"
|
||||
text={t('html.label.uniquePlayers') + ' ' + t('html.label.perDay')}
|
||||
values={[
|
||||
<>{data.unique_players_30d_avg}{' '}<SmallTrend
|
||||
trend={data.unique_players_30d_avg_trend}/></>,
|
||||
data.unique_players_7d_avg,
|
||||
data.unique_players_24h_avg
|
||||
]}/>
|
||||
<TableRow icon={faUsers} color="light-green" text={t('html.label.newPlayers')}
|
||||
values={[
|
||||
<>{data.new_players_30d}{' '}<SmallTrend trend={data.new_players_30d_trend}/></>,
|
||||
data.new_players_7d,
|
||||
data.new_players_24h
|
||||
]}/>
|
||||
<TableRow icon={faUserPlus} color="light-green"
|
||||
text={t('html.label.newPlayers') + ' ' + t('html.label.perDay')}
|
||||
values={[
|
||||
<>{data.new_players_30d_avg}{' '}<SmallTrend trend={data.new_players_30d_avg_trend}/></>,
|
||||
data.new_players_7d_avg,
|
||||
data.new_players_24h_avg
|
||||
]}/>
|
||||
<TableRow icon={faUserCircle} color="light-green" text={t('html.label.newPlayerRetention')}
|
||||
values={[
|
||||
`(${data.new_players_retention_30d}/${data.new_players_30d}) ${data.new_players_retention_30d_perc}`,
|
||||
`(${data.new_players_retention_7d}/${data.new_players_7d}) ${data.new_players_retention_7d_perc}`,
|
||||
<>{`(${data.new_players_retention_24h}/${data.new_players_24h}) ${data.new_players_retention_24h_perc}`}<Fa
|
||||
icon={faEye} title={t('html.description.newPlayerRetention')}/></>
|
||||
]}/>
|
||||
<TableRow icon={faClock} color="green"
|
||||
text={t('html.label.playtime')}
|
||||
values={[
|
||||
<>{data.playtime_30d}{' '}<SmallTrend trend={data.playtime_30d_trend}/></>,
|
||||
data.playtime_7d,
|
||||
data.playtime_24h
|
||||
]}/>
|
||||
<TableRow icon={faClock} color="green"
|
||||
text={t('html.label.averagePlaytime') + ' ' + t('html.label.perDay')}
|
||||
values={[
|
||||
<>{data.playtime_30d_avg}{' '}<SmallTrend trend={data.playtime_30d_avg_trend}/></>,
|
||||
data.playtime_7d_avg,
|
||||
data.playtime_24h_avg
|
||||
]}/>
|
||||
<TableRow icon={faClock} color="teal"
|
||||
text={t('html.label.averageSessionLength')}
|
||||
values={[
|
||||
<>{data.session_length_30d_avg}{' '}<SmallTrend
|
||||
trend={data.session_length_30d_avg_trend}/></>,
|
||||
data.session_length_7d_avg,
|
||||
data.session_length_24h_avg
|
||||
]}/>
|
||||
<TableRow icon={faCalendarCheck} color="teal"
|
||||
text={t('html.label.sessions')}
|
||||
values={[
|
||||
<>{data.sessions_30d}{' '}<SmallTrend trend={data.sessions_30d_trend}/></>,
|
||||
data.sessions_7d,
|
||||
data.sessions_24h
|
||||
]}/>
|
||||
</ComparisonTable>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnlineActivityAsNumbersTable;
|
|
@ -0,0 +1,45 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import AsNumbersTable from "./AsNumbersTable";
|
||||
import {faCrosshairs, faSkull} from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import {TableRow} from "./TableRow";
|
||||
|
||||
const PlayerPvpPveAsNumbersTable = ({killData}) => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<AsNumbersTable
|
||||
headers={[t('html.label.allTime'), t('html.label.last30days'), t('html.label.last7days')]}
|
||||
>
|
||||
<TableRow icon={faCrosshairs} color="red" text={t('html.label.kdr')} bold
|
||||
values={[killData.player_kdr_total,
|
||||
killData.player_kdr_30d,
|
||||
killData.player_kdr_7d]}/>
|
||||
<TableRow icon={faCrosshairs} color="red" text={t('html.label.playerKills')}
|
||||
values={[killData.player_kills_total,
|
||||
killData.player_kills_30d,
|
||||
killData.player_kills_7d]}/>
|
||||
<TableRow icon={faSkull} color="red" text={t('html.label.playerDeaths')}
|
||||
values={[killData.player_deaths_total,
|
||||
killData.player_deaths_30d,
|
||||
killData.player_deaths_7d]}/>
|
||||
<TableRow icon={faCrosshairs} color="green" text={t('html.label.mobKdr')} bold
|
||||
values={[killData.mob_kdr_total,
|
||||
killData.mob_kdr_30d,
|
||||
killData.mob_kdr_7d]}/>
|
||||
<TableRow icon={faCrosshairs} color="green" text={t('html.label.mobKills')}
|
||||
values={[killData.mob_kills_total,
|
||||
killData.mob_kills_30d,
|
||||
killData.mob_kills_7d]}/>
|
||||
<TableRow icon={faSkull} color="green" text={t('html.label.mobDeaths')}
|
||||
values={[killData.mob_deaths_total,
|
||||
killData.mob_deaths_30d,
|
||||
killData.mob_deaths_7d]}/>
|
||||
<TableRow icon={faSkull} color="black" text={t('html.label.deaths')}
|
||||
values={[killData.deaths_total,
|
||||
killData.deaths_30d,
|
||||
killData.deaths_7d]}/>
|
||||
</AsNumbersTable>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerPvpPveAsNumbersTable;
|
|
@ -0,0 +1,41 @@
|
|||
import {useTranslation} from "react-i18next";
|
||||
import AsNumbersTable from "./AsNumbersTable";
|
||||
import {faCrosshairs, faSkull} from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import {TableRow} from "./TableRow";
|
||||
|
||||
const ServerPvpPveAsNumbersTable = ({killData}) => {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<AsNumbersTable
|
||||
headers={[t('html.label.allTime'), t('html.label.last30days'), t('html.label.last7days')]}
|
||||
>
|
||||
<TableRow icon={faCrosshairs} color="red" text={t('html.label.averageKdr')}
|
||||
values={[killData.player_kdr_avg,
|
||||
killData.player_kdr_avg_30d,
|
||||
killData.player_kdr_avg_7d]}/>
|
||||
<TableRow icon={faCrosshairs} color="red" text={t('html.label.playerKills')}
|
||||
values={[killData.player_kills_total,
|
||||
killData.player_kills_30d,
|
||||
killData.player_kills_7d]}/>
|
||||
<TableRow icon={faCrosshairs} color="green" text={t('html.label.averageMobKdr')}
|
||||
values={[killData.mob_kdr_total,
|
||||
killData.mob_kdr_30d,
|
||||
killData.mob_kdr_7d]}/>
|
||||
<TableRow icon={faCrosshairs} color="green" text={t('html.label.mobKills')}
|
||||
values={[killData.mob_kills_total,
|
||||
killData.mob_kills_30d,
|
||||
killData.mob_kills_7d]}/>
|
||||
<TableRow icon={faSkull} color="green" text={t('html.label.mobDeaths')}
|
||||
values={[killData.mob_deaths_total,
|
||||
killData.mob_deaths_30d,
|
||||
killData.mob_deaths_7d]}/>
|
||||
<TableRow icon={faSkull} color="black" text={t('html.label.deaths')}
|
||||
values={[killData.deaths_total,
|
||||
killData.deaths_30d,
|
||||
killData.deaths_7d]}/>
|
||||
</AsNumbersTable>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerPvpPveAsNumbersTable;
|
|
@ -0,0 +1,12 @@
|
|||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
|
||||
export const TableRow = ({icon, text, color, values, bold}) => {
|
||||
const label = (<><Fa icon={icon} className={'col-' + color}/> {text}</>);
|
||||
return (
|
||||
<tr>
|
||||
<td>{bold ? <b>{label}</b> : label}</td>
|
||||
{values.map((value, j) => <td key={j}>{value}</td>)}
|
||||
</tr>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import React from "react";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCaretDown, faCaretRight, faCaretUp} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const TrendUpGood = ({value}) => <span className="badge bg-success"><Fa icon={faCaretUp}/>{value}</span>;
|
||||
const TrendUpBad = ({value}) => <span className="badge bg-danger"><Fa icon={faCaretUp}/>{value}</span>;
|
||||
const TrendDownBad = ({value}) => <span className="badge bg-danger"><Fa icon={faCaretDown}/>{value}</span>;
|
||||
const TrendDownGood = ({value}) => <span className="badge bg-success"><Fa icon={faCaretDown}/>{value}</span>;
|
||||
const TrendSame = ({value}) => <span className="badge bg-warning"><Fa icon={faCaretRight}/>{value}</span>;
|
||||
|
||||
|
||||
const BigTrend = ({trend}) => {
|
||||
if (!trend) {
|
||||
return <TrendSame value={'?'}/>;
|
||||
}
|
||||
switch (trend.direction) {
|
||||
case '+':
|
||||
return (trend.reversed ? <TrendUpBad value={trend.text}/> : <TrendUpGood value={trend.text}/>);
|
||||
case '-':
|
||||
return (trend.reversed ? <TrendDownGood value={trend.text}/> : <TrendDownBad value={trend.text}/>);
|
||||
default:
|
||||
return <TrendSame value={trend.text}/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default BigTrend
|
|
@ -0,0 +1,13 @@
|
|||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCaretDown, faCaretUp} from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
|
||||
const ComparingLabel = ({children}) => {
|
||||
return (<>
|
||||
<Fa icon={faCaretUp} className="comparing text-success"/>
|
||||
<Fa icon={faCaretDown} className="comparing text-danger"/>
|
||||
{' '}<small>{children}</small>
|
||||
</>);
|
||||
}
|
||||
|
||||
export default ComparingLabel;
|
|
@ -0,0 +1,26 @@
|
|||
import React from "react";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faCaretDown, faCaretRight, faCaretUp} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const TrendUpGood = ({value}) => <Fa icon={faCaretUp} className="trend text-success" title={value}/>;
|
||||
const TrendUpBad = ({value}) => <Fa icon={faCaretUp} className="trend text-danger" title={value}/>;
|
||||
const TrendDownBad = ({value}) => <Fa icon={faCaretDown} className="trend text-danger" title={value}/>;
|
||||
const TrendDownGood = ({value}) => <Fa icon={faCaretDown} className="trend text-success" title={value}/>;
|
||||
const TrendSame = ({value}) => <Fa icon={faCaretRight} className="trend text-warning" title={value}/>;
|
||||
|
||||
|
||||
const SmallTrend = ({trend}) => {
|
||||
if (!trend) {
|
||||
return <TrendSame value={'?'}/>;
|
||||
}
|
||||
switch (trend.direction) {
|
||||
case '+':
|
||||
return (trend.reversed ? <TrendUpBad value={trend.text}/> : <TrendUpGood value={trend.text}/>);
|
||||
case '-':
|
||||
return (trend.reversed ? <TrendDownGood value={trend.text}/> : <TrendDownBad value={trend.text}/>);
|
||||
default:
|
||||
return <TrendSame value={trend.text}/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default SmallTrend
|
|
@ -0,0 +1,66 @@
|
|||
import {createContext, useCallback, useContext, useEffect, useState} from "react";
|
||||
import {fetchWhoAmI} from "../service/authenticationService";
|
||||
|
||||
const AuthenticationContext = createContext({});
|
||||
|
||||
export const AuthenticationContextProvider = ({children}) => {
|
||||
const [loginError, setLoginError] = useState(undefined);
|
||||
|
||||
const [authLoaded, setAuthLoaded] = useState(false)
|
||||
const [authRequired, setAuthRequired] = useState(false);
|
||||
const [loggedIn, setLoggedIn] = useState(false);
|
||||
const [user, setUser] = useState(undefined);
|
||||
|
||||
const updateLoginDetails = useCallback(async () => {
|
||||
const {data: whoAmI, error} = await fetchWhoAmI();
|
||||
if (whoAmI) {
|
||||
setAuthRequired(whoAmI.authRequired);
|
||||
if (whoAmI.loggedIn) setUser(whoAmI.user);
|
||||
setLoggedIn(whoAmI.loggedIn);
|
||||
setAuthLoaded(true)
|
||||
} else if (error) {
|
||||
setLoginError(error);
|
||||
}
|
||||
}, [])
|
||||
|
||||
const login = useCallback(async (username, password) => {
|
||||
// TODO implement later when login page is done with React
|
||||
await updateLoginDetails();
|
||||
}, [updateLoginDetails]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
// TODO implement later when login page is done with React
|
||||
}, []);
|
||||
|
||||
const hasPermission = useCallback(permission => {
|
||||
return !authRequired || (loggedIn && user && user.permissions.filter(perm => perm === permission).length);
|
||||
}, [authRequired, loggedIn, user]);
|
||||
|
||||
const hasPermissionOtherThan = useCallback(permission => {
|
||||
return !authRequired || (loggedIn && user && user.permissions.filter(perm => perm !== permission).length);
|
||||
}, [authRequired, loggedIn, user]);
|
||||
|
||||
useEffect(() => {
|
||||
updateLoginDetails();
|
||||
}, [updateLoginDetails]);
|
||||
|
||||
const sharedState = {
|
||||
authLoaded,
|
||||
authRequired,
|
||||
loggedIn,
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
loginError,
|
||||
hasPermission,
|
||||
hasPermissionOtherThan
|
||||
}
|
||||
return (<AuthenticationContext.Provider value={sharedState}>
|
||||
{children}
|
||||
</AuthenticationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useAuth = () => {
|
||||
return useContext(AuthenticationContext);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import {useEffect, useState} from "react";
|
||||
import {useNavigation} from "./navigationHook";
|
||||
|
||||
export const useDataRequest = (fetchMethod, parameters) => {
|
||||
const [data, setData] = useState(undefined);
|
||||
const [loadingError, setLoadingError] = useState(undefined);
|
||||
const {updateRequested, finishUpdate} = useNavigation();
|
||||
|
||||
/*eslint-disable react-hooks/exhaustive-deps */
|
||||
useEffect(() => {
|
||||
fetchMethod(...parameters, updateRequested).then(({data: json, error}) => {
|
||||
if (json) {
|
||||
setData(json);
|
||||
finishUpdate(json.timestamp, json.timestamp_f);
|
||||
} else if (error) {
|
||||
setLoadingError(error);
|
||||
}
|
||||
});
|
||||
}, [fetchMethod, ...parameters, updateRequested])
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
|
||||
return {data, loadingError};
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import {createContext, useCallback, useContext, useEffect, useState} from "react";
|
||||
import {fetchPlanMetadata} from "../service/metadataService";
|
||||
|
||||
import terminal from '../Terminal-icon.png'
|
||||
|
||||
const MetadataContext = createContext({});
|
||||
|
||||
export const MetadataContextProvider = ({children}) => {
|
||||
const [metadata, setMetadata] = useState({});
|
||||
|
||||
const updateMetadata = useCallback(async () => {
|
||||
const {data, error} = await fetchPlanMetadata();
|
||||
if (data) {
|
||||
setMetadata(data);
|
||||
} else if (error) {
|
||||
setMetadata({metadataError: error})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getPlayerHeadImageUrl = useCallback((name, uuid) => {
|
||||
if (!uuid && name === 'console') {
|
||||
return terminal;
|
||||
}
|
||||
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
return (metadata.playerHeadImageUrl ? metadata.playerHeadImageUrl : "https://cravatar.eu/helmavatar/${playerUUID}/120.png")
|
||||
.replace('${playerUUID}', uuid)
|
||||
.replace('${playerUUIDNoDash}', uuid ? uuid.split('-').join('') : undefined)
|
||||
.replace('${playerName}', name)
|
||||
/* eslint-enable no-template-curly-in-string */
|
||||
}, [metadata.playerHeadImageUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
updateMetadata();
|
||||
}, [updateMetadata]);
|
||||
|
||||
const sharedState = {...metadata, getPlayerHeadImageUrl}
|
||||
return (<MetadataContext.Provider value={sharedState}>
|
||||
{children}
|
||||
</MetadataContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useMetadata = () => {
|
||||
return useContext(MetadataContext);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import {createContext, useCallback, useContext, useState} from "react";
|
||||
|
||||
const NavigationContext = createContext({});
|
||||
|
||||
export const NavigationContextProvider = ({children}) => {
|
||||
const [currentTab, setCurrentTab] = useState(undefined);
|
||||
const [updateRequested, setUpdateRequested] = useState(Date.now());
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const [lastUpdate, setLastUpdate] = useState({date: 0, formatted: ""});
|
||||
|
||||
const [sidebarExpanded, setSidebarExpanded] = useState(window.innerWidth > 1350);
|
||||
|
||||
const requestUpdate = useCallback(() => {
|
||||
if (!updating) {
|
||||
setUpdateRequested(Date.now());
|
||||
setUpdating(true);
|
||||
}
|
||||
}, [updating, setUpdateRequested, setUpdating]);
|
||||
|
||||
const finishUpdate = useCallback((date, formatted) => {
|
||||
// TODO Logic to retry if received data is too old
|
||||
setLastUpdate({date, formatted});
|
||||
setUpdating(false);
|
||||
}, [setLastUpdate, setUpdating]);
|
||||
|
||||
const toggleSidebar = useCallback(() => {
|
||||
setSidebarExpanded(!sidebarExpanded);
|
||||
}, [setSidebarExpanded, sidebarExpanded])
|
||||
|
||||
const sharedState = {
|
||||
currentTab, setCurrentTab,
|
||||
lastUpdate, updateRequested, updating, requestUpdate, finishUpdate,
|
||||
sidebarExpanded, setSidebarExpanded, toggleSidebar
|
||||
}
|
||||
return (<NavigationContext.Provider value={sharedState}>
|
||||
{children}
|
||||
</NavigationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useNavigation = () => {
|
||||
return useContext(NavigationContext);
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
import {createContext, useContext, useState} from "react";
|
||||
import {createNightModeCss, getColors} from "../util/colors";
|
||||
import {getLightModeChartTheming, getNightModeChartTheming} from "../util/graphColors";
|
||||
import {useMetadata} from "./metadataHook";
|
||||
|
||||
const themeColors = getColors();
|
||||
themeColors.splice(themeColors.length - 4, 4);
|
||||
|
||||
const getDefaultTheme = (metadata) => {
|
||||
const defaultTheme = metadata.defaultTheme;
|
||||
|
||||
// Use 'plan' if default or if default is undefined.
|
||||
// Avoid night mode staying on if default theme is night mode
|
||||
const invalidColor = !defaultTheme
|
||||
|| defaultTheme === 'default'
|
||||
|| defaultTheme === 'black'
|
||||
|| defaultTheme === 'white'
|
||||
|| !themeColors.map(color => color.name).includes(defaultTheme)
|
||||
|
||||
return invalidColor ? 'plan' : defaultTheme;
|
||||
}
|
||||
|
||||
const getStoredTheme = (defaultTheme) => {
|
||||
const stored = window.localStorage.getItem('themeColor');
|
||||
return stored && stored !== 'undefined' ? stored : defaultTheme;
|
||||
}
|
||||
|
||||
const setStoredTheme = themeColor => {
|
||||
if (themeColor) {
|
||||
window.localStorage.setItem('themeColor', themeColor);
|
||||
}
|
||||
}
|
||||
|
||||
const ThemeContext = createContext({});
|
||||
|
||||
export const ThemeContextProvider = ({children}) => {
|
||||
const metadata = useMetadata();
|
||||
|
||||
const [colorChooserOpen, setColorChooserOpen] = useState(false);
|
||||
const [selectedColor, setSelectedColor] = useState(getStoredTheme(getDefaultTheme(metadata)));
|
||||
const [previousColor, setPreviousColor] = useState(undefined);
|
||||
|
||||
const sharedState = {
|
||||
selectedColor,
|
||||
setSelectedColor,
|
||||
previousColor,
|
||||
setPreviousColor,
|
||||
colorChooserOpen,
|
||||
setColorChooserOpen
|
||||
}
|
||||
return (<ThemeContext.Provider value={sharedState}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const lightModeChartTheming = getLightModeChartTheming();
|
||||
const nightModeChartTheming = getNightModeChartTheming();
|
||||
|
||||
export const useTheme = () => {
|
||||
const {
|
||||
selectedColor,
|
||||
setSelectedColor,
|
||||
previousColor,
|
||||
setPreviousColor,
|
||||
colorChooserOpen,
|
||||
setColorChooserOpen
|
||||
} = useContext(ThemeContext);
|
||||
|
||||
const metadata = useMetadata();
|
||||
|
||||
const setTheme = color => {
|
||||
setStoredTheme(color);
|
||||
setSelectedColor(color);
|
||||
}
|
||||
|
||||
if (!selectedColor) setTheme(selectedColor);
|
||||
|
||||
const toggleColorChooser = () => {
|
||||
setColorChooserOpen(!colorChooserOpen);
|
||||
}
|
||||
|
||||
const isNightModeEnabled = () => {
|
||||
return selectedColor === 'night';
|
||||
}
|
||||
|
||||
const toggleNightMode = () => {
|
||||
if (isNightModeEnabled()) {
|
||||
setTheme(previousColor ? previousColor : getDefaultTheme(metadata));
|
||||
} else {
|
||||
setPreviousColor(selectedColor);
|
||||
setTheme('night');
|
||||
}
|
||||
}
|
||||
|
||||
const nightModeEnabled = isNightModeEnabled();
|
||||
return {
|
||||
color: selectedColor,
|
||||
setColor: setTheme,
|
||||
nightModeEnabled: nightModeEnabled,
|
||||
colorChooserOpen: colorChooserOpen,
|
||||
nightModeCss: nightModeEnabled ? createNightModeCss() : undefined,
|
||||
toggleNightMode: toggleNightMode,
|
||||
toggleColorChooser: toggleColorChooser,
|
||||
themeColors: themeColors,
|
||||
graphTheming: nightModeEnabled ? nightModeChartTheming : lightModeChartTheming
|
||||
};
|
||||
}
|
||||
|
||||
export const NightModeCss = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
{theme.nightModeEnabled ? <style>
|
||||
{theme.nightModeCss}
|
||||
</style> : ''}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||
|
||||
import {library} from '@fortawesome/fontawesome-svg-core';
|
||||
import {fas} from '@fortawesome/free-solid-svg-icons';
|
||||
import {far} from '@fortawesome/free-regular-svg-icons';
|
||||
import {fab} from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
import {localeService} from "./service/localeService";
|
||||
|
||||
library.add(fab);
|
||||
library.add(fas);
|
||||
library.add(far);
|
||||
|
||||
localeService.init().then(() => ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
));
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
|||
const reportWebVitals = onPerfEntry => {
|
||||
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;
|
|
@ -0,0 +1,6 @@
|
|||
import {doGetRequest} from "./backendConfiguration";
|
||||
|
||||
export const fetchWhoAmI = async () => {
|
||||
const url = '/v1/whoami';
|
||||
return doGetRequest(url);
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import axios from "axios";
|
||||
|
||||
const toBeReplaced = "PLAN_BASE_ADDRESS";
|
||||
|
||||
const isCurrentAddress = (address) => {
|
||||
const is = window.location.href.startsWith(address);
|
||||
if (!is) console.warn(`Configured address ${address} did not match start of ${window.location.href}, falling back to relative address. Configure 'Webserver.Alternative_IP' settings to point to your address.`)
|
||||
return is;
|
||||
}
|
||||
|
||||
export const baseAddress = "PLAN_BASE_ADDRESS" === toBeReplaced || !isCurrentAddress(toBeReplaced) ? "" : toBeReplaced;
|
||||
|
||||
export const doSomeGetRequest = async (url, statusOptions) => {
|
||||
let response = undefined;
|
||||
try {
|
||||
response = await axios.get(baseAddress + url);
|
||||
|
||||
for (const statusOption of statusOptions) {
|
||||
if (response.status === statusOption.status) {
|
||||
return {
|
||||
data: statusOption.get(response),
|
||||
error: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e.response !== undefined) {
|
||||
for (const statusOption of statusOptions) {
|
||||
if (e.response.status === statusOption.status) {
|
||||
return {
|
||||
data: undefined,
|
||||
error: statusOption.get(response, e)
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
data: undefined,
|
||||
error: {
|
||||
message: e.message,
|
||||
url,
|
||||
data: e.response.data
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: undefined,
|
||||
error: {
|
||||
message: e.message,
|
||||
url
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const standard200option = {status: 200, get: response => response.data}
|
||||
|
||||
export const doGetRequest = async url => {
|
||||
return doSomeGetRequest(url, [standard200option])
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import i18next from "i18next";
|
||||
import I18NextChainedBackend from "i18next-chained-backend";
|
||||
import I18NextLocalStorageBackend from "i18next-localstorage-backend";
|
||||
import I18NextHttpBackend from 'i18next-http-backend';
|
||||
import {initReactI18next} from 'react-i18next';
|
||||
import {fetchAvailableLocales} from "./metadataService";
|
||||
|
||||
/**
|
||||
* A locale system for localizing the website.
|
||||
*/
|
||||
export const localeService = {
|
||||
/**
|
||||
* @function
|
||||
* Localizes an element.
|
||||
* @param {Element} element Element to localize
|
||||
* @param {Object} [options] Options
|
||||
*/
|
||||
localize: {},
|
||||
|
||||
/**
|
||||
* The current default language reported by the server.
|
||||
* @type {string}
|
||||
* @readonly
|
||||
*/
|
||||
defaultLanguage: "",
|
||||
|
||||
/**
|
||||
* The current available languages reported by the server.
|
||||
* @type {Object.<string, string>}
|
||||
* @readonly
|
||||
*/
|
||||
availableLanguages: {},
|
||||
clientLocale: "",
|
||||
|
||||
/**
|
||||
* Initializes the locale system. Gets the default & available languages from `/v1/locale`, and initializes i18next.
|
||||
*/
|
||||
init: async function () {
|
||||
try {
|
||||
const {data} = await fetchAvailableLocales();
|
||||
if (data !== undefined) {
|
||||
this.defaultLanguage = data.defaultLanguage;
|
||||
this.availableLanguages = data.languages;
|
||||
this.languageVersions = data.languageVersions;
|
||||
} else {
|
||||
this.defaultLanguage = 'en'
|
||||
this.availableLanguages = {};
|
||||
this.languageVersions = [];
|
||||
}
|
||||
|
||||
this.clientLocale = window.localStorage.getItem("locale");
|
||||
if (!this.clientLocale) {
|
||||
this.clientLocale = this.defaultLanguage;
|
||||
}
|
||||
|
||||
await i18next
|
||||
.use(I18NextChainedBackend)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
debug: false,
|
||||
lng: this.clientLocale,
|
||||
fallbackLng: false,
|
||||
supportedLngs: Object.keys(this.availableLanguages),
|
||||
backend: {
|
||||
backends: [
|
||||
I18NextLocalStorageBackend,
|
||||
I18NextHttpBackend
|
||||
],
|
||||
backendOptions: [{
|
||||
expirationTime: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
versions: this.languageVersions
|
||||
}, {
|
||||
loadPath: '/v1/locale/{{lng}}'
|
||||
}]
|
||||
},
|
||||
}, () => {/* No need to initialize anything */
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads a locale and translates the page.
|
||||
*
|
||||
* @param {string} langCode The two-character code for the language to be loaded, e.g. EN
|
||||
* @throws Error if an invalid langCode is given
|
||||
* @see /v1/language endpoint for available language codes
|
||||
*/
|
||||
loadLocale: async function (langCode) {
|
||||
if (i18next.language === langCode) {
|
||||
return;
|
||||
}
|
||||
if (!(langCode in this.availableLanguages)) {
|
||||
throw Error(`The locale ${langCode} isn't available!`);
|
||||
}
|
||||
|
||||
window.localStorage.setItem("locale", langCode);
|
||||
await i18next.changeLanguage(langCode)
|
||||
},
|
||||
|
||||
getLanguages: function () {
|
||||
let languages = Object.fromEntries(Object.entries(this.availableLanguages).sort());
|
||||
if ('CUSTOM' in languages) {
|
||||
// Move "Custom" to first in list
|
||||
delete languages["CUSTOM"]
|
||||
languages = Object.assign({"CUSTOM": "Custom"}, languages);
|
||||
}
|
||||
|
||||
return Object.entries(languages)
|
||||
.map(entry => {
|
||||
return {name: entry[0], displayName: entry[1]}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import {doGetRequest} from "./backendConfiguration";
|
||||
|
||||
export const fetchPlanMetadata = async () => {
|
||||
const url = '/v1/metadata';
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPlanVersion = async () => {
|
||||
const url = '/v1/version';
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchAvailableLocales = async () => {
|
||||
const url = '/v1/locale';
|
||||
return doGetRequest(url);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import {faMapSigns} from "@fortawesome/free-solid-svg-icons";
|
||||
import {doSomeGetRequest, standard200option} from "./backendConfiguration";
|
||||
|
||||
export const fetchPlayer = async (uuid, timestamp) => {
|
||||
const url = `/v1/player?player=${uuid}×tamp=${timestamp}`;
|
||||
return doSomeGetRequest(url, [
|
||||
standard200option,
|
||||
{
|
||||
status: 400,
|
||||
get: () => ({
|
||||
message: 'Player not found: ' + uuid + ', try another player',
|
||||
title: '404 Player not found',
|
||||
icon: faMapSigns
|
||||
})
|
||||
}
|
||||
])
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import {doGetRequest} from "./backendConfiguration";
|
||||
|
||||
export const fetchServerOverview = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/serverOverview?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchOnlineActivityOverview = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/onlineOverview?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPlayerbaseOverview = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/playerbaseOverview?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchSessionOverview = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/sessionsOverview?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPvpPve = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/playerVersus?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchSessions = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/sessions?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchKills = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/kills?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPlayers = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = identifier ? `/v1/players?server=${identifier}×tamp=${timestamp}` : `/v1/players?timestamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPlayersOnlineGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=playersOnline&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPlayerbaseDevelopmentGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=activity&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchDayByDayGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=uniqueAndNew&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchHourByHourGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=hourlyUniqueAndNew&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchServerCalendarGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=serverCalendar&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPunchCardGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=punchCard&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchWorldPie = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=worldPie&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchGeolocations = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=geolocation&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
// 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';
|
|
@ -0,0 +1,24 @@
|
|||
@import "../../node_modules/bootstrap/scss/bootstrap"
|
||||
|
||||
@import "src/components/calendar/calendar-fixes"
|
||||
|
||||
p, span, td, .h3, a, button
|
||||
.svg-inline--fa, .fa, .far, .fas, .fab
|
||||
&:not(.comparing):not(.trend)
|
||||
text-align: center !important
|
||||
width: 1.25em !important
|
||||
|
||||
.card
|
||||
@extend .shadow
|
||||
@extend .mb-4
|
||||
|
||||
.card-header
|
||||
@extend .py-3
|
||||
@extend .d-flex
|
||||
@extend .flex-row
|
||||
@extend .align-items-center
|
||||
@extend .justify-content-between
|
||||
|
||||
h6
|
||||
@extend .m-0
|
||||
@extend .fw-bold
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,203 @@
|
|||
const colorMap = {
|
||||
PLAN: {
|
||||
name: "plan",
|
||||
hex: "#468F17"
|
||||
},
|
||||
RED: {
|
||||
name: "red",
|
||||
hex: "#F44336"
|
||||
},
|
||||
PINK: {
|
||||
name: "pink",
|
||||
hex: "#E91E63"
|
||||
},
|
||||
PURPLE: {
|
||||
name: "purple",
|
||||
hex: "#9C27B0"
|
||||
},
|
||||
DEEP_PURPLE: {
|
||||
name: "deep-purple",
|
||||
hex: "#673AB7"
|
||||
},
|
||||
INDIGO: {
|
||||
name: "indigo",
|
||||
hex: "#3F61B5"
|
||||
},
|
||||
BLUE: {
|
||||
name: "blue",
|
||||
hex: "#2196F3"
|
||||
},
|
||||
LIGHT_BLUE: {
|
||||
name: "light-blue",
|
||||
hex: "#03A9F4"
|
||||
},
|
||||
CYAN: {
|
||||
name: "cyan",
|
||||
hex: "#00BCD4"
|
||||
},
|
||||
TEAL: {
|
||||
name: "teal",
|
||||
hex: "#009688"
|
||||
},
|
||||
GREEN: {
|
||||
name: "green",
|
||||
hex: "#4CAF50"
|
||||
},
|
||||
LIGHT_GREEN: {
|
||||
name: "light-green",
|
||||
hex: "#8BC34A"
|
||||
},
|
||||
LIME: {
|
||||
name: "lime",
|
||||
hex: "#CDDC39"
|
||||
},
|
||||
YELLOW: {
|
||||
name: "yellow",
|
||||
hex: "#FFE821"
|
||||
},
|
||||
AMBER: {
|
||||
name: "amber",
|
||||
hex: "#FFC107"
|
||||
},
|
||||
ORANGE: {
|
||||
name: "orange",
|
||||
hex: "#FF9800"
|
||||
},
|
||||
DEEP_ORANGE: {
|
||||
name: "deep-orange",
|
||||
hex: "#FF5722"
|
||||
},
|
||||
BROWN: {
|
||||
name: "brown",
|
||||
hex: "#795548"
|
||||
},
|
||||
GREY: {
|
||||
name: "grey",
|
||||
hex: "#9E9E9E"
|
||||
},
|
||||
BLUE_GREY: {
|
||||
name: "blue-grey",
|
||||
hex: "#607D8B"
|
||||
},
|
||||
BLACK: {
|
||||
name: "black",
|
||||
hex: "#555555"
|
||||
},
|
||||
SUCCESS: {
|
||||
name: "success",
|
||||
hex: "#1CC88A"
|
||||
},
|
||||
WARNING: {
|
||||
name: "warning",
|
||||
hex: "#F6C23E"
|
||||
},
|
||||
DANGER: {
|
||||
name: "danger",
|
||||
hex: "#e74A3B"
|
||||
},
|
||||
NONE: ""
|
||||
};
|
||||
|
||||
export const getColors = () => {
|
||||
return Object.values(colorMap).filter(color => color);
|
||||
}
|
||||
|
||||
export const colorEnumToColorClass = color => {
|
||||
const mapped = "col-" + colorMap[color].name;
|
||||
return mapped ? mapped : "";
|
||||
}
|
||||
|
||||
export const bgClassToColorClass = bgClass => {
|
||||
return "col-" + bgClass.substring(3);
|
||||
}
|
||||
|
||||
export const colorClassToColorName = (colorClass) => {
|
||||
return colorClass.substring(4);
|
||||
}
|
||||
|
||||
export const colorEnumToBgClass = color => {
|
||||
return "bg-" + color;
|
||||
}
|
||||
|
||||
export const colorClassToBgClass = colorClass => {
|
||||
return "bg-" + colorClassToColorName(colorClass);
|
||||
}
|
||||
|
||||
// From https://stackoverflow.com/a/3732187
|
||||
export const withReducedSaturation = hex => {
|
||||
const saturationReduction = 0.70;
|
||||
// To RGB
|
||||
let r = parseInt(hex.substr(1, 2), 16); // Grab the hex representation of red (chars 1-2) and convert to decimal (base 10).
|
||||
let g = parseInt(hex.substr(3, 2), 16);
|
||||
let b = parseInt(hex.substr(5, 2), 16);
|
||||
|
||||
// To HSL
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h, s;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0; // achromatic
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
// To css property
|
||||
return 'hsl(' + h * 360 + ',' + s * 100 * saturationReduction + '%,' + l * 95 + '%)';
|
||||
}
|
||||
|
||||
const nightColors = {
|
||||
yellow: "#eee8d5",
|
||||
black: "#282a36",
|
||||
darkBlue: "#44475a",
|
||||
blue: "#6272a4",
|
||||
greyBlue: "#646e8c",
|
||||
darkGreyBlue: "#606270"
|
||||
}
|
||||
|
||||
const createNightModeColorCss = () => {
|
||||
return getColors()
|
||||
.filter(color => color.name !== 'white' && color.name !== 'black' && color.name !== 'plan')
|
||||
.map(color => {
|
||||
const desaturatedColor = withReducedSaturation(color.hex);
|
||||
return `.bg-${color.name}{background-color: ${desaturatedColor} !important;color: ${nightColors.yellow};}` +
|
||||
`.bg-${color.name}-outline{outline-color: ${desaturatedColor};border-color: ${desaturatedColor};}` +
|
||||
`.col-${color.name}{color: ${desaturatedColor} !important;}`
|
||||
}).join();
|
||||
}
|
||||
|
||||
export const createNightModeCss = () => {
|
||||
return `#content-wrapper {background-color:${nightColors.black}!important;}` +
|
||||
`body,.btn,.bg-transparent-light {color: ${nightColors.yellow};}` +
|
||||
`.card,.bg-white,.modal-content,.page-loader,.nav-tabs .nav-link:hover,.nav-tabs,hr,form .btn, .btn-outline-secondary{background-color:${nightColors.darkBlue}!important;border-color:${nightColors.blue}!important;}` +
|
||||
`.bg-white.collapse-inner {border:1px solid;}` +
|
||||
`.card-header {background-color:${nightColors.darkBlue};border-color:${nightColors.blue};}` +
|
||||
`#content,.col-black,.text-gray-900,.text-gray-800,.collapse-item,.modal-title,.modal-body,.page-loader,.fc-title,.fc-time,pre,.table-dark,input::placeholder{color:${nightColors.yellow} !important;}` +
|
||||
`.collapse-item:hover,.nav-link.active {background-color: ${nightColors.darkGreyBlue} !important;}` +
|
||||
`.nav-tabs .nav-link.active {background-color: ${nightColors.darkBlue} !important;border-color:${nightColors.blue} ${nightColors.blue} ${nightColors.darkBlue} !important;}` +
|
||||
`.fc-today {background:${nightColors.greyBlue} !important}` +
|
||||
`.fc-popover-body,.fc-popover-header{background-color: ${nightColors.darkBlue} !important;color: ${nightColors.yellow} !important;}` +
|
||||
`select,input,.dataTables_paginate .page-item:not(.active) a,.input-group-text,.input-group-text > * {background-color:${nightColors.darkBlue} !important;border-color:${nightColors.blue} !important;color: ${nightColors.yellow} !important;}` +
|
||||
`.topbar-divider,.fc td,.fc tr,.fc th, .fc table, .modal-header,.modal-body,.modal-footer{border-color:${nightColors.blue} !important;}` +
|
||||
`.fc a{color:${nightColors.yellow} !important;}` +
|
||||
`.fc-button{ background-color: ${withReducedSaturation(colorMap.PLAN.hex)} !important;}` +
|
||||
createNightModeColorCss()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue