Initial commit
This commit is contained in:
commit
d75e5b1f11
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") %>
|
Loading…
x
Reference in New Issue
Block a user