Add flake.nix and rewrite frontend #5
							
								
								
									
										1
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -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", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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" />} /> | ||||
| @@ -100,7 +102,7 @@ export function App({ api }: AppProps) { | ||||
|                         </Routes> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </BrowserRouter> | ||||
|             </BrowserRouter > | ||||
|         </> | ||||
|     ); | ||||
| } | ||||
| @@ -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,17 +382,17 @@ 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> | ||||
|         </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> | ||||
|                 })} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user