add initial project for frontend

Signed-off-by: Tobias Erbshäußer <tobias@tesoft.dev>
This commit is contained in:
2026-05-24 09:30:31 +02:00
commit b4b06bfa42
15 changed files with 3236 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
distMinified
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+2904
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build && html-minifier-terser --collapse-whitespace --remove-comments --input-dir ./dist --file-ext html --minify-css --output-dir ./distMinified && cp -r ./dist/assets ./distMinified",
"preview": "vite preview"
},
"devDependencies": {
"@vituum/vite-plugin-nunjucks": "^1.1.0",
"html-minifier-terser": "^7.2.0",
"typescript": "^5.2.2",
"vite": "^5.4.6",
"vite-plugin-checker": "^0.8.0",
"vituum": "^1.1.0"
},
"dependencies": {
"@directus/sdk": "^17.0.0",
"@types/dompurify": "^3.0.5",
"dompurify": "^3.1.6",
"marked": "^14.1.2"
}
}
+11
View File
@@ -0,0 +1,11 @@
<template id="tesoft-button-template">
<style>
@import "src/styles/default.css";
</style>
<button>
<slot></slot>
</button>
</template>
<script src="/src/components/button.ts" type="module"></script>
+15
View File
@@ -0,0 +1,15 @@
import {TesoftComponent} from "../scripts/main.ts";
export class TesoftButton extends TesoftComponent {
constructor() {
super();
const template = document.getElementById("tesoft-button-template") as HTMLTemplateElement;
const templateContent = template.content;
const shadowRoot = this.attachShadow({mode: "open"});
shadowRoot.appendChild(templateContent.cloneNode(true));
}
}
customElements.define("tesoft-button", TesoftButton);
+23
View File
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<link rel="stylesheet" href="/src/styles/main.css">
{% block styles %}
{% endblock %}
</head>
<body>
<main>
{% block content %}
{% endblock %}
</main>
{% block components %}
{% endblock %}
{% block sources %}
{% endblock %}
<script src="/src/scripts/main.ts" type="module"></script>
</body>
</html>
+17
View File
@@ -0,0 +1,17 @@
{% extends 'layouts/main.njk' %}
{% set title='TESOFT - Home' %}
{% block styles %}
<link rel="stylesheet" href="/src/styles/index.css">
{% endblock %}
{% block sources %}
<script src="/src/scripts/index.ts" type="module"></script>
{% endblock %}
{% block components %}
{% endblock %}
{% block content %}
{% endblock %}
View File
+85
View File
@@ -0,0 +1,85 @@
export abstract class TesoftComponent extends HTMLElement {
private ready: boolean;
protected constructor() {
super();
this.ready = false;
}
// noinspection JSUnusedGlobalSymbols
async connectedCallback() {
const promises = [];
for (const child of findComponents(this.shadowRoot)) {
promises.push(new Promise(resolve =>
window.customElements.whenDefined(child.tagName.toLowerCase()).then(() => {
if (child.isConnected) {
resolve(void {});
} else {
child.addEventListener("ready", (e: any) => {
e.preventDefault();
resolve(void {});
}, {once: true});
}
})
));
}
await Promise.all(promises);
await this.populate();
this.ready = true;
this.dispatchEvent(new CustomEvent("ready", {
bubbles: true,
cancelable: true,
}));
}
async populate() {
// Overwrite if necessary in components
}
get isReady(): boolean {
return this.ready;
}
}
function findComponents(root: any | null): TesoftComponent[] {
const components = [];
for (const child of root?.querySelectorAll("*") ?? []) {
if (child.tagName.toLowerCase().startsWith("snz-")) {
components.push(child);
}
}
return components;
}
export function onReady(callback: () => void, ...components: TesoftComponent[]) {
async function domLoaded() {
if (components.length === 0) {
components = findComponents(document);
}
const promises = [];
for (const component of components) {
if (!component.isReady) {
promises.push(new Promise(resolve => {
component.addEventListener("ready", resolve, {once: true});
}));
}
}
await Promise.all(promises);
callback();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", domLoaded, {once: true});
} else {
// noinspection JSIgnoredPromiseFromCall
domLoaded();
}
}
+33
View File
@@ -0,0 +1,33 @@
:root {
}
html {
/* Set font-size so that 1rem = 10 px */
font-size: 62.5%;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
line-height: 2.1rem;
font-size: 1.4rem;
}
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
*::before {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.hidden {
display: none !important;
}
View File
+1
View File
@@ -0,0 +1 @@
@import "default.css";
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
]
}
+64
View File
@@ -0,0 +1,64 @@
import vituum from "vituum"
import nunjucks from "@vituum/vite-plugin-nunjucks"
import checker from "vite-plugin-checker";
export default {
publicDir: "public",
plugins: [
vituum(),
nunjucks({
root: "./src",
extensions: {
IncludeOnceExtension: includeOnceExtension,
},
}),
checker({
typescript: true,
}),
],
server: {
cors: {
"origin": "*",
"methods": "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
"preflightContinue": false,
"optionsSuccessStatus": 204
},
proxy: {
'/api': {
target: 'http://localhost:8055',
changeOrigin: false,
rewrite: (path) => path.replace(/^\/api/, ''),
},
}
}
}
function includeOnceExtension() {
this.tagName = "includeOnce";
this.tags = [this.tagName];
this.alreadyIncluded = new Set();
this.parse = function (parser, nodes, _lexer) {
const tag = parser.peekToken();
if (!parser.skipSymbol(this.tagName)) {
parser.fail("parseInclude: expected " + this.tagName);
}
const node = new nodes.Include(tag.lineno, tag.colno);
node.template = parser.parseExpression();
if (parser.skipSymbol("ignore") && parser.skipSymbol("missing")) {
node.ignoreMissing = true;
}
parser.advanceAfterBlockEnd(tag.value);
const includedPath = node.template.value;
if (this.alreadyIncluded.has(includedPath)) {
return new nodes.NodeList(0, 0, []);
}
this.alreadyIncluded.add(includedPath);
return node;
};
}
+5
View File
@@ -0,0 +1,5 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = with pkgs.buildPackages; [ nodejs ];
}