Improve frontend
This commit is contained in:
parent
5ade1c0593
commit
89237c2b84
1
frontend/package-lock.json
generated
1
frontend/package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ngneat/falso": "^7.3.0",
|
"@ngneat/falso": "^7.3.0",
|
||||||
|
"@parcel/transformer-css": "^2.13.3",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ngneat/falso": "^7.3.0",
|
"@ngneat/falso": "^7.3.0",
|
||||||
|
"@parcel/transformer-css": "^2.13.3",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
--table-row-even: #f3f3f3;
|
--table-row-even: #f3f3f3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode {
|
.dark_mode {
|
||||||
--main-color-bg: #15141a;
|
--main-color-bg: #15141a;
|
||||||
--main-color: white;
|
--main-color: white;
|
||||||
/* Menu */
|
/* Menu */
|
||||||
@ -30,7 +30,6 @@
|
|||||||
--table-row-even: #232229;
|
--table-row-even: #232229;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -55,6 +54,27 @@ a {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
input[type=text] {
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px solid var(--menu-color-bg);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
background-color: var(--menu-color-bg);
|
background-color: var(--menu-color-bg);
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -115,72 +135,76 @@ a {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
}
|
}
|
||||||
.totals-key {
|
.totals_key {
|
||||||
grid-area: 1 / 1 / 2 / 2;
|
grid-area: 1 / 1 / 2 / 2;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
.totals-value {
|
.totals_value {
|
||||||
grid-area: 1 / 2 / 2 / 3;
|
grid-area: 1 / 2 / 2 / 3;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
#menu-title {
|
#menu_title {
|
||||||
color: var(--menu-color-text-hover);
|
color: var(--menu-color-text-hover);
|
||||||
font-family: "Secular One", sans-serif;
|
font-family: "Secular One", sans-serif;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
#menu-logo {
|
#menu_logo {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
content: url("../assets/apiary.svg");
|
content: url("../assets/apiary.svg");
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
.menu-link {
|
.menu_link {
|
||||||
color: var(--menu-color-text);
|
color: var(--menu-color-text);
|
||||||
}
|
}
|
||||||
.menu-link:hover {
|
.menu_link:hover {
|
||||||
color: var(--menu-color-text-hover);
|
color: var(--menu-color-text-hover);
|
||||||
}
|
}
|
||||||
.menu-link-active {
|
.menu_link_active {
|
||||||
color: var(--menu-color-text-hover);
|
color: var(--menu-color-text-hover);
|
||||||
}
|
}
|
||||||
.stats-pie {
|
.stats_pie {
|
||||||
height: 50vh;
|
max-height: 70vh;
|
||||||
width: 50vw;
|
text-align: left;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.live-table {
|
.live_table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin: 25px 0;
|
margin: 25px 0;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
|
width: 100%;
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
.live-table thead tr {
|
.live_table thead tr {
|
||||||
background-color: var(--table-color-header-bg);
|
background-color: var(--table-color-header-bg);
|
||||||
color: var(--table-color-header-text);
|
color: var(--table-color-header-text);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.live-table th,
|
.live_table th,
|
||||||
.live-table td {
|
.live_table td {
|
||||||
padding: 12px 15px;
|
padding: 12px 15px;
|
||||||
}
|
}
|
||||||
.live-table tbody tr {
|
.live_table tbody tr {
|
||||||
border-bottom: 1px solid var(--table-row-odd);
|
border-bottom: 1px solid var(--table-row-odd);
|
||||||
}
|
}
|
||||||
.live-table tbody tr:nth-of-type(even) {
|
.live_table tbody tr:nth-of-type(even) {
|
||||||
background-color: var(--table-row-even);
|
background-color: var(--table-row-even);
|
||||||
}
|
}
|
||||||
.live-table tbody tr:last-of-type {
|
.live_table tbody tr:last-of-type {
|
||||||
border-bottom: 2px solid var(--table-color-bg);
|
border-bottom: 2px solid var(--table-color-bg);
|
||||||
}
|
}
|
||||||
.live-table tbody tr.active-row {
|
.live_table tbody tr.active-row {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--table-color-bg);
|
color: var(--table-color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin-left: 10vw;
|
margin-left: 10vw;
|
||||||
|
width: 50vw;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
@ -3,7 +3,6 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title></title>
|
<title></title>
|
||||||
<link rel="stylesheet" href="./css/style.css" />
|
|
||||||
<link rel="stylesheet" href="./css/fonts.css" />
|
<link rel="stylesheet" href="./css/fonts.css" />
|
||||||
<script type="module" src="./js/app.tsx"></script>
|
<script type="module" src="./js/app.tsx"></script>
|
||||||
</head>
|
</head>
|
||||||
|
@ -3,11 +3,13 @@ import { createRoot } from "react-dom/client";
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ApiaryAPI, LoginAttempt, DummyApiaryAPIClient, ApiaryAPIClient, TotalStats, StatsResult, StatsType } from "./api";
|
import { ApiaryAPI, LoginAttempt, DummyApiaryAPIClient, ApiaryAPIClient, TotalStats, StatsResult, StatsType } from "./api";
|
||||||
import { BrowserRouter, NavLink, Routes, Route } from "react-router";
|
import { BrowserRouter, NavLink, Routes, Route } from "react-router";
|
||||||
import { Chart as ChartJS, Tooltip, ArcElement, Legend } from "chart.js";
|
import { Chart as ChartJS, Tooltip, ArcElement, Legend, ChartOptions } from "chart.js";
|
||||||
import { Pie } from "react-chartjs-2";
|
import { Pie } from "react-chartjs-2";
|
||||||
import humanizeDuration from "humanize-duration";
|
import humanizeDuration from "humanize-duration";
|
||||||
|
import * as classes from "../css/style.module.css";
|
||||||
|
|
||||||
ChartJS.register(Tooltip, ArcElement, Legend);
|
ChartJS.register(Tooltip, ArcElement, Legend);
|
||||||
|
console.log(classes);
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
api: ApiaryAPI
|
api: ApiaryAPI
|
||||||
@ -71,9 +73,9 @@ export function App({ api }: AppProps) {
|
|||||||
setMode(mode);
|
setMode(mode);
|
||||||
|
|
||||||
if (mode === "dark") {
|
if (mode === "dark") {
|
||||||
document.body.classList.add("dark-mode");
|
document.body.classList.add(classes.dark_mode);
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.remove("dark-mode");
|
document.body.classList.remove(classes.dark_mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +91,7 @@ export function App({ api }: AppProps) {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<HeaderMenu title={headerProps.title} items={headerProps.items} />
|
<HeaderMenu title={headerProps.title} items={headerProps.items} />
|
||||||
<div className="content">
|
<div className={classes.content}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home api={api} />} />
|
<Route path="/" element={<Home api={api} />} />
|
||||||
<Route path="/stats/password" element={<Stats api={api} type="password" />} />
|
<Route path="/stats/password" element={<Stats api={api} type="password" />} />
|
||||||
@ -167,9 +169,23 @@ export function StatsPie({ data }: StatsPieProps) {
|
|||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
const getTextColor = () => {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'white' : 'black'
|
||||||
|
}
|
||||||
|
const options: ChartOptions<"pie"> = {
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
align: "start",
|
||||||
|
labels: {
|
||||||
|
color: getTextColor(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="stats-pie">
|
<div className={classes.stats_pie}>
|
||||||
<Pie data={piedata} options={{ plugins: { legend: {} } }} />
|
<Pie data={piedata} options={options} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -207,14 +223,14 @@ export interface TotalsProps {
|
|||||||
|
|
||||||
export function Totals({ totals }: TotalsProps) {
|
export function Totals({ totals }: TotalsProps) {
|
||||||
return (
|
return (
|
||||||
<div className="totals">
|
<div className={classes.totals}>
|
||||||
<div className="totals-key">
|
<div className={classes.totals_key}>
|
||||||
<h2>Unique passwords</h2>
|
<h2>Unique passwords</h2>
|
||||||
<h2>Unique username</h2>
|
<h2>Unique username</h2>
|
||||||
<h2>Unique IPs</h2>
|
<h2>Unique IPs</h2>
|
||||||
<h2>Total attempts</h2>
|
<h2>Total attempts</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="totals-value">
|
<div className={classes.totals_value}>
|
||||||
<h2>{totals.password}</h2>
|
<h2>{totals.password}</h2>
|
||||||
<h2>{totals.username}</h2>
|
<h2>{totals.username}</h2>
|
||||||
<h2>{totals.ip}</h2>
|
<h2>{totals.ip}</h2>
|
||||||
@ -268,7 +284,7 @@ export function DateTD({ date, now }: DateTDProps) {
|
|||||||
}, [displayDate, now])
|
}, [displayDate, now])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td className="live-table-date">{displayDate}</td>
|
<td className={classes.live_table_date}>{displayDate}</td>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,10 +297,10 @@ export function LiveList({ list }: LiveListProps) {
|
|||||||
return (
|
return (
|
||||||
<tr key={a.date}>
|
<tr key={a.date}>
|
||||||
<DateTD date={attemptDate} now={now} />
|
<DateTD date={attemptDate} now={now} />
|
||||||
<td className="live-table-username">{a.username}</td>
|
<td className={classes.live_table_username}>{a.username}</td>
|
||||||
<td className="live-table-password">{a.password}</td>
|
<td className={classes.live_table_password}>{a.password}</td>
|
||||||
<td className="live-table-ip">{a.remoteIP}</td>
|
<td className={classes.live_table_ip}>{a.remoteIP}</td>
|
||||||
<td className="live-table-country">{a.country}</td>
|
<td className={classes.live_table_country}>{a.country}</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -301,14 +317,14 @@ export function LiveList({ list }: LiveListProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<table className="live-table">
|
<table className={classes.live_table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="live-table-date">Date</th>
|
<th className={classes.live_table_date}>Date</th>
|
||||||
<th className="live-table-username">Username</th>
|
<th className={classes.live_table_username}>Username</th>
|
||||||
<th className="live-table-password">Password</th>
|
<th className={classes.live_table_password}>Password</th>
|
||||||
<th className="live-table-ip">IP</th>
|
<th className={classes.live_table_ip}>IP</th>
|
||||||
<th className="live-table-country">Country</th>
|
<th className={classes.live_table_country}>Country</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -344,7 +360,7 @@ export function Query({ api }: AppProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<input placeholder="query" name="query" />
|
<input placeholder="Search..." name="query" type="text" />
|
||||||
</form>
|
</form>
|
||||||
{queryErr ? <ErrorBox message={queryErr.message} /> : null}
|
{queryErr ? <ErrorBox message={queryErr.message} /> : null}
|
||||||
<LiveList list={liveList} />
|
<LiveList list={liveList} />
|
||||||
@ -358,7 +374,7 @@ interface ErrorBoxProps {
|
|||||||
|
|
||||||
export function ErrorBox({ message }: ErrorBoxProps) {
|
export function ErrorBox({ message }: ErrorBoxProps) {
|
||||||
return (
|
return (
|
||||||
<div className="error-box">
|
<div className={classes.error_box}>
|
||||||
<p>Error: {message}</p>
|
<p>Error: {message}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -366,14 +382,14 @@ export function ErrorBox({ message }: ErrorBoxProps) {
|
|||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
return (
|
return (
|
||||||
<div className="navbar">
|
<div className={classes.navbar}>
|
||||||
<h2 id="menu-title">apiary.home.2rjus.net</h2>
|
<h2 id="menu-title">apiary.home.2rjus.net</h2>
|
||||||
<nav className="nav-flex">
|
<nav className={classes.nav_flex}>
|
||||||
<ul>
|
<ul>
|
||||||
<li><NavLink to="/" className={({ isActive }) => isActive ? "menu-link-active" : "menu-link"}>Home</NavLink></li>
|
<li><NavLink to="/" className={({ isActive }) => isActive ? classes.menu_link_active : classes.menu_link}>Home</NavLink></li>
|
||||||
<li><NavLink to="/stats/password" className={({ isActive }) => isActive ? "menu-link-active" : "menu-link"}>Stats</NavLink></li>
|
<li><NavLink to="/stats/password" className={({ isActive }) => isActive ? classes.menu_link_active : classes.menu_link}>Stats</NavLink></li>
|
||||||
<li><NavLink to="/live" className={({ isActive }) => isActive ? "menu-link-active" : "menu-link"}>Live</NavLink></li>
|
<li><NavLink to="/live" className={({ isActive }) => isActive ? classes.menu_link_active : classes.menu_link}>Live</NavLink></li>
|
||||||
<li><NavLink to="/query" className={({ isActive }) => isActive ? "menu-link-active" : "menu-link"}>Query</NavLink></li>
|
<li><NavLink to="/query" className={({ isActive }) => isActive ? classes.menu_link_active : classes.menu_link}>Query</NavLink></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div >
|
</div >
|
||||||
@ -394,16 +410,16 @@ export function HeaderMenu({ title, items }: HeaderMenuProps) {
|
|||||||
const menuItems = items.map((item) => {
|
const menuItems = items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<li key={item.path}>
|
<li key={item.path}>
|
||||||
<NavLink to={item.path} className={({ isActive }) => isActive ? "menu-link-active" : "menu-link"}>{item.name}</NavLink>
|
<NavLink to={item.path} className={({ isActive }) => isActive ? classes.menu_link_active : classes.menu_link}>{item.name}</NavLink>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="navbar">
|
<div className={classes.navbar}>
|
||||||
<img id="menu-logo"></img>
|
<img id={classes.menu_logo}></img>
|
||||||
<h2 id="menu-title">{title}</h2>
|
<h2 id={classes.menu_title}>{title}</h2>
|
||||||
<nav className="nav-flex">
|
<nav className={classes.nav_flex}>
|
||||||
<ul>
|
<ul>
|
||||||
{menuItems}
|
{menuItems}
|
||||||
</ul>
|
</ul>
|
||||||
@ -418,13 +434,13 @@ export interface SubMenuProps {
|
|||||||
|
|
||||||
export function SubMenu({ items }: SubMenuProps) {
|
export function SubMenu({ items }: SubMenuProps) {
|
||||||
return (
|
return (
|
||||||
<nav className="submenu">
|
<nav className={classes.submenu}>
|
||||||
<ul>
|
<ul>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
return <li>
|
return <li>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className={item.active() ? "sub-menu-link-active" : "sub-menu-link"}
|
className={item.active() ? classes.sub_menu_active : classes.sub_menu_link}
|
||||||
onClick={item.onClick}>{item.name}</a>
|
onClick={item.onClick}>{item.name}</a>
|
||||||
</li>
|
</li>
|
||||||
})}
|
})}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user