diff --git a/src/server/index.ts b/src/server/index.ts
index 0f0c6f1..2ce8ddc 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -13,6 +13,11 @@ export function serve(config: Config, port: number) {
app.set('views', path.join(__dirname, '../../views'));
app.use(express.static('public'));
+ app.use(function(req, res, next) {
+ res.header("Access-Control-Allow-Origin", "*");
+ res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
+ next();
+ });
const upload = multer({ storage: multer.memoryStorage() });
@@ -25,6 +30,15 @@ export function serve(config: Config, port: number) {
});
});
+ app.get("/config", (req, res) => {
+ res.json({
+ profiles: Object.keys(config.profiles),
+ servers: Object.keys(config.servers),
+ defaultProfile: config.defaultProfile,
+ defaultServer: config.defaultServer
+ });
+ });
+
app.post("/prepare", upload.single("file"), async (req, res) => {
if (!req.file) {
throw new Error("No file to upload");
diff --git a/web/package-lock.json b/web/package-lock.json
index 9abcb59..d2d6d2b 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -8,6 +8,7 @@
"name": "web",
"version": "0.0.0",
"dependencies": {
+ "bulma": "^1.0.4",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
@@ -1836,6 +1837,12 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/bulma": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
+ "integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
+ "license": "MIT"
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
diff --git a/web/package.json b/web/package.json
index e2c509e..23aa78f 100644
--- a/web/package.json
+++ b/web/package.json
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "bulma": "^1.0.4",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
diff --git a/web/src/App.css b/web/src/App.css
deleted file mode 100644
index b9d355d..0000000
--- a/web/src/App.css
+++ /dev/null
@@ -1,42 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 3d7ded3..ad09850 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,35 +1,6 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
+import LoadFilePage from "./pages/LoadFilePage/LoadFilePage";
-function App() {
- const [count, setCount] = useState(0)
- return (
- <>
-
- Vite + React
-
-
-
- Edit src/App.tsx and save to test HMR
-
-
-
- Click on the Vite and React logos to learn more
-
- >
- )
+export default function App() {
+ return ();
}
-
-export default App
diff --git a/web/src/assets/react.svg b/web/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/web/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/styles/index.css b/web/src/assets/styles/index.css
new file mode 100644
index 0000000..e69de29
diff --git a/web/src/index.css b/web/src/index.css
deleted file mode 100644
index 08a3ac9..0000000
--- a/web/src/index.css
+++ /dev/null
@@ -1,68 +0,0 @@
-:root {
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
diff --git a/web/src/main.tsx b/web/src/main.tsx
index bef5202..807e82a 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -1,6 +1,7 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
-import './index.css'
+import 'bulma/css/bulma.min.css';
+import './assets/styles/index.css';
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
diff --git a/web/src/pages/LoadFilePage/LoadFileForm.tsx b/web/src/pages/LoadFilePage/LoadFileForm.tsx
new file mode 100644
index 0000000..2ee0b3f
--- /dev/null
+++ b/web/src/pages/LoadFilePage/LoadFileForm.tsx
@@ -0,0 +1,106 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+
+export type LoadFileFormProps = {
+ profiles: string[];
+ servers: string[];
+ defaultProfile?: string;
+ defaultServer?: string;
+ onSubmit?: (csvFile: File, profile: string, server: string) => Promise;
+}
+
+type Form = {
+ files?: FileList;
+ profile?: string;
+ server?: string;
+};
+
+export default function LoadFileForm({ profiles, servers, defaultProfile, defaultServer, onSubmit }: LoadFileFormProps) {
+ const fileInput = useRef(null);
+
+ const [formData, setFormData] = useState
+ );
+}
\ No newline at end of file
diff --git a/web/src/pages/LoadFilePage/LoadFilePage.tsx b/web/src/pages/LoadFilePage/LoadFilePage.tsx
new file mode 100644
index 0000000..b026380
--- /dev/null
+++ b/web/src/pages/LoadFilePage/LoadFilePage.tsx
@@ -0,0 +1,45 @@
+import { useCallback, useEffect, useState } from 'react';
+import LoadFileForm from './LoadFileForm';
+import { fetchConfig, loadTransactions } from '../../services/api.service';
+
+export default function LoadFilePage() {
+
+ const [profiles, setProfiles] = useState([]);
+ const [servers, setServers] = useState([]);
+ const [defaultProfile, setDefaultProfile] = useState(undefined);
+ const [defaultServer, setDefaultServer] = useState(undefined);
+
+ const loadConfig = useCallback(async () => {
+ const config = await fetchConfig();
+
+ setProfiles(config.profiles);
+ setServers(config.servers);
+ setDefaultProfile(config.defaultProfile);
+ setDefaultServer(config.defaultServer);
+ }, [setProfiles, setServers, setDefaultProfile, setDefaultServer]);
+
+ useEffect(() => {
+ loadConfig();
+ }, [loadConfig]);
+
+ const handleSubmit = useCallback(async (csvFile: File, profile: string, server: string) => {
+ const data = await loadTransactions(csvFile, profile, server);
+ console.log(data);
+ }, []);
+
+ return (
+
+
+
+
Import Transactions
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/services/api.service.ts b/web/src/services/api.service.ts
new file mode 100644
index 0000000..b7d8833
--- /dev/null
+++ b/web/src/services/api.service.ts
@@ -0,0 +1,22 @@
+import type { ConfigResponse, PrepareResponse } from "../types/api";
+
+export async function fetchConfig(): Promise {
+ const response = await fetch("http://localhost:3000/config")
+ const data = await response.json();
+ return data as ConfigResponse;
+}
+
+export async function loadTransactions(csvFile: File, profile: string, server: string): Promise {
+ const payload = new FormData();
+ payload.append("file", csvFile);
+ payload.append("profile", profile);
+ payload.append("server", server);
+
+ const response = await fetch("http://localhost:3000/prepare", {
+ method: "POST",
+ body: payload
+ });
+
+ const data = await response.json();
+ return data as PrepareResponse;
+}
\ No newline at end of file
diff --git a/web/src/types/api.ts b/web/src/types/api.ts
new file mode 100644
index 0000000..116bb36
--- /dev/null
+++ b/web/src/types/api.ts
@@ -0,0 +1,23 @@
+export type ConfigResponse = {
+ profiles: string[];
+ servers: string[];
+ defaultProfile?: string;
+ defaultServer?: string;
+};
+
+export type PrepareResponse = {
+ transactions: Transaction[];
+ skipped: string[];
+};
+
+export type Transaction = {
+ kind: string;
+ from: string;
+ fromDetails: string;
+ to: string;
+ toDetails: string;
+ date: string;
+ title: string;
+ id: string;
+ amount: number;
+};
\ No newline at end of file