Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b306b8c
feat: scaffold rust crate with error types and Money parsing
AmauryAparicio Apr 10, 2026
4465829
feat: add Currency trait with USD and EUR implementations
AmauryAparicio Apr 10, 2026
9a3d649
feat: add ChangeStrategy trait with minimal and random implementations
AmauryAparicio Apr 10, 2026
7427b9c
feat: add RulesEngine with JSON config parsing and strategy selection
AmauryAparicio Apr 10, 2026
cdc4817
feat: add processor pipeline with parser, formatter, and file processing
AmauryAparicio Apr 10, 2026
4704a67
feat: expose WASM public API via wasm-bindgen
AmauryAparicio Apr 10, 2026
12577b4
test: add integration tests for full processing pipeline
AmauryAparicio Apr 10, 2026
3a709aa
feat: scaffold TanStack Start app with root layout and styles
AmauryAparicio Apr 10, 2026
2d822cc
feat: add Zod schemas, default config, and WASM loader
AmauryAparicio Apr 10, 2026
b196acd
feat: add processTransactions server function
AmauryAparicio Apr 10, 2026
b0bee0e
feat: add FileUpload and ErrorBanner components
AmauryAparicio Apr 10, 2026
ab4703a
feat: add RulesBuilder component with structured config controls
AmauryAparicio Apr 10, 2026
6047ba6
feat: add ResultsTable component with strategy badges
AmauryAparicio Apr 10, 2026
68075ac
feat: add split-panel index route with file processing flow
AmauryAparicio Apr 10, 2026
f3ad532
fix: migrate to vite + @tanstack/react-start, fix dep versions and se…
AmauryAparicio Apr 10, 2026
a311d73
fix: switch WASM to bundler target with vite-plugin-wasm
AmauryAparicio Apr 10, 2026
962177b
test: add unit, API, and component tests with vitest
AmauryAparicio Apr 11, 2026
abd9aa6
docs: add codebase map
AmauryAparicio Apr 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules/
dist/
.vinxi/
.output/
src/wasm-pkg/
src/routeTree.gen.ts
bun.lock
*.timestamp_*.js
37 changes: 37 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "cash-register-app",
"private": true,
"type": "module",
"scripts": {
"wasm:build": "cd ../cash-register-wasm && wasm-pack build --target bundler --out-dir ../app/src/wasm-pkg",
"wasm:test": "cd ../cash-register-wasm && cargo test",
"dev": "bun run wasm:build && vite dev --port 3456",
"build": "bun run wasm:build && vite build",
"start": "vite preview",
"test": "vitest run"
},
"dependencies": {
"@tanstack/react-router": "latest",
"@tanstack/react-start": "latest",
"@tanstack/router-plugin": "^1.132.0",
"react": "^19",
"react-dom": "^19",
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.6.0",
"zod": "^3"
},
"devDependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^25.6.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^5",
"jsdom": "^29.0.2",
"typescript": "^5",
"vite": "^7",
"vite-tsconfig-paths": "^5",
"vitest": "^3"
}
}
17 changes: 17 additions & 0 deletions app/src/components/ErrorBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type ErrorBannerProps = {
message: string
onDismiss?: () => void
}

export function ErrorBanner({ message, onDismiss }: ErrorBannerProps) {
return (
<div className="error-banner">
<span>{message}</span>
{onDismiss ? (
<button className="error-banner-dismiss" onClick={onDismiss}>
&times;
</button>
) : null}
</div>
)
}
79 changes: 79 additions & 0 deletions app/src/components/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useCallback, useRef, useState } from "react"

type FileUploadProps = {
onFileLoaded: (content: string, fileName: string) => void
}

export function FileUpload({ onFileLoaded }: FileUploadProps) {
const [fileName, setFileName] = useState<string | null>(null)
const [isDragging, setIsDragging] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)

const handleFile = useCallback(
(file: File) => {
const reader = new FileReader()
reader.onload = (e) => {
const content = e.target?.result as string
setFileName(file.name)
onFileLoaded(content, file.name)
}
reader.readAsText(file)
},
[onFileLoaded],
)

const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const file = e.dataTransfer.files[0]
if (file) {
handleFile(file)
}
},
[handleFile],
)

const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
handleFile(file)
}
},
[handleFile],
)

const isLoaded = fileName !== null

return (
<div
className={`dropzone ${isLoaded ? "dropzone-loaded" : ""} ${isDragging ? "dropzone-dragging" : ""}`}
onDragOver={(e) => {
e.preventDefault()
setIsDragging(true)
}}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
onClick={() => inputRef.current?.click()}
>
<div className="dropzone-icon">{isLoaded ? "\u{1F4C4}" : "\u{1F4C1}"}</div>
<div className="dropzone-text">
{isLoaded ? (
<strong>{fileName}</strong>
) : (
<>
<strong>Drop file here</strong> or click to browse
</>
)}
</div>
<input
ref={inputRef}
type="file"
accept=".csv,.txt"
onChange={handleChange}
style={{ display: "none" }}
/>
</div>
)
}
36 changes: 36 additions & 0 deletions app/src/components/ResultsTable.skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export function ResultsTableSkeleton() {
return (
<div className="results-container">
<div className="results-table-wrapper">
<table className="results-table">
<thead>
<tr>
<th>#</th>
<th>Input</th>
<th>Output</th>
<th>Strategy</th>
</tr>
</thead>
<tbody>
{[1, 2, 3].map((i) => (
<tr key={i}>
<td>
<div className="skeleton" style={{ width: 20, height: 16 }} />
</td>
<td>
<div className="skeleton" style={{ width: 80, height: 16 }} />
</td>
<td>
<div className="skeleton" style={{ width: 200, height: 16 }} />
</td>
<td>
<div className="skeleton" style={{ width: 60, height: 20, borderRadius: 99 }} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
58 changes: 58 additions & 0 deletions app/src/components/ResultsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { ProcessResponse } from "../lib/schemas"

type ResultsTableProps = {
response: ProcessResponse
}

export function ResultsTable({ response }: ResultsTableProps) {
return (
<div className="results-container">
<div className="results-table-wrapper">
<table className="results-table">
<thead>
<tr>
<th>#</th>
<th>Input</th>
<th>Output</th>
<th>Strategy</th>
</tr>
</thead>
<tbody>
{response.results.map((result) => (
<tr key={result.line} className={result.error ? "row-error" : ""}>
<td className="line-num">{result.line}</td>
<td className="input-col">{result.input}</td>
<td className="output-col">
{result.error ? (
<span className="error-text">{result.error}</span>
) : (
result.output
)}
</td>
<td className="strategy-col">
{result.strategy ? (
<span
className={`badge ${result.strategy === "random" ? "badge-random" : "badge-ok"}`}
>
{result.strategy}
</span>
) : null}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="results-summary">
<div className="summary-stat">
<span className="summary-dot dot-green" />
{response.total_lines} processed
</div>
<div className="summary-stat">
<span className="summary-dot dot-red" />
{response.error_count} errors
</div>
</div>
</div>
)
}
18 changes: 18 additions & 0 deletions app/src/components/RulesBuilder.skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function RulesBuilderSkeleton() {
return (
<div className="config-controls">
<div className="config-field">
<div className="skeleton skeleton-label" />
<div className="skeleton skeleton-select" />
</div>
<div className="config-field">
<div className="skeleton skeleton-label" />
<div className="skeleton skeleton-select" />
</div>
<div>
<div className="skeleton skeleton-label" />
<div className="skeleton skeleton-rule-card" />
</div>
</div>
)
}
Loading