Create loader
This commit is contained in:
34
web/src/hooks/useLoader.ts
Normal file
34
web/src/hooks/useLoader.ts
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -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>({});
|
||||
@@ -51,7 +53,7 @@ export default function LoadFileForm({ profiles, servers, defaultProfile, defaul
|
||||
<div className="field">
|
||||
<label className="label">CSV File</label>
|
||||
<div className="control">
|
||||
<div className={`file is-fullwidth ${!!selectedFile ? "has-name" : ""}`}>
|
||||
<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>
|
||||
|
||||
@@ -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();
|
||||
@@ -29,7 +30,7 @@ export default function LoadFilePage() {
|
||||
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}
|
||||
|
||||
@@ -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' });
|
||||
|
||||
|
||||
@@ -35,11 +37,11 @@ export default function ImportBar({ onSubmit }: ImportBarProps) {
|
||||
<div className="buttons has-addons is-left">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user