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
+2940 -4597

No files matched your search

+2 -2
View File
@@ -1,4 +1,4 @@
import { ApolloProvider } from "@apollo/client";
import { ApolloProvider } from "@apollo/client/react";
import React from "react";
import { BrowserRouter as Router } from "react-router-dom";
@@ -11,7 +11,7 @@ import { Toast } from "./components/Toast";
import { ToastProvider } from "./components/ToastProvider";
import { Wireframe } from "./components/Wireframe";
const App = (): JSX.Element => (
const App = (): React.JSX.Element => (
<ApolloProvider client={client}>
<Router>
<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 UserList = React.lazy(() => import("./features/users/UserList"));
export const Routes = (): JSX.Element => (
export const Routes = (): React.JSX.Element => (
<React.Suspense fallback={<div>Loading...</div>}>
<ReactRouterRoutes>
<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({
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(),
});
+2 -2
View File
@@ -1,9 +1,9 @@
import React from "react";
import Brick from "../assets/brick.svg?react";
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 (
<footer className={styles.footer}>
Made with <span className={styles.heart}>❤</span> by{" "}
+5 -5
View File
@@ -1,5 +1,5 @@
import { ApolloError } from "@apollo/client";
import React from "react";
import type { ErrorLike } from "@apollo/client";
import { CombinedGraphQLErrors } from "@apollo/client/errors";
import styles from "./GQLError.module.css";
@@ -11,9 +11,9 @@ const formatModel = (text: string) => {
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
};
export const GQLError = ({ error }: { error: ApolloError }) => {
if (error?.graphQLErrors?.length == 1) {
const single = error?.graphQLErrors[0];
export const GQLError = ({ error }: { error: ErrorLike }) => {
if (error instanceof CombinedGraphQLErrors && error.errors.length === 1) {
const single = error.errors[0];
if (single?.extensions?.code === "NOT_FOUND") {
const modelPretty = formatModel(single?.extensions?.model as string);
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 });
export const Header = (): JSX.Element => {
export const Header = (): React.JSX.Element => {
return (
<header className={styles.header}>
<nav className={styles.nav}>
+1 -1
View File
@@ -8,7 +8,7 @@ interface SplashProps {
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={styles.wrap}>
<div className={styles.sun}></div>
+2 -2
View File
@@ -2,9 +2,9 @@ import clsx from "clsx";
import React from "react";
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 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";
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);
import { ToastContext, defaultToastState } from "../contexts/ToastContext";
import type { Background } from "../contexts/ToastContext";
type ToastProviderProps = {
children: ReactNode;
};
export const ToastProvider = ({ children }: ToastProviderProps): JSX.Element => {
const [visible, setVisible] = useState(defaultState.visible);
const [message, setMessage] = useState(defaultState.message);
const [background, setBackground] = useState(defaultState.background);
export const ToastProvider = ({ children }: ToastProviderProps): React.JSX.Element => {
const [visible, setVisible] = useState(defaultToastState.visible);
const [message, setMessage] = useState(defaultToastState.message);
const [background, setBackground] = useState(defaultToastState.background);
const showToast = (newMessage: string, background: Background = "primary") => {
setMessage(newMessage);
@@ -49,5 +31,3 @@ export const ToastProvider = ({ children }: ToastProviderProps): JSX.Element =>
<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";
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 React from "react";
import { useMutation } from "@apollo/client/react";
import { SubmitHandler, useForm } from "react-hook-form";
import { CreateUserDocument } from "../../types";
import styles from "./UserCreate.module.css";
import { CreateUserDocument } from "../../types";
type FormValues = {
firstName: string;
+2 -2
View File
@@ -1,10 +1,10 @@
import React from "react";
import { UserCard } from "../users/UserCard";
import styles from "./Deposit.module.css";
import { useDeposit } from "./useDeposit";
import { UserCard } from "../users/UserCard";
const Deposit = (): JSX.Element => {
const Deposit = (): React.JSX.Element => {
const {
loading,
selectedUser,
+4 -4
View File
@@ -1,7 +1,7 @@
import { useMutation, useQuery } from "@apollo/client";
import { useMutation, useQuery } from "@apollo/client/react";
import { useState } from "react";
import { useToast } from "../../components/ToastProvider";
import { useToast } from "../../hooks/useToast";
import { AllUsersDocument, CreateDepositDocument } from "../../types";
export const useDeposit = () => {
@@ -32,9 +32,9 @@ export const useDeposit = () => {
}
if (res.data?.createDeposit?.__typename === "CreateDepositSuccess") {
const { transaction } = res.data?.createDeposit;
const { transaction } = res.data.createDeposit;
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}.`,
);
reset();
return;
@@ -1,12 +1,12 @@
import { useQuery } from "@apollo/client";
import React, { useState } from "react";
import { useQuery } from "@apollo/client/react";
import { useState } from "react";
import { Link } from "react-router-dom";
import styles from "./Inventory.module.css";
import { GQLError } from "../../components/GQLError";
import { InventoryPageDocument } from "../../types";
import { formatCurrency } from "../../utils/currency";
import { formatTime } from "../../utils/time";
import styles from "./Inventory.module.css";
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 { 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 { GQLError } from "../../components/GQLError";
import { useToast } from "../../hooks/useToast";
import { CreateInventoryTransactionDocument, InventoryProductsDocument } from "../../types";
const InventoryAdd = () => {
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 React, { useState } from "react";
import styles from "./Inventory.module.css";
import { GQLError } from "../../components/GQLError";
import { InventoryReportDocument } from "../../types";
import { formatCurrency } from "../../utils/currency";
import styles from "./Inventory.module.css";
const today = () => format(new Date(), "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 React from "react";
import { useUrlSearchParams } from "use-url-search-params";
import styles from "./Log.module.css";
import { OrderListDocument, TransactionListDocument } from "../../types";
import { formatTime } from "../../utils/time";
import styles from "./Log.module.css";
const PAGE_SIZE = 10;
@@ -1,8 +1,8 @@
import clsx from "clsx";
import React, { MouseEventHandler } from "react";
import { AllProductsQuery } from "../../types";
import styles from "./ProductCard.module.css";
import { AllProductsQuery } from "../../types";
type ProductCardProps = {
product: AllProductsQuery["allProducts"][number];
@@ -11,7 +11,7 @@ type ProductCardProps = {
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 });
return (
<button className={classes} type="button" onClick={onProduct} title={product.name}>
+3 -3
View File
@@ -1,14 +1,14 @@
import React from "react";
import { AllProductsQuery, OrderLineInput } from "../../types";
import { UserCard } from "../users/UserCard";
import { ProductCard } from "./ProductCard";
import styles from "./Register.module.css";
import { useRegister } from "./useRegister";
import { AllProductsQuery, OrderLineInput } from "../../types";
import { UserCard } from "../users/UserCard";
const USER_EXTERNAL = "external";
const Register = (): JSX.Element => {
const Register = (): React.JSX.Element => {
const {
users,
products,
@@ -1,7 +1,7 @@
import { useMutation, useQuery } from "@apollo/client";
import { useMutation, useQuery } from "@apollo/client/react";
import { useState } from "react";
import { useToast } from "../../components/ToastProvider";
import { useToast } from "../../hooks/useToast";
import {
AllProductsDocument,
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 React from "react";
import { OrderStatsDocument, SchemaOrderStatsByTime } from "../../types";
import styles from "./Stats.module.css";
import { StatsView } from "./StatsView";
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" };
+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 React from "react";
import { AllProductStatsDocument } from "../../types";
import styles from "./Stats.module.css";
import { StatsView } from "./StatsView";
import { chartTheme } from "./chartTheme";
import { AllProductStatsDocument } from "../../types";
type UserCount = {
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 React from "react";
import { ProductStatsDocument } from "../../types";
import styles from "./Stats.module.css";
import { StatsView } from "./StatsView";
import { chartTheme, chartTooltipStyle } from "./chartTheme";
import { ProductStatsDocument } from "../../types";
const Stats = () => {
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 React from "react";
import { GQLError } from "../../components/GQLError";
import { UserStatsDocument } from "../../types";
import styles from "./Stats.module.css";
import { StatsView } from "./StatsView";
import { chartTheme } from "./chartTheme";
import { GQLError } from "../../components/GQLError";
import { UserStatsDocument } from "../../types";
const UserStats = () => {
const { data, loading, error } = useQuery(UserStatsDocument);
+2 -2
View File
@@ -1,9 +1,9 @@
import clsx from "clsx";
import React, { MouseEventHandler } from "react";
import styles from "./UserCard.module.css";
import { AllUsersQuery } from "../../types";
import { formatCurrency } from "../../utils/currency";
import styles from "./UserCard.module.css";
type UserCardProps = {
user: AllUsersQuery["allUsers"][0] | null;
@@ -12,7 +12,7 @@ type UserCardProps = {
};
/** 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 userBalanceString = formatCurrency(Number(user?.profile?.balance));
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 React from "react";
import { useParams } from "react-router-dom";
import styles from "./UserDetail.module.css";
import { GQLError } from "../../components/GQLError";
import { SchemaUser as User, UserDetailDocument, SchemaUserProfile as UserProfile } from "../../types";
import { formatMonth, formatTime } from "../../utils/time";
import { chartTheme, chartTooltipStyle } from "../stats/chartTheme";
import styles from "./UserDetail.module.css";
const UserDetail = () => {
const { userId } = useParams();
+2 -2
View File
@@ -1,5 +1,5 @@
import { useQuery } from "@apollo/client";
import React, { useState } from "react";
import { useQuery } from "@apollo/client/react";
import { useState } from "react";
import { Link } from "react-router-dom";
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 ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./global.css";
ReactDOM.render(
createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root"),
);