Improve frontend

This commit is contained in:
Torjus Håkestad 2025-03-19 16:49:18 +01:00
parent 5ade1c0593
commit 89237c2b84
Signed by: torjus
SSH Key Fingerprint: SHA256:KjAds8wHfD2mBYK2H815s/+ABcSdcIHUndwHEdSxml4
5 changed files with 99 additions and 58 deletions

View File

@ -15,6 +15,7 @@
},
"devDependencies": {
"@ngneat/falso": "^7.3.0",
"@parcel/transformer-css": "^2.13.3",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",

View File

@ -7,6 +7,7 @@
},
"devDependencies": {
"@ngneat/falso": "^7.3.0",
"@parcel/transformer-css": "^2.13.3",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",

View File

@ -14,7 +14,7 @@
--table-row-even: #f3f3f3;
}
.dark-mode {
.dark_mode {
--main-color-bg: #15141a;
--main-color: white;
/* Menu */
@ -30,7 +30,6 @@
--table-row-even: #232229;
}
#root {
padding: 0;
margin: 0;
@ -55,6 +54,27 @@ a {
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 {
background-color: var(--menu-color-bg);
display: flex;
@ -115,72 +135,76 @@ a {
font-size: 20px;
font-weight: 200;
}
.totals-key {
.totals_key {
grid-area: 1 / 1 / 2 / 2;
text-align: right;
}
.totals-value {
.totals_value {
grid-area: 1 / 2 / 2 / 3;
padding-left: 10px;
text-align: left;
}
#menu-title {
#menu_title {
color: var(--menu-color-text-hover);
font-family: "Secular One", sans-serif;
font-size: 30px;
font-weight: 300;
}
#menu-logo {
#menu_logo {
padding: 5px;
content: url("../assets/apiary.svg");
width: 30px;
height: 30px;
}
.menu-link {
.menu_link {
color: var(--menu-color-text);
}
.menu-link:hover {
.menu_link:hover {
color: var(--menu-color-text-hover);
}
.menu-link-active {
.menu_link_active {
color: var(--menu-color-text-hover);
}
.stats-pie {
height: 50vh;
width: 50vw;
.stats_pie {
max-height: 70vh;
text-align: left;
color: white;
}
.live-table {
.live_table {
border-collapse: collapse;
margin: 25px 0;
font-size: 0.9em;
font-family: sans-serif;
min-width: 400px;
width: 100%;
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);
color: var(--table-color-header-text);
text-align: left;
}
.live-table th,
.live-table td {
.live_table th,
.live_table td {
padding: 12px 15px;
}
.live-table tbody tr {
.live_table tbody tr {
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);
}
.live-table tbody tr:last-of-type {
.live_table tbody tr:last-of-type {
border-bottom: 2px solid var(--table-color-bg);
}
.live-table tbody tr.active-row {
.live_table tbody tr.active-row {
font-weight: bold;
color: var(--table-color-bg);
}
.content {
margin-left: 10vw;
width: 50vw;
text-align: center;
}

View File

@ -3,7 +3,6 @@
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" href="./css/style.css" />
<link rel="stylesheet" href="./css/fonts.css" />
<script type="module" src="./js/app.tsx"></script>
</head>

View File

@ -3,11 +3,13 @@ import { createRoot } from "react-dom/client";
import { useState, useEffect } from 'react';
import { ApiaryAPI, LoginAttempt, DummyApiaryAPIClient, ApiaryAPIClient, TotalStats, StatsResult, StatsType } from "./api";
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 humanizeDuration from "humanize-duration";
import * as classes from "../css/style.module.css";
ChartJS.register(Tooltip, ArcElement, Legend);
console.log(classes);
interface AppProps {
api: ApiaryAPI
@ -71,9 +73,9 @@ export function App({ api }: AppProps) {
setMode(mode);
if (mode === "dark") {
document.body.classList.add("dark-mode");
document.body.classList.add(classes.dark_mode);
} else {
document.body.classList.remove("dark-mode");
document.body.classList.remove(classes.dark_mode);
}
}
@ -89,7 +91,7 @@ export function App({ api }: AppProps) {
<BrowserRouter>
<div id="app">
<HeaderMenu title={headerProps.title} items={headerProps.items} />
<div className="content">
<div className={classes.content}>
<Routes>
<Route path="/" element={<Home api={api} />} />
<Route path="/stats/password" element={<Stats api={api} type="password" />} />
@ -167,9 +169,23 @@ export function StatsPie({ data }: StatsPieProps) {
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 (
<div className="stats-pie">
<Pie data={piedata} options={{ plugins: { legend: {} } }} />
<div className={classes.stats_pie}>
<Pie data={piedata} options={options} />
</div>
)
}
@ -207,14 +223,14 @@ export interface TotalsProps {
export function Totals({ totals }: TotalsProps) {
return (
<div className="totals">
<div className="totals-key">
<div className={classes.totals}>
<div className={classes.totals_key}>
<h2>Unique passwords</h2>
<h2>Unique username</h2>
<h2>Unique IPs</h2>
<h2>Total attempts</h2>
</div>
<div className="totals-value">
<div className={classes.totals_value}>
<h2>{totals.password}</h2>
<h2>{totals.username}</h2>
<h2>{totals.ip}</h2>
@ -268,7 +284,7 @@ export function DateTD({ date, now }: DateTDProps) {
}, [displayDate, now])
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 (
<tr key={a.date}>
<DateTD date={attemptDate} now={now} />
<td className="live-table-username">{a.username}</td>
<td className="live-table-password">{a.password}</td>
<td className="live-table-ip">{a.remoteIP}</td>
<td className="live-table-country">{a.country}</td>
<td className={classes.live_table_username}>{a.username}</td>
<td className={classes.live_table_password}>{a.password}</td>
<td className={classes.live_table_ip}>{a.remoteIP}</td>
<td className={classes.live_table_country}>{a.country}</td>
</tr>
)
})
@ -301,14 +317,14 @@ export function LiveList({ list }: LiveListProps) {
return (
<>
<table className="live-table">
<table className={classes.live_table}>
<thead>
<tr>
<th className="live-table-date">Date</th>
<th className="live-table-username">Username</th>
<th className="live-table-password">Password</th>
<th className="live-table-ip">IP</th>
<th className="live-table-country">Country</th>
<th className={classes.live_table_date}>Date</th>
<th className={classes.live_table_username}>Username</th>
<th className={classes.live_table_password}>Password</th>
<th className={classes.live_table_ip}>IP</th>
<th className={classes.live_table_country}>Country</th>
</tr>
</thead>
<tbody>
@ -344,7 +360,7 @@ export function Query({ api }: AppProps) {
return (
<>
<form onSubmit={handleSubmit}>
<input placeholder="query" name="query" />
<input placeholder="Search..." name="query" type="text" />
</form>
{queryErr ? <ErrorBox message={queryErr.message} /> : null}
<LiveList list={liveList} />
@ -358,7 +374,7 @@ interface ErrorBoxProps {
export function ErrorBox({ message }: ErrorBoxProps) {
return (
<div className="error-box">
<div className={classes.error_box}>
<p>Error: {message}</p>
</div>
)
@ -366,14 +382,14 @@ export function ErrorBox({ message }: ErrorBoxProps) {
export function Header() {
return (
<div className="navbar">
<div className={classes.navbar}>
<h2 id="menu-title">apiary.home.2rjus.net</h2>
<nav className="nav-flex">
<nav className={classes.nav_flex}>
<ul>
<li><NavLink to="/" className={({ isActive }) => isActive ? "menu-link-active" : "menu-link"}>Home</NavLink></li>
<li><NavLink to="/stats/password" className={({ isActive }) => isActive ? "menu-link-active" : "menu-link"}>Stats</NavLink></li>
<li><NavLink to="/live" className={({ isActive }) => isActive ? "menu-link-active" : "menu-link"}>Live</NavLink></li>
<li><NavLink to="/query" className={({ isActive }) => isActive ? "menu-link-active" : "menu-link"}>Query</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 ? classes.menu_link_active : classes.menu_link}>Stats</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 ? classes.menu_link_active : classes.menu_link}>Query</NavLink></li>
</ul>
</nav>
</div >
@ -394,16 +410,16 @@ export function HeaderMenu({ title, items }: HeaderMenuProps) {
const menuItems = items.map((item) => {
return (
<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>
)
})
return (
<div className="navbar">
<img id="menu-logo"></img>
<h2 id="menu-title">{title}</h2>
<nav className="nav-flex">
<div className={classes.navbar}>
<img id={classes.menu_logo}></img>
<h2 id={classes.menu_title}>{title}</h2>
<nav className={classes.nav_flex}>
<ul>
{menuItems}
</ul>
@ -418,13 +434,13 @@ export interface SubMenuProps {
export function SubMenu({ items }: SubMenuProps) {
return (
<nav className="submenu">
<nav className={classes.submenu}>
<ul>
{items.map((item) => {
return <li>
<a
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>
</li>
})}