Upgrade frontend to Node 26, React 19, Vite 8, Apollo Client 4 and ESLint 10

This commit is contained in:
ponas committed 2026-10-23 02:19:33 +02:00
1 parent 0a65bb2c90
commit 336f6afa5d
44 files changed
+2938 -4595

No files matched your search

+2 -2
View File
@@ -7,7 +7,7 @@ on:
jobs: jobs:
build_frontend: build_frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: node:20-alpine container: node:26-alpine
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Build frontend - name: Build frontend
@@ -18,7 +18,7 @@ jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: nikolaik/python-nodejs:python3.14-nodejs20 container: nikolaik/python-nodejs:python3.14-nodejs26
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
-1
View File
@@ -1 +0,0 @@
src/types.ts
-54
View File
@@ -1,54 +0,0 @@
module.exports = {
env: {
browser: true,
},
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
extends: [
"plugin:prettier/recommended",
"plugin:react/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
],
rules: {
"no-console": "off",
"prefer-destructuring": "off",
"import/no-unresolved": ["error", { ignore: ["\\?react$"] }],
"import/no-extraneous-dependencies": [
"error",
{
devDependencies: ["**/*.test.js", "**/*.spec.js", "**/__tests__/**"],
},
],
"import/prefer-default-export": "off",
"import/order": [
"error",
{
groups: ["builtin", "external", "internal", ["parent", "sibling", "index"]],
"newlines-between": "always",
alphabetize: { order: "asc" },
},
],
"react-hooks/exhaustive-deps": "error",
"react-hooks/rules-of-hooks": "error",
"react/button-has-type": "error",
"react/jsx-filename-extension": ["warn", { extensions: [".js", ".jsx", ".ts", ".tsx"] }],
"react/jsx-uses-react": "off",
"react/prefer-stateless-function": "warn",
"react/prop-types": "off",
"react/react-in-jsx-scope": "off",
"react/sort-comp": "off",
},
plugins: ["prettier", "react", "react-hooks"],
settings: {
react: {
version: "detect",
},
},
};
+1 -1
View File
@@ -1 +1 @@
v20 v26
+54
View File
@@ -0,0 +1,54 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import importPlugin from "eslint-plugin-import-x";
import prettier from "eslint-plugin-prettier/recommended";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
globalIgnores(["dist", "src/types.ts"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
prettier,
],
plugins: {
"import-x": importPlugin,
},
languageOptions: {
globals: globals.browser,
},
settings: {
react: {
version: "detect",
},
},
rules: {
"no-console": "off",
"prefer-destructuring": "off",
"import-x/no-extraneous-dependencies": [
"error",
{
devDependencies: ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**"],
},
],
"import-x/prefer-default-export": "off",
"import-x/order": [
"error",
{
groups: ["builtin", "external", "internal", ["parent", "sibling", "index"]],
"newlines-between": "always",
alphabetize: { order: "asc" },
},
],
"react-hooks/exhaustive-deps": "error",
"react-hooks/rules-of-hooks": "error",
},
},
]);
+2701 -4361
View File
File diff suppressed because it is too large. Load diff
+30 -22
View File
@@ -3,8 +3,8 @@
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "tsc && vite build", "build": "tsc -b && vite build",
"check-types": "tsc", "check-types": "tsc -b",
"dev": "vite", "dev": "vite",
"format:js": "eslint --fix src", "format:js": "eslint --fix src",
"format:styles": "prettier --write \"src/**/*.css\"", "format:styles": "prettier --write \"src/**/*.css\"",
@@ -16,45 +16,53 @@
"test": "node --test" "test": "node --test"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.14.1", "@apollo/client": "^4.2.0",
"@nivo/bar": "^0.99.0", "@nivo/bar": "^0.99.0",
"@nivo/line": "^0.99.0", "@nivo/line": "^0.99.0",
"@nivo/pie": "^0.99.0", "@nivo/pie": "^0.99.0",
"clsx": "^1.1.1", "clsx": "^2.1.1",
"date-fns": "^2.28.0", "date-fns": "^4.3.0",
"graphql": "^16.3.0", "graphql": "^16.14.0",
"react": "^18.3.1", "react": "^19.2.6",
"react-dom": "^18.3.1", "react-dom": "^19.2.6",
"react-hook-form": "^7.30.0", "react-hook-form": "^7.76.1",
"react-router-dom": "^7.18.4", "react-router-dom": "^7.18.4",
"use-url-search-params": "^2.5.1" "use-url-search-params": "^2.5.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.29.7",
"@eslint/js": "^10.0.1",
"@graphql-codegen/core": "^6.2.2", "@graphql-codegen/core": "^6.2.2",
"@graphql-codegen/typescript": "^6.1.2",
"@graphql-codegen/typed-document-node": "^7.2.0", "@graphql-codegen/typed-document-node": "^7.2.0",
"@graphql-codegen/typescript": "^6.1.2",
"@graphql-codegen/typescript-operations": "^6.1.10", "@graphql-codegen/typescript-operations": "^6.1.10",
"@graphql-typed-document-node/core": "^3.1.1", "@graphql-typed-document-node/core": "^3.1.1",
"@types/react": "^18.3.28", "@rolldown/plugin-babel": "^0.2.3",
"@types/react-dom": "^18.3.7", "@types/babel__core": "^7.20.5",
"@typescript-eslint/eslint-plugin": "^8.72.0", "@types/node": "^26.6.5",
"@typescript-eslint/parser": "^8.72.0", "@types/react": "^19.2.15",
"eslint": "^8.57.1", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^10.4.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-prettier": "^5.5.7", "eslint-plugin-prettier": "^5.5.7",
"eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-hooks": "^4.4.0", "eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"typescript": "^5.9.3", "typescript": "~6.0.3",
"vite": "^6.4.5", "typescript-eslint": "^8.60.0",
"vite": "^8.0.14",
"vite-plugin-svgr": "^5.2.0", "vite-plugin-svgr": "^5.2.0",
"yaml": "^2.8.1" "yaml": "^2.8.1"
}, },
"engines": { "engines": {
"node": "20" "node": "26"
}, },
"overrides": { "overrides": {
"@whatwg-node/promise-helpers": "^1.3.2" "@whatwg-node/promise-helpers": "^1.3.2"
} },
"type": "module"
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import { ApolloProvider } from "@apollo/client"; import { ApolloProvider } from "@apollo/client/react";
import React from "react"; import React from "react";
import { BrowserRouter as Router } from "react-router-dom"; import { BrowserRouter as Router } from "react-router-dom";
@@ -11,7 +11,7 @@ import { Toast } from "./components/Toast";
import { ToastProvider } from "./components/ToastProvider"; import { ToastProvider } from "./components/ToastProvider";
import { Wireframe } from "./components/Wireframe"; import { Wireframe } from "./components/Wireframe";
const App = (): JSX.Element => ( const App = (): React.JSX.Element => (
<ApolloProvider client={client}> <ApolloProvider client={client}>
<Router> <Router>
<ToastProvider> <ToastProvider>
+1 -1
View File
@@ -16,7 +16,7 @@ const UserStats = React.lazy(() => import("./features/stats/UserStats"));
const UserDetail = React.lazy(() => import("./features/users/UserDetail")); const UserDetail = React.lazy(() => import("./features/users/UserDetail"));
const UserList = React.lazy(() => import("./features/users/UserList")); const UserList = React.lazy(() => import("./features/users/UserList"));
export const Routes = (): JSX.Element => ( export const Routes = (): React.JSX.Element => (
<React.Suspense fallback={<div>Loading...</div>}> <React.Suspense fallback={<div>Loading...</div>}>
<ReactRouterRoutes> <ReactRouterRoutes>
<Route path="/deposit" element={<Deposit />} /> <Route path="/deposit" element={<Deposit />} />
+2 -2
View File
@@ -1,7 +1,7 @@
import { ApolloClient, InMemoryCache } from "@apollo/client"; import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
const client = new ApolloClient({ const client = new ApolloClient({
uri: import.meta.env.DEV ? "/graphql/" : `${import.meta.env.VITE_API_URL}/graphql/`, link: new HttpLink({ uri: import.meta.env.DEV ? "/graphql/" : `${import.meta.env.VITE_API_URL}/graphql/` }),
cache: new InMemoryCache(), cache: new InMemoryCache(),
}); });
+2 -2
View File
@@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import Brick from "../assets/brick.svg?react";
import styles from "./Footer.module.css"; import styles from "./Footer.module.css";
import Brick from "../assets/brick.svg?react";
export const Footer = (): JSX.Element => { export const Footer = (): React.JSX.Element => {
return ( return (
<footer className={styles.footer}> <footer className={styles.footer}>
Made with <span className={styles.heart}>❤</span> by{" "} Made with <span className={styles.heart}>❤</span> by{" "}
+5 -5
View File
@@ -1,5 +1,5 @@
import { ApolloError } from "@apollo/client"; import type { ErrorLike } from "@apollo/client";
import React from "react"; import { CombinedGraphQLErrors } from "@apollo/client/errors";
import styles from "./GQLError.module.css"; import styles from "./GQLError.module.css";
@@ -11,9 +11,9 @@ const formatModel = (text: string) => {
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
}; };
export const GQLError = ({ error }: { error: ApolloError }) => { export const GQLError = ({ error }: { error: ErrorLike }) => {
if (error?.graphQLErrors?.length == 1) { if (error instanceof CombinedGraphQLErrors && error.errors.length === 1) {
const single = error?.graphQLErrors[0]; const single = error.errors[0];
if (single?.extensions?.code === "NOT_FOUND") { if (single?.extensions?.code === "NOT_FOUND") {
const modelPretty = formatModel(single?.extensions?.model as string); const modelPretty = formatModel(single?.extensions?.model as string);
return <div className={styles.errorMessage}>{`${modelPretty} not found`}</div>; return <div className={styles.errorMessage}>{`${modelPretty} not found`}</div>;
+1 -1
View File
@@ -8,7 +8,7 @@ const ADMIN_URL = `${import.meta.env.VITE_API_URL}/admin/`;
const navLinkStyles = ({ isActive }: { isActive: boolean }) => clsx(styles.navLink, { [styles.active]: isActive }); const navLinkStyles = ({ isActive }: { isActive: boolean }) => clsx(styles.navLink, { [styles.active]: isActive });
export const Header = (): JSX.Element => { export const Header = (): React.JSX.Element => {
return ( return (
<header className={styles.header}> <header className={styles.header}>
<nav className={styles.nav}> <nav className={styles.nav}>
+1 -1
View File
@@ -8,7 +8,7 @@ interface SplashProps {
noOverlay?: boolean; noOverlay?: boolean;
} }
const Splash = ({ brandText = "BBS", noOverlay = false }: SplashProps): JSX.Element => ( const Splash = ({ brandText = "BBS", noOverlay = false }: SplashProps): React.JSX.Element => (
<div className={clsx(styles.splash, { [styles.overlay]: !noOverlay })}> <div className={clsx(styles.splash, { [styles.overlay]: !noOverlay })}>
<div className={styles.wrap}> <div className={styles.wrap}>
<div className={styles.sun}></div> <div className={styles.sun}></div>
+2 -2
View File
@@ -2,9 +2,9 @@ import clsx from "clsx";
import React from "react"; import React from "react";
import styles from "./Toast.module.css"; import styles from "./Toast.module.css";
import { useToast } from "./ToastProvider"; import { useToast } from "../hooks/useToast";
export const Toast = (): JSX.Element => { export const Toast = (): React.JSX.Element => {
const { showToast, message, background, visible } = useToast(); const { showToast, message, background, visible } = useToast();
const classes = clsx([styles[background], styles.toast], { [styles.visible]: visible }); const classes = clsx([styles[background], styles.toast], { [styles.visible]: visible });
+9 -29
View File
@@ -1,35 +1,17 @@
import React, { ReactNode, useContext, useState } from "react"; import React, { useState } from "react";
import type { ReactNode } from "react";
type Background = "primary" | "danger"; import { ToastContext, defaultToastState } from "../contexts/ToastContext";
import type { Background } from "../contexts/ToastContext";
interface ToastProps {
visible: boolean;
message: string;
showToast: (message: string, background?: Background) => void;
hide: () => void;
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" as Background,
};
const ToastContext = React.createContext<ToastProps>(defaultState);
type ToastProviderProps = { type ToastProviderProps = {
children: ReactNode; children: ReactNode;
}; };
export const ToastProvider = ({ children }: ToastProviderProps): JSX.Element => {
const [visible, setVisible] = useState(defaultState.visible); export const ToastProvider = ({ children }: ToastProviderProps): React.JSX.Element => {
const [message, setMessage] = useState(defaultState.message); const [visible, setVisible] = useState(defaultToastState.visible);
const [background, setBackground] = useState(defaultState.background); const [message, setMessage] = useState(defaultToastState.message);
const [background, setBackground] = useState(defaultToastState.background);
const showToast = (newMessage: string, background: Background = "primary") => { const showToast = (newMessage: string, background: Background = "primary") => {
setMessage(newMessage); setMessage(newMessage);
@@ -49,5 +31,3 @@ export const ToastProvider = ({ children }: ToastProviderProps): JSX.Element =>
<ToastContext.Provider value={{ visible, showToast, message, hide, background }}>{children}</ToastContext.Provider> <ToastContext.Provider value={{ visible, showToast, message, hide, background }}>{children}</ToastContext.Provider>
); );
}; };
export const useToast = (): ToastProps => useContext(ToastContext);
-2
View File
@@ -1,5 +1,3 @@
import React from "react";
import styles from "./Wireframe.module.css"; import styles from "./Wireframe.module.css";
export const Wireframe = () => ( export const Wireframe = () => (
+22
View File
@@ -0,0 +1,22 @@
import React from "react";
export type Background = "primary" | "danger";
export interface ToastProps {
visible: boolean;
message: string;
showToast: (message: string, background?: Background) => void;
hide: () => void;
background: Background;
}
export const defaultToastState: ToastProps = {
// FIXME: revert
visible: false,
message: "",
showToast: () => {},
hide: () => {},
background: "primary",
};
export const ToastContext = React.createContext<ToastProps>(defaultToastState);
+2 -3
View File
@@ -1,9 +1,8 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client/react";
import React from "react";
import { SubmitHandler, useForm } from "react-hook-form"; import { SubmitHandler, useForm } from "react-hook-form";
import { CreateUserDocument } from "../../types";
import styles from "./UserCreate.module.css"; import styles from "./UserCreate.module.css";
import { CreateUserDocument } from "../../types";
type FormValues = { type FormValues = {
firstName: string; firstName: string;
+2 -2
View File
@@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import { UserCard } from "../users/UserCard";
import styles from "./Deposit.module.css"; import styles from "./Deposit.module.css";
import { useDeposit } from "./useDeposit"; import { useDeposit } from "./useDeposit";
import { UserCard } from "../users/UserCard";
const Deposit = (): JSX.Element => { const Deposit = (): React.JSX.Element => {
const { const {
loading, loading,
selectedUser, selectedUser,
+3 -3
View File
@@ -1,7 +1,7 @@
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client/react";
import { useState } from "react"; import { useState } from "react";
import { useToast } from "../../components/ToastProvider"; import { useToast } from "../../hooks/useToast";
import { AllUsersDocument, CreateDepositDocument } from "../../types"; import { AllUsersDocument, CreateDepositDocument } from "../../types";
export const useDeposit = () => { export const useDeposit = () => {
@@ -32,7 +32,7 @@ export const useDeposit = () => {
} }
if (res.data?.createDeposit?.__typename === "CreateDepositSuccess") { if (res.data?.createDeposit?.__typename === "CreateDepositSuccess") {
const { transaction } = res.data?.createDeposit; const { transaction } = res.data.createDeposit;
showToast( showToast(
`${transaction.user.firstName} har satt inn ${transaction.amount}. Ny saldo er ${transaction.user.profile?.balance}.`, `${transaction.user.firstName} har satt inn ${transaction.amount}. Ny saldo er ${transaction.user.profile?.balance}.`,
); );
@@ -1,12 +1,12 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client/react";
import React, { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styles from "./Inventory.module.css";
import { GQLError } from "../../components/GQLError"; import { GQLError } from "../../components/GQLError";
import { InventoryPageDocument } from "../../types"; import { InventoryPageDocument } from "../../types";
import { formatCurrency } from "../../utils/currency"; import { formatCurrency } from "../../utils/currency";
import { formatTime } from "../../utils/time"; import { formatTime } from "../../utils/time";
import styles from "./Inventory.module.css";
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
@@ -1,11 +1,11 @@
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client/react";
import React, { useState } from "react"; import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { GQLError } from "../../components/GQLError";
import { useToast } from "../../components/ToastProvider";
import { CreateInventoryTransactionDocument, InventoryProductsDocument } from "../../types";
import styles from "./Inventory.module.css"; import styles from "./Inventory.module.css";
import { GQLError } from "../../components/GQLError";
import { useToast } from "../../hooks/useToast";
import { CreateInventoryTransactionDocument, InventoryProductsDocument } from "../../types";
const InventoryAdd = () => { const InventoryAdd = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -1,11 +1,11 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client/react";
import { format, subDays } from "date-fns"; import { format, subDays } from "date-fns";
import React, { useState } from "react"; import React, { useState } from "react";
import styles from "./Inventory.module.css";
import { GQLError } from "../../components/GQLError"; import { GQLError } from "../../components/GQLError";
import { InventoryReportDocument } from "../../types"; import { InventoryReportDocument } from "../../types";
import { formatCurrency } from "../../utils/currency"; import { formatCurrency } from "../../utils/currency";
import styles from "./Inventory.module.css";
const today = () => format(new Date(), "yyyy-MM-dd"); const today = () => format(new Date(), "yyyy-MM-dd");
const monthAgo = () => format(subDays(new Date(), 30), "yyyy-MM-dd"); const monthAgo = () => format(subDays(new Date(), 30), "yyyy-MM-dd");
+2 -3
View File
@@ -1,11 +1,10 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client/react";
import clsx from "clsx"; import clsx from "clsx";
import React from "react";
import { useUrlSearchParams } from "use-url-search-params"; import { useUrlSearchParams } from "use-url-search-params";
import styles from "./Log.module.css";
import { OrderListDocument, TransactionListDocument } from "../../types"; import { OrderListDocument, TransactionListDocument } from "../../types";
import { formatTime } from "../../utils/time"; import { formatTime } from "../../utils/time";
import styles from "./Log.module.css";
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
@@ -1,8 +1,8 @@
import clsx from "clsx"; import clsx from "clsx";
import React, { MouseEventHandler } from "react"; import React, { MouseEventHandler } from "react";
import { AllProductsQuery } from "../../types";
import styles from "./ProductCard.module.css"; import styles from "./ProductCard.module.css";
import { AllProductsQuery } from "../../types";
type ProductCardProps = { type ProductCardProps = {
product: AllProductsQuery["allProducts"][number]; product: AllProductsQuery["allProducts"][number];
@@ -11,7 +11,7 @@ type ProductCardProps = {
amount: number; amount: number;
}; };
export const ProductCard = ({ product, onProduct, amount, active = false }: ProductCardProps): JSX.Element => { export const ProductCard = ({ product, onProduct, amount, active = false }: ProductCardProps): React.JSX.Element => {
const classes = clsx(styles.product, { [styles.active]: active }); const classes = clsx(styles.product, { [styles.active]: active });
return ( return (
<button className={classes} type="button" onClick={onProduct} title={product.name}> <button className={classes} type="button" onClick={onProduct} title={product.name}>
+3 -3
View File
@@ -1,14 +1,14 @@
import React from "react"; import React from "react";
import { AllProductsQuery, OrderLineInput } from "../../types";
import { UserCard } from "../users/UserCard";
import { ProductCard } from "./ProductCard"; import { ProductCard } from "./ProductCard";
import styles from "./Register.module.css"; import styles from "./Register.module.css";
import { useRegister } from "./useRegister"; import { useRegister } from "./useRegister";
import { AllProductsQuery, OrderLineInput } from "../../types";
import { UserCard } from "../users/UserCard";
const USER_EXTERNAL = "external"; const USER_EXTERNAL = "external";
const Register = (): JSX.Element => { const Register = (): React.JSX.Element => {
const { const {
users, users,
products, products,
@@ -1,7 +1,7 @@
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client/react";
import { useState } from "react"; import { useState } from "react";
import { useToast } from "../../components/ToastProvider"; import { useToast } from "../../hooks/useToast";
import { import {
AllProductsDocument, AllProductsDocument,
AllProductsQuery, AllProductsQuery,
+2 -3
View File
@@ -1,11 +1,10 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client/react";
import { BarDatum, ResponsiveBar } from "@nivo/bar"; import { BarDatum, ResponsiveBar } from "@nivo/bar";
import React from "react";
import { OrderStatsDocument, SchemaOrderStatsByTime } from "../../types";
import styles from "./Stats.module.css"; import styles from "./Stats.module.css";
import { StatsView } from "./StatsView"; import { StatsView } from "./StatsView";
import { chartTheme } from "./chartTheme"; import { chartTheme } from "./chartTheme";
import { OrderStatsDocument, SchemaOrderStatsByTime } from "../../types";
const days = { 1: "Mandag", 2: "Tirsdag", 3: "Onsdag", 4: "Torsdag", 5: "Fredag", 6: "Lørdag", 7: "Søndag" }; const days = { 1: "Mandag", 2: "Tirsdag", 3: "Onsdag", 4: "Torsdag", 5: "Fredag", 6: "Lørdag", 7: "Søndag" };
+2 -3
View File
@@ -1,11 +1,10 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client/react";
import { ResponsivePie } from "@nivo/pie"; import { ResponsivePie } from "@nivo/pie";
import React from "react";
import { AllProductStatsDocument } from "../../types";
import styles from "./Stats.module.css"; import styles from "./Stats.module.css";
import { StatsView } from "./StatsView"; import { StatsView } from "./StatsView";
import { chartTheme } from "./chartTheme"; import { chartTheme } from "./chartTheme";
import { AllProductStatsDocument } from "../../types";
type UserCount = { type UserCount = {
name: string; name: string;
+2 -3
View File
@@ -1,11 +1,10 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client/react";
import { ResponsiveLine } from "@nivo/line"; import { ResponsiveLine } from "@nivo/line";
import React from "react";
import { ProductStatsDocument } from "../../types";
import styles from "./Stats.module.css"; import styles from "./Stats.module.css";
import { StatsView } from "./StatsView"; import { StatsView } from "./StatsView";
import { chartTheme, chartTooltipStyle } from "./chartTheme"; import { chartTheme, chartTooltipStyle } from "./chartTheme";
import { ProductStatsDocument } from "../../types";
const Stats = () => { const Stats = () => {
const { data, loading } = useQuery(ProductStatsDocument); const { data, loading } = useQuery(ProductStatsDocument);
+3 -4
View File
@@ -1,12 +1,11 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client/react";
import { ResponsiveBar } from "@nivo/bar"; import { ResponsiveBar } from "@nivo/bar";
import React from "react";
import { GQLError } from "../../components/GQLError";
import { UserStatsDocument } from "../../types";
import styles from "./Stats.module.css"; import styles from "./Stats.module.css";
import { StatsView } from "./StatsView"; import { StatsView } from "./StatsView";
import { chartTheme } from "./chartTheme"; import { chartTheme } from "./chartTheme";
import { GQLError } from "../../components/GQLError";
import { UserStatsDocument } from "../../types";
const UserStats = () => { const UserStats = () => {
const { data, loading, error } = useQuery(UserStatsDocument); const { data, loading, error } = useQuery(UserStatsDocument);
+2 -2
View File
@@ -1,9 +1,9 @@
import clsx from "clsx"; import clsx from "clsx";
import React, { MouseEventHandler } from "react"; import React, { MouseEventHandler } from "react";
import styles from "./UserCard.module.css";
import { AllUsersQuery } from "../../types"; import { AllUsersQuery } from "../../types";
import { formatCurrency } from "../../utils/currency"; import { formatCurrency } from "../../utils/currency";
import styles from "./UserCard.module.css";
type UserCardProps = { type UserCardProps = {
user: AllUsersQuery["allUsers"][0] | null; user: AllUsersQuery["allUsers"][0] | null;
@@ -12,7 +12,7 @@ type UserCardProps = {
}; };
/** If user is null, then it's the special external user */ /** If user is null, then it's the special external user */
export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.Element => { export const UserCard = ({ user, onUser, active = false }: UserCardProps): React.JSX.Element => {
const classes = clsx(styles.user, { [styles.active]: active }); const classes = clsx(styles.user, { [styles.active]: active });
const userBalanceString = formatCurrency(Number(user?.profile?.balance)); const userBalanceString = formatCurrency(Number(user?.profile?.balance));
return ( return (
+2 -3
View File
@@ -1,13 +1,12 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client/react";
import { ResponsiveLine } from "@nivo/line"; import { ResponsiveLine } from "@nivo/line";
import React from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import styles from "./UserDetail.module.css";
import { GQLError } from "../../components/GQLError"; import { GQLError } from "../../components/GQLError";
import { SchemaUser as User, UserDetailDocument, SchemaUserProfile as UserProfile } from "../../types"; import { SchemaUser as User, UserDetailDocument, SchemaUserProfile as UserProfile } from "../../types";
import { formatMonth, formatTime } from "../../utils/time"; import { formatMonth, formatTime } from "../../utils/time";
import { chartTheme, chartTooltipStyle } from "../stats/chartTheme"; import { chartTheme, chartTooltipStyle } from "../stats/chartTheme";
import styles from "./UserDetail.module.css";
const UserDetail = () => { const UserDetail = () => {
const { userId } = useParams(); const { userId } = useParams();
+2 -2
View File
@@ -1,5 +1,5 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client/react";
import React, { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { AllUsersDocument } from "../../types"; import { AllUsersDocument } from "../../types";
-21
View File
@@ -1,21 +0,0 @@
import { useCallback, useEffect, useRef } from "react";
export function usePrevious<T>(value: T) {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref = useRef<T | undefined>(undefined);
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
export function useToast() {
const show = useCallback(() => {
console.log("showing toast with element from root");
}, []);
return { show };
}
+5
View File
@@ -0,0 +1,5 @@
import { useContext } from "react";
import { ToastContext } from "../contexts/ToastContext";
export const useToast = () => useContext(ToastContext);
+2 -3
View File
@@ -1,13 +1,12 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import { createRoot } from "react-dom/client";
import App from "./App"; import App from "./App";
import "./global.css"; import "./global.css";
ReactDOM.render( createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>,
document.getElementById("root"),
); );
-9
View File
@@ -1,9 +0,0 @@
module.exports = {
extends: ["stylelint-config-recommended", "stylelint-prettier/recommended", "stylelint-config-idiomatic-order"],
plugins: ["stylelint-order"],
rules: {
// Useful?
"no-descending-specificity": null,
},
ignoreFiles: ["**/*.md", "**/*.svg", "**/*.jpg", "**/*.graphql", "**/*.tsx", "**/*.ts"],
};
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Strictness */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+4 -24
View File
@@ -1,27 +1,7 @@
{ {
"compilerOptions": { "files": [],
"target": "ES2021", "references": [
"useDefineForClassFields": true, { "path": "./tsconfig.app.json" },
"lib": [ { "path": "./tsconfig.node.json" }
"DOM",
"DOM.Iterable",
"ES2021"
],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ES2020",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
},
"include": [
"./src",
"node_modules/vite/types/importMeta.d.ts"
] ]
} }
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Strictness */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
@@ -1,10 +1,11 @@
/* eslint-disable import/no-extraneous-dependencies */
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
import babel from "@rolldown/plugin-babel";
import svgr from "vite-plugin-svgr"; import svgr from "vite-plugin-svgr";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [svgr()], plugins: [react(), babel({ presets: [reactCompilerPreset()] }), svgr()],
define: { define: {
__DEV__: process.env.NODE_ENV !== "production", __DEV__: process.env.NODE_ENV !== "production",
}, },
+1 -1
View File
@@ -1,5 +1,5 @@
[tools] [tools]
node = "20" node = "26"
prek = "latest" prek = "latest"
python = "3.14" python = "3.14"
uv = "latest" uv = "latest"