feat(web-client): implement a basic web client
This commit is contained in:
parent
991b82d750
commit
8b8f84ede1
13 changed files with 12506 additions and 9 deletions
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
24
web-client/.gitignore
vendored
Normal file
24
web-client/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
17
web-client/README.md
Normal file
17
web-client/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# csv-to-json web client
|
||||||
|
|
||||||
|
A simple Preact SPA designed to showcase the csv-to-json server in action.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Have a recent version of node (>=v14) and npm installed, and install any dependencies needed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$> npm i
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure that you have `csv-to-json` running and listening on port 8000, then fire up the dev server and visit the url indicated in your browser (defaults to http://localhost:3000):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$> npm run dev
|
||||||
|
```
|
||||||
12
web-client/index.html
Normal file
12
web-client/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>CSV to JSON</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12205
web-client/package-lock.json
generated
Normal file
12205
web-client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,11 +1,25 @@
|
||||||
{
|
{
|
||||||
"name": "web-client",
|
"name": "web-client",
|
||||||
"version": "0.1.0",
|
"description": "A web client for csv-to-json.",
|
||||||
"description": "A web client for csv-to-json.",
|
"version": "0.1.0",
|
||||||
"main": "index.js",
|
"private": true,
|
||||||
"scripts": {
|
"author": "M. George Hansen",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"license": "UNLICENSED",
|
||||||
},
|
"scripts": {
|
||||||
"author": "M. George Hansen",
|
"dev": "vite"
|
||||||
"license": "UNLICENSED"
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"preact": "^10.7.2",
|
||||||
|
"react-query": "^3.39.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@preact/preset-vite": "^2.1.7",
|
||||||
|
"@testing-library/preact": "^2.0.1",
|
||||||
|
"@types/jest": "^27.4.1",
|
||||||
|
"jest": "^27.5.1",
|
||||||
|
"ts-node": "^10.6.0",
|
||||||
|
"typescript": "^4.5.5",
|
||||||
|
"vite": "^2.8.4",
|
||||||
|
"vite-tsconfig-paths": "^3.4.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
94
web-client/src/app.tsx
Normal file
94
web-client/src/app.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
export { App };
|
||||||
|
|
||||||
|
import { QueryClient, QueryClientProvider, useMutation } from "react-query";
|
||||||
|
import { FunctionComponent } from "preact";
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
const App: FunctionComponent = () => (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<FileLoader />
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FileLoader: FunctionComponent = () => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "100%",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
action="http://localhost:8000"
|
||||||
|
method="POST"
|
||||||
|
encType="multipart/form-data"
|
||||||
|
>
|
||||||
|
<h1>Convert Your CSV to JSON:</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "1px solid #fff",
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: "20px 10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label>
|
||||||
|
Upload CSV{" "}
|
||||||
|
<input
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
type="file"
|
||||||
|
name="file"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
border: "1px solid #fff",
|
||||||
|
background: "none",
|
||||||
|
color: "inherit",
|
||||||
|
padding: "5px 10px",
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Convert
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Motto />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const MOTTOS = [
|
||||||
|
"It's fun AND educational!",
|
||||||
|
"Everyone's doing it, don't get left behind!",
|
||||||
|
"Do iiittt!",
|
||||||
|
];
|
||||||
|
|
||||||
|
const UPDATE_INTERVAL = 5_000;
|
||||||
|
|
||||||
|
const Motto: FunctionComponent = () => {
|
||||||
|
const [motto, setMotto] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
const handle = setInterval(
|
||||||
|
() =>
|
||||||
|
setMotto((i) => {
|
||||||
|
const newIndex = i + 1;
|
||||||
|
if (newIndex >= MOTTOS.length) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return newIndex;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
UPDATE_INTERVAL
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
clearInterval(handle);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return <div>{MOTTOS[motto]}</div>;
|
||||||
|
};
|
||||||
5
web-client/src/index.tsx
Normal file
5
web-client/src/index.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { render } from "preact";
|
||||||
|
import { App } from "./app";
|
||||||
|
import "./reset.css";
|
||||||
|
|
||||||
|
render(<App />, document.body);
|
||||||
58
web-client/src/reset.css
Normal file
58
web-client/src/reset.css
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
/* Based on https://www.joshwcomeau.com/css/custom-css-reset/ */
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
picture,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
text-decoration: unset;
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
25
web-client/src/tsconfig.json
Normal file
25
web-client/src/tsconfig.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact",
|
||||||
|
"rootDir": "./",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"typeRoots": ["./types"],
|
||||||
|
"paths": {
|
||||||
|
"#/*": ["./*"],
|
||||||
|
"react": ["../node_modules/preact/compat"],
|
||||||
|
"react-dom": ["../node_modules/preact/compat"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
web-client/tsconfig.base.json
Normal file
9
web-client/tsconfig.base.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
}
|
||||||
|
}
|
||||||
9
web-client/tsconfig.json
Normal file
9
web-client/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
||||||
16
web-client/vite.config.ts
Normal file
16
web-client/vite.config.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import preact from "@preact/preset-vite";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [preact(), tsconfigPaths({ projects: ["./src/tsconfig.json"] })],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
react: "preact/compat",
|
||||||
|
"react-dom": "preact/compat",
|
||||||
|
"react-dom/test-utils": "preact/test-utils",
|
||||||
|
"react/jsx-runtime": "preact/jsx-runtime",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue