Create loader

This commit is contained in:
2025-05-09 17:07:48 +02:00
parent 6a557cc060
commit 90b24ec865
7 changed files with 63 additions and 21 deletions

View File

@@ -0,0 +1,34 @@
import { useCallback, useState } from "react";
export type UseLoaderReturnType<T extends any[], R> = {
fn: (...args: T) => Promise<R>,
loading: boolean;
completed: boolean;
error?: unknown;
};
export function useLoader<T extends any[], R>(fn: (...args: T) => Promise<R>, deps?: React.DependencyList) {
const [loading, setLoading] = useState(false);
const [completed, setCompleted] = useState(false);
const [error, setError] = useState<unknown|undefined>(undefined);
const callback = useCallback(async (...args: T) => {
try {
setLoading(true);
const value = await fn(...args);
setCompleted(true);
return value;
} catch(e: unknown) {
setError(e);
} finally {
setLoading(false);
}
}, [setLoading, setCompleted, setError, ...(deps ?? [])]);
return {
fn: callback,
loading,
completed,
error
};
}

View File

@@ -1,8 +1,10 @@
import classNames from "classnames";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
export type LoadFileFormProps = {
profiles: string[];
servers: string[];
loading: boolean;
defaultProfile?: string;
defaultServer?: string;
onSubmit?: (csvFile: File, profile: string, server: string) => Promise<void>;
@@ -14,7 +16,7 @@ type Form = {
server?: string;
};
export default function LoadFileForm({ profiles, servers, defaultProfile, defaultServer, onSubmit }: LoadFileFormProps) {
export default function LoadFileForm({ profiles, servers, defaultProfile, defaultServer, onSubmit, loading }: LoadFileFormProps) {
const fileInput = useRef<HTMLInputElement>(null);
const [formData, setFormData] = useState<Form>({});
@@ -42,7 +44,7 @@ export default function LoadFileForm({ profiles, servers, defaultProfile, defaul
if (!selectedFile || !formData.profile || !formData.server) {
return;
}
onSubmit?.(selectedFile, formData.profile, formData.server);
}, [formData, formData, selectedFile, onSubmit]);
@@ -50,8 +52,8 @@ export default function LoadFileForm({ profiles, servers, defaultProfile, defaul
<form onSubmit={handleSubmit}>
<div className="field">
<label className="label">CSV File</label>
<div className="control">
<div className={`file is-fullwidth ${!!selectedFile ? "has-name" : ""}`}>
<div className="control">
<div className={classNames("file", "is-fullwidth", { "has-name": selectedFile })}>
<label className="file-label">
<input className="file-input" ref={fileInput} type="file" accept=".csv" onChange={(e) => setFormData({...formData, files: e.target.files ?? undefined})} required />
<span className="file-cta">
@@ -95,9 +97,8 @@ export default function LoadFileForm({ profiles, servers, defaultProfile, defaul
<div className="control">
<button
type="submit"
className="button is-link is-fullwidth"
>
Load transactions
className={classNames("button", "is-link", "is-fullwidth", { 'is-loading': loading })}
disabled={loading}>Load transactions
</button>
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import LoadFileForm from './LoadFileForm';
import { fetchConfig, loadTransactions } from '../../services/api.service';
import { useStore } from '../../store/AppStore';
import { useLoader } from '../../hooks/useLoader';
export default function LoadFilePage() {
const { dispatch } = useStore();
@@ -27,9 +28,9 @@ export default function LoadFilePage() {
useEffect(() => {
loadConfig();
}, [loadConfig]);
}, [loadConfig]);
const handleSubmit = useCallback(async (csvFile: File, profile: string, server: string) => {
const { fn: handleSubmit, loading } = useLoader(async (csvFile: File, profile: string, server: string) => {
const data = await loadTransactions(csvFile, profile, server);
dispatch({
@@ -47,7 +48,7 @@ export default function LoadFilePage() {
offset: 1
}
});
}, []);
});
return (
<div className="columns mt-6">
@@ -55,6 +56,7 @@ export default function LoadFilePage() {
<div className="box">
<h2 className="title is-4 has-text-centered">Import Transactions</h2>
<LoadFileForm
loading={loading}
profiles={profiles}
servers={servers}
defaultProfile={defaultProfile}

View File

@@ -1,11 +1,13 @@
import { useCallback, useState } from "react";
import type { ImportOptions } from "../../types/api";
import classNames from "classnames";
export type ImportBarProps = {
onSubmit: (opts: ImportOptions) => void;
loading: boolean;
};
export default function ImportBar({ onSubmit }: ImportBarProps) {
export default function ImportBar({ onSubmit, loading }: ImportBarProps) {
const [formData, setFormData] = useState<ImportOptions>({ mode: 'import' });
@@ -34,12 +36,12 @@ export default function ImportBar({ onSubmit }: ImportBarProps) {
<div className="control">
<div className="buttons has-addons is-left">
<button
type="button"
className={`button ${formData.mode === 'import' ? "is-primary is-selected" : ""}`}
type="button"
className={classNames("button", { 'is-primary': formData.mode === 'import', 'is-selected': formData.mode === 'import'})}
onClick={() => changeMode('import')}>Import</button>
<button
type="button"
className={`button ${formData.mode === 'add' ? "is-primary is-selected" : ""}`}
className={classNames("button", { 'is-primary': formData.mode === 'add', 'is-selected': formData.mode === 'add'})}
onClick={() => changeMode('add')}>Add</button>
</div>
</div>
@@ -69,7 +71,9 @@ export default function ImportBar({ onSubmit }: ImportBarProps) {
<div className="control">
<button
type="submit"
className="button is-link is-fullwidth">Submit transactions</button>
disabled={loading}
className={classNames("button", "is-link", "is-fullwidth", { 'is-loading': loading })}>Submit transactions
</button>
</div>
</div>
</form>

View File

@@ -5,6 +5,7 @@ import { SkippedLines } from "./SkippedLines";
import ImportBar from "./ImportBar";
import type { ImportOptions } from "../../types/api";
import { submitTransactions } from "../../services/api.service";
import { useLoader } from "../../hooks/useLoader";
export default function PrepareTransactionsPage() {
@@ -12,7 +13,7 @@ export default function PrepareTransactionsPage() {
const desiredTransactions = useMemo(() => state.transactions.filter((_, i) => !state.filteredOut.includes(i)), [state, state.filteredOut]);
const handleSubmit = useCallback(async (opts: ImportOptions) => {
const { fn: handleSubmit, loading } = useLoader(async (opts: ImportOptions) => {
if(!state.server) {
return;
}
@@ -63,7 +64,7 @@ export default function PrepareTransactionsPage() {
</div>
<div className="content">
<ImportBar onSubmit={handleSubmit} />
<ImportBar loading={loading} onSubmit={handleSubmit} />
</div>
</div>

View File

@@ -25,7 +25,7 @@ export default function ResultPage() {
return (
<div className="columns mt-6">
<div className="column is-8 is-offset-2">
<div className="column is-6 is-offset-3">
<div className="box">
<h2 className="title is-4 has-text-centered">Import Result</h2>
<div className="content">

View File

@@ -1,7 +1,7 @@
import type { ConfigResponse, ImportOptions, PrepareResponse, SubmitResponse, Transaction } from "../types/api";
export async function fetchConfig(): Promise<ConfigResponse> {
const response = await fetch("/config")
const response = await fetch("http://localhost:3000/config")
const data = await response.json();
return data as ConfigResponse;
}
@@ -12,7 +12,7 @@ export async function loadTransactions(csvFile: File, profile: string, server: s
payload.append("profile", profile);
payload.append("server", server);
const response = await fetch("/prepare", {
const response = await fetch("http://localhost:3000/prepare", {
method: "POST",
body: payload
});
@@ -28,7 +28,7 @@ export async function submitTransactions(transactions: Transaction[], server: st
opts
};
const response = await fetch("/submit", {
const response = await fetch("http://localhost:3000/submit", {
method: "POST",
body: JSON.stringify(payload),
headers: {