[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