feat: 🎉 implement web-based files organizer with Go wasm bindings
This commit is contained in:
commit
257065e11d
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"gopls": {
|
||||||
|
"build.env": {
|
||||||
|
"GOOS": "js",
|
||||||
|
"GOARCH": "wasm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
go.mod
Normal file
16
go.mod
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module gitea.dikurium.ch/InnoPeak/customer-files-organizer-web
|
||||||
|
|
||||||
|
go 1.23.2
|
||||||
|
|
||||||
|
require github.com/pdfcpu/pdfcpu v0.9.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||||
|
github.com/hhrutter/tiff v1.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
golang.org/x/image v0.21.0 // indirect
|
||||||
|
golang.org/x/text v0.19.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
)
|
21
go.sum
Normal file
21
go.sum
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
|
||||||
|
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
|
||||||
|
github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0=
|
||||||
|
github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/pdfcpu/pdfcpu v0.9.1 h1:q8/KlBdHjkE7ZJU4ofhKG5Rjf7M6L324CVM6BMDySao=
|
||||||
|
github.com/pdfcpu/pdfcpu v0.9.1/go.mod h1:fVfOloBzs2+W2VJCCbq60XIxc3yJHAZ0Gahv1oO0gyI=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
|
||||||
|
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
|
||||||
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
|
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>customer-files-organizer</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="./src/main.tsx" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
136
main.go
Normal file
136
main.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"syscall/js"
|
||||||
|
|
||||||
|
pdfcpu "github.com/pdfcpu/pdfcpu/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Customer struct {
|
||||||
|
FirstName string `json:"firstName"`
|
||||||
|
LastName string `json:"lastName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomerFile struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Blob string `json:"blob"`
|
||||||
|
SelectedPages []string `json:"selectedPages"`
|
||||||
|
Suffix string `json:"suffix"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateArchiveInput struct {
|
||||||
|
Customer Customer `json:"customer"`
|
||||||
|
Files []CustomerFile `json:"files"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateArchiveResult struct {
|
||||||
|
ResultArchive *string `json:"resultArchive"`
|
||||||
|
Error *string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func createArchive(this js.Value, args []js.Value) any {
|
||||||
|
var input CreateArchiveInput
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(args[0].String()), &input)
|
||||||
|
|
||||||
|
handler := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
|
var (
|
||||||
|
resolve = args[0]
|
||||||
|
reject = args[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
reject.Invoke(err.Error())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
w := zip.NewWriter(buf)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
filePrefix := fmt.Sprintf("%s_%s_%s", now.Format("2006-01-02_15-04-05"), input.Customer.LastName, input.Customer.FirstName)
|
||||||
|
|
||||||
|
var fileNames []string
|
||||||
|
|
||||||
|
for _, file := range input.Files {
|
||||||
|
ext := filepath.Ext(file.Name)
|
||||||
|
|
||||||
|
fileName := fmt.Sprintf("%s_%s%s", filePrefix, file.Suffix, ext)
|
||||||
|
i := 1
|
||||||
|
|
||||||
|
for slices.Index(fileNames, fileName) != -1 {
|
||||||
|
fileName = fmt.Sprintf("%s_%s-%d%s", filePrefix, file.Suffix, i, ext)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
fileNames = append(fileNames, fileName)
|
||||||
|
|
||||||
|
f, err := w.Create(fileName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
reject.Invoke(err.Error())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := base64.StdEncoding.DecodeString(file.Blob)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
reject.Invoke(err.Error())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext == ".pdf" && len(file.SelectedPages) > 0 {
|
||||||
|
rs := bytes.NewReader(b)
|
||||||
|
|
||||||
|
err = pdfcpu.Trim(rs, f, file.SelectedPages, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
reject.Invoke(err.Error())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = f.Write(b)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
reject.Invoke(err.Error())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = w.Close(); err != nil {
|
||||||
|
reject.Invoke(err.Error())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve.Invoke(base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
promiseConstructor := js.Global().Get("Promise")
|
||||||
|
return promiseConstructor.New(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := make(chan struct{}, 0)
|
||||||
|
js.Global().Set("createArchive", js.FuncOf(createArchive))
|
||||||
|
<-c
|
||||||
|
}
|
3484
package-lock.json
generated
Normal file
3484
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/core": "^7.17.2",
|
||||||
|
"@mantine/dropzone": "^7.17.2",
|
||||||
|
"@mantine/form": "^7.17.2",
|
||||||
|
"@mantine/hooks": "^7.17.2",
|
||||||
|
"@mantine/modals": "^7.17.2",
|
||||||
|
"@mantine/notifications": "^7.17.2",
|
||||||
|
"@mantine/nprogress": "^7.17.2",
|
||||||
|
"@tabler/icons-react": "^3.31.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-pdf": "^9.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.12",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"typescript": "^5.8.2",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"vite-plugin-top-level-await": "^1.5.0",
|
||||||
|
"vite-plugin-wasm": "^3.4.1"
|
||||||
|
}
|
||||||
|
}
|
1
package.json.md5
Normal file
1
package.json.md5
Normal file
@ -0,0 +1 @@
|
|||||||
|
a295624d7ead0855089bd295503d1006
|
14
postcss.config.js
Normal file
14
postcss.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
"postcss-preset-mantine": {},
|
||||||
|
"postcss-simple-vars": {
|
||||||
|
variables: {
|
||||||
|
"mantine-breakpoint-xs": "36em",
|
||||||
|
"mantine-breakpoint-sm": "48em",
|
||||||
|
"mantine-breakpoint-md": "62em",
|
||||||
|
"mantine-breakpoint-lg": "75em",
|
||||||
|
"mantine-breakpoint-xl": "88em",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
42
src/App.tsx
Normal file
42
src/App.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import "@mantine/core/styles.css";
|
||||||
|
import "@mantine/notifications/styles.css";
|
||||||
|
import "./wasm_exec";
|
||||||
|
|
||||||
|
import FileOrganizer from "./FileOrganizer";
|
||||||
|
import { MantineProvider } from "@mantine/core";
|
||||||
|
import { ModalsProvider } from "@mantine/modals";
|
||||||
|
import { Notifications } from "@mantine/notifications";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
declare class Go {
|
||||||
|
argv: string[];
|
||||||
|
env: { [envKey: string]: string };
|
||||||
|
exit: (code: number) => void;
|
||||||
|
importObject: WebAssembly.Imports;
|
||||||
|
exited: boolean;
|
||||||
|
mem: DataView;
|
||||||
|
run(instance: WebAssembly.Instance): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
useEffect(() => {
|
||||||
|
const go = new Go();
|
||||||
|
|
||||||
|
WebAssembly.instantiateStreaming(fetch("/main.wasm"), go.importObject).then(
|
||||||
|
(result) => {
|
||||||
|
go.run(result.instance);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineProvider>
|
||||||
|
<Notifications />
|
||||||
|
<ModalsProvider>
|
||||||
|
<FileOrganizer />
|
||||||
|
</ModalsProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
319
src/FileOrganizer.tsx
Normal file
319
src/FileOrganizer.tsx
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
import {
|
||||||
|
Autocomplete,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Document, Page } from "react-pdf";
|
||||||
|
import { Dropzone, FileWithPath } from "@mantine/dropzone";
|
||||||
|
import { Form, isNotEmpty, useForm } from "@mantine/form";
|
||||||
|
import {
|
||||||
|
IconCheck,
|
||||||
|
IconExclamationMark,
|
||||||
|
IconFile,
|
||||||
|
IconUpload,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
function createArchive(payload: string): Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
files: { file: FileWithPath; suffix: string; selectedPages: string[] }[];
|
||||||
|
customer: { firstName: string; lastName: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
function FileOrganizer() {
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
mode: "uncontrolled",
|
||||||
|
initialValues: { files: [], customer: { firstName: "", lastName: "" } },
|
||||||
|
validate: {
|
||||||
|
customer: {
|
||||||
|
firstName: isNotEmpty("Wert ist erforderlich"),
|
||||||
|
lastName: isNotEmpty("Wert ist erforderlich"),
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
suffix: isNotEmpty("Wert ist erforderlich"),
|
||||||
|
selectedPages: (value) => (!value ? "Wert ist erforderlich" : null),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateInputOnBlur: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedFile, setSelectedFile] = useState<number | null>(null);
|
||||||
|
const [numPages, setNumPages] = useState<number>();
|
||||||
|
const [pageNumber, setPageNumber] = useState<number>(1);
|
||||||
|
|
||||||
|
const onDocumentLoadSuccess = useCallback(
|
||||||
|
({ numPages }: { numPages: number }) => {
|
||||||
|
setNumPages(numPages);
|
||||||
|
},
|
||||||
|
[setNumPages]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = async (values: FormValues) => {
|
||||||
|
const id = Math.random().toString(36).replace("0.", "notification_");
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
id,
|
||||||
|
title: "Dateien werden verarbeitet",
|
||||||
|
message: "Dateien werden nun benennt und als ZIP gespeichert",
|
||||||
|
autoClose: false,
|
||||||
|
withCloseButton: false,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const f = await Promise.all(
|
||||||
|
values.files.map(async (f) => {
|
||||||
|
const getDataURL = () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
const promise = new Promise<string>((res, rej) => {
|
||||||
|
reader.onload = () => {
|
||||||
|
res((reader.result as string)?.split(",")[1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
rej();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsDataURL(f.file);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataURL = await getDataURL();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...f,
|
||||||
|
name: f.file.name,
|
||||||
|
blob: dataURL,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resultArchive = await createArchive(
|
||||||
|
JSON.stringify({
|
||||||
|
customer: {
|
||||||
|
firstName: values.customer.firstName,
|
||||||
|
lastName: values.customer.lastName,
|
||||||
|
},
|
||||||
|
files: f,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
notifications.update({
|
||||||
|
id,
|
||||||
|
color: "teal",
|
||||||
|
title: "Dateien wurden verarbeitet",
|
||||||
|
message: `Dateien werden unter ${resultArchive} gespeichert`,
|
||||||
|
icon: <IconCheck size={18} />,
|
||||||
|
loading: false,
|
||||||
|
autoClose: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uri = `data:application/zip;base64,${resultArchive}`;
|
||||||
|
|
||||||
|
const date = `${new Date().getFullYear()}-${new Date()
|
||||||
|
.getMonth()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}-${new Date()
|
||||||
|
.getDate()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}_${new Date()
|
||||||
|
.getHours()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}-${new Date()
|
||||||
|
.getMinutes()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}-${new Date()
|
||||||
|
.getSeconds()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = `${date}_${values.customer.firstName}_${values.customer.lastName}`;
|
||||||
|
link.href = uri;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
notifications.update({
|
||||||
|
id,
|
||||||
|
color: "red",
|
||||||
|
title: "Ein Fehler ist passiert",
|
||||||
|
message: error as string,
|
||||||
|
icon: <IconExclamationMark size={18} />,
|
||||||
|
loading: false,
|
||||||
|
autoClose: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form form={form} onSubmit={handleSubmit}>
|
||||||
|
<Stack p="md">
|
||||||
|
<Title order={2}>Kunde</Title>
|
||||||
|
<Group>
|
||||||
|
<TextInput
|
||||||
|
label="Vorname"
|
||||||
|
placeholder="Vorname"
|
||||||
|
withAsterisk
|
||||||
|
required
|
||||||
|
{...form.getInputProps("customer.firstName")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Nachname"
|
||||||
|
placeholder="Nachname"
|
||||||
|
withAsterisk
|
||||||
|
required
|
||||||
|
{...form.getInputProps("customer.lastName")}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Flex direction="row-reverse" p="md">
|
||||||
|
<Stack flex={2}>
|
||||||
|
<Title order={2}>Dateien</Title>
|
||||||
|
<Stack mah="50vh" style={{ overflow: "auto" }} p="md">
|
||||||
|
{form.values.files.map((file, idx) => (
|
||||||
|
<Stack>
|
||||||
|
<Group align="end" key={`${file.file.name}-${idx}`}>
|
||||||
|
<Button onClick={() => setSelectedFile(idx)} flex={1}>
|
||||||
|
{file.file.name}
|
||||||
|
</Button>
|
||||||
|
<Autocomplete
|
||||||
|
data={[
|
||||||
|
"Lohnausweis",
|
||||||
|
"Öffentlicher Verkehr",
|
||||||
|
"Steuererklärung",
|
||||||
|
"Weiterbildung",
|
||||||
|
"3A Bescheinigung",
|
||||||
|
"Steuerzugangsdaten",
|
||||||
|
"Wertschriften",
|
||||||
|
"Kontoauszug",
|
||||||
|
"Zinsauweis",
|
||||||
|
"Krankenkassenprämie",
|
||||||
|
"Krankenkassenrechnung",
|
||||||
|
]}
|
||||||
|
label="Kategorie"
|
||||||
|
placeholder="Kategorie"
|
||||||
|
withAsterisk
|
||||||
|
required
|
||||||
|
{...form.getInputProps(`files.${idx}.suffix`)}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
{/* Disabled due to lack of support for mkdir in js wasm environment
|
||||||
|
<Group align="end">
|
||||||
|
<Text>Seiten auswählen:</Text>
|
||||||
|
{file.selectedPages.map((_, i) => (
|
||||||
|
<TextInput
|
||||||
|
label="Auswahl"
|
||||||
|
placeholder="Auswahl"
|
||||||
|
withAsterisk
|
||||||
|
required
|
||||||
|
{...form.getInputProps(
|
||||||
|
`files.${idx}.selectedPages.${i}`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onClick={() =>
|
||||||
|
form.insertListItem(`files.${idx}.selectedPages`, "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
</Group> */}
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) =>
|
||||||
|
files.forEach((f) =>
|
||||||
|
form.insertListItem("files", {
|
||||||
|
file: f,
|
||||||
|
suffix: "",
|
||||||
|
selectedPages: [],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
accept={["application/pdf"]}
|
||||||
|
>
|
||||||
|
<Group
|
||||||
|
justify="center"
|
||||||
|
gap="xl"
|
||||||
|
mih={120}
|
||||||
|
style={{ pointerEvents: "none" }}
|
||||||
|
>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload
|
||||||
|
size={52}
|
||||||
|
color="var(--mantine-color-blue-6)"
|
||||||
|
stroke={1.5}
|
||||||
|
/>
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX
|
||||||
|
size={52}
|
||||||
|
color="var(--mantine-color-red-6)"
|
||||||
|
stroke={1.5}
|
||||||
|
/>
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconFile
|
||||||
|
size={52}
|
||||||
|
color="var(--mantine-color-dimmed)"
|
||||||
|
stroke={1.5}
|
||||||
|
/>
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag files here or click to select
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</Stack>
|
||||||
|
<Flex flex={2} align="center" direction="column">
|
||||||
|
{selectedFile && (
|
||||||
|
<Stack>
|
||||||
|
<Title order={2}>Vorschau</Title>
|
||||||
|
<Document
|
||||||
|
file={form.values.files[selectedFile].file}
|
||||||
|
onLoadSuccess={onDocumentLoadSuccess}
|
||||||
|
>
|
||||||
|
<Page pageNumber={pageNumber} />
|
||||||
|
</Document>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<Pagination
|
||||||
|
value={pageNumber}
|
||||||
|
onChange={setPageNumber}
|
||||||
|
total={numPages ?? 0}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileOrganizer;
|
23
src/main.tsx
Normal file
23
src/main.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import "react-pdf/dist/Page/TextLayer.css";
|
||||||
|
import "react-pdf/dist/Page/AnnotationLayer.css";
|
||||||
|
import "@mantine/dropzone/styles.css";
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { pdfjs } from "react-pdf";
|
||||||
|
|
||||||
|
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
"pdfjs-dist/build/pdf.worker.min.mjs",
|
||||||
|
import.meta.url
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
const container = document.getElementById("root");
|
||||||
|
|
||||||
|
const root = createRoot(container!);
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
31
tsconfig.json
Normal file
31
tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable",
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
|
}
|
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user