Initial commit
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | node_modules/ | ||||||
|  | dist/ | ||||||
|  |  | ||||||
|  | .env | ||||||
|  | config.json | ||||||
|  | .vsls.json | ||||||
							
								
								
									
										20
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | { | ||||||
|  | 	"tabWidth": 8, | ||||||
|  | 	"useTabs": true, | ||||||
|  | 	"arrowParens": "always", | ||||||
|  |  | ||||||
|  | 	"bracketSameLine": true, | ||||||
|  | 	"bracketSpacing": true, | ||||||
|  | 	"embeddedLanguageFormatting": "auto", | ||||||
|  | 	"endOfLine": "lf", | ||||||
|  | 	"htmlWhitespaceSensitivity": "css", | ||||||
|  | 	"insertPragma": false, | ||||||
|  | 	"jsxSingleQuote": false, | ||||||
|  | 	"printWidth": 225, | ||||||
|  | 	"proseWrap": "preserve", | ||||||
|  | 	"quoteProps": "as-needed", | ||||||
|  | 	"requirePragma": false, | ||||||
|  | 	"semi": true, | ||||||
|  | 	"singleQuote": true, | ||||||
|  | 	"trailingComma": "none" | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								README.MD
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								README.MD
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | # HydrationHUB | ||||||
|  | HydrationHUB - TODO: Luistiger slogan? | ||||||
|  |  | ||||||
|  | ## Serving static files from node_modules | ||||||
|  | Files from explicit dirs inside `node_modules` will be served below `/libs`. | ||||||
|  |  | ||||||
|  | ## Serving static files from /static | ||||||
|  | Files from the `/static` folder will be served below `/static`. | ||||||
|  |  | ||||||
							
								
								
									
										3422
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3422
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										53
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | { | ||||||
|  | 	"name": "hydration_hub", | ||||||
|  | 	"version": "1.0.0", | ||||||
|  | 	"main": "dist/index.js", | ||||||
|  | 	"type": "module", | ||||||
|  | 	"scripts": { | ||||||
|  | 		"build": "tsc", | ||||||
|  | 		"prestart": "npm run build", | ||||||
|  | 		"start": "node .", | ||||||
|  | 		"test": "echo \"Error: no test specified\" && exit 1" | ||||||
|  | 	}, | ||||||
|  | 	"author": "[Project-Name-Here]", | ||||||
|  | 	"license": "GPL-3.0", | ||||||
|  | 	"description": "HydrationHUB", | ||||||
|  | 	"repository": { | ||||||
|  | 		"type": "git", | ||||||
|  | 		"url": "https://git.project-name-here.de/Project-Name-Here/hydrationhub" | ||||||
|  | 	}, | ||||||
|  | 	"keywords": [ | ||||||
|  | 		"hydration", | ||||||
|  | 		"pos" | ||||||
|  | 	], | ||||||
|  | 	"devDependencies": { | ||||||
|  | 		"@types/cors": "^2.8.17", | ||||||
|  | 		"@types/express": "^5.0.0", | ||||||
|  | 		"@types/express-fileupload": "^1.5.1", | ||||||
|  | 		"@types/express-session": "^1.18.1", | ||||||
|  | 		"@types/joi": "^17.2.2", | ||||||
|  | 		"@types/lodash": "^4.17.14", | ||||||
|  | 		"@types/node": "^22.10.5", | ||||||
|  | 		"@types/passport": "^1.0.17", | ||||||
|  | 		"@types/passport-local": "^1.0.38", | ||||||
|  | 		"@types/signale": "^1.4.7", | ||||||
|  | 		"eslint": "^9.18.0", | ||||||
|  | 		"eslint-config-prettier": "^9.1.0", | ||||||
|  | 		"prisma": "^6.2.1", | ||||||
|  | 		"tsx": "^3.12.10", | ||||||
|  | 		"typescript": "^5.7.3" | ||||||
|  | 	}, | ||||||
|  | 	"dependencies": { | ||||||
|  | 		"@prisma/client": "^6.4.0", | ||||||
|  | 		"bootstrap-icons": "^1.11.3", | ||||||
|  | 		"bulma": "^1.0.3", | ||||||
|  | 		"eta": "^3.5.0", | ||||||
|  | 		"express": "^4.21.2", | ||||||
|  | 		"express-fileupload": "^1.5.1", | ||||||
|  | 		"helmet": "^8.0.0", | ||||||
|  | 		"joi": "^17.13.3", | ||||||
|  | 		"jquery": "^3.7.1", | ||||||
|  | 		"lodash": "^4.17.21", | ||||||
|  | 		"tslog": "^4.9.3" | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								prisma/schema.prisma
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								prisma/schema.prisma
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | // This is your Prisma schema file, | ||||||
|  | // learn more about it in the docs: https://pris.ly/d/prisma-schema | ||||||
|  |  | ||||||
|  | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? | ||||||
|  | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init | ||||||
|  |  | ||||||
|  | generator client { | ||||||
|  |   provider = "prisma-client-js" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | datasource db { | ||||||
|  |   provider = "mysql" | ||||||
|  |   url      = env("DATABASE_URL") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | model user { | ||||||
|  |   id   Int    @id @unique @default(autoincrement()) | ||||||
|  |   name String @unique | ||||||
|  |   code String? | ||||||
|  |  | ||||||
|  |   // TODO: Prüfen ob nötig, erstmal vorbereitet. | ||||||
|  |   transactions transactions[] | ||||||
|  |  | ||||||
|  |   @@fulltext([name]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | model transactions { | ||||||
|  |   id Int @id @unique @default(autoincrement()) | ||||||
|  |  | ||||||
|  |   sales sales[] | ||||||
|  |  | ||||||
|  |   user   user @relation(fields: [userId], references: [id]) | ||||||
|  |   userId Int | ||||||
|  |  | ||||||
|  |   total Float | ||||||
|  |   paid      Boolean @default(false) | ||||||
|  |   paidAt    Boolean? | ||||||
|  |   createdAt DateTime @default(now()) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | model sales { | ||||||
|  |   id             Int          @id @unique @default(autoincrement()) | ||||||
|  |   transactions   transactions @relation(fields: [transactionsId], references: [id]) | ||||||
|  |   transactionsId Int | ||||||
|  |  | ||||||
|  |   product   products? @relation(fields: [productId], references: [id]) | ||||||
|  |   productId Int? | ||||||
|  |  | ||||||
|  |   price    Float | ||||||
|  | } | ||||||
|  |  | ||||||
|  | model products { | ||||||
|  |   id   Int    @id @unique @default(autoincrement()) | ||||||
|  |   name String @unique | ||||||
|  |   price Float | ||||||
|  |   stock Int | ||||||
|  |   visible Boolean @default(true) | ||||||
|  |  | ||||||
|  |   sales   sales[] | ||||||
|  |  | ||||||
|  |   @@fulltext([name]) | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								src/handlers/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/handlers/config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | import ConfigManager from '../libs/configManager.js'; | ||||||
|  | import __path from './path.js'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import log from './log.js'; | ||||||
|  |  | ||||||
|  | // Create a new config instance. | ||||||
|  | const config = new ConfigManager(__path + '/config.json', true, { | ||||||
|  | 	db_connection_string: 'mysql://USER:PASSWORD@HOST:3306/DATABASE', | ||||||
|  | 	http_listen_address: '0.0.0.0', | ||||||
|  | 	http_port: 3000, | ||||||
|  | 	http_domain: 'example.org', | ||||||
|  | 	http_enable_hsts: false, | ||||||
|  | 	devmode: true | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | !config.global.devmode && log.core.error('devmode active! Do NOT use this in prod!'); | ||||||
|  |  | ||||||
|  | export default config; | ||||||
							
								
								
									
										40
									
								
								src/handlers/db.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/handlers/db.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | import { PrismaClient, Prisma } from '@prisma/client'; // Database | ||||||
|  | import { Response } from 'express'; | ||||||
|  | import config from './config.js'; | ||||||
|  | import log from './log.js'; | ||||||
|  |  | ||||||
|  | // TODO: Add errorhandling with some sort of message. | ||||||
|  | const prisma = new PrismaClient({ | ||||||
|  | 	datasources: { | ||||||
|  | 		db: { | ||||||
|  | 			url: config.global.db_connection_string | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // FIXME: any | ||||||
|  | export function handlePrismaError(errorObj: any, res: Response, source: string) { | ||||||
|  | 	log.db.error(source, errorObj); | ||||||
|  | 	if (errorObj instanceof Prisma.PrismaClientKnownRequestError) { | ||||||
|  | 		switch (errorObj.code) { | ||||||
|  |  | ||||||
|  | 			// P2002 -> "Unique constraint failed on the {constraint}" | ||||||
|  | 			case 'P2002': | ||||||
|  | 				res.status(409).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'The object needs to be unique', meta: errorObj.meta }); | ||||||
|  | 				break; | ||||||
|  |  | ||||||
|  | 			// P2003 -> "Foreign key constraint failed on the field: {field_name}" | ||||||
|  | 			case 'P2003': | ||||||
|  | 				res.status(404).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'Relation object does not exist', meta: errorObj.meta }); | ||||||
|  | 				break; | ||||||
|  |  | ||||||
|  | 			default: | ||||||
|  | 				res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'An error occurred during the database operation' }); | ||||||
|  | 				break; | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'If you can read this something went terribly wrong!' }); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default prisma; | ||||||
							
								
								
									
										52
									
								
								src/handlers/log.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/handlers/log.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | import { Logger,ISettingsParam } from "tslog"; | ||||||
|  |  | ||||||
|  | function loggerConfig(name: string): ISettingsParam<unknown> { | ||||||
|  | 	return { | ||||||
|  | 		type: "pretty", // pretty, json, hidden | ||||||
|  | 		name: name, | ||||||
|  | 		hideLogPositionForProduction: true, | ||||||
|  | 		prettyLogTemplate: "{{dateIsoStr}} {{logLevelName}} {{nameWithDelimiterPrefix}} " | ||||||
|  | 	 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type log = { | ||||||
|  | 	core: Logger<unknown> | ||||||
|  | 	db: Logger<unknown> | ||||||
|  | 	web: Logger<unknown> | ||||||
|  | 	S3: Logger<unknown> | ||||||
|  | 	auth: Logger<unknown> | ||||||
|  | 	api?: Logger<unknown> | ||||||
|  | 	frontend?: Logger<unknown> | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // FIXME: any type | ||||||
|  | let log: log = { | ||||||
|  | 	core: new Logger(loggerConfig("Core")), | ||||||
|  | 	db: new Logger(loggerConfig("DB")), | ||||||
|  | 	web: new Logger(loggerConfig("Web")), | ||||||
|  | 	S3: new Logger(loggerConfig("S3")), | ||||||
|  | 	auth: new Logger(loggerConfig("Auth")), | ||||||
|  | //	helper: new Logger(loggerConfig("HELPER")), | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | log["api"] = log.web.getSubLogger({ name: "API" }); | ||||||
|  | log["frontend"] = log.web.getSubLogger({ name: "Frontend" }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // log.core.silly("Hello from core"); | ||||||
|  | //log.api.trace("Hello from api"); | ||||||
|  | //log.frontend.trace("Hello from frontend"); | ||||||
|  | // log.core.debug("Hello from core"); | ||||||
|  | // log.core.info("Hello from core"); | ||||||
|  | // log.core.warn("Hello from core"); | ||||||
|  | // log.core.error("Hello from core"); | ||||||
|  | // log.db.silly("Hello from db"); | ||||||
|  | // log.db.trace("Hello from db"); | ||||||
|  | // log.web.debug("Hello from db"); | ||||||
|  | // log.auth.info("Hello from db"); | ||||||
|  | // log.helper.warn("Hello from db"); | ||||||
|  | // log.db.error("Hello from db"); | ||||||
|  | // log.core.fatal(new Error("I am a pretty Error with a stacktrace.")); | ||||||
|  |  | ||||||
|  | export default log; | ||||||
							
								
								
									
										4
									
								
								src/handlers/path.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/handlers/path.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | // Return the app directory as an absolute path | ||||||
|  | const __path = process.argv[1]; | ||||||
|  |  | ||||||
|  | export default __path; | ||||||
							
								
								
									
										22
									
								
								src/helpers/prisma_helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/helpers/prisma_helpers.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | /** | ||||||
|  |  * A function to create a sortBy compatible object from a string | ||||||
|  |  * | ||||||
|  |  * @export | ||||||
|  |  * @param {string} SortField | ||||||
|  |  * @param {string} Order | ||||||
|  |  * @returns {object} | ||||||
|  |  */ | ||||||
|  | export function parseDynamicSortBy(SortField: string, Order: string) { | ||||||
|  | 	return JSON.parse(`{ "${SortField}": "${Order}" }`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Function to parse a string into a number or return undefined if it is not a number | ||||||
|  |  * Deprecated since all empty strings in bodys are now undefined. This happens in api/v1 router | ||||||
|  |  * @export | ||||||
|  |  * @param {string || any} data | ||||||
|  |  * @returns {object} | ||||||
|  |  */ | ||||||
|  | export function parseIntOrUndefined(data: any) { | ||||||
|  | 	return isNaN(parseInt(data)) ? undefined : parseInt(data); | ||||||
|  | } | ||||||
							
								
								
									
										109
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | |||||||
|  | // MARK: Imports | ||||||
|  | import path from 'node:path'; | ||||||
|  | import __path from './handlers/path.js'; | ||||||
|  | import log from './handlers/log.js'; | ||||||
|  | import db from './handlers/db.js'; | ||||||
|  | import config from './handlers/config.js'; | ||||||
|  |  | ||||||
|  | // Express & more | ||||||
|  | import express from 'express'; | ||||||
|  | import cors from 'cors'; | ||||||
|  | import helmet from 'helmet'; | ||||||
|  | import session from 'express-session'; | ||||||
|  | import fileUpload from 'express-fileupload'; | ||||||
|  | import bodyParser, { Options } from 'body-parser'; | ||||||
|  | import { Eta } from 'eta'; | ||||||
|  | import passport from 'passport'; | ||||||
|  |  | ||||||
|  | import ChildProcess from 'child_process'; | ||||||
|  |  | ||||||
|  | import routes from './routes/index.js'; | ||||||
|  |  | ||||||
|  | import fs from 'node:fs'; | ||||||
|  |  | ||||||
|  | log.core.trace('Running from path: ' + __path); | ||||||
|  |  | ||||||
|  | // MARK: Express | ||||||
|  | const app = express(); | ||||||
|  |  | ||||||
|  | // Versioning | ||||||
|  | try { | ||||||
|  | 	const rawPkg = fs.readFileSync('package.json', 'utf8'); | ||||||
|  | 	const pkgJson = JSON.parse(rawPkg); | ||||||
|  | 	app.locals.version = pkgJson.version; | ||||||
|  | } catch (error) { | ||||||
|  | 	log.core.error('Failed to get version from package.json.'); | ||||||
|  | 	app.locals.version = '0.0.0'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | try { | ||||||
|  | 	app.locals.versionRevLong = ChildProcess.execSync('git rev-parse HEAD').toString().trim(); | ||||||
|  | 	app.locals.versionRev = app.locals.versionRevLong.substring(0, 7); | ||||||
|  | } catch (error) { | ||||||
|  | 	log.core.error('Failed to get git revision hash.'); | ||||||
|  | 	app.locals.versionRev = '0'; | ||||||
|  | 	app.locals.versionRevLong = '0'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | try { | ||||||
|  | 	app.locals.versionRevLatest = ChildProcess.execSync('git ls-remote --refs -q').toString().trim().split('\t')[0]; | ||||||
|  | } catch (error) { | ||||||
|  | 	log.core.error('Failed to get latest git revision hash.'); | ||||||
|  | 	app.locals.versionRevLatest = '0'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | app.locals.versionUpdateAvailable = false; | ||||||
|  | if (app.locals.versionRevLong === app.locals.versionRevLatest) { | ||||||
|  | 	log.core.info(`Running Latest Version (${app.locals.versionRevLong}; ${app.locals.version})`); | ||||||
|  | } else { | ||||||
|  | 	log.core.info(`Running Version: ${app.locals.versionRevLong}; ${app.locals.version} (Latest: ${app.locals.versionRevLatest})`); | ||||||
|  | 	app.locals.versionUpdateAvailable = true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ETA Init | ||||||
|  | const eta = new Eta({ views: path.join(__path, 'views') }); | ||||||
|  | app.engine('eta', buildEtaEngine()); | ||||||
|  | app.set('view engine', 'eta'); | ||||||
|  |  | ||||||
|  | // MARK: Express Middleware & Config | ||||||
|  | app.set('x-powered-by', false); // helmet does this too. But not in devmode | ||||||
|  | if (!config.global.devmode) { | ||||||
|  | 	app.use( | ||||||
|  | 		helmet({ | ||||||
|  | 			strictTransportSecurity: config.global.http_enable_hsts, | ||||||
|  | 			contentSecurityPolicy: { | ||||||
|  | 				useDefaults: false, | ||||||
|  | 				directives: { | ||||||
|  | 					defaultSrc: ["'self'"], | ||||||
|  | 					scriptSrc: ["'self'", config.global.http_domain], | ||||||
|  | 					objectSrc: ["'none'"], | ||||||
|  | 					upgradeInsecureRequests: config.global.devmode ? null : [] | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	); // Add headers | ||||||
|  | } | ||||||
|  |  | ||||||
|  | app.use(fileUpload()); | ||||||
|  | app.use(bodyParser.urlencoded({ extended: false })); | ||||||
|  | app.use(bodyParser.json()); | ||||||
|  |  | ||||||
|  | app.use(routes); | ||||||
|  |  | ||||||
|  | // TODO: Remove hardcoded http | ||||||
|  | app.listen(config.global.http_port, config.global.http_listen_address, () => { | ||||||
|  | 	log.web.info(`Listening at http://${config.global.http_listen_address}:${config.global.http_port}`); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // MARK: Helper Functions | ||||||
|  | function buildEtaEngine() { | ||||||
|  | 	return (path: string, opts: Options, callback: CallableFunction) => { | ||||||
|  | 		try { | ||||||
|  | 			const fileContent = eta.readFile(path); | ||||||
|  | 			const renderedTemplate = eta.renderString(fileContent, opts); | ||||||
|  | 			callback(null, renderedTemplate); | ||||||
|  | 		} catch (error) { | ||||||
|  | 			callback(error); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | } | ||||||
							
								
								
									
										140
									
								
								src/libs/configManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/libs/configManager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | import fs from 'node:fs'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import { randomUUID, randomBytes } from 'crypto'; | ||||||
|  |  | ||||||
|  | export type configObject = Record<any, any>; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This class is responsible to save/edit config files. | ||||||
|  |  * | ||||||
|  |  * @export | ||||||
|  |  * @class config | ||||||
|  |  * @typedef {config} | ||||||
|  |  */ | ||||||
|  | export default class config { | ||||||
|  | 	#configPath: string; | ||||||
|  | 	//global = {[key: string] : string} | ||||||
|  | 	global: configObject; | ||||||
|  | 	replaceSecrets: boolean; | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Creates an instance of config. | ||||||
|  | 	 * | ||||||
|  | 	 * @constructor | ||||||
|  | 	 * @param {string} configPath Path to config file. | ||||||
|  | 	 * @param {object} configPreset Default config object with default values. | ||||||
|  | 	 */ | ||||||
|  | 	constructor(configPath: string, replaceSecrets: boolean, configPreset: object) { | ||||||
|  | 		this.#configPath = configPath; | ||||||
|  | 		this.global = configPreset; | ||||||
|  | 		this.replaceSecrets = replaceSecrets; | ||||||
|  |  | ||||||
|  | 		try { | ||||||
|  | 			// Read config | ||||||
|  | 			const data = fs.readFileSync(this.#configPath, 'utf8'); | ||||||
|  |  | ||||||
|  | 			// Extend config with missing parameters from configPreset. | ||||||
|  | 			this.global = _.defaultsDeep(JSON.parse(data), this.global); | ||||||
|  | 			// Save config. | ||||||
|  | 			this.save_config(); | ||||||
|  | 		} catch (err: any) { | ||||||
|  | 			// If file does not exist, create it. | ||||||
|  | 			if (err.code === 'ENOENT') { | ||||||
|  | 				console.log(`Config file does not exist. Creating it at ${this.#configPath} now.`); | ||||||
|  | 				this.save_config(); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			console.error(`Could not read config file at ${this.#configPath} due to: ${err}`); | ||||||
|  | 			// Exit process. | ||||||
|  | 			process.exit(1); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Saves the jsonified config object to the config file. | ||||||
|  | 	 */ | ||||||
|  | 	save_config() { | ||||||
|  | 		try { | ||||||
|  | 			// If enabled replace tokens defines as "gen" with random token | ||||||
|  | 			if (this.replaceSecrets) { | ||||||
|  | 				// Replace tokens with value "gen" | ||||||
|  | 				this.generate_secrets(this.global, 'gen') | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			fs.writeFileSync(this.#configPath, JSON.stringify(this.global, null, 8)); | ||||||
|  | 		} catch (err) { | ||||||
|  | 			console.error(`Could not write config file at ${this.#configPath} due to: ${err}`); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		console.log(`Successfully written config file to ${this.#configPath}`); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Replaces each item matching the value of placeholder with a random UUID. | ||||||
|  | 	 * Thanks to https://stackoverflow.com/questions/8085004/iterate-through-nested-javascript-objects | ||||||
|  | 	 * @param {configObject} obj | ||||||
|  | 	 */ | ||||||
|  | 	generate_secrets(obj: configObject, placeholder: string) { | ||||||
|  | 		const stack = [obj]; | ||||||
|  | 		while (stack?.length > 0) { | ||||||
|  | 			const currentObj:any = stack.pop(); | ||||||
|  | 			Object.keys(currentObj).forEach((key) => { | ||||||
|  |  | ||||||
|  | 				if (currentObj[key] === placeholder) { | ||||||
|  | 					console.log('Generating secret: ' + key); | ||||||
|  | 					currentObj[key] = randomBytes(48).toString('base64').replace(/\W/g, ''); | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if (typeof currentObj[key] === 'object' && currentObj[key] !== null) { | ||||||
|  | 					stack.push(currentObj[key]); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /*  | ||||||
|  |  | ||||||
|  | **** Example **** | ||||||
|  |  | ||||||
|  | import ConfigHandlerNG from './assets/configHandlerNG.js'; | ||||||
|  |  | ||||||
|  | // Create a new config instance. | ||||||
|  | export const config = new ConfigHandler(__path + '/config.json', true, { | ||||||
|  | 	test1: 't1', | ||||||
|  | 	test2: 't2', | ||||||
|  | 	test3: 'gen', | ||||||
|  | 	test4: 't4', | ||||||
|  | 	test5: 'gen', | ||||||
|  | 	testObj: { | ||||||
|  | 		local: { | ||||||
|  | 			active: true, | ||||||
|  | 			users: { | ||||||
|  | 				user1: 'gen', | ||||||
|  | 				user2: 'gen', | ||||||
|  | 				user3: 'gen', | ||||||
|  | 				user4: 'gen', | ||||||
|  |  | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 		oidc: { | ||||||
|  | 			active: false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | console.log('Base Config:'); | ||||||
|  | console.log(config.global); | ||||||
|  |  | ||||||
|  | console.log('Add some new key to config and call save_config().'); | ||||||
|  | config.global.NewKey = 'ThisIsANewKey!' | ||||||
|  | config.save_config() | ||||||
|  |  | ||||||
|  | console.log('This will add a new key with value gen, but gen gets replaced with a random UUID when save_config() is called.'); | ||||||
|  | config.global.someSecret = 'gen' | ||||||
|  | config.save_config() // global.someSecret is getting replaced with some random UUID since it was set to 'gen'. | ||||||
|  |  | ||||||
|  | console.log('Complete Config:'); | ||||||
|  | console.log(config.global); | ||||||
|  | */ | ||||||
							
								
								
									
										11
									
								
								src/routes/api/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/routes/api/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | import express from 'express'; | ||||||
|  |  | ||||||
|  | // Route imports | ||||||
|  | import v1_routes from './v1/index.js'; | ||||||
|  |  | ||||||
|  | // Router base is '/api' | ||||||
|  | const Router = express.Router({ strict: false }); | ||||||
|  |  | ||||||
|  | Router.use('/v1', v1_routes); | ||||||
|  |  | ||||||
|  | export default Router; | ||||||
							
								
								
									
										51
									
								
								src/routes/api/v1/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/routes/api/v1/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | import express from 'express'; | ||||||
|  | import passport from 'passport'; | ||||||
|  |  | ||||||
|  | // Route imports | ||||||
|  | import testRoute from './test.js'; | ||||||
|  | import versionRoute from './version.js' | ||||||
|  |  | ||||||
|  | import userRoute from './user.js'; | ||||||
|  | import userRoute_schema from './user_schema.js'; | ||||||
|  |  | ||||||
|  | // import content_route from './content.js'; | ||||||
|  | // import content_schema from './content_schema.js'; | ||||||
|  |  | ||||||
|  | // import * as content_s3_sub_route from './content_s3_sub.js'; | ||||||
|  | // import * as content_s3_sub_schema from './content_s3_sub_schema.js'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // Router base is '/api/v1' | ||||||
|  | const Router = express.Router({ strict: false }); | ||||||
|  |  | ||||||
|  | // All empty strings are undefined (not null!) values (body) | ||||||
|  | Router.use('*', function (req, res, next) { | ||||||
|  | 	for (let key in req.body) { | ||||||
|  | 		if (req.body[key] === '') { | ||||||
|  | 			req.body[key] = undefined; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	next(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // All api routes lowercase! Yea I know but when strict: true it matters. | ||||||
|  | Router.route('/user').get(userRoute.get).post(userRoute.post).patch(userRoute.patch).delete(userRoute.del); | ||||||
|  | Router.route('/user/describe').get(userRoute_schema); | ||||||
|  |  | ||||||
|  | // Router.route('/content').get(content_route.get).post(content_route.post).patch(content_route.patch).delete(content_route.del); | ||||||
|  | // Router.route('/content/describe').get(content_schema); | ||||||
|  |  | ||||||
|  | // Router.route('/content/downloadurl').get(content_s3_sub_route.get_downloadurl); | ||||||
|  | // Router.route('/content/uploadurl').get(content_s3_sub_route.get_uploadurl); | ||||||
|  | // Router.route('/content/downloadurl/describe').get(content_s3_sub_schema.get_describe_downloadurl); | ||||||
|  | // Router.route('/content/uploadurl/describe').get(content_s3_sub_schema.get_describe_uploadurl); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Router.route('/version').get(versionRoute.get); | ||||||
|  | //Router.use('/search', search_routes); | ||||||
|  |  | ||||||
|  | Router.route('/test').get(testRoute.get); | ||||||
|  |  | ||||||
|  | export default Router; | ||||||
							
								
								
									
										7
									
								
								src/routes/api/v1/test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/routes/api/v1/test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import express, { Request, Response } from 'express'; | ||||||
|  |  | ||||||
|  | function get(req: Request, res: Response) { | ||||||
|  | 	res.status(200).send('API v1 Test successful!'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default { get }; | ||||||
							
								
								
									
										165
									
								
								src/routes/api/v1/user.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								src/routes/api/v1/user.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | |||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import db, { handlePrismaError } from '../../../handlers/db.js'; // Database | ||||||
|  | import log from '../../../handlers/log.js'; | ||||||
|  | import { parseDynamicSortBy } from '../../../helpers/prisma_helpers.js'; | ||||||
|  | import { schema_get, schema_post, schema_patch, schema_del } from './user_schema.js'; | ||||||
|  |  | ||||||
|  | // MARK: GET user | ||||||
|  | async function get(req: Request, res: Response) { | ||||||
|  | 	const { error, value } = schema_get.validate(req.query); | ||||||
|  | 	if (error) { | ||||||
|  | 		log.api?.debug('GET user Error:', req.query, value, error.details[0].message); | ||||||
|  | 		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message }); | ||||||
|  | 	} else { | ||||||
|  | 		log.api?.debug('GET user Success:', req.query, value); | ||||||
|  |  | ||||||
|  | 		if (value.search !== undefined || value.id !== undefined) { | ||||||
|  | 			// if search or get by id | ||||||
|  | 			await db | ||||||
|  | 				.$transaction([ | ||||||
|  | 					// Same query for count and findMany | ||||||
|  | 					db.user.count({ | ||||||
|  | 						where: { | ||||||
|  | 							OR: [{ id: value.id }, { name: { search: value.search } }] | ||||||
|  | 						}, | ||||||
|  | 						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()), | ||||||
|  | 						skip: value.skip, | ||||||
|  | 						take: value.take | ||||||
|  | 					}), | ||||||
|  | 					db.user.findMany({ | ||||||
|  | 						where: { | ||||||
|  | 							OR: [{ id: value.id }, { name: { search: value.search } }] | ||||||
|  | 						}, | ||||||
|  | 						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()), | ||||||
|  | 						skip: value.skip, | ||||||
|  | 						take: value.take | ||||||
|  | 					}) | ||||||
|  | 				]) | ||||||
|  | 				.then(([count, result]) => { | ||||||
|  | 					if (result.length !== 0) { | ||||||
|  | 						result.forEach((element: { id: number; name: string; code: string | null | boolean }) => { | ||||||
|  | 							// code-> true if code is set | ||||||
|  | 							element.code = element.code !== null; | ||||||
|  | 						}); | ||||||
|  | 						res.status(200).json({ count, result }); | ||||||
|  | 					} else { | ||||||
|  | 						res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified object' }); | ||||||
|  | 					} | ||||||
|  | 				}) | ||||||
|  | 				.catch((err) => { | ||||||
|  | 					handlePrismaError(err, res, 'GET user'); | ||||||
|  | 				}); | ||||||
|  | 		} else { | ||||||
|  | 			// get all | ||||||
|  | 			await db | ||||||
|  | 				.$transaction([ | ||||||
|  | 					// Same query for count and findMany | ||||||
|  | 					db.user.count({ | ||||||
|  | 						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()), | ||||||
|  | 						skip: value.skip, | ||||||
|  | 						take: value.take | ||||||
|  | 					}), | ||||||
|  | 					db.user.findMany({ | ||||||
|  | 						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()), | ||||||
|  | 						skip: value.skip, | ||||||
|  | 						take: value.take | ||||||
|  | 					}) | ||||||
|  | 				]) | ||||||
|  | 				.then(([count, result]) => { | ||||||
|  | 					if (result.length !== 0) { | ||||||
|  | 						result.forEach((element: { id: number; name: string; code: string | null | boolean }) => { | ||||||
|  | 							// code-> true if code is set | ||||||
|  | 							element.code = element.code !== null; | ||||||
|  | 						}); | ||||||
|  | 						res.status(200).json({ count, result }); | ||||||
|  | 					} else { | ||||||
|  | 						res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified object' }); | ||||||
|  | 					} | ||||||
|  | 				}) | ||||||
|  | 				.catch((err) => { | ||||||
|  | 					handlePrismaError(err, res, 'GET user'); | ||||||
|  | 				}); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MARK: CREATE user | ||||||
|  | async function post(req: Request, res: Response) { | ||||||
|  | 	const { error, value } = schema_post.validate(req.body); | ||||||
|  | 	if (error) { | ||||||
|  | 		log.api?.debug('POST user Error:', req.body, value, error.details[0].message); | ||||||
|  | 		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message }); | ||||||
|  | 	} else { | ||||||
|  | 		log.api?.debug('POST user Success:', req.body, value); | ||||||
|  | 		await db.user | ||||||
|  | 			.create({ | ||||||
|  | 				data: { | ||||||
|  | 					name: value.name, | ||||||
|  | 					code: value.code | ||||||
|  | 				}, | ||||||
|  | 				select: { | ||||||
|  | 					id: true | ||||||
|  | 				} | ||||||
|  | 			}) | ||||||
|  | 			.then((result) => { | ||||||
|  | 				res.status(201).json({ status: 'CREATED', message: 'Successfully created user', id: result.id }); | ||||||
|  | 			}) | ||||||
|  | 			.catch((err) => { | ||||||
|  | 				handlePrismaError(err, res, 'POST user'); | ||||||
|  | 			}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MARK: UPDATE user | ||||||
|  | async function patch(req: Request, res: Response) { | ||||||
|  | 	const { error, value } = schema_patch.validate(req.body); | ||||||
|  | 	if (error) { | ||||||
|  | 		log.api?.debug('PATCH user Error:', req.body, value, error.details[0].message); | ||||||
|  | 		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message }); | ||||||
|  | 	} else { | ||||||
|  | 		log.api?.debug('PATCH user Success:', req.body, value); | ||||||
|  | 		await db.user | ||||||
|  | 			.update({ | ||||||
|  | 				where: { | ||||||
|  | 					id: value.id | ||||||
|  | 				}, | ||||||
|  | 				data: { | ||||||
|  | 					name: value.name, | ||||||
|  | 					code: value.code | ||||||
|  | 				}, | ||||||
|  | 				select: { | ||||||
|  | 					id: true | ||||||
|  | 				} | ||||||
|  | 			}) | ||||||
|  | 			.then((result) => { | ||||||
|  | 				res.status(200).json({ status: 'UPDATED', message: 'Successfully updated user', id: result.id }); | ||||||
|  | 			}) | ||||||
|  | 			.catch((err) => { | ||||||
|  | 				handlePrismaError(err, res, 'PATCH user'); | ||||||
|  | 			}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MARK: DELETE user | ||||||
|  | async function del(req: Request, res: Response) { | ||||||
|  | 	const { error, value } = schema_del.validate(req.body); | ||||||
|  | 	if (error) { | ||||||
|  | 		log.api?.debug('DEL user Error:', req.body, value, error.details[0].message); | ||||||
|  | 		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message }); | ||||||
|  | 	} else { | ||||||
|  | 		log.api?.debug('DEL user Success:', req.body, value); | ||||||
|  | 		await db.user | ||||||
|  | 			.delete({ | ||||||
|  | 				where: { | ||||||
|  | 					id: value.id | ||||||
|  | 				} | ||||||
|  | 			}) | ||||||
|  | 			.then((result) => { | ||||||
|  | 				res.status(200).json({ status: 'DELETED', message: 'Successfully deleted user', id: result.id }); | ||||||
|  | 			}).catch((err) => { | ||||||
|  | 				handlePrismaError(err, res, 'DEL user'); | ||||||
|  | 			}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { get, post, patch, del }; | ||||||
							
								
								
									
										58
									
								
								src/routes/api/v1/user_schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/routes/api/v1/user_schema.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import validator from 'joi'; // DOCS: https://joi.dev/api | ||||||
|  | import { Prisma } from '@prisma/client'; | ||||||
|  |  | ||||||
|  | // MARK: GET user | ||||||
|  | const schema_get = validator | ||||||
|  | 	.object({ | ||||||
|  | 		sort: validator | ||||||
|  | 			.string() | ||||||
|  | 			.valid(...Object.keys(Prisma.UserScalarFieldEnum)) | ||||||
|  | 			.default('id'), | ||||||
|  |  | ||||||
|  | 		order: validator.string().valid('asc', 'desc').default('asc'), | ||||||
|  | 		take: validator.number().min(1).max(512), | ||||||
|  | 		skip: validator.number().min(0), | ||||||
|  | 		// This regex ensures that the search string does not contain consecutive asterisks (**) and is at least 3 characters long. | ||||||
|  | 		search: validator.string().min(3).max(20).regex(new RegExp('^(?!.*\\*{2,}).*$')), | ||||||
|  | 		id: validator.number().positive().precision(0) | ||||||
|  | 	}) | ||||||
|  | 	.nand('id', 'search'); // Allow id or search. not both. | ||||||
|  |  | ||||||
|  | // MARK: CREATE alertContact | ||||||
|  | const schema_post = validator.object({ | ||||||
|  | 	name: validator.string().min(1).max(32).required(), | ||||||
|  | 	code: validator.string().min(4).max(4).trim().regex(new RegExp('/^[0-9]+$/')) | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // MARK: UPDATE alertContact | ||||||
|  | const schema_patch = validator | ||||||
|  | 	.object({ | ||||||
|  | 		id: validator.number().positive().precision(0).required(), | ||||||
|  | 		name: validator.string().min(1).max(32), | ||||||
|  | 		code: validator.string().min(4).max(4).trim().regex(new RegExp('/^[0-9]+$/')) | ||||||
|  | 	}) | ||||||
|  | 	.or('name', 'code'); | ||||||
|  |  | ||||||
|  | // MARK: DELETE alertContact | ||||||
|  | const schema_del = validator.object({ | ||||||
|  | 	id: validator.number().positive().precision(0).required() | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // Describe all schemas | ||||||
|  | const schema_get_desc = schema_get.describe(); | ||||||
|  | const schema_post_desc = schema_post.describe(); | ||||||
|  | const schema_patch_desc = schema_patch.describe(); | ||||||
|  | const schema_del_desc = schema_del.describe(); | ||||||
|  |  | ||||||
|  | // GET route | ||||||
|  | export default async function get(req: Request, res: Response) { | ||||||
|  | 	res.status(200).json({ | ||||||
|  | 		GET: schema_get_desc, | ||||||
|  | 		POST: schema_post_desc, | ||||||
|  | 		PATCH: schema_patch_desc, | ||||||
|  | 		DELETE: schema_del_desc | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { schema_get, schema_post, schema_patch, schema_del }; | ||||||
							
								
								
									
										9
									
								
								src/routes/api/v1/version.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/routes/api/v1/version.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | import { Request, Response } from 'express'; | ||||||
|  |  | ||||||
|  | function get(req: Request, res: Response) { | ||||||
|  | 	res.status(200).send({ version: '1.0.0', commit: req.app.locals.versionRev, updateAvailable: req.app.locals.versionUpdateAvailable }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default { get }; | ||||||
|  |  | ||||||
|  | // TODO: FIXME!!!!!! | ||||||
							
								
								
									
										7
									
								
								src/routes/frontend/contact.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/routes/frontend/contact.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import express, { Request, Response } from 'express'; | ||||||
|  |  | ||||||
|  | function get(req: Request, res: Response) { | ||||||
|  | 	res.render("contacts") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { get }; | ||||||
							
								
								
									
										7
									
								
								src/routes/frontend/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/routes/frontend/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import express, { Request, Response } from 'express'; | ||||||
|  |  | ||||||
|  | function get(req: Request, res: Response) { | ||||||
|  | 	res.render("lockscreen", { message: "Hello world from eta!" }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { get }; | ||||||
							
								
								
									
										25
									
								
								src/routes/frontend/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/routes/frontend/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | import express from 'express'; | ||||||
|  |  | ||||||
|  | // Route imports | ||||||
|  | import dashboardRoute from './dashboard.js'; | ||||||
|  | import testRoute from './test.js'; | ||||||
|  | import contactRoute from './contact.js'; | ||||||
|  | // import itemsRoute from './items.js'; | ||||||
|  | // import manage_routes from './manage/index.js'; | ||||||
|  |  | ||||||
|  | // Router base is '/' | ||||||
|  | const Router = express.Router({ strict: false }); | ||||||
|  |  | ||||||
|  | // Router.route('/test').get(testRoute.get); | ||||||
|  | // Router.route('/items').get(itemsRoute.get); | ||||||
|  |  | ||||||
|  | // Router.route('/:id(\\w{8})').get(skuRoute.get); | ||||||
|  | // Router.route('/s/:id').get(skuRouteDash.get); | ||||||
|  |  | ||||||
|  | // Router.use('/manage', manage_routes); | ||||||
|  |  | ||||||
|  | Router.route('/').get(dashboardRoute.get); | ||||||
|  | Router.route('/dbTest').get(testRoute.get); | ||||||
|  | Router.route('/contact').get(contactRoute.get); | ||||||
|  |  | ||||||
|  | export default Router; | ||||||
							
								
								
									
										7
									
								
								src/routes/frontend/test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/routes/frontend/test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import express, { Request, Response } from 'express'; | ||||||
|  |  | ||||||
|  | function get(req: Request, res: Response) { | ||||||
|  | 	res.render("test", { message: "Hello world from eta!" }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { get }; | ||||||
							
								
								
									
										34
									
								
								src/routes/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/routes/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | import express from 'express'; | ||||||
|  | import path from 'node:path'; | ||||||
|  | import __path from "../handlers/path.js"; | ||||||
|  | import log from "../handlers/log.js"; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // Route imports | ||||||
|  | import frontend_routes from './frontend/index.js'; | ||||||
|  | import api_routes from './api/index.js'; | ||||||
|  |  | ||||||
|  | const Router = express.Router({ strict: false }); | ||||||
|  |  | ||||||
|  | // static / libs routes | ||||||
|  | Router.use('/static', express.static(__path + '/static')); | ||||||
|  | Router.use('/libs/bulma', express.static(path.join(__path, 'node_modules', 'bulma', 'css'))); // http://192.168.221.10:3000/libs/bulma/bulma.css | ||||||
|  | Router.use('/libs/jquery', express.static(path.join(__path, 'node_modules', 'jquery', 'dist'))); | ||||||
|  | Router.use('/libs/bootstrap-icons', express.static(path.join(__path, 'node_modules', 'bootstrap-icons'))); | ||||||
|  |  | ||||||
|  | // Other routers | ||||||
|  | Router.use('/api', api_routes); | ||||||
|  | Router.use('/', frontend_routes); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // Default route. | ||||||
|  | Router.all('*', function (req, res) { | ||||||
|  | 	// TODO: Respond based on content-type (with req.is('application/json')) | ||||||
|  | 	if (req.is('application/json')) { | ||||||
|  | 		res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified page' }); | ||||||
|  | 	} else { | ||||||
|  | 		res.status(404).render('errors/404', { url: req.originalUrl }); | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default Router; | ||||||
							
								
								
									
										237
									
								
								static/apiWrapper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								static/apiWrapper.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,237 @@ | |||||||
|  | _wrapperVersion = '1.0.0'; | ||||||
|  | _minApiVersion = '1.0.0'; | ||||||
|  | _maxApiVersion = '1.0.0'; | ||||||
|  |  | ||||||
|  | _defaultTTL = 60000; | ||||||
|  |  | ||||||
|  | _apiConfig = { | ||||||
|  | 	basePath: '/api/v1/' | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | if (!window.localStorage) { | ||||||
|  | 	console.warn('Local Storage is not available, some features may not work'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Generic driver functions | ||||||
|  | let _api = { | ||||||
|  | 	get: async function (path) { | ||||||
|  | 		const options = { | ||||||
|  | 			headers: new Headers({ 'content-type': 'application/json' }) | ||||||
|  | 		}; | ||||||
|  | 		const response = await fetch(_apiConfig.basePath + path, options); | ||||||
|  | 		// Handle the response | ||||||
|  | 		if (!response.ok) { | ||||||
|  | 			console.error('Failed to fetch:', response.statusText); | ||||||
|  | 			_testPageFail(response.statusText); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		const result = await response.json(); | ||||||
|  | 		// Handle the result, was json valid? | ||||||
|  | 		if (!result) { | ||||||
|  | 			// Is it a number instead? | ||||||
|  | 			if (typeof result === 'number') { | ||||||
|  | 				return result; | ||||||
|  | 			} | ||||||
|  | 			console.error('Invalid JSON response'); | ||||||
|  | 			_testPageFail('Invalid JSON response'); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return result; | ||||||
|  | 	}, | ||||||
|  | 	post: async function (path, data) { | ||||||
|  | 		const options = { | ||||||
|  | 			method: 'POST', | ||||||
|  | 			headers: new Headers({ 'content-type': 'application/json' }), | ||||||
|  | 			body: JSON.stringify(data) | ||||||
|  | 		}; | ||||||
|  | 		const response = await fetch(_apiConfig.basePath + path, options); | ||||||
|  | 		// Handle the response | ||||||
|  | 		if (!response.ok) { | ||||||
|  | 			_testPageFail(response.statusText); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		const result = await response.json(); | ||||||
|  | 		// Handle the result, was json valid? | ||||||
|  | 		if (!result) { | ||||||
|  | 			_testPageFail('Invalid JSON response'); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return result; | ||||||
|  | 	}, | ||||||
|  | 	delete: async function (path, data) { | ||||||
|  | 		const options = { | ||||||
|  | 			method: 'DELETE', | ||||||
|  | 			headers: new Headers({ 'content-type': 'application/json' }), | ||||||
|  | 			body: JSON.stringify(data) | ||||||
|  | 		}; | ||||||
|  | 		const response = await fetch(_apiConfig.basePath + path, options); | ||||||
|  | 		// Handle the response | ||||||
|  | 		if (!response.ok) { | ||||||
|  | 			_testPageFail(response.statusText); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		const result = await response.json(); | ||||||
|  | 		// Handle the result, was json valid? | ||||||
|  | 		if (!result) { | ||||||
|  | 			_testPageFail('Invalid JSON response'); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return result; | ||||||
|  | 	}, | ||||||
|  | 	patch: async function (path, data) { | ||||||
|  | 		const options = { | ||||||
|  | 			method: 'PATCH', | ||||||
|  | 			headers: new Headers({ 'content-type': 'application/json' }), | ||||||
|  | 			body: JSON.stringify(data) | ||||||
|  | 		}; | ||||||
|  | 		const response = await fetch(_apiConfig.basePath + path, options); | ||||||
|  | 		// Handle the response | ||||||
|  | 		if (!response.ok) { | ||||||
|  | 			_testPageFail(response.statusText); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		const result = await response.json(); | ||||||
|  | 		// Handle the result, was json valid? | ||||||
|  | 		if (!result) { | ||||||
|  | 			_testPageFail('Invalid JSON response'); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return result; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function updateRow(tableName, id, data) { | ||||||
|  | 	invalidateCache(tableName); | ||||||
|  | 	return _api.patch(`${tableName}`, { id: id, ...data }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function deleteRow(tableName, id) { | ||||||
|  | 	invalidateCache(tableName); | ||||||
|  | 	return _api.delete(`${tableName}`, { id: id }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getApiDescriptionByTable(tableName) { | ||||||
|  | 	const keyDesc = `desc:${tableName}`; | ||||||
|  | 	const keyTime = `${keyDesc}:time`; | ||||||
|  | 	const keyTTL = `${keyDesc}:ttl`; | ||||||
|  |  | ||||||
|  | 	// Retrieve cached data | ||||||
|  | 	const description = JSON.parse(localStorage.getItem(keyDesc)); | ||||||
|  | 	const timeCreated = parseInt(localStorage.getItem(keyTime)); | ||||||
|  | 	const ttl = parseInt(localStorage.getItem(keyTTL)); | ||||||
|  |  | ||||||
|  | 	// Check if valid cached data exists | ||||||
|  | 	if (description && timeCreated && ttl) { | ||||||
|  | 		const currentTime = Date.now(); | ||||||
|  | 		const age = currentTime - parseInt(timeCreated, 10); | ||||||
|  | 		if (age < parseInt(ttl, 10)) { | ||||||
|  | 			// Return cached data immediately | ||||||
|  | 			return Promise.resolve(description); | ||||||
|  | 		} else { | ||||||
|  | 			console.warn('Cached description expired; fetching new data'); | ||||||
|  | 			// Fetch new data, update cache, and return it | ||||||
|  | 			return fetchAndUpdateCache(tableName); | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		console.warn('No cached description; fetching from server'); | ||||||
|  | 		// Fetch data, update cache, and return it | ||||||
|  | 		return fetchAndUpdateCache(tableName); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	function fetchAndUpdateCache(tableName) { | ||||||
|  | 		return _api | ||||||
|  | 			.get(`${tableName}/describe`) | ||||||
|  | 			.then((data) => { | ||||||
|  | 				if (data) { | ||||||
|  | 					// Update local storage with new data | ||||||
|  | 					localStorage.setItem(keyDesc, JSON.stringify(data)); | ||||||
|  | 					localStorage.setItem(keyTime, Date.now().toString()); | ||||||
|  | 					localStorage.setItem(keyTTL, '60000'); // 60 seconds TTL | ||||||
|  | 				} | ||||||
|  | 				return data; // Return the fetched data | ||||||
|  | 			}) | ||||||
|  | 			.catch((error) => { | ||||||
|  | 				console.error('Failed to fetch description:', error); | ||||||
|  | 				// Fallback to cached data if available (even if expired) | ||||||
|  | 				return description || null; | ||||||
|  | 			}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function returnTableDataByTableName(tableName, search="", orderBy="asc", sort="", take=-1, skip=0) { | ||||||
|  | 	var orderBy = orderBy.toLowerCase(); | ||||||
|  | 	if(orderBy == "") { | ||||||
|  | 		orderBy = "asc"; | ||||||
|  | 	} | ||||||
|  | 	var baseString = tableName + "?order=" + orderBy; | ||||||
|  | 	if(sort && sort.length > 0) { | ||||||
|  | 		baseString += "&sort=" + sort; | ||||||
|  | 	} | ||||||
|  | 	if(take > 0) { | ||||||
|  | 		baseString += "&take=" + take; | ||||||
|  | 	} | ||||||
|  | 	if(skip > 0) { | ||||||
|  | 		baseString += "&skip=" + skip; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (search && search.length > 0) { | ||||||
|  | 		return _api.get(baseString + '&search=' + search); | ||||||
|  | 	} else { | ||||||
|  | 		return _api.get(baseString); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getCountByTable(tableName, search="") { | ||||||
|  | 	let baseString = tableName + '?count=true'; | ||||||
|  | 	if (search && search.length > 0) { | ||||||
|  | 		baseString += '&search=' + search; | ||||||
|  | 	} | ||||||
|  | 	// Stored in `data:count:${tableName}` | ||||||
|  | 	let result = await _api.get(baseString); | ||||||
|  | 	console.debug('Count result:', result); | ||||||
|  | 	if (typeof result !== 'number') { | ||||||
|  | 		_testPageWarn('Count was not a number, was: ' + result); | ||||||
|  | 		console.warn('Count was not a number, was: ' + result); | ||||||
|  | 		return -1; | ||||||
|  | 	} | ||||||
|  | 	return result; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | function _testPageFail(reason) { | ||||||
|  | 	document.getElementById('heroStatus').classList.remove('is-success'); | ||||||
|  | 	document.getElementById('heroStatus').classList.add('is-danger'); | ||||||
|  |  | ||||||
|  | 	document.getElementById('heroExplainer').innerHTML = 'API Wrapper Test Failed, reason: ' + reason; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function _testPageWarn(reason) { | ||||||
|  | 	document.getElementById('heroStatus').classList.remove('is-success'); | ||||||
|  | 	document.getElementById('heroStatus').classList.add('is-warning'); | ||||||
|  |  | ||||||
|  | 	document.getElementById('heroExplainer').innerHTML = 'API Wrapper Test Warning, reason: ' + reason; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getServerVersion() { | ||||||
|  | 	return _api.get('version'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function createEntry(tableName, data) { | ||||||
|  | 	invalidateCache(tableName); | ||||||
|  | 	return _api.post(tableName, data); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function invalidateCache(tableName) { | ||||||
|  | 	const keyDesc = `desc:${tableName}`; | ||||||
|  | 	const keyTime = `${keyDesc}:time`; | ||||||
|  | 	const keyTTL = `${keyDesc}:ttl`; | ||||||
|  |  | ||||||
|  | 	localStorage.removeItem(keyDesc); | ||||||
|  | 	localStorage.removeItem(keyTime); | ||||||
|  | 	localStorage.removeItem(keyTTL); | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								static/css/lockscreen.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								static/css/lockscreen.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | #clock { | ||||||
|  | 	    font-size: 120px; | ||||||
|  |     font-weight: 100; | ||||||
|  |     color: #fff; | ||||||
|  |     text-align: center; | ||||||
|  |     bottom: 10%; | ||||||
|  |     right: 5%; | ||||||
|  |     z-index: 900010; | ||||||
|  |     position: absolute; | ||||||
|  |     display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   align-items: center; | ||||||
|  |   flex-direction: row; | ||||||
|  | flex-wrap: wrap; | ||||||
|  |  | ||||||
|  |     width: 30%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #time { | ||||||
|  | 	margin-bottom: 0px; | ||||||
|  | 	padding-bottom: 0px; | ||||||
|  | 	text-align: center; | ||||||
|  | 	width: 95%; | ||||||
|  | 	vertical-align: middle; | ||||||
|  | 	font-family: monospace; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #date { | ||||||
|  | 	font-size: 50px; | ||||||
|  | 	margin-top: -40px; | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								static/favicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								static/favicon.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | <svg id="favicon" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="138.654" height="146.519" viewBox="0 0 36.685 38.767"> | ||||||
|  |   <path d="M18.775 0A24.388 24.388 0 0 0 6.82 3.115C3.15 5.165-1.91 9.252.736 13.985c.37.66.9 1.221 1.47 1.713 1.532 1.322 2.98.222 4.554-.457.975-.42 1.95-.842 2.922-1.27.434-.19 1.01-.33 1.328-.698.858-.99.494-2.994.05-4.095a27.25 27.25 0 0 1 3.65-1.24v30.828h7.215V7.671c1.05.184 2.438.432 3.266 1.041.387.284.113.908.076 1.297-.08.827-.027 1.817.344 2.581.308.632 1.16.784 1.765 1.008l4.564 1.704c.628.232 1.33.643 1.979.297 2.822-1.507 3.574-5.39 1.843-8.023-1.165-1.77-3.255-3.13-5.035-4.216C27.037 1.107 22.906.014 18.775 0z"/> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 678 B | 
							
								
								
									
										144
									
								
								static/js/lockscreenBgHandler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								static/js/lockscreenBgHandler.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | |||||||
|  | // Image Handler | ||||||
|  | const baseUrl = "https://api.unsplash.com/photos/random?client_id=[KEY]&orientation=landscape&topics=nature"; | ||||||
|  | const apiKey = "tYOt7Jo94U7dunVcP5gt-kDKDMjWFOGQNsHuhLDLV8k"; // Take from config | ||||||
|  | const fullUrl = baseUrl.replace("[KEY]", apiKey); | ||||||
|  |  | ||||||
|  | const showModeImage = "/static/media/showModeLockscreen.jpg" | ||||||
|  |  | ||||||
|  | let credits = document.getElementById("credits"); | ||||||
|  |  | ||||||
|  | let currentImageHandle; | ||||||
|  |  | ||||||
|  | // Lock screen or show mode | ||||||
|  | let screenState = "lock"; | ||||||
|  |  | ||||||
|  | function handleImage() { | ||||||
|  | 	if(screenState === "lock") { | ||||||
|  | 		fetch("https://staging.thegreydiamond.de/projects/photoPortfolio/api/getRand.php?uuid=01919dec-b2cd-7adc-8ca2-a071d1169cbc&unsplash=true") | ||||||
|  | 			.then(response => response.json()) | ||||||
|  | 			.then(data => { | ||||||
|  | 				// data = { | ||||||
|  | 				// 	urls: { | ||||||
|  | 				// 		regular: "https://imageproxy.thegreydiamond.de/ra5iqxlyve6HpjNvC1tzG50a14oIOgiWP95CxIvbBC8/sm:1/kcr:1/aHR0cHM6Ly9zdGFn/aW5nLnRoZWdyZXlk/aWFtb25kLmRlL3By/b2plY3RzL3Bob3Rv/UG9ydGZvbGlvL2Rl/bW9IaVJlcy9QMTE5/MDgzMC1zY2hpbGQu/anBn.webp" | ||||||
|  | 				// 	}, | ||||||
|  | 				// 	user: { | ||||||
|  | 				// 		name: "Sören Oesterwind", | ||||||
|  | 				// 		links: { | ||||||
|  | 				// 			html: "https://thegreydiamond.de" | ||||||
|  | 				// 		} | ||||||
|  | 				// 	} | ||||||
|  | 				// } | ||||||
|  | 				if(!currentImageHandle) { | ||||||
|  | 					// Create a page filling div which contains the image | ||||||
|  | 					currentImageHandle = document.createElement("div"); | ||||||
|  | 					currentImageHandle.style.position = "absolute"; | ||||||
|  | 					currentImageHandle.style.top = "0"; | ||||||
|  | 					currentImageHandle.style.left = "0"; | ||||||
|  | 					currentImageHandle.style.width = "100%"; | ||||||
|  | 					currentImageHandle.style.height = "100%"; | ||||||
|  | 					currentImageHandle.style.backgroundImage = `url(${data.urls.regular})`; | ||||||
|  | 					currentImageHandle.style.backgroundSize = "cover"; | ||||||
|  | 					currentImageHandle.style.opacity = 1; | ||||||
|  | 				} else { | ||||||
|  | 					// Create a new div behind the current one and delete the old one when the new one is loaded | ||||||
|  | 					let newImageHandle = document.createElement("div"); | ||||||
|  | 					newImageHandle.style.position = "absolute"; | ||||||
|  | 					newImageHandle.style.top = "0"; | ||||||
|  | 					newImageHandle.style.left = "0"; | ||||||
|  | 					newImageHandle.style.width = "100%"; | ||||||
|  | 					newImageHandle.style.height = "100%"; | ||||||
|  | 					newImageHandle.style.backgroundImage = `url(${data.urls.regular})`; | ||||||
|  | 					newImageHandle.style.backgroundSize = "cover"; | ||||||
|  | 					newImageHandle.style.opacity = 1; | ||||||
|  | 					newImageHandle.style.transition = "1s"; | ||||||
|  | 					newImageHandle.style.zIndex = 19999; | ||||||
|  | 					document.body.appendChild(newImageHandle); | ||||||
|  |  | ||||||
|  | 					currentImageHandle.style.opacity = 0; | ||||||
|  | 					setTimeout(() => { | ||||||
|  | 						currentImageHandle.remove(); | ||||||
|  | 						newImageHandle.style.zIndex = 200000; | ||||||
|  | 						currentImageHandle = newImageHandle; | ||||||
|  | 					}, 1000); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 					// Set the credits | ||||||
|  | 					credits.innerHTML = `Photo by <a href="${data.user.links.html}" target="_blank">${data.user.name}</a> on <a href="https://unsplash.com" target="_blank">Unsplash</a>`; | ||||||
|  | 					credits.style.zIndex = 300000; | ||||||
|  | 				} | ||||||
|  | 			}) | ||||||
|  | 			.catch(error => { | ||||||
|  | 				console.error("Error fetching image: ", error); | ||||||
|  | 			}); | ||||||
|  | 	} else { | ||||||
|  | 		if(currentImageHandle) { | ||||||
|  | 			// Check if the image is already loaded | ||||||
|  | 			if(currentImageHandle.style.backgroundImage === `url("${showModeImage}")`) { | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			// Create a new div behind the current one and delete the old one when the new one is loaded | ||||||
|  | 			let newImageHandle = document.createElement("div"); | ||||||
|  | 			newImageHandle.style.position = "absolute"; | ||||||
|  | 			newImageHandle.style.top = "0"; | ||||||
|  | 			newImageHandle.style.left = "0"; | ||||||
|  | 			newImageHandle.style.width = "100%"; | ||||||
|  | 			newImageHandle.style.height = "100%"; | ||||||
|  | 			newImageHandle.style.backgroundImage = `url(${showModeImage})`; | ||||||
|  | 			newImageHandle.style.backgroundSize = "cover"; | ||||||
|  | 			newImageHandle.style.opacity = 1; | ||||||
|  | 			newImageHandle.style.transition = "1s"; | ||||||
|  | 			document.body.appendChild(newImageHandle); | ||||||
|  |  | ||||||
|  | 			setTimeout(() => { | ||||||
|  | 				currentImageHandle.remove(); | ||||||
|  | 				currentImageHandle = newImageHandle; | ||||||
|  | 			}, 1000); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function handleTimeAndDate() { | ||||||
|  | 	let time = new Date(); | ||||||
|  | 	let hours = time.getHours(); | ||||||
|  | 	let minutes = time.getMinutes(); | ||||||
|  | 	let day = time.getDate(); | ||||||
|  | 	let month = time.getMonth(); | ||||||
|  | 	month += 1; | ||||||
|  | 	let year = time.getFullYear(); | ||||||
|  |  | ||||||
|  | 	let timeHandle = document.getElementById("time"); | ||||||
|  | 	let dateHandle = document.getElementById("date"); | ||||||
|  |  | ||||||
|  | 	timeHandle.innerHTML = `${hours < 10 ? "0" + hours : hours}:${minutes < 10 ? "0" + minutes : minutes}:${time.getSeconds() < 10 ? "0" + time.getSeconds() : time.getSeconds()}`; | ||||||
|  | 	// Datum in format Montag, 22.12.2024 | ||||||
|  | 	dateHandle.innerHTML = `${getDay(time.getDay())}, ${day < 10 ? "0" + day : day}.${month < 10 ? "0" + month : month}.${year}`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getDay(day) { | ||||||
|  | 	switch(day) { | ||||||
|  | 		case 0: | ||||||
|  | 			return "Sonntag"; | ||||||
|  | 		case 1: | ||||||
|  | 			return "Montag"; | ||||||
|  | 		case 2: | ||||||
|  | 			return "Dienstag"; | ||||||
|  | 		case 3: | ||||||
|  | 			return "Mittwoch"; | ||||||
|  | 		case 4: | ||||||
|  | 			return "Donnerstag"; | ||||||
|  | 		case 5: | ||||||
|  | 			return "Freitag"; | ||||||
|  | 		case 6: | ||||||
|  | 			return "Samstag"; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Set the image handler to run every 10 minutes | ||||||
|  | setInterval(handleImage, 60 * 1000 * 10); | ||||||
|  | handleImage(); | ||||||
|  | handleImage() | ||||||
|  |  | ||||||
|  | // Set the time and date handler to run every minute | ||||||
|  | setInterval(handleTimeAndDate, 500); | ||||||
|  | handleTimeAndDate(); | ||||||
							
								
								
									
										3
									
								
								static/logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								static/logo.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | <svg id="logo" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="498.424" height="148.888" viewBox="0 0 131.875 39.393"> | ||||||
|  |   <path d="M118.368 51.177c-3.682 0-6.537.32-8.566.958-1.99.6-3.419 1.635-4.283 3.099-.827 1.466-1.239 3.533-1.239 6.2 0 2.03.356 3.7 1.07 5.016.714 1.315 1.916 2.46 3.607 3.438 1.728.939 4.17 1.878 7.326 2.817 2.517.752 4.452 1.466 5.805 2.142 1.39.64 2.386 1.352 2.987 2.142.601.79.9 1.747.9 2.874 0 1.24-.224 2.198-.675 2.874-.451.64-1.202 1.09-2.254 1.353-1.052.263-2.536.375-4.452.338-1.916-.038-3.4-.226-4.453-.564-1.051-.376-1.822-.977-2.31-1.804-.451-.826-.733-2.01-.845-3.55h-7.045c-.113 3.157.263 5.598 1.127 7.327.864 1.728 2.348 2.95 4.452 3.663 2.142.714 5.166 1.07 9.074 1.07 3.795 0 6.706-.318 8.735-.958 2.066-.638 3.532-1.728 4.396-3.268.864-1.54 1.296-3.72 1.296-6.538 0-2.254-.357-4.095-1.07-5.522-.715-1.466-1.917-2.706-3.608-3.72-1.653-1.015-4.02-2.01-7.1-2.987-2.518-.79-4.49-1.485-5.918-2.085-1.39-.6-2.404-1.202-3.043-1.804-.639-.6-.959-1.277-.959-2.028 0-1.165.207-2.049.62-2.649.414-.601 1.09-1.033 2.03-1.296.976-.263 2.366-.395 4.17-.395 1.728 0 3.061.15 4.001.45.977.264 1.672.734 2.085 1.41.451.638.733 1.578.846 2.818h7.157c.038-2.856-.376-5.054-1.24-6.594-.863-1.54-2.292-2.63-4.283-3.27-1.954-.637-4.734-.957-8.34-.957zm-67.058.12a24.388 24.388 0 0 0-11.954 3.114c-3.67 2.051-8.73 6.137-6.085 10.87.37.66.9 1.222 1.47 1.714 1.53 1.322 2.98.222 4.554-.458.975-.42 1.95-.842 2.922-1.268.433-.19 1.01-.331 1.328-.7.858-.99.494-2.994.05-4.094a27.22 27.22 0 0 1 3.651-1.24v30.828h7.214V58.968c1.05.182 2.439.43 3.266 1.04.387.285.113.91.075 1.298-.08.827-.027 1.816.345 2.58.307.632 1.16.785 1.765 1.009l4.564 1.703c.628.233 1.33.644 1.979.298 2.822-1.508 3.574-5.39 1.842-8.023-1.164-1.771-3.254-3.13-5.034-4.216-3.69-2.254-7.822-3.347-11.952-3.36zm-39.287.443L1.146 90.063h7.045l2.423-8.453h12.962l2.48 8.453h7.101L22.055 51.74H12.023zm67.628.001L68.773 90.063h7.045l2.423-8.453h12.964l2.48 8.453h7.1L89.683 51.74H79.65zm-62.668 6.537h.056l4.903 17.076h-9.637l4.678-17.076zm67.628 0h.056l4.903 17.076h-9.637l4.678-17.076z" style="display:inline;fill:current;fill-opacity:1;stroke:none;stroke-width:.408654;stroke-opacity:1" transform="translate(-1.146 -51.177)"/> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										3
									
								
								static/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								static/main.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | body { | ||||||
|  | 	min-height: 100vh; | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								static/media/showModeLockscreen.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/media/showModeLockscreen.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										620
									
								
								static/pageDriver.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										620
									
								
								static/pageDriver.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,620 @@ | |||||||
|  | _pageDriverVersion = '1.0.1'; | ||||||
|  |  | ||||||
|  | // Handle color for icon svg with id="logo" based on the current theme | ||||||
|  | const logo = document.getElementById('logo'); | ||||||
|  | if (logo) { | ||||||
|  | 	logo.style.fill = getComputedStyle(document.documentElement).getPropertyValue('--bulma-text'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | if (_wrapperVersion === undefined) { | ||||||
|  | 	console.error('API Wrapper not found; Please include the API Wrapper before including the Page Driver'); | ||||||
|  | 	exit(); | ||||||
|  | } else { | ||||||
|  | 	console.log('API Wrapper found; Page Driver is ready to use'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Find all tables on the page which have data-dataSource attribute | ||||||
|  | var tables = document.querySelectorAll('table[data-dataSource]'); | ||||||
|  | //var tables = [] | ||||||
|  |  | ||||||
|  | // Get all single values with data-dataSource, data-dataCol and data-dataAction | ||||||
|  | var singleValues = document.querySelectorAll('span[data-dataSource]'); | ||||||
|  |  | ||||||
|  | // Find all search fields with data-searchTargetId | ||||||
|  | var searchFields = document.querySelectorAll('input[data-searchTargetId]'); | ||||||
|  |  | ||||||
|  | // Find all modalForms | ||||||
|  | var modalForms = document.querySelectorAll('form[data-targetTable]'); | ||||||
|  |  | ||||||
|  | console.info('Processing single values'); | ||||||
|  | console.info(singleValues); | ||||||
|  |  | ||||||
|  | // Iterate over all single values | ||||||
|  | singleValues.forEach(async (singleValue) => { | ||||||
|  | 	writeSingelton(singleValue); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // Iterate over all tables | ||||||
|  | tables.forEach(async (table) => { | ||||||
|  | 	// Get THs and attach onClick event to sort | ||||||
|  | 	const ths = table.querySelectorAll('th'); | ||||||
|  | 	ths.forEach((th) => { | ||||||
|  | 		if(th.getAttribute('fnc') == "actions") { | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		th.style.cursor = 'pointer'; | ||||||
|  | 		th.style.userSelect = 'none'; | ||||||
|  | 		th.addEventListener('click', async function () { | ||||||
|  | 			const table = th.closest('table'); | ||||||
|  | 			const order = th.getAttribute('data-order'); | ||||||
|  | 			// Clear all other order attributes | ||||||
|  | 			ths.forEach((th) => { | ||||||
|  | 				if (th != this) { | ||||||
|  | 					th.removeAttribute('data-order'); | ||||||
|  | 					th.classList.remove("bi-caret-up-fill") | ||||||
|  | 					th.classList.remove("bi-caret-down-fill") | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 			if (order == 'ASC') { | ||||||
|  | 				th.setAttribute('data-order', 'DESC'); | ||||||
|  | 				th.classList.add("bi-caret-down-fill") | ||||||
|  | 				th.classList.remove("bi-caret-up-fill") | ||||||
|  | 			} else { | ||||||
|  | 				th.setAttribute('data-order', 'ASC'); | ||||||
|  | 				th.classList.add("bi-caret-up-fill") | ||||||
|  | 				th.classList.remove("bi-caret-down-fill") | ||||||
|  | 			} | ||||||
|  | 			refreshTable(table); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | 	refreshTable(table); | ||||||
|  |  | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async function writeSingelton(element) { | ||||||
|  | 	const table = element.getAttribute('data-dataSource'); | ||||||
|  | 	console.log('Table: ', table, ' Action: ', element.getAttribute('data-dataAction'), ' Element: ', element); | ||||||
|  | 	switch (element.getAttribute('data-dataAction')) { | ||||||
|  | 		case 'COUNT': { | ||||||
|  | 			console.log('Count action found'); | ||||||
|  | 			element.innerHTML = await getCountByTable(table); | ||||||
|  | 			break; | ||||||
|  | 		} | ||||||
|  | 		case 'SPECIAL': { | ||||||
|  | 			if (table == 'version') { | ||||||
|  | 				element.innerHTML = (await getServerVersion())['version']; | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		default: { | ||||||
|  | 			console.error('Unknown action found: ', element.getAttribute('data-dataAction')); | ||||||
|  | 			break; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	element.classList.remove('is-skeleton'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Attach listeners to search fields | ||||||
|  | searchFields.forEach((searchField) => { | ||||||
|  | 	// Apply restrictions to search field (min, max, chars, etc) | ||||||
|  |  | ||||||
|  | 	getApiDescriptionByTable(document.getElementById(searchField.getAttribute('data-searchTargetId')).getAttribute('data-dataSource')).then((desc) => { | ||||||
|  | 		desc = desc['GET']['keys']['search']; | ||||||
|  | 		var rules = desc['rules']; | ||||||
|  | 		rules.forEach((rule) => { | ||||||
|  | 			switch (rule['name']) { | ||||||
|  | 				case 'min': { | ||||||
|  | 					searchField.setAttribute('minlength', rule['args']['limit']); | ||||||
|  | 					break; | ||||||
|  | 				} | ||||||
|  | 				case 'max': { | ||||||
|  | 					searchField.setAttribute('maxlength', rule['args']['limit']); | ||||||
|  | 					break; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | 	searchField.addEventListener('input', async function () { | ||||||
|  | 		console.log('Search field changed: ', searchField); | ||||||
|  | 		if (searchField.checkValidity() == false) { | ||||||
|  | 			console.log('Invalid input'); | ||||||
|  | 			searchField.classList.add('is-danger'); | ||||||
|  | 			return; | ||||||
|  | 		} else { | ||||||
|  | 			searchField.classList.remove('is-danger'); | ||||||
|  | 			const targetId = searchField.getAttribute('data-searchTargetId'); | ||||||
|  | 			const target = document.getElementById(targetId); | ||||||
|  | 			const table = target.getAttribute('data-dataSource'); | ||||||
|  | 			const column = target.getAttribute('data-dataCol'); | ||||||
|  | 			const value = searchField.value; | ||||||
|  | 			console.log('Searching for ', value, ' in ', table, ' column ', column); | ||||||
|  | 			refreshTableByName(table); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // Attach listeners to modal forms | ||||||
|  | modalForms.forEach((modalForm) => { | ||||||
|  | 	// Add validation to form by using API description (everything is assumed POST for now) | ||||||
|  | 	modalForm.addEventListener('input', async function (event) { | ||||||
|  | 		if (event.target.checkValidity() == false) { | ||||||
|  | 			modalForm.querySelector("input[type='submit']").setAttribute('disabled', true); | ||||||
|  | 			event.target.classList.add('is-danger'); | ||||||
|  | 			return; | ||||||
|  | 		} else { | ||||||
|  | 			modalForm.querySelector("input[type='submit']").removeAttribute('disabled'); | ||||||
|  | 			event.target.classList.remove('is-danger'); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	getApiDescriptionByTable(modalForm.getAttribute('data-targetTable')).then((desc) => { | ||||||
|  | 		console.log('Description: ', desc); | ||||||
|  | 		const keys = desc['POST']['keys']; | ||||||
|  | 		// Apply resitrictions and types to form fields | ||||||
|  | 		for (key in keys) { | ||||||
|  | 			const field = modalForm.querySelector("input[name='" + key + "']"); | ||||||
|  | 			if (field) { | ||||||
|  | 				const rules = keys[key]['rules']; | ||||||
|  | 				const flags = keys[key]['flags']; | ||||||
|  | 				console.log('Field: ', field, ' Rules: ', rules, ' Flags: ', flags); | ||||||
|  | 				rules.forEach((rule) => { | ||||||
|  | 					switch (rule['name']) { | ||||||
|  | 						case 'min': { | ||||||
|  | 							field.setAttribute('minlength', rule['args']['limit']); | ||||||
|  | 							break; | ||||||
|  | 						} | ||||||
|  | 						case 'max': { | ||||||
|  | 							field.setAttribute('maxlength', rule['args']['limit']); | ||||||
|  | 							break; | ||||||
|  | 						} | ||||||
|  | 						case 'pattern': { | ||||||
|  | 							field.setAttribute('pattern', rule['args']['regex'].substring(1, rule['args']['regex'].length - 1)); | ||||||
|  | 							//field.setAttribute("pattern", "^[\\+]?[\\(]?[0-9]{3}[\\)]?[\\-\\s\\.]?[0-9]{3}[\\-\\s\\.]?[0-9]{4,9}$"); | ||||||
|  | 							break; | ||||||
|  | 						} | ||||||
|  | 						case 'type': { | ||||||
|  | 							//field.setAttribute("type", rule["args"]["type"]); | ||||||
|  | 							console.log('Type: ', rule['args']['type']); | ||||||
|  | 							break; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				}); | ||||||
|  | 				if (flags) { | ||||||
|  | 					flags['presence'] == 'required' ? field.setAttribute('required', true) : field.removeAttribute('required'); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		console.log('Keys: ', keys); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	modalForm.addEventListener('submit', async function (event) { | ||||||
|  | 		event.preventDefault(); | ||||||
|  | 		// Check what button submitted the form and if it has data-actionBtn = save | ||||||
|  | 		// If not, close modal | ||||||
|  | 		const pressedBtn = event.submitter; | ||||||
|  | 		if (pressedBtn.getAttribute('data-actionBtn') != 'save') { | ||||||
|  | 			modalForm.closest('.modal').classList.remove('is-active'); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Find .entryPhase and hide it | ||||||
|  | 		const entryPhase = modalForm.querySelector('.entryPhase'); | ||||||
|  | 		const loadPhase = modalForm.querySelector('.loadPhase'); | ||||||
|  | 		if (entryPhase) { | ||||||
|  | 			entryPhase.classList.add('is-hidden'); | ||||||
|  | 		} | ||||||
|  | 		if (loadPhase) { | ||||||
|  | 			loadPhase.classList.remove('is-hidden'); | ||||||
|  | 		} | ||||||
|  | 		console.log('Form submitted: ', modalForm); | ||||||
|  | 		const table = modalForm.getAttribute('data-targetTable'); | ||||||
|  | 		const data = new FormData(modalForm); | ||||||
|  | 		// Convert to JSON object | ||||||
|  | 		let jsonData = {}; | ||||||
|  | 		data.forEach((value, key) => { | ||||||
|  | 			jsonData[key] = value; | ||||||
|  | 		}); | ||||||
|  | 		console.log('JSON Data: ', jsonData); | ||||||
|  | 		let resp = {}; | ||||||
|  | 		if(modalForm.getAttribute('data-action') == 'edit') { | ||||||
|  | 			Rid = modalForm.getAttribute('data-rid'); | ||||||
|  | 			resp = await updateRow(table, Rid,jsonData); | ||||||
|  | 			modalForm.setAttribute('data-action', 'create'); | ||||||
|  | 		} else { | ||||||
|  | 			resp = await createEntry(table, jsonData); | ||||||
|  | 		} | ||||||
|  | 		 | ||||||
|  | 		console.log('Response: ', resp); | ||||||
|  | 		if (resp['status'] == 'CREATED' || resp['status'] == 'UPDATED') { | ||||||
|  | 			console.log('Entry created successfully'); | ||||||
|  | 			modalForm.closest('.modal').classList.remove('is-active'); | ||||||
|  | 			modalForm.reset(); | ||||||
|  | 			// Hide loadPhase | ||||||
|  | 			if (loadPhase) { | ||||||
|  | 				loadPhase.classList.add('is-hidden'); | ||||||
|  | 			} | ||||||
|  | 			// Show entryPhase | ||||||
|  | 			if (entryPhase) { | ||||||
|  | 				entryPhase.classList.remove('is-hidden'); | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			// Hide loadPhase | ||||||
|  | 			if (loadPhase) { | ||||||
|  | 				loadPhase.classList.add('is-hidden'); | ||||||
|  | 			} | ||||||
|  | 			// Show entryPhase | ||||||
|  | 			if (entryPhase) { | ||||||
|  | 				entryPhase.classList.remove('is-hidden'); | ||||||
|  | 			} | ||||||
|  | 			// TODO: Show error message | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Find all tables with data-searchTargetId set to table | ||||||
|  | 		setTimeout(() => { | ||||||
|  | 			refreshTableByName(table); | ||||||
|  | 			updateSingeltonsByTableName(table); | ||||||
|  | 		}, 500); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // Helper | ||||||
|  | async function refreshTable(table) { | ||||||
|  | 	// Refresh a table while keeping (optionally set) search value | ||||||
|  | 	const searchField = document.querySelector("input[data-searchTargetId='" + table.id + "']"); | ||||||
|  | 	// Get state of order and sort | ||||||
|  | 	const ths = table.querySelectorAll('th'); | ||||||
|  | 	const columnIndices = []; | ||||||
|  | 	ths.forEach((th, index) => { | ||||||
|  | 		columnIndices[th.getAttribute('data-dataCol')] = index; | ||||||
|  | 	}); | ||||||
|  | 	let order = ''; | ||||||
|  | 	let column = ''; | ||||||
|  | 	ths.forEach((th) => { | ||||||
|  | 		if (th.hasAttribute('data-order')) { | ||||||
|  | 			order = th.getAttribute('data-order'); | ||||||
|  | 			column = th.getAttribute('data-dataCol'); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 	console.log('Order: ', order, ' Column: ', column); | ||||||
|  |  | ||||||
|  | 	const maxLinesPerPage = table.getAttribute('data-pageSize'); | ||||||
|  | 	let currentPage = table.getAttribute('data-currentPage'); | ||||||
|  | 	if(currentPage == null) { | ||||||
|  | 		table.setAttribute('data-currentPage', 1); | ||||||
|  | 		currentPage = 1; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	const start = (currentPage - 1) * maxLinesPerPage; | ||||||
|  | 	const end = start + maxLinesPerPage; | ||||||
|  |  | ||||||
|  | 	let paginationPassOnPre = { | ||||||
|  | 		'start': start, | ||||||
|  | 		'end': end, | ||||||
|  | 		'currentPage': currentPage, | ||||||
|  | 		'maxLinesPerPage': maxLinesPerPage | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	if (searchField) { | ||||||
|  | 		const value = searchField.value; | ||||||
|  | 		const dbTable = table.getAttribute('data-dataSource'); | ||||||
|  | 		const result = await returnTableDataByTableName(dbTable, value, order, column, take= maxLinesPerPage, skip= start); | ||||||
|  | 		const totalResultCount = await getCountByTable(dbTable, value); | ||||||
|  | 		paginationPassOnPre['dataLength'] = totalResultCount; | ||||||
|  | 		var magMiddl = managePaginationMiddleware(result, paginationPassOnPre); | ||||||
|  | 		var data = magMiddl[0]; | ||||||
|  | 		var paginationPassOn = magMiddl[1]; | ||||||
|  | 		clearTable(table); | ||||||
|  | 		writeDataToTable(table, data, paginationPassOn); | ||||||
|  | 	} else { | ||||||
|  | 		const result = await returnTableDataByTableName(table.getAttribute('data-dataSource'), undefined, order, column, take= maxLinesPerPage, skip= start); | ||||||
|  | 		const resultCount = await getCountByTable(table.getAttribute('data-dataSource')); | ||||||
|  | 		paginationPassOnPre['dataLength'] = resultCount; | ||||||
|  | 		var magMiddl = managePaginationMiddleware(result, paginationPassOnPre); | ||||||
|  | 		var data = magMiddl[0]; | ||||||
|  | 		var paginationPassOn = magMiddl[1]; | ||||||
|  | 		clearTable(table); | ||||||
|  | 		writeDataToTable(table, data, paginationPassOn); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function managePaginationMiddleware(data, paginationPassOnPre) { | ||||||
|  | 	const maxLinesPerPage = paginationPassOnPre['maxLinesPerPage']; | ||||||
|  |  | ||||||
|  | 	const dataLength = paginationPassOnPre['dataLength']; | ||||||
|  | 	const maxPages = Math.ceil(dataLength / maxLinesPerPage); | ||||||
|  | 	paginationPassOn = paginationPassOnPre; | ||||||
|  | 	paginationPassOn['maxPages'] = maxPages; | ||||||
|  | 	// paginationPassOn['dataLength'] = dataLength; | ||||||
|  | 	return [data, paginationPassOn]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function refreshTableByName(name) { | ||||||
|  | 	const dirtyTables = document.querySelectorAll("table[data-dataSource='" + name + "']"); | ||||||
|  | 	for (dirty of dirtyTables) { | ||||||
|  | 		refreshTable(dirty); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function updateSingeltonsByTableName(name) { | ||||||
|  | 	const dirtySingles = document.querySelectorAll("span[data-dataSource='" + name + "']"); | ||||||
|  | 	for (dirty of dirtySingles) { | ||||||
|  | 		writeSingelton(dirty); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function clearTable(table) { | ||||||
|  | 	const tbody = table.querySelector('tbody'); | ||||||
|  | 	tbody.innerHTML = ''; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function writeDataToTable(table, data, paginationPassOn) { | ||||||
|  | 	if(data == undefined || data == null || data.length == 0) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 	console.log('Writing data to table: ', table, data); | ||||||
|  | 	// Get THEAD and TBODY elements | ||||||
|  | 	const thead = table.querySelector('thead'); | ||||||
|  | 	const tbody = table.querySelector('tbody'); | ||||||
|  |  | ||||||
|  | 	// get index per column | ||||||
|  | 	const columns = thead.querySelectorAll('th'); | ||||||
|  | 	const columnIndices = []; | ||||||
|  | 	columns.forEach((column, index) => { | ||||||
|  | 		columnIndices[column.getAttribute('data-dataCol')] = index; | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	// All required cols | ||||||
|  | 	let requiredCols = []; | ||||||
|  | 	let actionFields = []; | ||||||
|  | 	columns.forEach((column) => { | ||||||
|  | 		// console.log('Column: ', column, ' FNC: ', column.getAttribute('data-fnc'), column.attributes); | ||||||
|  | 		if(column.getAttribute('data-fnc') == "actions") { | ||||||
|  | 			console.log('!!! Found actions column !!!'); | ||||||
|  | 			actionFields.push(column); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		requiredCols.push(column.getAttribute('data-dataCol')); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	// Get paginationPassOn | ||||||
|  | 	const start = paginationPassOn['start']; | ||||||
|  | 	const end = paginationPassOn['end']; | ||||||
|  | 	const currentPage = paginationPassOn['currentPage']; | ||||||
|  | 	const maxLinesPerPage = paginationPassOn['maxLinesPerPage']; | ||||||
|  | 	const maxPages = paginationPassOn['maxPages']; | ||||||
|  | 	const dataLength = paginationPassOn['dataLength']; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	// Find nav with class pagination and data-targetTable="table.id" | ||||||
|  | 	const paginationElement = document.querySelector("nav.pagination[data-targetTable='" + table.id + "']"); | ||||||
|  | 	const paginationList = paginationElement.querySelector('ul.pagination-list'); | ||||||
|  | 	console.log('Data length: ', dataLength, ' Max pages: ', maxPages); | ||||||
|  |  | ||||||
|  | 	if(maxPages > 1) { | ||||||
|  | 		// Clear pagination list | ||||||
|  | 		paginationList.innerHTML = ''; | ||||||
|  |  | ||||||
|  | 		for (let i = 1; i <= maxPages; i++) { | ||||||
|  | 			const li = document.createElement('li'); | ||||||
|  | 			li.innerHTML = '<a class="pagination-link" aria-label="Goto page ' + i + '" data-page="' + i + '">' + i + '</a>'; | ||||||
|  | 			if(i == currentPage) { | ||||||
|  | 				li.querySelector('a').classList.add('is-current'); | ||||||
|  | 			} | ||||||
|  | 			paginationList.appendChild(li); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 		// Remove unused pages, only leave first, last, current and 2 neighbors | ||||||
|  | 		let pages = paginationList.querySelectorAll('li'); | ||||||
|  | 		let friends = [] | ||||||
|  | 		// Always add first and last | ||||||
|  | 		friends.push(0); | ||||||
|  | 		friends.push(pages.length - 1); | ||||||
|  | 		friends.push(currentPage-1); | ||||||
|  |  | ||||||
|  | 		// Add direct neighbors | ||||||
|  | 		// friends.push(currentPage - 2); | ||||||
|  | 		friends.push(currentPage); | ||||||
|  | 		friends.push(currentPage - 2); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 		// Deduplicate friends | ||||||
|  | 		friends = [...new Set(friends)]; | ||||||
|  | 		// Sort friends | ||||||
|  | 		friends.sort((a, b) => a - b); | ||||||
|  | 		// Parse friends (string to int) | ||||||
|  | 		friends = friends.map((x) => parseInt(x)); | ||||||
|  |  | ||||||
|  | 		console.log('Friends: ', friends, ' Pages: ', pages.length, ' Current: ', currentPage); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 		// Remove everyone who is not a friend | ||||||
|  | 		for(let i = 0; i < pages.length; i++) { | ||||||
|  | 			if(friends.includes(i)) { | ||||||
|  | 				continue; | ||||||
|  | 			} | ||||||
|  | 			pages[i].remove(); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Find all gaps (step size bigger then 1) and add an ellipsis in between the two numbers | ||||||
|  | 		let last = 0; | ||||||
|  | 		for(let i = 0; i < friends.length; i++) { | ||||||
|  | 			if(friends[i] - last > 1) { | ||||||
|  | 				const li = document.createElement('li'); | ||||||
|  | 				li.innerHTML = '<span class="pagination-ellipsis">…</span>'; | ||||||
|  | 				paginationList.insertBefore(li, pages[friends[i]]); | ||||||
|  | 			} | ||||||
|  | 			last = friends[i]; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 		// Append on click event to all pagination links | ||||||
|  | 		paginationList.querySelectorAll('a').forEach((link) => { | ||||||
|  | 			link.addEventListener('click', async function() { | ||||||
|  | 				const page = link.getAttribute('data-page'); | ||||||
|  | 				table.setAttribute('data-currentPage', page); | ||||||
|  | 				refreshTable(table); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		paginationElement.classList.remove('is-hidden'); | ||||||
|  | 	} else { | ||||||
|  | 		paginationElement.classList.add('is-hidden'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	for (resultIndex in data) { | ||||||
|  | 		const row = data[resultIndex]; | ||||||
|  | 		const tr = document.createElement('tr'); | ||||||
|  | 		requiredCols.forEach((column) => { | ||||||
|  | 			const td = document.createElement('td'); | ||||||
|  | 			td.innerText = row[column]; | ||||||
|  | 			tr.appendChild(td); | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		// Add action fields | ||||||
|  | 		actionFields.forEach((actionField) => { | ||||||
|  | 			const td = document.createElement('td'); | ||||||
|  | 			const actions = actionField.getAttribute('data-actions').split(','); | ||||||
|  | 			actions.forEach((action) => { | ||||||
|  | 				const button = document.createElement('button'); | ||||||
|  | 				let icon = ''; | ||||||
|  | 				let color = 'is-primary'; | ||||||
|  | 				switch(action) { | ||||||
|  | 					case 'edit': { | ||||||
|  | 						icon = '<i class="bi bi-pencil"></i>'; | ||||||
|  | 						break; | ||||||
|  | 					} | ||||||
|  | 					case 'delete': { | ||||||
|  | 						icon = '<i class="bi bi-trash"></i>'; | ||||||
|  | 						color = 'is-danger'; | ||||||
|  | 						break; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				// Add classes | ||||||
|  | 				button.classList.add('button'); | ||||||
|  | 				button.classList.add('is-small'); | ||||||
|  | 				button.classList.add(color); | ||||||
|  | 				button.classList.add('is-outlined'); | ||||||
|  | 				button.innerHTML = ` <span class="icon is-small">${icon}</span> `; | ||||||
|  | 				button.style.marginRight = '5px'; | ||||||
|  |  | ||||||
|  | 				// Add data-action and data-id | ||||||
|  | 				button.setAttribute('data-action', action); | ||||||
|  | 				button.setAttribute("data-id", row["id"]); | ||||||
|  |  | ||||||
|  | 				// Add event listener | ||||||
|  | 				button.addEventListener('click', async function() { | ||||||
|  | 					const table = actionField.closest('table'); | ||||||
|  | 					const row = button.closest('tr'); | ||||||
|  | 					const columns = table.querySelectorAll('th'); | ||||||
|  | 					const columnIndices = []; | ||||||
|  | 					columns.forEach((column, index) => { | ||||||
|  | 						columnIndices[column.getAttribute('data-dataCol')] = index; | ||||||
|  | 					}); | ||||||
|  | 					const data = []; | ||||||
|  | 					columns.forEach((column) => { | ||||||
|  | 						data[column.getAttribute('data-dataCol')] = row.children[columnIndices[column.getAttribute('data-dataCol')]].innerText; | ||||||
|  | 					}); | ||||||
|  | 					console.log('Data: ', data); | ||||||
|  | 					switch(action) { | ||||||
|  | 						case 'edit': { | ||||||
|  | 							// Open modal with form | ||||||
|  | 							const form = document.querySelector("form[data-targetTable='" + table.getAttribute('data-dataSource') + "']"); | ||||||
|  | 							const formTitle = form.querySelector('.title'); | ||||||
|  | 							const entryPhase = form.querySelector('.entryPhase'); | ||||||
|  | 							const loadPhase = form.querySelector('.loadPhase'); | ||||||
|  | 							const fields = form.querySelectorAll('input'); | ||||||
|  | 							// Set modal to edit mode | ||||||
|  | 							form.setAttribute('data-action', 'edit'); | ||||||
|  | 							form.setAttribute('data-rid', button.getAttribute('data-id')); | ||||||
|  | 							formTitle.innerText = 'Edit entry'; | ||||||
|  | 							fields.forEach((field) => { | ||||||
|  | 								// Skip for submit button | ||||||
|  | 								if(field.getAttribute('type') == 'submit') { | ||||||
|  | 									return; | ||||||
|  | 								} | ||||||
|  | 								field.value = data[field.getAttribute('name')]; | ||||||
|  | 							}); | ||||||
|  | 							form.closest('.modal').classList.add('is-active'); | ||||||
|  | 							// TBD | ||||||
|  | 							break; | ||||||
|  | 						} | ||||||
|  | 						case 'delete': { | ||||||
|  | 							// confirm | ||||||
|  | 							const confirm = window.confirm('Do you really want to delete this entry?'); | ||||||
|  | 							// Delete entry | ||||||
|  | 							if(confirm) { | ||||||
|  | 							const table = actionField.closest('table'); | ||||||
|  | 							const id = button.getAttribute('data-id'); | ||||||
|  | 							const resp = await deleteRow(table.getAttribute('data-dataSource'), id); | ||||||
|  | 							if(resp['status'] == 'DELETED') { | ||||||
|  | 								refreshTable(table); | ||||||
|  | 								updateSingeltonsByTableName(table.getAttribute('data-dataSource')); | ||||||
|  | 							} else { | ||||||
|  | 								// Show error message | ||||||
|  | 								// TODO: Show error message | ||||||
|  | 							} | ||||||
|  | 							} | ||||||
|  | 							break; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				); | ||||||
|  | 				td.appendChild(button); | ||||||
|  | 			}); | ||||||
|  | 			tr.appendChild(td); | ||||||
|  | 		}); | ||||||
|  | 		tbody.appendChild(tr); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // Handle modal | ||||||
|  | document.addEventListener('DOMContentLoaded', () => { | ||||||
|  | 	// Functions to open and close a modal | ||||||
|  | 	function openModal($el) { | ||||||
|  | 		$el.classList.add('is-active'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	function closeModal($el) { | ||||||
|  | 		$el.classList.remove('is-active'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	function closeAllModals() { | ||||||
|  | 		(document.querySelectorAll('.modal') || []).forEach(($modal) => { | ||||||
|  | 			closeModal($modal); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Add a click event on buttons to open a specific modal | ||||||
|  | 	(document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => { | ||||||
|  | 		const modal = $trigger.dataset.target; | ||||||
|  | 		const $target = document.getElementById(modal); | ||||||
|  |  | ||||||
|  | 		$trigger.addEventListener('click', () => { | ||||||
|  | 			openModal($target); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	// Add a click event on various child elements to close the parent modal | ||||||
|  | 	(document.querySelectorAll('.modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => { | ||||||
|  | 		const $target = $close.closest('.modal'); | ||||||
|  |  | ||||||
|  | 		$close.addEventListener('click', () => { | ||||||
|  | 			closeModal($target); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	// Add a keyboard event to close all modals | ||||||
|  | 	document.addEventListener('keydown', (event) => { | ||||||
|  | 		if (event.key === 'Escape') { | ||||||
|  | 			closeAllModals(); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
							
								
								
									
										1
									
								
								static/placeholder.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/placeholder.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | {} | ||||||
							
								
								
									
										1
									
								
								static/test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | console.log('test.js'); | ||||||
							
								
								
									
										117
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | |||||||
|  | { | ||||||
|  | 	"compilerOptions": { | ||||||
|  | 		/* Visit https://aka.ms/tsconfig to read more about this file */ | ||||||
|  |  | ||||||
|  | 		/* Projects */ | ||||||
|  | 		// "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ | ||||||
|  | 		// "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */ | ||||||
|  | 		// "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */ | ||||||
|  | 		// "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */ | ||||||
|  | 		// "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */ | ||||||
|  | 		// "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */ | ||||||
|  |  | ||||||
|  | 		/* Language and Environment */ | ||||||
|  | 		"target": "esnext",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ | ||||||
|  | 		// "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */ | ||||||
|  | 		// "jsx": "preserve",                                /* Specify what JSX code is generated. */ | ||||||
|  | 		// "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */ | ||||||
|  | 		// "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */ | ||||||
|  | 		// "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ | ||||||
|  | 		// "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ | ||||||
|  | 		// "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ | ||||||
|  | 		// "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ | ||||||
|  | 		// "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */ | ||||||
|  | 		// "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */ | ||||||
|  | 		// "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */ | ||||||
|  |  | ||||||
|  | 		/* Modules */ | ||||||
|  | 		"module": "nodenext",                                /* Specify what module code is generated. */ | ||||||
|  | 		// "rootDir": "./",                                  /* Specify the root folder within your source files. */ | ||||||
|  | 		"moduleResolution": "nodenext",                     /* Specify how TypeScript looks up a file from a given module specifier. */ | ||||||
|  | 		// "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */ | ||||||
|  | 		"paths": { | ||||||
|  | 			"*": [ | ||||||
|  | 				"./node_modules/*" | ||||||
|  | 			] | ||||||
|  | 				},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */ | ||||||
|  | 				// "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */ | ||||||
|  | 				// "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */ | ||||||
|  | 				// "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */ | ||||||
|  | 		// "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */ | ||||||
|  | 		// "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */ | ||||||
|  | 		// "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ | ||||||
|  | 		// "rewriteRelativeImportExtensions": true,          /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ | ||||||
|  | 		// "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */ | ||||||
|  | 		// "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */ | ||||||
|  | 		// "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ | ||||||
|  | 		// "noUncheckedSideEffectImports": true,             /* Check side effect imports. */ | ||||||
|  | 		// "resolveJsonModule": true,                        /* Enable importing .json files. */ | ||||||
|  | 		// "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */ | ||||||
|  | 		// "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ | ||||||
|  |  | ||||||
|  | 		/* JavaScript Support */ | ||||||
|  | 		// "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ | ||||||
|  | 		// "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */ | ||||||
|  | 		// "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ | ||||||
|  |  | ||||||
|  | 		/* Emit */ | ||||||
|  | 		// "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ | ||||||
|  | 		// "declarationMap": true,                           /* Create sourcemaps for d.ts files. */ | ||||||
|  | 		// "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */ | ||||||
|  | 		"sourceMap": true,                                /* Create source map files for emitted JavaScript files. */ | ||||||
|  | 		// "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */ | ||||||
|  | 		// "noEmit": true,                                   /* Disable emitting files from a compilation. */ | ||||||
|  | 		// "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ | ||||||
|  | 		"outDir": "dist",                                   /* Specify an output folder for all emitted files. */ | ||||||
|  | 		// "removeComments": true,                           /* Disable emitting comments. */ | ||||||
|  | 		// "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ | ||||||
|  | 		// "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ | ||||||
|  | 		"sourceRoot": "src",                                 /* Specify the root path for debuggers to find the reference source code. */ | ||||||
|  | 		// "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */ | ||||||
|  | 		// "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */ | ||||||
|  | 		// "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ | ||||||
|  | 		// "newLine": "crlf",                                /* Set the newline character for emitting files. */ | ||||||
|  | 		// "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ | ||||||
|  | 		// "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */ | ||||||
|  | 		// "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */ | ||||||
|  | 		// "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */ | ||||||
|  | 		// "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */ | ||||||
|  |  | ||||||
|  | 		/* Interop Constraints */ | ||||||
|  | 		// "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */ | ||||||
|  | 		// "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ | ||||||
|  | 		// "isolatedDeclarations": true,                     /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ | ||||||
|  | 		// "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */ | ||||||
|  | 		"esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ | ||||||
|  | 		// "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ | ||||||
|  | 		"forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */ | ||||||
|  |  | ||||||
|  | 		/* Type Checking */ | ||||||
|  | 		"strict": true,                                      /* Enable all strict type-checking options. */ | ||||||
|  | 		"noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */ | ||||||
|  | 		// "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */ | ||||||
|  | 		// "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ | ||||||
|  | 		// "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ | ||||||
|  | 		// "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */ | ||||||
|  | 		// "strictBuiltinIteratorReturn": true,              /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ | ||||||
|  | 		// "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */ | ||||||
|  | 		// "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */ | ||||||
|  | 		"alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */ | ||||||
|  | 		// "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */ | ||||||
|  | 		// "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */ | ||||||
|  | 		// "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */ | ||||||
|  | 		// "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */ | ||||||
|  | 		// "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */ | ||||||
|  | 		// "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */ | ||||||
|  | 		// "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */ | ||||||
|  | 		// "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */ | ||||||
|  | 		// "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */ | ||||||
|  | 		// "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */ | ||||||
|  |  | ||||||
|  | 		/* Completeness */ | ||||||
|  | 		// "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */ | ||||||
|  | 		"skipLibCheck": true                                 /* Skip type checking all .d.ts files. */ | ||||||
|  | 	}, | ||||||
|  | 		"include": ["src"], | ||||||
|  | 		"exclude": ["node_modules", "dist"], | ||||||
|  | } | ||||||
							
								
								
									
										119
									
								
								views/contacts.eta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								views/contacts.eta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | |||||||
|  | <%~ include("partials/base_head.eta", {"title": "Kontakte"}) %> | ||||||
|  | <%~ include("partials/nav.eta") %> | ||||||
|  |  | ||||||
|  | <section class="hero is-primary" id="heroStatus"> | ||||||
|  | 	<div class="hero-body"> | ||||||
|  | 		<p class="title" data-tK="start-hero-header-welcome">Kontaktverwaltung</p> | ||||||
|  | 		<p class="subtitle" data-tK="start-hero-header-subtitle-default" id="heroExplainer">Erklärungstext</p> | ||||||
|  | 	</div> | ||||||
|  | </section> | ||||||
|  | <section class="section"> | ||||||
|  | 	<nav class="level"> | ||||||
|  | 		<div class="level-item has-text-centered"> | ||||||
|  | 			<div> | ||||||
|  | 				<p class="heading">Kontakte</p> | ||||||
|  | 				<p class="title"><span data-dataSource="AlertContacts" data-dataAction="COUNT" class="is-skeleton">Load.</span></p> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="level-item has-text-centered"> | ||||||
|  | 			<div> | ||||||
|  | 				<p class="heading"><button class="js-modal-trigger button" data-target="modal-js-example"> | ||||||
|  | 				Neuen Konakt anlegen | ||||||
|  | 	</button></p> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</nav> | ||||||
|  | </section> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | <!-- TODO: Mark required fields as required; add handling for validation --> | ||||||
|  | <div id="modal-js-example" class="modal"> | ||||||
|  | 	<div class="modal-background"></div> | ||||||
|  |  | ||||||
|  | 	<div class="modal-content"> | ||||||
|  | 		<div class="box entryPhase is-hidden"> | ||||||
|  | 			<h2 class="title">Neuer Kontakt</h1> | ||||||
|  |  | ||||||
|  | 				<i class="bi bi-arrow-clockwise title"></i> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="box entryPhase"> | ||||||
|  | 		 | ||||||
|  | 			<form data-targetTable="AlertContacts"> | ||||||
|  | 			<h2 class="title">Neuer Kontakt</h1> | ||||||
|  | 			<div class="field"> | ||||||
|  | 				<label class="label">Name</label> | ||||||
|  | 				<div class="control has-icons-left"> | ||||||
|  | 					<input class="input" type="text" placeholder="John Doe" value="" name="name"> | ||||||
|  | 					<span class="icon is-small is-left"> | ||||||
|  | 					<i class="bi bi-file-earmark-person-fill"></i> | ||||||
|  | 					</span> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  |  | ||||||
|  | 			<div class="field"> | ||||||
|  | 			<label class="label">Telefonummer</label> | ||||||
|  | 			<div class="control has-icons-left"> | ||||||
|  | 				<input class="input" type="text" placeholder="+49 1234 5678912" value="" name="phone"> | ||||||
|  | 				<span class="icon is-small is-left"> | ||||||
|  | 				<i class="bi bi-telephone-fill"></i> | ||||||
|  | 				</span> | ||||||
|  | 			</div> | ||||||
|  | 			</div> | ||||||
|  |  | ||||||
|  | 			<div class="field"> | ||||||
|  | 			<label class="label">Anmerkung</label> | ||||||
|  | 			<div class="control has-icons-left"> | ||||||
|  | 				<input class="input" type="text" placeholder="" value="" name="comment"> | ||||||
|  | 				<span class="icon is-small is-left"> | ||||||
|  | 				<i class="bi bi-chat-fill"></i> | ||||||
|  | 				</span> | ||||||
|  | 			</div> | ||||||
|  | 			</div> | ||||||
|  | 			<br> | ||||||
|  |  | ||||||
|  | 			<div class="field is-grouped"> | ||||||
|  | 			<div class="control"> | ||||||
|  | 				<input type="submit" class="button is-link" value="Save" data-actionBtn="save"> | ||||||
|  | 			</div> | ||||||
|  | 			<!--<div class="control"> | ||||||
|  | 				<button type="button" class="button is-link is-light" data-actionBtn="cancel">Cancel</button> | ||||||
|  | 			</div>--> | ||||||
|  | 			</div> | ||||||
|  | 		</form> | ||||||
|  | 			 | ||||||
|  | 			 | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  |  | ||||||
|  | 	<button class="modal-close is-large" aria-label="close"></button> | ||||||
|  | 	</div> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	 | ||||||
|  |  | ||||||
|  | <section class="section"> | ||||||
|  | 	<h1 class="title" data-tK="start-recent-header">Kontaktübersicht</h1> | ||||||
|  | 	<input class="input" type="text"  data-searchTargetId="contactTable" placeholder="Search..." /> | ||||||
|  | 	<table class="table is-striped is-fullwidth is-hoverable" data-dataSource="AlertContacts" id="contactTable" data-pageSize="5"> | ||||||
|  | 		<thead> | ||||||
|  | 			<tr> | ||||||
|  | 				<th data-dataCol = "id"><abbr title="Position">Pos</abbr></th> | ||||||
|  | 				<th data-dataCol = "name">Name</th> | ||||||
|  | 				<th data-dataCol = "phone">Telefonnummer</th> | ||||||
|  | 				<th data-dataCol = "comment">Kommentar</th> | ||||||
|  | 				<th data-fnc="actions" data-actions="edit,delete">Aktionen</th> | ||||||
|  | 			</tr> | ||||||
|  | 		</thead> | ||||||
|  | 		<tbody> | ||||||
|  | 		</tbody> | ||||||
|  | 		 | ||||||
|  | 	</table> | ||||||
|  | 	<nav class="pagination is-hidden" role="navigation" aria-label="pagination" data-targetTable="contactTable"> | ||||||
|  | 		<ul class="pagination-list"> | ||||||
|  | 			 | ||||||
|  | 		</ul> | ||||||
|  | 		</nav> | ||||||
|  | </section> | ||||||
|  |  | ||||||
|  | <%~ include("partials/footer.eta") %> | ||||||
|  | <%~ include("partials/base_foot.eta") %> | ||||||
							
								
								
									
										26
									
								
								views/errors/404.eta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								views/errors/404.eta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  |  | ||||||
|  | <head> | ||||||
|  | 	<meta charset="utf-8"> | ||||||
|  | 	<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  | 	<title>404</title> | ||||||
|  | 	<link rel="stylesheet" href="/libs/bulma/bulma.min.css"> | ||||||
|  | </head> | ||||||
|  |  | ||||||
|  | <body> | ||||||
|  | 	<section class="hero is-fullheight"> | ||||||
|  | 		<div class="hero-body has-text-centered"> | ||||||
|  | 			<div class="container"> | ||||||
|  | 				<div class="box"> | ||||||
|  | 					<h1>404</h1> | ||||||
|  | 					<h2>An error occured</h2> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</section> | ||||||
|  | </body> | ||||||
|  |  | ||||||
|  | </html> | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										73
									
								
								views/index.eta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								views/index.eta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | <%~ include("partials/base_head.eta", {"title": "Dashboard"}) %> | ||||||
|  | <%~ include("partials/nav.eta") %> | ||||||
|  |  | ||||||
|  | <section class="hero is-primary"> | ||||||
|  | 	<div class="hero-body"> | ||||||
|  | 		<p class="title" data-tK="start-hero-header-welcome">Willkommen</p> | ||||||
|  | 		<p class="subtitle" data-tK="start-hero-header-subtitle-default">Alles gut!</p> | ||||||
|  | 	</div> | ||||||
|  | </section> | ||||||
|  | <section class="section"> | ||||||
|  | 	<h1 class="title" data-tK="start-sysinfo-header">Systeminformationen</h1> | ||||||
|  | 	<nav class="level"> | ||||||
|  | 		<div class="level-item has-text-centered"> | ||||||
|  | 			<div> | ||||||
|  | 				<p class="heading">Aktionspläne</p> | ||||||
|  | 				<p class="title">π</p> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="level-item has-text-centered"> | ||||||
|  | 			<div> | ||||||
|  | 				<p class="heading">Alarmkonakte</p> | ||||||
|  | 				<p class="title"><span data-dataSource="AlertContacts" data-dataAction="COUNT" class="is-skeleton">Load.</span></p> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="level-item has-text-centered"> | ||||||
|  | 			<div> | ||||||
|  | 				<p class="heading">Alarme in den letzten 24h</p> | ||||||
|  | 				<p class="title">Keine</p> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="level-item has-text-centered"> | ||||||
|  | 			<div> | ||||||
|  | 				<p class="heading">Metrik 4</p> | ||||||
|  | 				<p class="title">Dreieck</p> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</nav> | ||||||
|  | </section> | ||||||
|  |  | ||||||
|  | <section class="section"> | ||||||
|  | 	<h1 class="title" data-tK="start-recent-header">Letze Alarme</h1> | ||||||
|  | 	<table class="table is-striped is-fullwidth"> | ||||||
|  | 		<thead> | ||||||
|  | 			<tr> | ||||||
|  | 				<th><abbr title="Position">Pos</abbr></th> | ||||||
|  | 				<th>Alarmierungszeit</th> | ||||||
|  | 				<th><abbr title="Quitierungszeit">Quit.zeit</abbr></th> | ||||||
|  | 				<th>Quelle</th> | ||||||
|  | 				<th><abbr title="Niveau">Niv.</abbr></th> | ||||||
|  | 			</tr> | ||||||
|  | 		</thead> | ||||||
|  | 		<tbody> | ||||||
|  | 			<tr class="is-danger"> | ||||||
|  | 				<th>1</th> | ||||||
|  | 				<td>1.1.2025 12:00</td> | ||||||
|  | 				<td>-</td> | ||||||
|  | 				<td>BMA</td> | ||||||
|  | 				<td>Brandmeldung</td> | ||||||
|  | 			</tr> | ||||||
|  |  | ||||||
|  | 			<tr class="is-warning"> | ||||||
|  | 				<th>1</th> | ||||||
|  | 				<td>1.1.2025 10:00</td> | ||||||
|  | 				<td>-</td> | ||||||
|  | 				<td>EMA</td> | ||||||
|  | 				<td>Störung</td> | ||||||
|  | 			</tr> | ||||||
|  | 		</tbody> | ||||||
|  | 	</table> | ||||||
|  | </section> | ||||||
|  |  | ||||||
|  | <%~ include("partials/footer.eta") %> | ||||||
|  | <%~ include("partials/base_foot.eta") %> | ||||||
							
								
								
									
										18
									
								
								views/lockscreen.eta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								views/lockscreen.eta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | <%~ include("partials/base_head.eta", {"title": "Dashboard"}) %> | ||||||
|  | <%~ include("partials/nav.eta") %> | ||||||
|  |  | ||||||
|  | <link rel="stylesheet" href="/static/css/lockscreen.css"> | ||||||
|  |  | ||||||
|  | <div id="credits"> | ||||||
|  |  | ||||||
|  | </div> | ||||||
|  | <div id="clock"> | ||||||
|  |     <div id="time"></div> | ||||||
|  |     <div id="date"></div> | ||||||
|  |  | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | <script src="/static/js/lockscreenBgHandler.js"></script> | ||||||
|  |  | ||||||
|  | <%~ include("partials/footer.eta") %> | ||||||
|  | <%~ include("partials/base_foot.eta") %> | ||||||
							
								
								
									
										4
									
								
								views/partials/base_foot.eta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								views/partials/base_foot.eta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | <script src="/static/apiWrapper.js"></script> | ||||||
|  | <script src="/static/pageDriver.js"></script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										20
									
								
								views/partials/base_head.eta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								views/partials/base_head.eta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  |  | ||||||
|  | <html lang="en"> | ||||||
|  | 	<head> | ||||||
|  | 		<meta charset="utf-8" /> | ||||||
|  | 		<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  |  | ||||||
|  | 		<title>HydrationHUB - <%= it.title %></title> | ||||||
|  | 		<meta name="author" content="[Project-Name-Here]"/> | ||||||
|  |  | ||||||
|  | 		<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" /> | ||||||
|  |  | ||||||
|  | 		<script src="/libs/jquery/jquery.min.js"></script> | ||||||
|  | 		 | ||||||
|  | 		<link rel="stylesheet" href="/libs/bulma/bulma.min.css"> | ||||||
|  | 		<link rel="stylesheet" href="/static/main.css"> | ||||||
|  | 		<link rel="stylesheet" href="/libs/bootstrap-icons/font/bootstrap-icons.min.css"> | ||||||
|  | 	</head> | ||||||
|  | 	<body> | ||||||
|  | 		<!-- The body and html tag need to be left open! --> | ||||||
							
								
								
									
										9
									
								
								views/partials/footer.eta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								views/partials/footer.eta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <footer class="footer"> | ||||||
|  | 	<div class="content has-text-centered"> | ||||||
|  | 		<p> | ||||||
|  | 		<i class="bi bi-universal-access"></i> | ||||||
|  | 			<strong>HydrationHUB</strong> by <a href="https://pnh.fyi">[Project-name-here]</a>.<br> | ||||||
|  | 			Running Version <span data-dataSource="version" data-dataAction="SPECIAL" class="is-skeleton">Load.</span> | ||||||
|  | 		</p> | ||||||
|  | 	</div> | ||||||
|  | </footer> | ||||||
							
								
								
									
										47
									
								
								views/partials/nav.eta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								views/partials/nav.eta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | <nav class="navbar" role="navigation" aria-label="main navigation"> | ||||||
|  | 	<div class="navbar-brand"> | ||||||
|  | 		<a class="navbar-item primary" href="/"> | ||||||
|  | 			<svg id="logo" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="498.424" height="148.888" viewBox="0 0 131.875 39.393"> | ||||||
|  | 				<path d="M118.368 51.177c-3.682 0-6.537.32-8.566.958-1.99.6-3.419 1.635-4.283 3.099-.827 1.466-1.239 3.533-1.239 6.2 0 2.03.356 3.7 1.07 5.016.714 1.315 1.916 2.46 3.607 3.438 1.728.939 4.17 1.878 7.326 2.817 2.517.752 4.452 1.466 5.805 2.142 1.39.64 2.386 1.352 2.987 2.142.601.79.9 1.747.9 2.874 0 1.24-.224 2.198-.675 2.874-.451.64-1.202 1.09-2.254 1.353-1.052.263-2.536.375-4.452.338-1.916-.038-3.4-.226-4.453-.564-1.051-.376-1.822-.977-2.31-1.804-.451-.826-.733-2.01-.845-3.55h-7.045c-.113 3.157.263 5.598 1.127 7.327.864 1.728 2.348 2.95 4.452 3.663 2.142.714 5.166 1.07 9.074 1.07 3.795 0 6.706-.318 8.735-.958 2.066-.638 3.532-1.728 4.396-3.268.864-1.54 1.296-3.72 1.296-6.538 0-2.254-.357-4.095-1.07-5.522-.715-1.466-1.917-2.706-3.608-3.72-1.653-1.015-4.02-2.01-7.1-2.987-2.518-.79-4.49-1.485-5.918-2.085-1.39-.6-2.404-1.202-3.043-1.804-.639-.6-.959-1.277-.959-2.028 0-1.165.207-2.049.62-2.649.414-.601 1.09-1.033 2.03-1.296.976-.263 2.366-.395 4.17-.395 1.728 0 3.061.15 4.001.45.977.264 1.672.734 2.085 1.41.451.638.733 1.578.846 2.818h7.157c.038-2.856-.376-5.054-1.24-6.594-.863-1.54-2.292-2.63-4.283-3.27-1.954-.637-4.734-.957-8.34-.957zm-67.058.12a24.388 24.388 0 0 0-11.954 3.114c-3.67 2.051-8.73 6.137-6.085 10.87.37.66.9 1.222 1.47 1.714 1.53 1.322 2.98.222 4.554-.458.975-.42 1.95-.842 2.922-1.268.433-.19 1.01-.331 1.328-.7.858-.99.494-2.994.05-4.094a27.22 27.22 0 0 1 3.651-1.24v30.828h7.214V58.968c1.05.182 2.439.43 3.266 1.04.387.285.113.91.075 1.298-.08.827-.027 1.816.345 2.58.307.632 1.16.785 1.765 1.009l4.564 1.703c.628.233 1.33.644 1.979.298 2.822-1.508 3.574-5.39 1.842-8.023-1.164-1.771-3.254-3.13-5.034-4.216-3.69-2.254-7.822-3.347-11.952-3.36zm-39.287.443L1.146 90.063h7.045l2.423-8.453h12.962l2.48 8.453h7.101L22.055 51.74H12.023zm67.628.001L68.773 90.063h7.045l2.423-8.453h12.964l2.48 8.453h7.1L89.683 51.74H79.65zm-62.668 6.537h.056l4.903 17.076h-9.637l4.678-17.076zm67.628 0h.056l4.903 17.076h-9.637l4.678-17.076z" style="display:inline;fill:current;fill-opacity:1;stroke:none;stroke-width:.408654;stroke-opacity:1" transform="translate(-1.146 -51.177)"/> | ||||||
|  | 			</svg> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 		</a> | ||||||
|  |  | ||||||
|  | 		<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample"> | ||||||
|  | 			<span aria-hidden="true"></span> | ||||||
|  | 			<span aria-hidden="true"></span> | ||||||
|  | 			<span aria-hidden="true"></span> | ||||||
|  | 			<span aria-hidden="true"></span> | ||||||
|  | 		</a> | ||||||
|  | 	</div> | ||||||
|  |  | ||||||
|  | 	<div id="navbarBasicExample" class="navbar-menu"> | ||||||
|  | 		<div class="navbar-start"> | ||||||
|  | 			<a class="navbar-item" href="/">Home</a> | ||||||
|  | 			<a class="navbar-item" href="/dbTest">API Integration <span class="tag is-info">Dev</span></a> | ||||||
|  | 			<a class="navbar-item" href="/contact">Kontakte <span class="tag is-primary">Neu!</span></a> | ||||||
|  | 			<!--<div class="navbar-item has-dropdown is-hoverable"> | ||||||
|  | 				<a class="navbar-link">More</a> | ||||||
|  | 				<div class="navbar-dropdown"> | ||||||
|  | 					<a class="navbar-item">About</a> | ||||||
|  | 					<a class="navbar-item is-selected">Jobs</a> | ||||||
|  | 					<a class="navbar-item">Contact</a> | ||||||
|  | 					<hr class="navbar-divider"> | ||||||
|  | 					<a class="navbar-item">Report an issue</a> | ||||||
|  | 				</div> | ||||||
|  | 			</div>--> | ||||||
|  | 		</div> | ||||||
|  |  | ||||||
|  | 		<div class="navbar-end"> | ||||||
|  | 			<div class="navbar-item"> | ||||||
|  | 				<div class="buttons"> | ||||||
|  | 					<a class="button is-primary"> | ||||||
|  | 						<strong>Sign up</strong> | ||||||
|  | 					</a> | ||||||
|  | 					<a class="button is-light">Log in</a> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | </nav> | ||||||
							
								
								
									
										155
									
								
								views/test.eta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								views/test.eta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | |||||||
|  | <%~ include("partials/base_head.eta", {"title": "API Test"}) %> | ||||||
|  | <%~ include("partials/nav.eta") %> | ||||||
|  |  | ||||||
|  | <section class="hero is-primary" id="heroStatus"> | ||||||
|  | 	<div class="hero-body"> | ||||||
|  | 		<p class="title" data-tK="start-hero-header-welcome">Test Page</p> | ||||||
|  | 		<p class="subtitle" data-tK="start-hero-header-subtitle-default" id="heroExplainer">API Integration test page</p> | ||||||
|  | 	</div> | ||||||
|  | </section> | ||||||
|  | <section class="section"> | ||||||
|  | 	<h1 class="title" data-tK="start-sysinfo-header">Singelton Query</h1> | ||||||
|  | 	<nav class="level"> | ||||||
|  | 		<div class="level-item has-text-centered"> | ||||||
|  | 			<div> | ||||||
|  | 				<p class="heading">Kontakte</p> | ||||||
|  | 				<p class="title"><span data-dataSource="AlertContacts" data-dataAction="COUNT" class="is-skeleton">Load.</span></p> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 		<!--<div class="level-item has-text-centered"> | ||||||
|  | 			<div> | ||||||
|  | 				<p class="heading">Alarme</p> | ||||||
|  | 				<p class="title"><span data-dataSource="Alerts" data-dataCol="id" data-dataAction="COUNT" class="is-skeleton">Load.</span></p> | ||||||
|  | 			</div> | ||||||
|  | 		</div>--> | ||||||
|  | 		<div class="level-item has-text-centered"> | ||||||
|  | 			<div> | ||||||
|  | 				<p class="heading">Version</p> | ||||||
|  | 				<p class="title"><span data-dataSource="version" data-dataAction="SPECIAL" class="is-skeleton">Load.</span></p> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</nav> | ||||||
|  | </section> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | <section class="section"> | ||||||
|  | <h1 class="title" data-tK="start-sysinfo-header">File test</h1> | ||||||
|  | <form method="put" action="/api/upload" enctype="multipart/form-data" id="uploadForm"> | ||||||
|  | 	<div class="field"> | ||||||
|  | 		<label class="label">File</label> | ||||||
|  | 		<div class="control"> | ||||||
|  | 			<input class="input" type="file" name="file" id="file"> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | 	<!-- URL field --> | ||||||
|  | 	<div class="field"> | ||||||
|  | 		<label class="label">URL</label> | ||||||
|  | 		<div class="control"> | ||||||
|  | 			<input class="input" type="text" name="url" id="url"> | ||||||
|  | 		</div> | ||||||
|  | 	<div class="field is-grouped"> | ||||||
|  | 		<div class="control"> | ||||||
|  | 			<input type="submit" class="button is-link" value="Upload" data-actionBtn="upload"> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | 	<script> | ||||||
|  | 		document.getElementById("url").addEventListener("input", function() { | ||||||
|  | 			document.getElementById("uploadForm").action = document.getElementById("url").value; | ||||||
|  | 		}); | ||||||
|  | 	</script> | ||||||
|  | </form> | ||||||
|  | </section> | ||||||
|  |  | ||||||
|  | <!-- TODO: Mark required fields as required; add handling for validation --> | ||||||
|  | <div id="modal-js-example" class="modal"> | ||||||
|  | 	<div class="modal-background"></div> | ||||||
|  |  | ||||||
|  | 	<div class="modal-content"> | ||||||
|  | 		<div class="box entryPhase is-hidden"> | ||||||
|  | 			<h2 class="title">New Contact</h1> | ||||||
|  |  | ||||||
|  | 				<i class="bi bi-arrow-clockwise title"></i> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="box entryPhase"> | ||||||
|  | 		 | ||||||
|  | 			<form data-targetTable="AlertContacts"> | ||||||
|  | 			<h2 class="title">New Contact</h1> | ||||||
|  | 			<div class="field"> | ||||||
|  | 				<label class="label">Name</label> | ||||||
|  | 				<div class="control has-icons-left"> | ||||||
|  | 					<input class="input" type="text" placeholder="John Doe" value="" name="name"> | ||||||
|  | 					<span class="icon is-small is-left"> | ||||||
|  | 					<i class="bi bi-file-earmark-person-fill"></i> | ||||||
|  | 					</span> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  |  | ||||||
|  | 			<div class="field"> | ||||||
|  | 			<label class="label">Telephone</label> | ||||||
|  | 			<div class="control has-icons-left"> | ||||||
|  | 				<input class="input" type="text" placeholder="+49 1234 5678912" value="" name="phone"> | ||||||
|  | 				<span class="icon is-small is-left"> | ||||||
|  | 				<i class="bi bi-telephone-fill"></i> | ||||||
|  | 				</span> | ||||||
|  | 			</div> | ||||||
|  | 			</div> | ||||||
|  |  | ||||||
|  | 			<div class="field"> | ||||||
|  | 			<label class="label">Comment</label> | ||||||
|  | 			<div class="control has-icons-left"> | ||||||
|  | 				<input class="input" type="text" placeholder="" value="" name="comment"> | ||||||
|  | 				<span class="icon is-small is-left"> | ||||||
|  | 				<i class="bi bi-chat-fill"></i> | ||||||
|  | 				</span> | ||||||
|  | 			</div> | ||||||
|  | 			</div> | ||||||
|  | 			<br> | ||||||
|  |  | ||||||
|  | 			<div class="field is-grouped"> | ||||||
|  | 			<div class="control"> | ||||||
|  | 				<input type="submit" class="button is-link" value="Save" data-actionBtn="save"> | ||||||
|  | 			</div> | ||||||
|  | 			<!--<div class="control"> | ||||||
|  | 				<button type="button" class="button is-link is-light" data-actionBtn="cancel">Cancel</button> | ||||||
|  | 			</div>--> | ||||||
|  | 			</div> | ||||||
|  | 		</form> | ||||||
|  | 			 | ||||||
|  | 			 | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  |  | ||||||
|  | 	<button class="modal-close is-large" aria-label="close"></button> | ||||||
|  | 	</div> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	<button class="js-modal-trigger button" data-target="modal-js-example"> | ||||||
|  | 	Create new Contact | ||||||
|  | 	</button> | ||||||
|  |  | ||||||
|  | <section class="section"> | ||||||
|  | 	<h1 class="title" data-tK="start-recent-header">Alarm Kontakte</h1> | ||||||
|  | 	<input class="input" type="text"  data-searchTargetId="contactTable" placeholder="Search..." /> | ||||||
|  | 	<table class="table is-striped is-fullwidth is-hoverable" data-dataSource="AlertContacts" id="contactTable" data-pageSize="5"> | ||||||
|  | 		<thead> | ||||||
|  | 			<tr> | ||||||
|  | 				<th data-dataCol = "id"><abbr title="Position">Pos</abbr></th> | ||||||
|  | 				<th data-dataCol = "name">Name</th> | ||||||
|  | 				<th data-dataCol = "phone">Telefon Nummer</th> | ||||||
|  | 				<th data-dataCol = "comment">Kommentar</th> | ||||||
|  | 				<th data-fnc="actions" data-actions="edit,delete">Aktionen</th> | ||||||
|  | 			</tr> | ||||||
|  | 		</thead> | ||||||
|  | 		<tbody> | ||||||
|  | 		</tbody> | ||||||
|  | 		 | ||||||
|  | 	</table> | ||||||
|  | 	<nav class="pagination is-hidden" role="navigation" aria-label="pagination" data-targetTable="contactTable"> | ||||||
|  | 		<ul class="pagination-list"> | ||||||
|  | 			 | ||||||
|  | 		</ul> | ||||||
|  | 		</nav> | ||||||
|  | </section> | ||||||
|  |  | ||||||
|  | <%~ include("partials/footer.eta") %> | ||||||
|  | <%~ include("partials/base_foot.eta") %> | ||||||
		Reference in New Issue
	
	Block a user