[Certs] Migrate UI from previous implementations
This commit is contained in:
commit
3f49209c29
89
frontend/.eslintrc.yml
Normal file
89
frontend/.eslintrc.yml
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
env:
|
||||||
|
browser: true
|
||||||
|
es2021: true
|
||||||
|
extends:
|
||||||
|
- 'eslint:recommended'
|
||||||
|
- 'plugin:@typescript-eslint/recommended'
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
parserOptions:
|
||||||
|
ecmaVersion: 12
|
||||||
|
sourceType: module
|
||||||
|
plugins:
|
||||||
|
- '@typescript-eslint'
|
||||||
|
- react
|
||||||
|
rules:
|
||||||
|
"@typescript-eslint/ban-ts-comment": off
|
||||||
|
"@typescript-eslint/no-empty-function": off
|
||||||
|
indent:
|
||||||
|
- error
|
||||||
|
- 4
|
||||||
|
- SwitchCase: 1
|
||||||
|
linebreak-style:
|
||||||
|
- error
|
||||||
|
- unix
|
||||||
|
quotes:
|
||||||
|
- error
|
||||||
|
- double
|
||||||
|
semi:
|
||||||
|
- error
|
||||||
|
- always
|
||||||
|
react/jsx-pascal-case: error
|
||||||
|
react/jsx-closing-bracket-location: error
|
||||||
|
react/jsx-closing-tag-location: error
|
||||||
|
no-multi-spaces: error
|
||||||
|
react/jsx-tag-spacing: error
|
||||||
|
react/jsx-boolean-value: error
|
||||||
|
react/jsx-wrap-multilines: error
|
||||||
|
react/self-closing-comp: error
|
||||||
|
prefer-const: error
|
||||||
|
no-const-assign: error
|
||||||
|
no-var: error
|
||||||
|
array-callback-return: error
|
||||||
|
prefer-template: error
|
||||||
|
template-curly-spacing: error
|
||||||
|
no-useless-escape: error
|
||||||
|
wrap-iife: error
|
||||||
|
no-loop-func: error
|
||||||
|
default-param-last: error
|
||||||
|
space-before-function-paren:
|
||||||
|
- error
|
||||||
|
- never
|
||||||
|
space-before-blocks: error
|
||||||
|
no-param-reassign: error
|
||||||
|
function-paren-newline: error
|
||||||
|
comma-dangle:
|
||||||
|
- error
|
||||||
|
- always-multiline
|
||||||
|
arrow-spacing: error
|
||||||
|
arrow-parens: error
|
||||||
|
arrow-body-style: error
|
||||||
|
no-confusing-arrow: error
|
||||||
|
implicit-arrow-linebreak: error
|
||||||
|
no-duplicate-imports: error
|
||||||
|
object-curly-newline: error
|
||||||
|
dot-notation: error
|
||||||
|
one-var:
|
||||||
|
- error
|
||||||
|
- never
|
||||||
|
no-multi-assign: error
|
||||||
|
no-plusplus: error
|
||||||
|
operator-linebreak: error
|
||||||
|
eqeqeq: error
|
||||||
|
no-case-declarations: error
|
||||||
|
no-nested-ternary: error
|
||||||
|
no-unneeded-ternary: error
|
||||||
|
no-mixed-operators: error
|
||||||
|
nonblock-statement-body-position: error
|
||||||
|
brace-style: error
|
||||||
|
keyword-spacing: error
|
||||||
|
space-infix-ops: error
|
||||||
|
eol-last: error
|
||||||
|
newline-per-chained-call: error
|
||||||
|
no-whitespace-before-property: error
|
||||||
|
space-in-parens: error
|
||||||
|
array-bracket-spacing: error
|
||||||
|
key-spacing: error
|
||||||
|
no-trailing-spaces: error
|
||||||
|
comma-style: error
|
||||||
|
radix: error
|
||||||
|
no-new-wrappers: error
|
2
frontend/.gitignore
vendored
Normal file
2
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
34
frontend/README.md
Normal file
34
frontend/README.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
## Usage
|
||||||
|
|
||||||
|
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
||||||
|
|
||||||
|
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install # or pnpm install or yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm run dev` or `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.<br>
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.<br>
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `dist` folder.<br>
|
||||||
|
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.<br>
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
16
frontend/index.html
Normal file
16
frontend/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
|
||||||
|
<title>Solid App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<script src="/src/index.tsx" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
32
frontend/package.json
Normal file
32
frontend/package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "eeg-system-frontend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"scripts": {
|
||||||
|
"start": "vite",
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"serve": "vite preview",
|
||||||
|
"lint": "eslint --fix"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||||
|
"@typescript-eslint/parser": "^6.4.1",
|
||||||
|
"autoprefixer": "^10.4.15",
|
||||||
|
"eslint": "^8.47.0",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"postcss": "^8.4.28",
|
||||||
|
"solid-devtools": "^0.27.3",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"typescript": "^5.1.3",
|
||||||
|
"vite": "^4.3.9",
|
||||||
|
"vite-plugin-solid": "^2.7.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@solidjs/router": "^0.8.3",
|
||||||
|
"@types/qrcode": "^1.5.1",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
|
"solid-js": "^1.7.6"
|
||||||
|
}
|
||||||
|
}
|
3308
frontend/pnpm-lock.yaml
Normal file
3308
frontend/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
16
frontend/src/App.tsx
Normal file
16
frontend/src/App.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Route, Routes } from "@solidjs/router";
|
||||||
|
import type { Component } from "solid-js";
|
||||||
|
import { Certs } from "./certs";
|
||||||
|
import { NavRail } from "./components/NavRail";
|
||||||
|
|
||||||
|
const App: Component = () => (
|
||||||
|
<div class="grid grid-cols-[5rem_auto]">
|
||||||
|
<NavRail />
|
||||||
|
<Routes>
|
||||||
|
<Route path="/certs" component={Certs} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export default App;
|
BIN
frontend/src/assets/favicon.ico
Normal file
BIN
frontend/src/assets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
97
frontend/src/certs/NewRegister/RegisterPreview.tsx
Normal file
97
frontend/src/certs/NewRegister/RegisterPreview.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { FilledCard } from "../../components/FilledCard";
|
||||||
|
import { For } from "solid-js";
|
||||||
|
// import { subjects } from "src/views/subjects";
|
||||||
|
import { XIcon } from "../../icons/XIcon";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const subjects: () => Array<any> = () => [];
|
||||||
|
|
||||||
|
function isoDateToLocalDate(date: string): string {
|
||||||
|
const [,month, day] = /\d{4}-(\d{2})-(\d{2})/.exec(date) ?? "";
|
||||||
|
return `${day}/${month}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegisterPreview(props: {selections: Array<[number, string]>, personId: number | null, onDelete: (v: number) => void, onRegister: () => void}) {
|
||||||
|
const submit = async() => {
|
||||||
|
console.log("Submit...");
|
||||||
|
|
||||||
|
for (const [courseId, date] of props.selections) {
|
||||||
|
const result = await defaultNewRegisterFn(
|
||||||
|
props.personId ?? -1,
|
||||||
|
courseId,
|
||||||
|
date,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
console.log("Success");
|
||||||
|
} else {
|
||||||
|
console.log(`error. ${result}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onRegister();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilledCard class="border border-c-outline overflow-hidden">
|
||||||
|
<h2 class="p-4 font-bold text-xl">Confirmar registro</h2>
|
||||||
|
<div class="bg-c-surface p-4">
|
||||||
|
<For each={props.selections}>
|
||||||
|
{([courseId, date]) => <Register courseId={courseId} date={date} onDelete={props.onDelete} />}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-full cursor-pointer mt-4
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="button"
|
||||||
|
disabled={props.selections.length === 0}
|
||||||
|
onclick={submit}
|
||||||
|
>
|
||||||
|
Registrar los {props.selections.length} cursos
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</FilledCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Register(props: {courseId: number, date: string, onDelete: (v: number) => void}) {
|
||||||
|
const courseName = () => {
|
||||||
|
const courses = subjects();
|
||||||
|
return courses.find((c) => c.id === props.courseId)?.nombre ?? "!";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="grid grid-cols-[auto_4rem_1.5rem] py-1 px-2 rounded-md border border-c-outline my-1">
|
||||||
|
{courseName()}
|
||||||
|
<span class="font-mono">{isoDateToLocalDate(props.date)}</span>
|
||||||
|
<button
|
||||||
|
class="hover:bg-c-surface-variant rounded-md"
|
||||||
|
onclick={() => props.onDelete(props.courseId)}
|
||||||
|
>
|
||||||
|
<XIcon fill="var(--c-on-surface)" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function defaultNewRegisterFn(personId: number, subjectId: number, date: string): Promise<null | string> {
|
||||||
|
const response = await fetch("/certificate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
personId,
|
||||||
|
subjectId,
|
||||||
|
date,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
}
|
109
frontend/src/certs/NewRegister/SearchableSelect.tsx
Normal file
109
frontend/src/certs/NewRegister/SearchableSelect.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { createEffect, createSignal, For } from "solid-js";
|
||||||
|
import type {CursoGIE} from "../../../model/CursoGIE/cursoGIE.entity";
|
||||||
|
import { isServer } from "solid-js/web";
|
||||||
|
|
||||||
|
export function SearchableSelect(props: {
|
||||||
|
subjects: Array<CursoGIE>,
|
||||||
|
onChange: (id: number | null) => void,
|
||||||
|
count: number
|
||||||
|
}) {
|
||||||
|
const [filter, setFilter] = createSignal("");
|
||||||
|
const [selected, setSelected] = createSignal<number | null>(null);
|
||||||
|
const [inputValue, setInputValue] = createSignal("");
|
||||||
|
|
||||||
|
const iHandler = (ev: KeyboardEvent & {currentTarget: HTMLInputElement, target: Element}) => {
|
||||||
|
const inputEl = ev.target as HTMLInputElement;
|
||||||
|
// Clear current selection
|
||||||
|
setSelected(null);
|
||||||
|
|
||||||
|
let filter: string = inputEl.value.toLowerCase();
|
||||||
|
filter = filter.replace("á", "a");
|
||||||
|
filter = filter.replace("é", "e");
|
||||||
|
filter = filter.replace("í", "i");
|
||||||
|
filter = filter.replace("ó", "o");
|
||||||
|
filter = filter.replace("ú", "u");
|
||||||
|
setFilter(filter);
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
props.onChange(selected());
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
// Makes reactivity happen
|
||||||
|
console.log(props.count);
|
||||||
|
|
||||||
|
setFilter("");
|
||||||
|
setSelected(null);
|
||||||
|
setInputValue("");
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputElement = (
|
||||||
|
<input
|
||||||
|
id="create-subject"
|
||||||
|
class={`bg-c-background text-c-on-background
|
||||||
|
${selected() !== null ? "border-c-green" : "border-c-outline"}
|
||||||
|
border-2 rounded px-2 py-1
|
||||||
|
w-full
|
||||||
|
invalid:border-c-error invalid:text-c-error
|
||||||
|
focus:border-c-primary outline-none
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||||
|
type="text"
|
||||||
|
placeholder="Curso"
|
||||||
|
onkeyup={iHandler}
|
||||||
|
value={inputValue()}
|
||||||
|
onchange={(ev) => setInputValue(ev.target.value)}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isServer) {
|
||||||
|
(inputElement as HTMLInputElement).addEventListener("keydown", (ev) => {
|
||||||
|
if (ev.code === "Enter") {
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredOptions = () => {
|
||||||
|
const filterText = filter();
|
||||||
|
|
||||||
|
return props.subjects.filter((subject) => {
|
||||||
|
let subjectText = subject.nombre.toLowerCase();
|
||||||
|
subjectText = subjectText.replace("á", "a");
|
||||||
|
subjectText = subjectText.replace("é", "e");
|
||||||
|
subjectText = subjectText.replace("í", "i");
|
||||||
|
subjectText = subjectText.replace("ó", "o");
|
||||||
|
subjectText = subjectText.replace("ú", "u");
|
||||||
|
|
||||||
|
return selected() === null && subjectText.indexOf(filterText) !== -1;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{inputElement}
|
||||||
|
<br/>
|
||||||
|
<div
|
||||||
|
class="border-c-outline border-2 rounded overflow-y-scroll h-[10rem]"
|
||||||
|
>
|
||||||
|
<For each={filteredOptions()}>
|
||||||
|
{(s) => (
|
||||||
|
<button
|
||||||
|
class="w-full text-left py-1 px-2
|
||||||
|
hover:bg-c-primary-container hover:text-c-on-primary-container"
|
||||||
|
onclick={(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
setSelected(s.id);
|
||||||
|
setInputValue(s.nombre);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.nombre}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
156
frontend/src/certs/NewRegister/index.tsx
Normal file
156
frontend/src/certs/NewRegister/index.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { createSignal, Show } from "solid-js";
|
||||||
|
import { SearchableSelect } from "./SearchableSelect";
|
||||||
|
import { JSX } from "solid-js/jsx-runtime";
|
||||||
|
// import { subjects } from "../subjects";
|
||||||
|
import { FilledCard } from "../../components/FilledCard";
|
||||||
|
import { RegisterPreview } from "./RegisterPreview";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const subjects: () => Array<any> = () => [];
|
||||||
|
|
||||||
|
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
|
||||||
|
submitter: HTMLElement;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
type TabType = "Presets" | "Manual";
|
||||||
|
|
||||||
|
export function NewRegister(props: {
|
||||||
|
personId: number | null,
|
||||||
|
onSuccess: () => void,
|
||||||
|
}) {
|
||||||
|
const [active, setActive] = createSignal<TabType>("Manual");
|
||||||
|
const [selections, setSelections] = createSignal<Array<[number, string]>>([]);
|
||||||
|
|
||||||
|
const onRegister = () => {
|
||||||
|
setSelections([]);
|
||||||
|
props.onSuccess();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="h-screen overflow-y-scroll">
|
||||||
|
<FilledCard class="border border-c-outline overflow-hidden">
|
||||||
|
<h2 class="p-4 font-bold text-xl">Registrar certs</h2>
|
||||||
|
|
||||||
|
<RegisterTabs active={active()} setActive={setActive} />
|
||||||
|
|
||||||
|
<div class="bg-c-surface p-4 h-[22rem]">
|
||||||
|
<Show when={active() === "Presets"}>
|
||||||
|
<p>Proximamente...</p>
|
||||||
|
</Show>
|
||||||
|
<Show when={active() === "Manual"}>
|
||||||
|
<ManualCerts personId={props.personId} onAdd={(v) => setSelections((x) => [...x, v])} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</FilledCard>
|
||||||
|
|
||||||
|
<RegisterPreview
|
||||||
|
selections={selections()}
|
||||||
|
personId={props.personId}
|
||||||
|
onDelete={(deleteId) => setSelections((s) => [...s.filter(([id]) => id !== deleteId)])}
|
||||||
|
onRegister={onRegister}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function RegisterTabs(props: {active: TabType, setActive: (v: TabType) => void}) {
|
||||||
|
const presetsClasses = () => ((props.active === "Presets") ? "font-bold border-c-primary" : "border-c-transparent");
|
||||||
|
const manualClasses = () => ((props.active === "Manual") ? "font-bold border-c-primary" : "border-c-transparent");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<button
|
||||||
|
class={`py-2 border-b-4 ${presetsClasses()}`}
|
||||||
|
onclick={() => props.setActive("Presets")}
|
||||||
|
>
|
||||||
|
Presets
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class={`py-2 border-b-4 ${manualClasses()}`}
|
||||||
|
onclick={() => props.setActive("Manual")}
|
||||||
|
>
|
||||||
|
Manual
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManualCerts(props: {personId: number | null, onAdd: (v: [number, string]) => void}) {
|
||||||
|
// Used to update SearchableSelect.tsx manually
|
||||||
|
const [count, setCount] = createSignal(0);
|
||||||
|
const [error, setError] = createSignal("");
|
||||||
|
|
||||||
|
const [selectedSubject, setSelectedSubject] = createSignal<number | null>(null);
|
||||||
|
|
||||||
|
const datePicker = (
|
||||||
|
<input
|
||||||
|
id="create-date"
|
||||||
|
class="bg-c-surface text-c-on-surface border border-c-outline rounded-lg p-2"
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const register: HTMLEventFn = async(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
const subject = selectedSubject();
|
||||||
|
const date = (datePicker as HTMLInputElement).value;
|
||||||
|
|
||||||
|
if (subject === null) {
|
||||||
|
setError("Selecciona un curso");
|
||||||
|
|
||||||
|
setTimeout(() => setError(""), 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (date === "") {
|
||||||
|
setError("Selecciona una fecha");
|
||||||
|
|
||||||
|
setTimeout(() => setError(""), 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onAdd([subject, date]);
|
||||||
|
// This is used to update & refresh the <SearchableSelect> component
|
||||||
|
setCount((x) => x + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Person ID: ${props.personId}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form onsubmit={register}>
|
||||||
|
<div>
|
||||||
|
<SearchableSelect
|
||||||
|
subjects={subjects()}
|
||||||
|
onChange={setSelectedSubject}
|
||||||
|
count={count()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative my-4">
|
||||||
|
{datePicker}
|
||||||
|
<label for="create-date" class="absolute -top-2 left-2 text-xs bg-c-surface px-1">Fecha</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-full cursor-pointer
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="submit"
|
||||||
|
value="Agregar"
|
||||||
|
disabled={props.personId === null}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="my-2 p-1 rounded w-fit mx-4 bg-c-error text-c-on-error"
|
||||||
|
style={{opacity: error() === "" ? "0" : "1", "user-select": "none"}}
|
||||||
|
>
|
||||||
|
{error()}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
299
frontend/src/certs/Search.tsx
Normal file
299
frontend/src/certs/Search.tsx
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
import { createEffect, createSignal, Show } from "solid-js";
|
||||||
|
import { JSX } from "solid-js/jsx-runtime";
|
||||||
|
// import { Person } from "../../types/Person";
|
||||||
|
// import { RegisterPerson } from "./Search/RegisterPerson";
|
||||||
|
import QR from "qrcode";
|
||||||
|
import { CopyIcon } from "../icons/CopyIcon";
|
||||||
|
import { MagnifyingGlassIcon } from "../icons/MagnifyingGlassIcon";
|
||||||
|
import { XIcon } from "../icons/XIcon";
|
||||||
|
|
||||||
|
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
|
||||||
|
submitter: HTMLElement;
|
||||||
|
}>;
|
||||||
|
type HTMLButtonEvent = JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type Person = any;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Form that retrieves a user from the DB given an ID
|
||||||
|
*/
|
||||||
|
export function Search(props: {setPerson: (p: Person | null) => void}) {
|
||||||
|
const [dni, setDni] = createSignal("");
|
||||||
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
const [error, setError] = createSignal("");
|
||||||
|
const [warning, setWarning] = createSignal("");
|
||||||
|
const [qrBase64, setQrBase64] = createSignal<string | null>(null);
|
||||||
|
const [persona, setPersona] = createSignal<Person | null>(null);
|
||||||
|
|
||||||
|
// Update QR
|
||||||
|
createEffect(() => {
|
||||||
|
if (dni() !== "") {
|
||||||
|
// Old URL: https://www.eegsac.com/alumnoscertificados.php?DNI=${dni()}
|
||||||
|
// New URL: https://eegsac.com/certificado/${dni()}
|
||||||
|
QR.toDataURL(`https://eegsac.com/certificado/${dni()}`, {margin: 1}, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Error creating QR code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setQrBase64(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get the user data from the DB
|
||||||
|
*/
|
||||||
|
const searchDNI: HTMLEventFn = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = async() => {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
setWarning("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/person/${dni()}`);
|
||||||
|
const body = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
setPersona(body);
|
||||||
|
props.setPerson(body);
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
console.error(body);
|
||||||
|
|
||||||
|
setWarning("Persona no encontrada. Se debe insertar manualmente sus datos.");
|
||||||
|
props.setPerson(null);
|
||||||
|
} else {
|
||||||
|
setError(body);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError(JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nombresYApellidos = () => {
|
||||||
|
const p = persona();
|
||||||
|
if (p === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `${p.nombres} ${p.apellidoPaterno} ${p.apellidoMaterno}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const apellidosYNombres = () => {
|
||||||
|
const p = persona();
|
||||||
|
if (p === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `${p.apellidoPaterno} ${p.apellidoMaterno} ${p.nombres}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const apellidos = () => {
|
||||||
|
const p = persona();
|
||||||
|
if (p === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `${p.apellidoPaterno} ${p.apellidoMaterno}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nombres = () => {
|
||||||
|
const p = persona();
|
||||||
|
if (p === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return p.nombres;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="my-4 inline-block w-[10rem] h-[10rem] rounded-lg bg-c-surface-variant overflow-hidden">
|
||||||
|
<img class={`${qrBase64() === null ? "hidden" : ""} inline-block w-[10rem] h-[10rem]`} src={qrBase64() ?? ""} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form onSubmit={searchDNI} class="px-4">
|
||||||
|
<InputBox dni={dni()} setDni={setDni} loading={loading()} />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="relative max-w-[14rem] mx-auto p-1 text-c-error text-sm"
|
||||||
|
style={{display: error() === "" ? "none" : "block"}}
|
||||||
|
>
|
||||||
|
Error:
|
||||||
|
<br />
|
||||||
|
{error()}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class={`${persona() === null ? "opacity-50 cursor-not-allowed" : ""}`}>
|
||||||
|
<MaterialLabel text={persona()?.apellidoPaterno ?? null} resource="Apellido Paterno" />
|
||||||
|
<MaterialLabel text={persona()?.apellidoMaterno ?? null} resource="Apellido Materno" />
|
||||||
|
<MaterialLabel text={persona()?.nombres ?? null} resource="Nombres" />
|
||||||
|
|
||||||
|
<div class={"relative max-w-[14rem] mx-auto my-6"}>
|
||||||
|
<CopyButton copyText={nombresYApellidos()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
Nombres y <b>Apellidos</b>
|
||||||
|
</CopyButton>
|
||||||
|
|
||||||
|
<CopyButton copyText={apellidosYNombres()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
<b>Apellidos</b> y Nombres
|
||||||
|
</CopyButton>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<CopyButton copyText={apellidos()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
<b>Apellidos</b>
|
||||||
|
</CopyButton>
|
||||||
|
<CopyButton copyText={nombres()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
Nombres
|
||||||
|
</CopyButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={warning() !== ""}>
|
||||||
|
{/*
|
||||||
|
<RegisterPerson dni={dni()} onSuccess={search} />
|
||||||
|
*/}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputBox(props: {
|
||||||
|
loading: boolean,
|
||||||
|
dni: string,
|
||||||
|
setDni: (v: string) => void,
|
||||||
|
}) {
|
||||||
|
const inputElement = (
|
||||||
|
<input
|
||||||
|
id="search-dni"
|
||||||
|
class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1 w-full
|
||||||
|
invalid:border-c-error invalid:text-c-error
|
||||||
|
focus:border-c-primary outline-none font-mono
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="text"
|
||||||
|
minLength={8}
|
||||||
|
maxLength={8}
|
||||||
|
pattern="[0-9]{8}"
|
||||||
|
placeholder="Número de DNI"
|
||||||
|
value={props.dni}
|
||||||
|
required
|
||||||
|
onChange={(e) => props.setDni(e.target.value)}
|
||||||
|
disabled={props.loading}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const copyToClipboard: HTMLButtonEvent = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (props.dni.length === 8) {
|
||||||
|
navigator.clipboard.writeText(props.dni);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearDni: HTMLButtonEvent = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
props.setDni("");
|
||||||
|
(inputElement as HTMLInputElement).focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative max-w-[14rem] mx-auto">
|
||||||
|
{inputElement}
|
||||||
|
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1">DNI</label>
|
||||||
|
<button
|
||||||
|
class="absolute top-1 right-[3.75rem] rounded hover:bg-c-surface-variant"
|
||||||
|
>
|
||||||
|
<MagnifyingGlassIcon fill="var(--c-on-surface)" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1 right-8 rounded hover:bg-c-surface-variant"
|
||||||
|
onclick={copyToClipboard}
|
||||||
|
>
|
||||||
|
<CopyIcon fill="var(--c-on-surface)" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1 right-1 rounded hover:bg-c-surface-variant"
|
||||||
|
onclick={clearDni}
|
||||||
|
>
|
||||||
|
<XIcon fill="var(--c-on-surface)" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function MaterialLabel(props: {text: string | null, resource: string}) {
|
||||||
|
const copyToClipboard: HTMLButtonEvent = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (props.text !== null) {
|
||||||
|
navigator.clipboard.writeText(props.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative max-w-[14rem] mx-auto my-6">
|
||||||
|
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1 select-none">{props.resource}</label>
|
||||||
|
<span
|
||||||
|
class="bg-c-background text-c-on-background border-c-outline
|
||||||
|
border-2 rounded px-2 py-1 w-full inline-block font-mono
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{props.text ?? ""}
|
||||||
|
<span class="select-none"> </span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1 right-1 rounded hover:bg-c-surface-variant"
|
||||||
|
onclick={copyToClipboard}
|
||||||
|
>
|
||||||
|
<CopyIcon fill="var(--c-on-surface)" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CopyButton(props: {copyText: string, children: Array<JSX.Element> | JSX.Element}) {
|
||||||
|
const [successAnimation, setSuccessAnimation] = createSignal(false);
|
||||||
|
|
||||||
|
const onclick = () => {
|
||||||
|
if (props.copyText !== "") {
|
||||||
|
navigator.clipboard.writeText(props.copyText);
|
||||||
|
setSuccessAnimation(true);
|
||||||
|
setTimeout(() => setSuccessAnimation(false), 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onclick={onclick}
|
||||||
|
class={
|
||||||
|
`${successAnimation() ? "bg-c-success text-c-on-success" : "bg-c-primary text-c-on-primary"
|
||||||
|
} rounded-lg transition-colors py-1 my-1 relative overflow-hidden inline-block w-full`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
12
frontend/src/certs/index.tsx
Normal file
12
frontend/src/certs/index.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { NewRegister } from "./NewRegister";
|
||||||
|
import { Search } from "./Search";
|
||||||
|
|
||||||
|
export function Certs() {
|
||||||
|
return (
|
||||||
|
<div class="grid grid-cols-[18rem_25rem_1fr]">
|
||||||
|
<Search setPerson={() => {}} />
|
||||||
|
<NewRegister personId={0} onSuccess={() => {}} />
|
||||||
|
<div>Registers</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
9
frontend/src/components/FilledCard.tsx
Normal file
9
frontend/src/components/FilledCard.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
export function FilledCard(props: {children?: Array<JSX.Element> | JSX.Element, class?: string}) {
|
||||||
|
return (
|
||||||
|
<div class={`bg-c-surface-variant text-c-on-surface-variant rounded-xl m-2 shadow ${props.class ?? ""}`}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
47
frontend/src/components/NavRail.tsx
Normal file
47
frontend/src/components/NavRail.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { HomeIcon } from "../icons/HomeIcon";
|
||||||
|
import type {JSX} from "solid-js";
|
||||||
|
import { DocxIcon } from "../icons/DocxIcon";
|
||||||
|
import { StackIcon } from "../icons/StackIcon";
|
||||||
|
import { ScanIcon } from "../icons/ScanIcon";
|
||||||
|
import { KeyIcon } from "../icons/KeyIcon";
|
||||||
|
import { PaletteIcon } from "../icons/PaletteIcon";
|
||||||
|
import { A } from "@solidjs/router";
|
||||||
|
|
||||||
|
export function NavRail() {
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col justify-center items-center h-screen overflow-x-hidden">
|
||||||
|
<NavRailButton path="/" name="Inicio" icon={<HomeIcon />} />
|
||||||
|
<NavRailButton path="/certs" name="Certs" icon={<DocxIcon />} />
|
||||||
|
<NavRailButton path="/batch" name="Batch" icon={<StackIcon />} />
|
||||||
|
<NavRailButton path="/accesos" name="Accesos" icon={<KeyIcon />} />
|
||||||
|
<NavRailButton path="/escaneo" name="Escaneo" icon={<ScanIcon />} />
|
||||||
|
<NavRailButton path="/colores" name="Colores" icon={<PaletteIcon />} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavRailButton(props: {
|
||||||
|
name: string,
|
||||||
|
icon: JSX.Element,
|
||||||
|
path: string,
|
||||||
|
}) {
|
||||||
|
const anchorEl = (
|
||||||
|
<A
|
||||||
|
class="my-3 text-center group hover:bg-c-surface-variant rounded-xl transition-colors
|
||||||
|
max-w-[4rem] inline-block select-none"
|
||||||
|
href={props.path}
|
||||||
|
end
|
||||||
|
>
|
||||||
|
<div class={"p-1 inline-block group-[.active]:bg-c-surface-variant min-w-[4rem] rounded-full transition-colors"}>
|
||||||
|
{props.icon}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm">
|
||||||
|
{props.name}
|
||||||
|
</span>
|
||||||
|
</A>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return anchorEl;
|
||||||
|
}
|
5
frontend/src/icons/CopyIcon.tsx
Normal file
5
frontend/src/icons/CopyIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function CopyIcon(props: {fill: string}) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
frontend/src/icons/DocxIcon.tsx
Normal file
5
frontend/src/icons/DocxIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function DocxIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M52,144H36a8,8,0,0,0-8,8v56a8,8,0,0,0,8,8H52a36,36,0,0,0,0-72Zm0,56H44V160h8a20,20,0,0,1,0,40Zm169.53-4.91a8,8,0,0,1,.25,11.31A30.06,30.06,0,0,1,200,216c-17.65,0-32-16.15-32-36s14.35-36,32-36a30.06,30.06,0,0,1,21.78,9.6,8,8,0,0,1-11.56,11.06A14.24,14.24,0,0,0,200,160c-8.82,0-16,9-16,20s7.18,20,16,20a14.24,14.24,0,0,0,10.22-4.66A8,8,0,0,1,221.53,195.09ZM128,144c-17.65,0-32,16.15-32,36s14.35,36,32,36,32-16.15,32-36S145.65,144,128,144Zm0,56c-8.82,0-16-9-16-20s7.18-20,16-20,16,9,16,20S136.82,200,128,200ZM48,120a8,8,0,0,0,8-8V40h88V88a8,8,0,0,0,8,8h48v16a8,8,0,0,0,16,0V88a8,8,0,0,0-2.34-5.66l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v72A8,8,0,0,0,48,120ZM160,51.31,188.69,80H160Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
frontend/src/icons/DownloadIcon.tsx
Normal file
5
frontend/src/icons/DownloadIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function DownloadIcon(props: {fill: string}) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H72a8,8,0,0,1,0,16H32v64H224V136H184a8,8,0,0,1,0-16h40A16,16,0,0,1,240,136Zm-117.66-2.34a8,8,0,0,0,11.32,0l48-48a8,8,0,0,0-11.32-11.32L136,108.69V24a8,8,0,0,0-16,0v84.69L85.66,74.34A8,8,0,0,0,74.34,85.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
frontend/src/icons/HomeIcon.tsx
Normal file
5
frontend/src/icons/HomeIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function HomeIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M218.83,103.77l-80-75.48a1.14,1.14,0,0,1-.11-.11,16,16,0,0,0-21.53,0l-.11.11L37.17,103.77A16,16,0,0,0,32,115.55V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V115.55A16,16,0,0,0,218.83,103.77ZM208,208H48V115.55l.11-.1L128,40l79.9,75.43.11.1Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
frontend/src/icons/KeyIcon.tsx
Normal file
5
frontend/src/icons/KeyIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function KeyIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M48,56V200a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0Zm84,54.5L112,117V96a8,8,0,0,0-16,0v21L76,110.5a8,8,0,0,0-5,15.22l20,6.49-12.34,17a8,8,0,1,0,12.94,9.4l12.34-17,12.34,17a8,8,0,1,0,12.94-9.4l-12.34-17,20-6.49A8,8,0,0,0,132,110.5ZM238,115.64A8,8,0,0,0,228,110.5L208,117V96a8,8,0,0,0-16,0v21l-20-6.49a8,8,0,0,0-4.95,15.22l20,6.49-12.34,17a8,8,0,1,0,12.94,9.4l12.34-17,12.34,17a8,8,0,1,0,12.94-9.4l-12.34-17,20-6.49A8,8,0,0,0,238,115.64Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
frontend/src/icons/MagnifyingGlassIcon.tsx
Normal file
5
frontend/src/icons/MagnifyingGlassIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function MagnifyingGlassIcon(props: {fill: string}) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
frontend/src/icons/PaletteIcon.tsx
Normal file
5
frontend/src/icons/PaletteIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function PaletteIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M200.77,53.89A103.27,103.27,0,0,0,128,24h-1.07A104,104,0,0,0,24,128c0,43,26.58,79.06,69.36,94.17A32,32,0,0,0,136,192a16,16,0,0,1,16-16h46.21a31.81,31.81,0,0,0,31.2-24.88,104.43,104.43,0,0,0,2.59-24A103.28,103.28,0,0,0,200.77,53.89Zm13,93.71A15.89,15.89,0,0,1,198.21,160H152a32,32,0,0,0-32,32,16,16,0,0,1-21.31,15.07C62.49,194.3,40,164,40,128a88,88,0,0,1,87.09-88h.9a88.35,88.35,0,0,1,88,87.25A88.86,88.86,0,0,1,213.81,147.6ZM140,76a12,12,0,1,1-12-12A12,12,0,0,1,140,76ZM96,100A12,12,0,1,1,84,88,12,12,0,0,1,96,100Zm0,56a12,12,0,1,1-12-12A12,12,0,0,1,96,156Zm88-56a12,12,0,1,1-12-12A12,12,0,0,1,184,100Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
frontend/src/icons/ScanIcon.tsx
Normal file
5
frontend/src/icons/ScanIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function ScanIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M224,40V80a8,8,0,0,1-16,0V48H176a8,8,0,0,1,0-16h40A8,8,0,0,1,224,40ZM80,208H48V176a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H80a8,8,0,0,0,0-16Zm136-40a8,8,0,0,0-8,8v32H176a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V176A8,8,0,0,0,216,168ZM40,88a8,8,0,0,0,8-8V48H80a8,8,0,0,0,0-16H40a8,8,0,0,0-8,8V80A8,8,0,0,0,40,88Zm128,96H88a16,16,0,0,1-16-16V88A16,16,0,0,1,88,72h80a16,16,0,0,1,16,16v80A16,16,0,0,1,168,184ZM88,168h80V88H88Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
frontend/src/icons/StackIcon.tsx
Normal file
5
frontend/src/icons/StackIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function StackIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M230.91,172A8,8,0,0,1,228,182.91l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,36,169.09l92,53.65,92-53.65A8,8,0,0,1,230.91,172ZM220,121.09l-92,53.65L36,121.09A8,8,0,0,0,28,134.91l96,56a8,8,0,0,0,8.06,0l96-56A8,8,0,1,0,220,121.09ZM24,80a8,8,0,0,1,4-6.91l96-56a8,8,0,0,1,8.06,0l96,56a8,8,0,0,1,0,13.82l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,24,80Zm23.88,0L128,126.74,208.12,80,128,33.26Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
frontend/src/icons/TrashIcon.tsx
Normal file
5
frontend/src/icons/TrashIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function TrashIcon(props: {fill: string}) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
6
frontend/src/icons/XIcon.tsx
Normal file
6
frontend/src/icons/XIcon.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export function XIcon(props: {fill: string}) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M165.66,101.66,139.31,128l26.35,26.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
55
frontend/src/index.css
Normal file
55
frontend/src/index.css
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
:root {
|
||||||
|
--c-primary: #b6c4ff;
|
||||||
|
--c-on-primary: #00287d;
|
||||||
|
--c-primary-container: #003baf;
|
||||||
|
--c-on-primary-container: #dce1ff;
|
||||||
|
--c-error: #ffb4ab;
|
||||||
|
--c-on-error: #690005;
|
||||||
|
--c-error-container: #93000a;
|
||||||
|
--c-on-error-container: #ffdad6;
|
||||||
|
--c-background: #1b1b1f;
|
||||||
|
--c-on-background: #e4e1e6;
|
||||||
|
--c-surface: #1b1b1f;
|
||||||
|
--c-on-surface: #e4e1e6;
|
||||||
|
--c-outline: #8f909a;
|
||||||
|
--c-surface-variant: #45464f;
|
||||||
|
--c-on-surface-variant: #c6c6d0;
|
||||||
|
|
||||||
|
--c-success: #78da9f;
|
||||||
|
--c-on-success: #00391f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--c-primary: #2a55cb;
|
||||||
|
--c-on-primary: #ffffff;
|
||||||
|
--c-primary-container: #dce1ff;
|
||||||
|
--c-on-primary-container: #00164f;
|
||||||
|
--c-error: #ba1a1a;
|
||||||
|
--c-on-error: #ffffff;
|
||||||
|
--c-error-container: #ffdad6;
|
||||||
|
--c-on-error-container: #410002;
|
||||||
|
--c-background: #fefbff;
|
||||||
|
--c-on-background: #1b1b1f;
|
||||||
|
--c-surface: #fefbff;
|
||||||
|
--c-on-surface: #1b1b1f;
|
||||||
|
--c-outline: #767680;
|
||||||
|
--c-surface-variant: #e2e1ec;
|
||||||
|
--c-on-surface-variant: #45464f;
|
||||||
|
|
||||||
|
--c-success: #006d40;
|
||||||
|
--c-on-success: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--c-background);
|
||||||
|
color: var(--c-on-background);
|
||||||
|
font-family: Inter, "Inter Nerd Font", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
14
frontend/src/index.tsx
Normal file
14
frontend/src/index.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/* @refresh reload */
|
||||||
|
import { render } from "solid-js/web";
|
||||||
|
import { Router } from "@solidjs/router";
|
||||||
|
|
||||||
|
import "./index.css";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
|
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||||
|
throw new Error("Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?");
|
||||||
|
}
|
||||||
|
|
||||||
|
render(() => <Router><App /></Router>, root!);
|
34
frontend/tailwind.config.js
Normal file
34
frontend/tailwind.config.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./src/**/*.{js,jsx,ts,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
"c-primary": "var(--c-primary)",
|
||||||
|
"c-on-primary": "var(--c-on-primary)",
|
||||||
|
"c-primary-container": "var(--c-primary-container)",
|
||||||
|
"c-on-primary-container": "var(--c-on-primary-container)",
|
||||||
|
"c-error": "var(--c-error)",
|
||||||
|
"c-on-error": "var(--c-on-error)",
|
||||||
|
"c-error-container": "var(--c-error-container)",
|
||||||
|
"c-on-error-container": "var(--c-on-error-container)",
|
||||||
|
"c-background": "var(--c-background)",
|
||||||
|
"c-on-background": "var(--c-on-background)",
|
||||||
|
"c-surface": "var(--c-surface)",
|
||||||
|
"c-on-surface": "var(--c-on-surface)",
|
||||||
|
"c-outline": "var(--c-outline)",
|
||||||
|
"c-surface-variant": "var(--c-surface-variant)",
|
||||||
|
"c-on-surface-variant": "var(--c-on-surface-variant)",
|
||||||
|
"c-green": "#006b54",
|
||||||
|
"c-success": "var(--c-success)",
|
||||||
|
"c-on-success": "var(--c-on-success)",
|
||||||
|
"c-transparent": "rgba(0,0,0,0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
|
15
frontend/tsconfig.json
Normal file
15
frontend/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
}
|
||||||
|
}
|
20
frontend/vite.config.ts
Normal file
20
frontend/vite.config.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import solidPlugin from 'vite-plugin-solid';
|
||||||
|
// import devtools from 'solid-devtools/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
/*
|
||||||
|
Uncomment the following line to enable solid-devtools.
|
||||||
|
For more info see https://github.com/thetarnav/solid-devtools/tree/main/packages/extension#readme
|
||||||
|
*/
|
||||||
|
// devtools(),
|
||||||
|
solidPlugin(),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user