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": { "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",

View File

@ -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",

View File

@ -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;
} }

View File

@ -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>

View File

@ -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" />} />
@ -100,7 +102,7 @@ export function App({ api }: AppProps) {
</Routes> </Routes>
</div> </div>
</div> </div>
</BrowserRouter> </BrowserRouter >
</> </>
); );
} }
@ -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,17 +382,17 @@ 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>
})} })}