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 logo - - - React logo - -
-

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
({}); + + useEffect(() => { + setFormData({ + ...formData, + profile: defaultProfile, + server: defaultServer + }); + }, [setFormData, defaultProfile, defaultServer]); + + + const selectedFile = useMemo(() => { + if ((formData.files?.length ?? 0) > 0) { + return formData.files?.[0]; + } + + return undefined; + }, [formData.files]); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + + if (!selectedFile || !formData.profile || !formData.server) { + return; + } + + onSubmit?.(selectedFile, formData.profile, formData.server); + }, [formData, formData, selectedFile, onSubmit]); + + return ( + +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ ); +} \ 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