Merge branch 'feature/synthwave' into 'main'

✨ BBS 2222 synthwave theme

See merge request edb/bbs!3
This commit is contained in:
nikolark committed 2022-04-17 14:39:44 +00:00
commit b56d103ae7
42 files changed
+1740 -1317

No files matched your search

+1 -2
View File
@@ -23,9 +23,8 @@ npm start
## TODO
- update emails
- improve product listing (light purple background)
- Stats: products over time
- Light on dark colors
- inventory views
+3
View File
@@ -3,6 +3,9 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BBS</title>
</head>
+943 -978
View File
File diff suppressed because it is too large. Load diff
+21 -24
View File
@@ -16,51 +16,48 @@
"test": "jest"
},
"dependencies": {
"@apollo/client": "^3.5.8",
"@apollo/client": "^3.5.10",
"@nivo/bar": "^0.79.1",
"@nivo/line": "^0.79.1",
"@nivo/pie": "^0.79.1",
"bootstrap": "^5.1.3",
"bootstrap-icons": "^1.8.1",
"clsx": "^1.1.1",
"date-fns": "^2.28.0",
"graphql": "^16.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hook-form": "^7.27.1",
"react-router-dom": "^6.2.1",
"react-hook-form": "^7.30.0",
"react-router-dom": "^6.3.0",
"use-url-search-params": "^2.5.1"
},
"devDependencies": {
"@graphql-codegen/cli": "^2.6.1",
"@graphql-codegen/typed-document-node": "^2.2.3",
"@graphql-codegen/typescript-operations": "^2.3.0",
"@graphql-codegen/cli": "^2.6.2",
"@graphql-codegen/typed-document-node": "^2.2.8",
"@graphql-codegen/typescript-operations": "^2.3.5",
"@graphql-typed-document-node/core": "^3.1.1",
"@types/bootstrap": "^5.1.9",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"@types/react": "^17.0.44",
"@types/react-dom": "^17.0.15",
"@types/react-router-dom": "^5.3.3",
"@typescript-eslint/eslint-plugin": "^5.11.0",
"@typescript-eslint/parser": "^5.11.0",
"@vitejs/plugin-react": "^1.2.0",
"@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.19.0",
"@vitejs/plugin-react": "^1.3.1",
"@vitejs/plugin-react-refresh": "^1.3.6",
"eslint": "^8.9.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.4",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
"jest": "^27.5.1",
"prettier": "^2.5.1",
"stylelint": "^14.5.0",
"prettier": "^2.6.2",
"stylelint": "^14.7.0",
"stylelint-config-idiomatic-order": "^8.1.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^7.0.0",
"stylelint-order": "^5.0.0",
"stylelint-prettier": "^2.0.0",
"typescript": "^4.5.5",
"vite": "^2.8.1",
"vite-plugin-svgr": "^1.0.1"
"typescript": "^4.6.3",
"vite": "^2.9.5",
"vite-plugin-svgr": "^1.1.0"
},
"engines": {
"node": ">=16",
+2 -1
View File
@@ -1,3 +1,4 @@
.app {
margin: 16px;
margin: 1rem auto;
max-width: 1220px;
}
+6 -2
View File
@@ -1,23 +1,27 @@
import { ApolloProvider } from "@apollo/client";
import React from "react";
import { BrowserRouter as Router } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import styles from "./App.module.css";
import { Routes } from "./Routes";
import client from "./client";
import { Footer } from "./components/Footer";
import { Header } from "./components/Header";
import { Splash as Splash } from "./components/Splash";
import { Toast } from "./components/Toast";
import { ToastProvider } from "./components/ToastProvider";
import { Wireframe } from "./components/Wireframe";
const App: React.FC = () => (
const App = (): JSX.Element => (
<ApolloProvider client={client}>
<Router>
<ToastProvider>
<Header />
<div className={styles.app}>
<Routes />
<Wireframe />
</div>
<Footer />
<Toast />
</ToastProvider>
</Router>
+3 -1
View File
@@ -1,6 +1,7 @@
import React from "react";
import { Route, Routes as ReactRouterRoutes } from "react-router-dom";
import { Splash } from "./components/Splash";
import { UserCreate } from "./features/auth/UserCreate";
import { Deposit } from "./features/deposit/Deposit";
import { Log } from "./features/log/Log";
@@ -11,7 +12,7 @@ import { Stats } from "./features/stats/Stats";
import { UserDetail } from "./features/users/UserDetail";
import { UserList } from "./features/users/UserList";
export const Routes: React.FC = () => (
export const Routes = (): JSX.Element => (
<ReactRouterRoutes>
<Route path="/deposit" element={<Deposit />} />
<Route path="/log" element={<Log />} />
@@ -22,5 +23,6 @@ export const Routes: React.FC = () => (
<Route path="/users/create" element={<UserCreate />} />
<Route path="/user/:userId" element={<UserDetail />} />
<Route path="/" element={<Register />} />
<Route path="*" element={<Splash brandText="404" noOverlay />} />
</ReactRouterRoutes>
);
Binary file not shown.
Binary file not shown.
+22
View File
@@ -0,0 +1,22 @@
.footer {
position: absolute;
right: 0;
bottom: 1rem;
left: 0;
height: 20px;
color: rgba(255, 255, 255, 0.5);
font-size: 0.8rem;
text-align: center;
}
.heart {
color: var(--brand);
}
.brick {
position: relative;
top: 7px;
display: inline-block;
width: 20px;
height: 20px;
}
+15
View File
@@ -0,0 +1,15 @@
import React from "react";
import { ReactComponent as Brick } from "../assets/brick.svg";
import styles from "./Footer.module.css";
export const Footer = (): JSX.Element => {
return (
<footer className={styles.footer}>
Made with <span className={styles.heart}>❤</span> by{" "}
<span title="EDB-gjengen">
<Brick className={styles.brick} />
</span>
</footer>
);
};
+90 -9
View File
@@ -1,13 +1,94 @@
.logo {
position: relative;
top: -1px;
display: inline-block;
width: 24px;
height: 24px;
margin: 0 8px;
.header {
margin: 1rem auto;
max-width: 1220px;
}
.navIcon {
.nav {
display: flex;
height: 5rem;
align-items: center;
text-transform: uppercase;
}
.navLink:after {
position: absolute;
right: 12px;
left: 14px;
display: block;
height: 1px;
background-color: rgba(243, 33, 61, 0.4);
box-shadow: 0 0 4px rgba(243, 33, 61, 0.4), 0 0 11px rgba(243, 33, 61, 0.4), 0 0 19px rgba(243, 33, 61, 0.4);
content: "";
transform: rotate(-2.5deg);
}
.navLink:hover:after {
background-color: var(--brand);
box-shadow: 0 0 4px var(--brand), 0 0 11px var(--brand), 0 0 19px var(--brand), 0 0 30px var(--brand);
}
.logoNav {
margin-left: 8px;
}
.navItems {
display: flex;
align-items: center;
padding: 0;
margin: 0;
list-style: none;
}
.navLink {
position: relative;
top: -2px;
padding: 0.4rem 0.8rem;
border: 1px solid transparent;
transition: color 0.1s ease-in-out;
}
.navLink:hover {
color: white;
}
.active.active {
color: white;
}
/* FIXME: Duplicates Splash styles */
.brand {
padding-left: 1rem;
color: var(--brand);
font-family: Rocket Rinder, monospace;
font-size: 3rem;
font-style: italic;
letter-spacing: 2px;
line-height: 1;
text-shadow: 0 0 4px rgba(243, 33, 61, 0.8), 0 0 11px rgba(243, 33, 61, 0.8), 0 0 19px rgba(243, 33, 61, 0.8);
}
.subtitle {
position: relative;
left: -8px;
animation: flicker 1.5s infinite alternate;
color: var(--interlace);
font-family: Rocket Clouds DEMO, serif;
font-size: 10px;
font-weight: bold;
line-height: 1;
transform: rotate(-6deg);
}
@keyframes flicker {
0%,
18%,
22%,
25%,
53%,
57%,
100% {
text-shadow: 0 0 4px rgba(121, 152, 238, 0.8), 0 0 11px rgba(121, 152, 238, 0.8), 0 0 19px rgba(121, 152, 238, 0.8);
}
20%,
24%,
55% {
text-shadow: none;
}
}
+16 -49
View File
@@ -1,83 +1,50 @@
import { ReactComponent as BarChart } from "bootstrap-icons/icons/bar-chart.svg";
import { ReactComponent as CardList } from "bootstrap-icons/icons/card-list.svg";
import { ReactComponent as House } from "bootstrap-icons/icons/house.svg";
import { ReactComponent as PeopleIcon } from "bootstrap-icons/icons/people.svg";
import clsx from "clsx";
import React from "react";
import { NavLink } from "react-router-dom";
import { ReactComponent as Logo } from "../assets/brick.svg";
import styles from "./Header.module.css";
const ADMIN_URL = `${import.meta.env.VITE_API_URL}/admin/`;
const navLinkStyles = ({ isActive }: { isActive: boolean }) => (isActive ? "nav-link active" : "nav-link");
const navLinkStyles = ({ isActive }: { isActive: boolean }) => clsx(styles.navLink, { [styles.active]: isActive });
export const Header: React.FC = () => {
export const Header = (): JSX.Element => {
return (
<header>
<nav className="navbar navbar-dark bg-dark navbar-expand-md">
<NavLink to="/" className="navbar-brand">
<span className={styles.logo}>
<Logo />
</span>
Biceps Bar System - BBS
<header className={styles.header}>
<nav className={styles.nav}>
<NavLink to="/" className={styles.logoNav}>
<div className={styles.brand}>BBS</div>
<div className={styles.subtitle}>Biceps Bar System</div>
</NavLink>
<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon" />
</button>
<div className="collapse navbar-collapse" id="navbarNav">
<ul className="navbar-nav">
<li className="nav-item">
<ul className={styles.navItems}>
<li>
<NavLink to="/" className={navLinkStyles} end>
<span className={styles.navIcon}>
<House />
</span>{" "}
Kjøp drikke
</NavLink>
</li>
<li className="nav-item">
<li>
<NavLink to="/deposit" className={navLinkStyles}>
Sett inn penger
</NavLink>
</li>
<li className="nav-item">
<li>
<NavLink to="/log" className={navLinkStyles}>
<span className={styles.navIcon}>
<CardList />
</span>{" "}
Logg
</NavLink>
</li>
<li className="nav-item">
<li>
<NavLink to="/stats" className={navLinkStyles}>
<span className={styles.navIcon}>
<BarChart />
</span>{" "}
Statistikk
</NavLink>
</li>
<li className="nav-item">
<li>
<NavLink to="/users" className={navLinkStyles}>
<span className={styles.navIcon}>
<PeopleIcon />
</span>{" "}
Folk
</NavLink>
</li>
</ul>
</div>
<ul className="navbar-nav">
<li className="nav-item">
<a href={ADMIN_URL} className="nav-link">
<li>
<a href={ADMIN_URL} className={styles.navLink}>
Admin
</a>
</li>
@@ -1,57 +0,0 @@
.product {
display: flex;
overflow: hidden;
flex-direction: column;
align-items: center;
justify-content: center;
border: 4px solid transparent;
background: transparent;
border-radius: 8px;
transition: background-color 0.1s ease-out, border 0.1s ease-out;
}
.product:hover {
border-color: rgba(25, 135, 84, 0.6);
background: rgba(0, 0, 0, 0.1);
}
.active {
border-color: rgba(25, 135, 84, 0.6);
background: rgba(25, 135, 84, 0.2);
}
.active:hover {
background: rgba(25, 135, 84, 0.2);
}
.productText {
position: absolute;
right: 0;
bottom: 0;
left: 0;
padding: 4px;
background: rgba(0, 0, 0, 0.6);
border-radius: 8px;
color: white;
font-weight: bold;
}
.productImage {
width: 140px;
height: 140px;
object-fit: contain;
}
.noProductImage {
width: 140px;
height: 140px;
}
.amount {
position: absolute;
top: 0px;
left: 0px;
padding: 4px;
background: rgba(0, 0, 0, 0.6);
border-radius: 8px;
color: white;
font-size: 2rem;
font-weight: bold;
line-height: 1;
}
.wrap {
position: relative;
}
-26
View File
@@ -1,26 +0,0 @@
import clsx from "clsx";
import React, { MouseEventHandler } from "react";
import { Product } from "../types";
import styles from "./ProductCard.module.css";
type ProductCardProps = {
product: Omit<Product, "userCounts">;
onProduct: MouseEventHandler;
active: boolean;
amount: number;
};
export const ProductCard: React.FC<ProductCardProps> = ({ product, onProduct, amount, active = false }) => {
const classes = clsx(styles.product, { [styles.active]: active });
return (
<button className={classes} type="button" onClick={onProduct}>
<div className={styles.wrap}>
<span className={styles.amount}>{amount}</span>
{product.imageUrl && <img src={product.imageUrl} className={styles.productImage} alt={product.name} />}
{!product.imageUrl && <div className={styles.noProductImage} />}
<span className={styles.productText}>{product.name}</span>
</div>
</button>
);
};
+68
View File
@@ -0,0 +1,68 @@
.splash {
top: 0;
right: 0;
bottom: 0;
left: 0;
display: grid;
place-items: center;
text-align: center;
}
.overlay {
position: fixed;
}
.wrap {
position: relative;
}
.sun {
width: clamp(100px, 65vw, 600px);
height: clamp(100px, 65vw, 600px);
margin: 0 auto;
background: linear-gradient(var(--sun) 50%, var(--sun-end) 90%);
border-radius: 50% 50%;
box-shadow: 0 0 4px var(--sun), 0 0 11px var(--sun), 0 0 19px var(--sun), 0 0 30px var(--sun);
}
.brand {
position: relative;
left: -0.6rem;
padding-top: 3rem;
color: var(--brand);
font-family: Rocket Rinder, monospace;
font-size: clamp(20px, 20vw, 200px);
font-style: italic;
letter-spacing: 1rem;
line-height: 1;
text-shadow: 0 0 4px rgba(243, 33, 61, 0.8), 0 0 11px rgba(243, 33, 61, 0.8), 0 0 19px rgba(243, 33, 61, 0.8);
}
.subtitle {
position: relative;
bottom: 0.5rem;
left: 0rem;
animation: flicker 1.5s infinite alternate;
color: var(--interlace);
font-family: Rocket Clouds DEMO, serif;
font-size: clamp(20px, 5vw, 50px);
font-weight: bold;
line-height: 1;
transform: rotate(-6deg);
}
@keyframes flicker {
0%,
18%,
22%,
25%,
53%,
57%,
100% {
text-shadow: 0 0 4px rgba(121, 152, 238, 0.8), 0 0 11px rgba(121, 152, 238, 0.8), 0 0 19px rgba(121, 152, 238, 0.8);
}
20%,
24%,
55% {
text-shadow: none;
}
}
+19
View File
@@ -0,0 +1,19 @@
import clsx from "clsx";
import React from "react";
import styles from "./Splash.module.css";
interface SplashProps {
brandText?: string;
noOverlay?: boolean;
}
export const Splash = ({ brandText = "BBS", noOverlay = false }: SplashProps): JSX.Element => (
<div className={clsx(styles.splash, { [styles.overlay]: !noOverlay })}>
<div className={styles.wrap}>
<div className={styles.sun}></div>
<div className={styles.brand}>{brandText}</div>
<div className={styles.subtitle}>Biceps Bar System</div>
</div>
</div>
);
+23
View File
@@ -2,4 +2,27 @@
position: absolute;
bottom: 32px;
left: 32px;
padding: 0.6rem 1.2rem;
border-radius: 4px;
opacity: 0;
pointer-events: none;
transition: opacity 0.1s ease-in-out;
}
.visible {
opacity: 1;
pointer-events: initial;
}
.primary {
background: linear-gradient(var(--sun) 30%, var(--sun-end) 100%);
box-shadow: 0 0 4px var(--sun), 0 0 11px var(--sun), 0 0 19px var(--sun);
}
.danger {
background: var(--brand);
box-shadow: 0 0 4px rgba(243, 33, 61, 0.8), 0 0 11px rgba(243, 33, 61, 0.8), 0 0 19px rgba(243, 33, 61, 0.8);
}
.close {
display: none;
}
+7 -30
View File
@@ -1,41 +1,18 @@
import { Toast as BSToast } from "bootstrap";
import clsx from "clsx";
import React, { useEffect, useRef } from "react";
import React from "react";
import styles from "./Toast.module.css";
import { useToast } from "./ToastProvider";
export const Toast: React.FC = () => {
const ref = useRef<HTMLInputElement>(null);
const { visible, showToast, message, background } = useToast();
export const Toast = (): JSX.Element => {
const { showToast, message, background, visible } = useToast();
const backgroundColor = `bg-${background}`;
const classes = clsx("toast align-items-center text-white bg-primary border-0", backgroundColor, styles.toast);
useEffect(() => {
if (!ref.current) return;
const bsToast = BSToast.getInstance(ref.current) || new BSToast(ref.current);
if (visible) {
bsToast?.show();
} else if (!visible) {
bsToast?.hide();
}
return () => bsToast?.hide();
}, [visible]);
const classes = clsx([styles[background], styles.toast], { [styles.visible]: visible });
return (
<div ref={ref} className={classes} role="alert" aria-live="assertive" aria-atomic="true">
<div className="d-flex">
<div className="toast-body">{message}</div>
<button
type="button"
className="btn-close btn-close-white me-2 m-auto"
data-bs-dismiss="toast"
aria-label="Close"
onClick={() => showToast(message)}
/>
</div>
<div className={classes} role="alert" aria-live="assertive" aria-atomic="true">
<div>{message}</div>
<button type="button" aria-label="Close" onClick={() => showToast(message)} className={styles.close} />
</div>
);
};
+10 -8
View File
@@ -1,21 +1,24 @@
import React, { ReactNode, useContext, useState } from "react";
type Background = "primary" | "danger";
interface ToastProps {
visible: boolean;
message: string;
showToast: (message: string, background?: string) => void;
showToast: (message: string, background?: Background) => void;
hide: () => void;
background: string;
background: Background;
}
const defaultState = {
// FIXME: revert
visible: false,
message: "",
// eslint-disable-next-line @typescript-eslint/no-empty-function
showToast: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
hide: () => {},
background: "primary",
background: "primary" as Background,
};
const ToastContext = React.createContext<ToastProps>(defaultState);
@@ -23,19 +26,18 @@ const ToastContext = React.createContext<ToastProps>(defaultState);
type ToastProviderProps = {
children: ReactNode;
};
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
export const ToastProvider = ({ children }: ToastProviderProps): JSX.Element => {
const [visible, setVisible] = useState(defaultState.visible);
const [message, setMessage] = useState("");
const [background, setBackground] = useState("");
const [message, setMessage] = useState(defaultState.message);
const [background, setBackground] = useState(defaultState.background);
const showToast = (newMessage: string, background = "primary") => {
const showToast = (newMessage: string, background: Background = "primary") => {
setMessage(newMessage);
setBackground(background);
if (!visible) {
setVisible(true);
setTimeout(() => {
setVisible(false);
setMessage("");
}, 3000);
}
};
@@ -1,34 +0,0 @@
.user {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px;
border: 3px solid transparent;
background: transparent;
border-radius: 8px;
transition: background-color 0.1s ease-out, border 0.1s ease-out;
}
.user:hover {
border-color: rgba(25, 135, 84, 0.6);
background: rgba(0, 0, 0, 0.1);
}
.active {
border-color: rgba(25, 135, 84, 0.6);
background: rgba(25, 135, 84, 0.2);
}
.active:hover {
background: rgba(25, 135, 84, 0.2);
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50px;
}
.avatar svg {
width: 50px;
height: 50px;
color: #ccc;
}
.name {
font-weight: bold;
}
-31
View File
@@ -1,31 +0,0 @@
import { ReactComponent as PersonFill } from "bootstrap-icons/icons/person-fill.svg";
import clsx from "clsx";
import React, { MouseEventHandler } from "react";
import { AllUsersQuery } from "../types";
import styles from "./UserCard.module.css";
type UserCardProps = {
user: AllUsersQuery["allUsers"][0] | null;
onUser: MouseEventHandler;
active: boolean;
};
/** If user is null, then it's the special external user */
export const UserCard: React.FC<UserCardProps> = ({ user, onUser, active = false }) => {
const classes = clsx(styles.user, { [styles.active]: active });
return (
<button className={classes} onClick={onUser} type="button">
{user?.profile?.image && (
<img src={user.profile?.image} alt={`Profile for ${user.firstName}`} className={styles.avatar} />
)}
{!user?.profile?.image && (
<span className={styles.avatar}>
<PersonFill />
</span>
)}
<span className={styles.name}>{user?.firstName || "Ekstern"}</span>
<span className={styles.balance}>{user?.profile?.balance || ""}</span>
</button>
);
};
@@ -0,0 +1,38 @@
.wireframe {
position: absolute;
bottom: calc(22% - 512px);
left: calc(50% - 512px);
width: 1024px;
height: 1024px;
display: grid;
grid-template-columns: repeat(8, 128px);
animation-iteration-count: infinite;
animation-name: isometric-rotate;
animation-duration: 10s;
animation-timing-function: linear;
pointer-events: none;
z-index: -1;
}
.cell {
border-bottom: 2px solid var(--sun);
border-right: 2px solid var(--sun);
}
.cell:nth-child(8n) {
border-right: none;
}
.cell:nth-child(n+57) {
border-bottom: none;
}
@keyframes isometric-rotate {
0% {
transform: rotateX(70deg) rotateY(0deg) rotateZ(-45deg);
}
100% {
transform: rotateX(70deg) rotateY(0deg) rotateZ(-405deg);
}
}
+11
View File
@@ -0,0 +1,11 @@
import React from "react";
import styles from "./Wireframe.module.css";
export const Wireframe = () => (
<div className={styles.wireframe}>
{[...Array(64)].map((_, idx) => (
<div key={idx} className={styles.cell} />
))}
</div>
);
@@ -0,0 +1,5 @@
.form {
display: grid;
max-width: 500px;
gap: 1rem;
}
+6 -5
View File
@@ -3,6 +3,7 @@ import React from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { CreateUserDocument } from "../../types";
import styles from "./UserCreate.module.css";
type FormValues = {
firstName: string;
@@ -30,20 +31,20 @@ export const UserCreate = () => {
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<h2>Opprett ny</h2>
<form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
<label>Fornavn</label>
<input {...register("firstName", { required: true })} />
{errors.firstName && <span>This field is required</span>}
<br />
<label>Etternavn</label>
<input {...register("lastName", { required: true })} />
{errors.lastName && <span>This field is required</span>}
<br />
<label>E-post</label>
<input type="email" {...register("email", { required: true })} />
{errors.email && <span>This field is required</span>}
<br />
<input type="submit" disabled={creating} />
<input type="submit" disabled={creating} className="btn-primary" />
</form>
</div>
);
};
@@ -1,13 +1,17 @@
.userList {
display: grid;
height: 400px;
grid-template-columns: repeat(12, 100px);
height: 800px;
padding-top: 2rem;
grid-template-columns: repeat(5, 220px);
overflow-y: auto;
row-gap: 16px;
column-gap: 16px;
row-gap: 24px;
}
.amountSection {
margin-top: 32px;
}
.checkout {
margin-top: 32px;
}
+5 -5
View File
@@ -1,10 +1,10 @@
import React from "react";
import { UserCard } from "../../components/UserCard";
import { UserCard } from "../users/UserCard";
import styles from "./Deposit.module.css";
import { useDeposit } from "./useDeposit";
export const Deposit: React.FC = () => {
export const Deposit = (): JSX.Element => {
const {
loading,
selectedUser,
@@ -33,7 +33,7 @@ export const Deposit: React.FC = () => {
placeholder="Filtrér på navn"
/>
{userQuery !== "" && (
<button className="btn btn-link" type="button" onClick={() => setUserQuery("")}>
<button type="button" onClick={() => setUserQuery("")}>
Vis alle
</button>
)}
@@ -55,10 +55,10 @@ export const Deposit: React.FC = () => {
<input name="amount" value={amount} onChange={(event) => setAmount(event.target.value)} placeholder="0" />
</section>
<section className={styles.checkout}>
<button type="button" className="btn btn-primary btn-lg" onClick={onSubmit}>
<button type="button" onClick={onSubmit} className="primary">
Sett inn
</button>{" "}
<button type="button" className="btn btn-outline-secondary btn-lg" onClick={reset}>
<button type="button" onClick={reset}>
Start på nytt
</button>
</section>
+27
View File
@@ -3,3 +3,30 @@
gap: 1rem;
grid-template-columns: repeat(2, 1fr);
}
.disabled,
.disabled a:link {
color: rgba(121, 152, 238, 0.5);
}
.pagination {
display: flex;
align-items: flex-end;
padding: 0;
margin: 0;
margin-top: 1.5rem;
gap: 0 1rem;
list-style: none;
}
.pagination a {
padding: 0.4rem 0.8rem;
border: 1px solid var(--border);
margin-top: 1rem;
border-radius: 4px;
}
.pagination a:hover {
color: white;
}
.disabled a:hover {
color: rgba(121, 152, 238, 0.5);
}
+16 -20
View File
@@ -34,7 +34,7 @@ export const Log = () => {
<div className={styles.log}>
<div>
<h2>Ordre</h2>
<table className="table">
<table>
<thead>
<tr>
<th scope="col">Når</th>
@@ -61,29 +61,27 @@ export const Log = () => {
</tbody>
</table>
<nav aria-label="Order pagination">
<ul className="pagination">
<li className={clsx("page-item", { disabled: orderOffset <= 0 })}>
<ul className={styles.pagination}>
<li className={clsx({ [styles.disabled]: orderOffset <= 0 })}>
<a
href="#"
className="page-link"
onClick={(e) => {
e.preventDefault();
setParams({ orderOffset: orderOffset - PAGE_SIZE });
setParams({ orderOffset: Math.max(0, orderOffset - PAGE_SIZE) });
}}
>
Previous
⬅
</a>
</li>
<li className="page-item">
<li>
<a
href="#"
className="page-link"
onClick={(e) => {
e.preventDefault();
setParams({ orderOffset: orderOffset + PAGE_SIZE });
setParams({ orderOffset: Math.max(0, orderOffset + PAGE_SIZE) });
}}
>
Next
➡
</a>
</li>
</ul>
@@ -91,7 +89,7 @@ export const Log = () => {
</div>
<div>
<h2>Transaksjoner</h2>
<table className="table">
<table>
<thead>
<tr>
<th scope="col">Når</th>
@@ -112,29 +110,27 @@ export const Log = () => {
</tbody>
</table>
<nav aria-label="Transaction pagination">
<ul className="pagination">
<li className={clsx("page-item", { disabled: transactionOffset <= 0 })}>
<ul className={styles.pagination}>
<li className={clsx({ [styles.disabled]: transactionOffset <= 0 })}>
<a
href="#"
className="page-link"
onClick={(e) => {
e.preventDefault();
setParams({ transactionOffset: transactionOffset - PAGE_SIZE });
setParams({ transactionOffset: Math.max(0, transactionOffset - PAGE_SIZE) });
}}
>
Previous
⬅
</a>
</li>
<li className="page-item">
<li>
<a
href="#"
className="page-link"
onClick={(e) => {
e.preventDefault();
setParams({ transactionOffset: transactionOffset + PAGE_SIZE });
setParams({ transactionOffset: Math.max(transactionOffset + PAGE_SIZE) });
}}
>
Next
➡
</a>
</li>
</ul>
@@ -0,0 +1,51 @@
.product {
display: flex;
border: none;
background: rgb(35, 15, 41);
border-radius: 4px;
transition: background-color 0.1s ease-out, border 0.1s ease-out;
position: relative;
align-items: flex-start;
padding: 1rem;
}
.product:hover,
.active.product {
background-color: var(--background);
box-shadow: 0 0 4px var(--background), 0 0 11px var(--background), 0 0 19px var(--background);
}
.productText {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 1.2rem;
text-shadow: none;
color: white;
}
.amount {
position: absolute;
z-index: 1;
top: 0;
bottom: 0;
right: 0;
padding: 12px 12px 8px 8px;
border-left: 1px solid transparent;
color: transparent;
font-size: 2rem;
font-weight: bold;
line-height: 1;
background-color: transparent;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
display: none;
}
.active .amount {
display: block;
color: var(--background);
text-shadow: none;
border-left-color: rgb(35, 15, 41);
background-color: var(--sun-end);
}
@@ -0,0 +1,22 @@
import clsx from "clsx";
import React, { MouseEventHandler } from "react";
import { Product } from "../../types";
import styles from "./ProductCard.module.css";
type ProductCardProps = {
product: Omit<Product, "userCounts">;
onProduct: MouseEventHandler;
active: boolean;
amount: number;
};
export const ProductCard = ({ product, onProduct, amount, active = false }: ProductCardProps): JSX.Element => {
const classes = clsx(styles.product, { [styles.active]: active });
return (
<button className={classes} type="button" onClick={onProduct} title={product.name}>
<span className={styles.amount}>{amount}</span>
<span className={styles.productText}>{product.name}</span>
</button>
);
};
@@ -1,14 +1,21 @@
.userList {
display: grid;
grid-template-columns: repeat(12, 128px);
row-gap: 16px;
grid-template-columns: repeat(5, 220px);
column-gap: 16px;
row-gap: 24px;
}
.showAll {
margin-top: 1rem;
}
.productList {
display: grid;
grid-template-columns: repeat(8, 150px);
row-gap: 16px;
grid-template-columns: repeat(5, 220px);
column-gap: 16px;
row-gap: 24px;
}
.productListSection {
margin-top: 2rem;
}
@@ -22,7 +29,6 @@
min-width: 200px;
padding: 4px 8px;
margin-right: 8px;
background: black;
color: white;
font-size: 1.8rem;
}
+6 -6
View File
@@ -1,14 +1,14 @@
import React from "react";
import { ProductCard } from "../../components/ProductCard";
import { UserCard } from "../../components/UserCard";
import { OrderLineInput, Product } from "../../types";
import { UserCard } from "../users/UserCard";
import { ProductCard } from "./ProductCard";
import styles from "./Register.module.css";
import { useRegister } from "./useRegister";
const USER_EXTERNAL = "external";
export const Register: React.FC = () => {
export const Register = (): JSX.Element => {
const {
users,
products,
@@ -48,7 +48,7 @@ export const Register: React.FC = () => {
/>
))}
</div>
<button type="button" className="btn btn-link" onClick={() => setShowAll(!showAll)}>
<button type="button" onClick={() => setShowAll(!showAll)} className={styles.showAll}>
{showAll ? "Skjul gamliser" : "Vis alle"}
</button>
</section>
@@ -70,10 +70,10 @@ export const Register: React.FC = () => {
<span className={styles.total}>
Totalt: <span>{total}</span>
</span>
<button type="button" className="btn btn-primary btn-lg" onClick={onSubmit}>
<button type="button" onClick={onSubmit} className="primary">
Kjøp
</button>{" "}
<button type="button" className="btn btn-outline-secondary btn-lg" onClick={reset}>
<button type="button" onClick={reset}>
Start på nytt
</button>
</section>
@@ -4,6 +4,26 @@
gap: 1rem;
grid-template-columns: repeat(3, 1fr);
}
.statsNav {
margin-bottom: 1rem;
}
.statsNav ul {
display: flex;
padding: 0;
margin: 0;
gap: 0 1rem;
list-style: none;
}
.statsNav a {
padding: 0.4rem 0.8rem;
border: 1px solid transparent;
}
.active.active {
color: white;
}
.statsNav a:hover {
border: 1px solid var(--border);
border-radius: 4px;
}
+11 -9
View File
@@ -1,34 +1,36 @@
import clsx from "clsx";
import React from "react";
import { NavLink } from "react-router-dom";
import styles from "./Stats.module.css";
const navLinkStyles = ({ isActive }: { isActive: boolean }) => (isActive ? "nav-link active" : "nav-link");
const navLinkStyles = ({ isActive }: { isActive: boolean }) => clsx(styles.navLink, { [styles.active]: isActive });
export const StatsNav = () => (
<div className={styles.statsNav}>
<ul className="nav nav-tabs">
<li className="nav-item">
<nav className={styles.statsNav}>
<ul>
<li>
<NavLink className={navLinkStyles} to="/stats" end>
Oversikt
</NavLink>
</li>
<li className="nav-item">
<li>
<NavLink className={navLinkStyles} to="/stats/products">
Produkter
</NavLink>
</li>
<li className="nav-item">
<li>
<NavLink className={navLinkStyles} to="/stats/orders">
Ordre
</NavLink>
</li>
<li className="nav-item">
<NavLink className="nav-link disabled" to="/stats/users" title="TODO">
<li>
<NavLink className="disabled" to="/stats/users" title="TODO">
Folk
</NavLink>
</li>
</ul>
</div>
</nav>
);
export const StatsView = ({ children, loading }: { children: React.ReactNode; loading: boolean }) => (
@@ -0,0 +1,61 @@
.user {
display: flex;
align-items: center;
padding: 0;
border: none;
border-left: 4px solid transparent;
background: rgb(35, 15, 41);
border-radius: 4px;
color: var(--text);
transition: background-color 0.1s ease-out, border 0.1s ease-out;
max-height: 82px;
}
.user:hover {
cursor: pointer;
}
.user:hover,
.active.user {
background-color: var(--background);
box-shadow: 0 0 4px var(--background), 0 0 11px var(--background), 0 0 19px var(--background);
}
.active.user {
border-left: 4px solid var(--sun)
}
.avatar {
margin-left: 1rem;
width: 50px;
height: 50px;
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}
.avatarWrap {
filter: drop-shadow(0 0 4px var(--background)) drop-shadow(0 0 11px var(--background))
drop-shadow(0 0 19px var(--background));
}
.noAvatar {
margin-left: 1rem;
display: block;
width: 50px;
height: 50px;
background-color: black;
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
content: "";
}
.meta {
display: flex;
padding: 1rem;
flex-direction: column;
align-items: flex-start;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 1.2rem;
text-shadow: none;
}
.balance {
color: var(--sun)
}
+34
View File
@@ -0,0 +1,34 @@
import clsx from "clsx";
import React, { MouseEventHandler } from "react";
import { AllUsersQuery } from "../../types";
import styles from "./UserCard.module.css";
type UserCardProps = {
user: AllUsersQuery["allUsers"][0] | null;
onUser: MouseEventHandler;
active: boolean;
};
/** If user is null, then it's the special external user */
export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.Element => {
const classes = clsx(styles.user, { [styles.active]: active });
return (
<button className={classes} onClick={onUser} type="button" title={user?.firstName || "Ekstern"}>
<div className={styles.avatarWrap}>
{user?.profile?.image && (
<img src={user.profile?.image} alt={`Profile for ${user.firstName}`} className={styles.avatar} />
)}
{!user?.profile?.image && (
<span className={styles.avatar}>
<span className={styles.noAvatar} />
</span>
)}
</div>
<div className={styles.meta}>
{user?.firstName || "Ekstern"}
<span className={styles.balance}>{user?.profile?.balance || ""}</span>
</div>
</button>
);
};
+10 -10
View File
@@ -30,26 +30,26 @@ export const UserDetail = () => {
<h1>
{profile.image && <img src={profile.image} alt={`${user.firstName}s profile picture`} />} {user.firstName}
</h1>
<div className="card">
<div className="card-header">Stats</div>
<ul className="list-group list-group-flush">
<li className="list-group-item">
<div>
<div>Stats</div>
<ul>
<li>
Saldo: <strong>{profile.balance.toLocaleString("no")} kr</strong>
</li>
<li className="list-group-item">
<li>
Siste kjøp: <strong>{formatTime(profile.lastPurchaseDate)}</strong>
</li>
<li className="list-group-item">
<li>
Kjøp totalt: <strong>{profile.orderSumTotal.toLocaleString("no")} kr</strong>
</li>
</ul>
</div>
<br />
<div className="card">
<div className="card-header">Top 5 måneder</div>
<ul className="list-group list-group-flush">
<div>
<div>Top 5 måneder</div>
<ul>
{profile.topMonths.map((topMonth) => (
<li key={topMonth.period} className="list-group-item">
<li key={topMonth.period}>
{formatMonth(topMonth.period)}: <strong>{topMonth.count}</strong> kjøp på totalt{" "}
<strong>{topMonth.sum} kr</strong>
</li>
+3 -2
View File
@@ -15,7 +15,8 @@ export const UserList = () => {
const users = showAll ? allUsers : allUsers.filter((u) => activeUserFilter(u));
return (
<div>
<button type="button" className="btn btn-link" onClick={() => setShowAll(!showAll)}>
<h2>Folk</h2>
<button type="button" onClick={() => setShowAll(!showAll)}>
{showAll ? "Skjul gamliser" : "Vis alle"}
</button>
<ul>
@@ -28,7 +29,7 @@ export const UserList = () => {
))}
</ul>
<br />
<Link to="/users/create" className="btn btn-primary">
<Link to="/users/create" className="btn-primary">
Opprett ny
</Link>
</div>
+145
View File
@@ -0,0 +1,145 @@
/* Custom props */
:root {
--brand: rgb(243, 33, 61);
--background: rgb(70, 30, 82);
--text: rgb(255, 255, 255);
--sun: rgb(221, 81, 127);
--sun-end: rgb(230, 142, 54);
--border: rgb(85, 109, 200);
--interlace: rgb(121, 152, 238);
--font-family: "IBM Plex Mono", monospace;
}
/* Resets */
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
/* Body and interitable styles */
body {
background: linear-gradient(rgba(0, 0, 0, 0.8) 60%, var(--background) 100%);
background-color: var(--background);
color: var(--text);
font-family: var(--font-family);
text-shadow: 0 0 4px currentColor, 0 0 11px currentColor, 0 0 19px currentColor;
}
/* Links */
a:link {
color: var(--interlace);
text-decoration: none;
}
a:visited {
color: var(--interlace);
}
a:active {
color: var(--interlace);
}
a:hover {
color: var(--text);
}
h1,
h2,
h3,
h4,
h5 {
color: var(--sun);
}
a.btn-primary {
padding: 0.4rem 1rem;
border: 1px solid transparent;
background: linear-gradient(var(--sun) 30%, var(--sun-end) 100%);
border-radius: 4px;
box-shadow: 0 0 4px var(--sun), 0 0 11px var(--sun), 0 0 19px var(--sun);
color: var(--text);
}
/* Buttons */
button {
padding: 0.4rem 1rem;
border: 1px solid var(--border);
background: transparent;
border-radius: 4px;
color: var(--border);
font-family: var(--font-family);
text-shadow: 0 0 4px currentColor, 0 0 11px currentColor, 0 0 19px currentColor;
}
button:hover {
color: var(--text);
cursor: pointer;
}
button.primary {
border: 1px solid transparent;
background: linear-gradient(var(--sun) 30%, var(--sun-end) 100%);
box-shadow: 0 0 4px var(--sun), 0 0 11px var(--sun), 0 0 19px var(--sun);
color: var(--text);
}
/* Inputs */
input {
padding: 0.4rem 0.4rem;
border: 1px solid var(--border);
background: transparent;
border-radius: 4px;
color: var(--text);
font-family: var(--font-family);
font-size: 1.5rem;
text-shadow: 0 0 4px currentColor, 0 0 11px currentColor, 0 0 19px currentColor;
}
input:focus {
background: rgba(85, 109, 200, 0.1);
outline: none;
}
input.btn-primary {
padding: 0.4rem 1rem;
border: 1px solid transparent;
background: linear-gradient(var(--sun) 30%, var(--sun-end) 100%);
border-radius: 4px;
box-shadow: 0 0 4px var(--sun), 0 0 11px var(--sun), 0 0 19px var(--sun);
color: var(--text);
}
::placeholder {
color: var(--border);
}
/* Tables */
table {
border: 1px solid var(--border);
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0.5rem 0.8rem;
border: 1px solid var(--border);
}
/* Fonts*/
@font-face {
font-family: "Rocket Clouds DEMO";
font-style: normal;
font-weight: normal;
src: url("assets/rocket-clouds.ttf") format("truetype");
}
@font-face {
font-family: "Rocket Rinder";
font-style: normal;
font-weight: normal;
src: url("assets/rocket-rinder.ttf") format("truetype");
}
+2
View File
@@ -3,6 +3,8 @@ import ReactDOM from "react-dom";
import App from "./App";
import "./global.css";
ReactDOM.render(
<React.StrictMode>
<App />