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";
|
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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user