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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import { SkippedLines } from "./SkippedLines";
import ImportBar from "./ImportBar"; import ImportBar from "./ImportBar";
import type { ImportOptions } from "../../types/api"; import type { ImportOptions } from "../../types/api";
import { submitTransactions } from "../../services/api.service"; import { submitTransactions } from "../../services/api.service";
import { useLoader } from "../../hooks/useLoader";
export default function PrepareTransactionsPage() { 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 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) { if(!state.server) {
return; return;
} }
@@ -63,7 +64,7 @@ export default function PrepareTransactionsPage() {
</div> </div>
<div className="content"> <div className="content">
<ImportBar onSubmit={handleSubmit} /> <ImportBar loading={loading} onSubmit={handleSubmit} />
</div> </div>
</div> </div>

View File

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

View File

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