Compare commits
No commits in common. "84259d37d42234e1be75f3a5c95f4b442a9e5844" and "4585a83838b80552160c9d9e5be0af891eae39c8" have entirely different histories.
84259d37d4
...
4585a83838
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,7 +4,6 @@
|
|||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
!.vscode/i18n-ally-custom-framework.yml
|
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
|
||||||
# Local History for Visual Studio Code
|
# Local History for Visual Studio Code
|
||||||
|
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@ -2,10 +2,7 @@
|
|||||||
"recommendations": [
|
"recommendations": [
|
||||||
"2gua.rainbow-brackets",
|
"2gua.rainbow-brackets",
|
||||||
"christian-kohler.npm-intellisense",
|
"christian-kohler.npm-intellisense",
|
||||||
"remimarsal.prettier-now",
|
"remimarsal.prettier-now"
|
||||||
"lokalise.i18n-ally",
|
|
||||||
],
|
],
|
||||||
"unwantedRecommendations": [
|
"unwantedRecommendations": []
|
||||||
"antfu.i18n-ally"
|
|
||||||
]
|
|
||||||
}
|
}
|
8
.vscode/i18n-ally-custom-framework.yml
vendored
8
.vscode/i18n-ally-custom-framework.yml
vendored
@ -1,8 +0,0 @@
|
|||||||
languageIds:
|
|
||||||
- javascript
|
|
||||||
- html
|
|
||||||
keyMatchReg:
|
|
||||||
- '\{\{__ "([a-zA-Z0-9_]+)"\}\}'
|
|
||||||
monopoly: false
|
|
||||||
refactorTemplates:
|
|
||||||
- '{{__ "$1"}}'
|
|
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@ -5,6 +5,7 @@
|
|||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "vscode.json-language-features"
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
},
|
},
|
||||||
|
"prettier.enable": false,
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "vscode.typescript-language-features",
|
"editor.defaultFormatter": "vscode.typescript-language-features",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
@ -15,13 +16,5 @@
|
|||||||
"javascript.preferences.quoteStyle": "single",
|
"javascript.preferences.quoteStyle": "single",
|
||||||
"javascript.preferences.importModuleSpecifierEnding": "minimal",
|
"javascript.preferences.importModuleSpecifierEnding": "minimal",
|
||||||
"typescript.preferences.importModuleSpecifierEnding": "minimal",
|
"typescript.preferences.importModuleSpecifierEnding": "minimal",
|
||||||
"typescript.preferences.includePackageJsonAutoImports": "on",
|
"typescript.preferences.includePackageJsonAutoImports": "on"
|
||||||
"i18n-ally.localesPaths": "src/locales",
|
|
||||||
"i18n-ally.keystyle": "nested",
|
|
||||||
"i18n-ally.extract.keygenStrategy":"slug",
|
|
||||||
"i18n-ally.enabledFrameworks": [
|
|
||||||
"custom"
|
|
||||||
],
|
|
||||||
"i18n-ally.sourceLanguage": "en"
|
|
||||||
|
|
||||||
}
|
}
|
11
README.md
11
README.md
@ -37,7 +37,7 @@ The basic generation mechanism makes the templates and routes interchangeable (i
|
|||||||
> The templates are stored in src/templates by default.
|
> The templates are stored in src/templates by default.
|
||||||
|
|
||||||
We provide a set of default templates that we use for the ["Lauf für Kaya!" charity run](https://lauf-fuer-kaya.de).
|
We provide a set of default templates that we use for the ["Lauf für Kaya!" charity run](https://lauf-fuer-kaya.de).
|
||||||
We use handlebars for templateing utilizing i18next for translation - the i18n string format in the templates is : `{{__ "string"}}`
|
We use mustache-style templating strings to fill the templates with real information (exact strings are explained below).
|
||||||
You can provide your own templates by replacing the ones we provided before compiling the project or by simply mounting your custom templates into the docker container.
|
You can provide your own templates by replacing the ones we provided before compiling the project or by simply mounting your custom templates into the docker container.
|
||||||
|
|
||||||
The server currently needs the following templates to work:
|
The server currently needs the following templates to work:
|
||||||
@ -45,10 +45,12 @@ The server currently needs the following templates to work:
|
|||||||
|
|
||||||
### Sponsoring Contracts
|
### Sponsoring Contracts
|
||||||
|
|
||||||
| Template Data | Type | Optional | Description
|
| Template String | Type | Optional | Description
|
||||||
| - | - | - | -
|
| - | - | - | -
|
||||||
| runners | array(Runner) | ❌ | The runner objects. We generate a contract for each runner on a new DIN-A5 page.
|
| runner_firstname | string | ❌ | The runner's first name
|
||||||
|
| runner_middlename | string | ✅ | The runner's middle name
|
||||||
|
| runner_lastname | string | ❌ | The runner's last name
|
||||||
|
| runner_id | int | ❌ | The runner's id
|
||||||
|
|
||||||
## Recommended Editor
|
## Recommended Editor
|
||||||
|
|
||||||
@ -57,7 +59,6 @@ The server currently needs the following templates to work:
|
|||||||
### Recommended Extensions
|
### Recommended Extensions
|
||||||
|
|
||||||
* will be automatically recommended via ./vscode/extensions.json
|
* will be automatically recommended via ./vscode/extensions.json
|
||||||
* we also provide a config for i18n-ally in the .vscode folder
|
|
||||||
|
|
||||||
## Staging
|
## Staging
|
||||||
### Branches & Tags
|
### Branches & Tags
|
||||||
|
13
package.json
13
package.json
@ -7,8 +7,7 @@
|
|||||||
"dev": "nodemon src/app.ts",
|
"dev": "nodemon src/app.ts",
|
||||||
"build": "rimraf ./dist && tsc",
|
"build": "rimraf ./dist && tsc",
|
||||||
"licenses:export": "license-exporter --markdown",
|
"licenses:export": "license-exporter --markdown",
|
||||||
"release": "release-it --only-version",
|
"release": "release-it --only-version"
|
||||||
"translations:sort": "node sort_translations.js"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -40,20 +39,13 @@
|
|||||||
"license": "CC-BY-NC-SA-4.0",
|
"license": "CC-BY-NC-SA-4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@odit/class-validator-jsonschema": "^2.1.1",
|
"@odit/class-validator-jsonschema": "^2.1.1",
|
||||||
"axios": "^0.21.1",
|
|
||||||
"cheerio": "^1.0.0-rc.5",
|
|
||||||
"class-transformer": "0.3.1",
|
"class-transformer": "0.3.1",
|
||||||
"class-validator": "^0.13.1",
|
"class-validator": "^0.13.1",
|
||||||
"consola": "^2.15.0",
|
"consola": "^2.15.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"handlebars": "^4.7.6",
|
"html-pdf": "^2.2.0",
|
||||||
"i18next": "^19.8.7",
|
|
||||||
"i18next-fs-backend": "^1.0.8",
|
|
||||||
"mime-types": "^2.1.28",
|
|
||||||
"pdf-lib": "^1.16.0",
|
|
||||||
"puppeteer": "^7.0.1",
|
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"routing-controllers": "^0.9.0-alpha.6",
|
"routing-controllers": "^0.9.0-alpha.6",
|
||||||
"routing-controllers-openapi": "^2.2.0"
|
"routing-controllers-openapi": "^2.2.0"
|
||||||
@ -62,7 +54,6 @@
|
|||||||
"@odit/license-exporter": "^0.0.9",
|
"@odit/license-exporter": "^0.0.9",
|
||||||
"@types/express": "^4.17.11",
|
"@types/express": "^4.17.11",
|
||||||
"@types/node": "^14.14.22",
|
"@types/node": "^14.14.22",
|
||||||
"@types/puppeteer": "^5.4.3",
|
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"release-it": "^14.2.2",
|
"release-it": "^14.2.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
// get all language files
|
|
||||||
const files = fs.readdirSync('./src/locales/');
|
|
||||||
files.forEach((f) => {
|
|
||||||
// read file as object
|
|
||||||
const unordered = JSON.parse(fs.readFileSync(`src/locales/${f}`));
|
|
||||||
// order object by keys alpabetically A-Z
|
|
||||||
const ordered = Object.keys(unordered).sort().reduce((obj, key) => {
|
|
||||||
obj[key] = unordered[key];
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
// format output as json for commit diff compatibility
|
|
||||||
const out = JSON.stringify(ordered, 0, 4);
|
|
||||||
// write output file
|
|
||||||
fs.writeFileSync(`src/locales/${f}`, out);
|
|
||||||
});
|
|
@ -1,197 +1,56 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import cheerio from "cheerio";
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import Handlebars from 'handlebars';
|
import pdf_converter from "html-pdf";
|
||||||
import i18next from "i18next";
|
|
||||||
import Backend from 'i18next-fs-backend';
|
|
||||||
import mime from "mime-types";
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { Stream } from 'stream';
|
||||||
import puppeteer from "puppeteer";
|
|
||||||
import { Runner } from './models/Runner';
|
|
||||||
import { RunnerGroup } from './models/RunnerGroup';
|
|
||||||
/**
|
/**
|
||||||
* This class is responsible for all things pdf creation.
|
* This class is responsible for all things pdf creation.
|
||||||
* This uses the html templates from src/templates.
|
* This uses the html templates from src/templates.
|
||||||
*/
|
*/
|
||||||
export class PdfCreator {
|
export class PdfCreator {
|
||||||
private templateDir = path.join(__dirname, '/templates');
|
private templateDir = path.join(__dirname, '/templates');
|
||||||
private browser;
|
|
||||||
private static interpolations = { eventname: "Lauf für Kaya! 2021", sponsoring_receipt_minimum_amount: '10', currency_symbol: "€" }
|
|
||||||
|
|
||||||
/**
|
//TODO: Accept the runner class
|
||||||
* Main constructor.
|
public async generateSponsoringContract(): Promise<Pdf> {
|
||||||
* Initializes i18n(ext), Handlebars and puppeteer.
|
let template = fs.readFileSync(`${this.templateDir}/sponsoring_contract.html`, 'utf8');
|
||||||
*/
|
template = template.replace("{{Runner Name}}", "lelele");
|
||||||
constructor() {
|
return new Pdf(await pdf_converter.create(template, { format: "A5", orientation: "landscape" }));
|
||||||
this.init();
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is a wrapper for the pdf objects created by html-pdf.
|
||||||
|
* It offers typed conversion to Buffer and Stream.
|
||||||
|
*/
|
||||||
|
export class Pdf {
|
||||||
|
content: any;
|
||||||
|
|
||||||
|
constructor(pdf: any) {
|
||||||
|
this.content = pdf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main constructor.
|
* Promise wrapper function that resolves the toBuffer promise for pdf generation.
|
||||||
* Initializes i18n(ext), Handlebars and puppeteer.
|
|
||||||
*/
|
*/
|
||||||
public async init() {
|
public async toBuffer(): Promise<Buffer> {
|
||||||
const minimal_args = [
|
let promise = await new Promise<Buffer>((resolve, reject) => {
|
||||||
'--autoplay-policy=user-gesture-required',
|
this.content.toBuffer(function (err, buffer: Buffer) {
|
||||||
'--disable-background-networking',
|
resolve(buffer);
|
||||||
'--disable-background-timer-throttling',
|
|
||||||
'--disable-backgrounding-occluded-windows',
|
|
||||||
'--disable-breakpad',
|
|
||||||
'--disable-client-side-phishing-detection',
|
|
||||||
'--disable-component-update',
|
|
||||||
'--disable-default-apps',
|
|
||||||
'--disable-dev-shm-usage',
|
|
||||||
'--disable-domain-reliability',
|
|
||||||
'--disable-extensions',
|
|
||||||
'--disable-features=AudioServiceOutOfProcess',
|
|
||||||
'--disable-hang-monitor',
|
|
||||||
'--disable-ipc-flooding-protection',
|
|
||||||
'--disable-notifications',
|
|
||||||
'--disable-offer-store-unmasked-wallet-cards',
|
|
||||||
'--disable-popup-blocking',
|
|
||||||
'--disable-print-preview',
|
|
||||||
'--disable-prompt-on-repost',
|
|
||||||
'--disable-renderer-backgrounding',
|
|
||||||
'--disable-speech-api',
|
|
||||||
'--disable-sync',
|
|
||||||
'--hide-scrollbars',
|
|
||||||
'--ignore-gpu-blacklist',
|
|
||||||
'--metrics-recording-only',
|
|
||||||
'--mute-audio',
|
|
||||||
'--no-default-browser-check',
|
|
||||||
'--no-first-run',
|
|
||||||
'--no-pings',
|
|
||||||
'--no-zygote',
|
|
||||||
'--password-store=basic',
|
|
||||||
'--use-gl=swiftshader'
|
|
||||||
];
|
|
||||||
|
|
||||||
await i18next
|
|
||||||
.use(Backend)
|
|
||||||
.init({
|
|
||||||
fallbackLng: 'en',
|
|
||||||
lng: 'en',
|
|
||||||
backend: {
|
|
||||||
loadPath: path.join(__dirname, '/locales/{{lng}}.json')
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
await Handlebars.registerHelper('__',
|
});
|
||||||
function (str) {
|
return await promise;
|
||||||
return i18next.t(str, PdfCreator.interpolations).toString();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
this.browser = await puppeteer.launch({ headless: true, args: minimal_args });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate sponsoring contract pdfs.
|
* Promise wrapper function that resolves the toStream promise for pdf generation.
|
||||||
* @param runner The runner you want to generate the contracts for.
|
|
||||||
* @param locale The locale used for the contracts (default:en)
|
|
||||||
*/
|
*/
|
||||||
public async generateSponsoringContract(runners: Runner[], locale: string = "en"): Promise<Buffer> {
|
public async toStream(): Promise<Stream> {
|
||||||
if (runners.length == 1 && Object.keys(runners[0]).length == 0) {
|
let promise = await new Promise<Stream>((resolve, reject) => {
|
||||||
runners[0] = this.generateEmptyRunner();
|
this.content.toStream(function (err, stream: Stream) {
|
||||||
}
|
resolve(stream);
|
||||||
if (runners.length > 50) {
|
|
||||||
let pdf_promises = new Array<Promise<Buffer>>();
|
|
||||||
let i, j;
|
|
||||||
for (i = 0, j = runners.length; i < j; i += 50) {
|
|
||||||
let chunk = runners.slice(i, i + 50);
|
|
||||||
pdf_promises.push(this.generateSponsoringContract(chunk, locale));
|
|
||||||
}
|
|
||||||
const pdfs = await Promise.all(pdf_promises);
|
|
||||||
return await this.mergePdfs(pdfs);
|
|
||||||
}
|
|
||||||
await i18next.changeLanguage(locale);
|
|
||||||
const template_source = fs.readFileSync(`${this.templateDir}/sponsoring_contract.html`, 'utf8');
|
|
||||||
const template = Handlebars.compile(template_source);
|
|
||||||
const result = template({ runners })
|
|
||||||
const pdf = await this.renderPdf(result, { format: "A5", landscape: true });
|
|
||||||
return pdf
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts all images in html to base64.
|
|
||||||
* Works with image files in the template directory or images from urls.
|
|
||||||
* @param html The html string whoms images shall get replaced.
|
|
||||||
*/
|
|
||||||
public async imgToBase64(html): Promise<string> {
|
|
||||||
const $ = cheerio.load(html)
|
|
||||||
$('img').each(async (index, element) => {
|
|
||||||
let imgsrc = $(element).attr("src");
|
|
||||||
const img_type = mime.lookup(imgsrc);
|
|
||||||
|
|
||||||
if (!(img_type.includes("image"))) {
|
|
||||||
throw new Error("File is not image mime type");
|
|
||||||
}
|
|
||||||
|
|
||||||
let image;
|
|
||||||
if (imgsrc.startsWith("http")) {
|
|
||||||
image = (await axios.get(imgsrc)).data;
|
|
||||||
image = Buffer.from(image).toString('base64');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (imgsrc.startsWith("./")) {
|
|
||||||
imgsrc = imgsrc.replace("./", "");
|
|
||||||
}
|
|
||||||
image = fs.readFileSync(`${this.templateDir}/${imgsrc}`, { encoding: "base64" });
|
|
||||||
}
|
|
||||||
|
|
||||||
image = `data:${img_type};base64,${image}`
|
|
||||||
$(element).attr("src", image)
|
|
||||||
})
|
|
||||||
|
|
||||||
return $.html();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method manages the creation of pdfs via puppeteer.
|
|
||||||
* @param html The HTML that should get rendered.
|
|
||||||
* @param options Puppeteer PDF option (eg: {format: "A4"})
|
|
||||||
*/
|
|
||||||
public async renderPdf(html: string, options): Promise<any> {
|
|
||||||
html = await this.imgToBase64(html);
|
|
||||||
let page = await this.browser.newPage();
|
|
||||||
await page.setContent(html);
|
|
||||||
const pdf = await page.pdf(options);
|
|
||||||
await page.close();
|
|
||||||
return pdf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merges multiple pdfs into one.
|
|
||||||
* @param pdfs The pdfs you want to merge as an buffer array.
|
|
||||||
* @returns The merged pdf as a buffer.
|
|
||||||
*/
|
|
||||||
private async mergePdfs(pdfs: Buffer[]): Promise<Buffer> {
|
|
||||||
const mergedPdf = await PDFDocument.create();
|
|
||||||
|
|
||||||
for (const pdfBuffer of pdfs) {
|
|
||||||
const pdf = await PDFDocument.load(pdfBuffer);
|
|
||||||
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
|
|
||||||
copiedPages.forEach((page) => {
|
|
||||||
mergedPdf.addPage(page);
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
return await promise;
|
||||||
return <Buffer>(await mergedPdf.save());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a new dummy runner with halfspaces for all strings.
|
|
||||||
* Can be used to generate empty sponsoring contracts.
|
|
||||||
* @returns A new runner object that apears to be empty.
|
|
||||||
*/
|
|
||||||
private generateEmptyRunner(): Runner {
|
|
||||||
let group = new RunnerGroup();
|
|
||||||
group.id = 0;
|
|
||||||
group.name = " ";
|
|
||||||
let runner = new Runner();
|
|
||||||
runner.id = 0;
|
|
||||||
runner.firstname = " ";
|
|
||||||
runner.lastname = " ";
|
|
||||||
runner.group = group;
|
|
||||||
return runner;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,30 +1,20 @@
|
|||||||
import { Body, JsonController, Post, QueryParam, Res } from 'routing-controllers';
|
import { ContentType, Controller, Get } from 'routing-controllers';
|
||||||
import { OpenAPI } from 'routing-controllers-openapi';
|
import { OpenAPI } from 'routing-controllers-openapi';
|
||||||
import { Runner } from '../models/Runner';
|
|
||||||
import { PdfCreator } from '../PdfCreator';
|
import { PdfCreator } from '../PdfCreator';
|
||||||
|
|
||||||
/**
|
@Controller()
|
||||||
* The pdf controller handels all endpoints concerning pdf generation.
|
|
||||||
* It therefore is the hearth of the document-generation server's endpoints.
|
|
||||||
* All endpoints have to accept a locale query-param to support i18n.
|
|
||||||
*/
|
|
||||||
@JsonController()
|
|
||||||
export class PdfController {
|
export class PdfController {
|
||||||
private pdf: PdfCreator = new PdfCreator();
|
private pdf: PdfCreator;
|
||||||
private initialized: boolean = false;
|
constructor() {
|
||||||
|
this.pdf = new PdfCreator();
|
||||||
@Post('/contracts')
|
|
||||||
@OpenAPI({ description: "Generate Sponsoring contract pdfs from runner objects.<br>You can choose your prefered locale by passing the 'locale' query-param.<br> If you provide more than 100 runenrs this could take a moment or two (we tested up to 1000 runners in about 70sec so far)." })
|
|
||||||
async generateContracts(@Body({ validate: true, options: { limit: "500mb" } }) runners: Runner | Runner[], @Res() res: any, @QueryParam("locale") locale: string) {
|
|
||||||
if (!this.initialized) {
|
|
||||||
await this.pdf.init();
|
|
||||||
this.initialized = true;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(runners)) {
|
|
||||||
runners = [runners];
|
|
||||||
}
|
|
||||||
const contracts = await this.pdf.generateSponsoringContract(runners, locale);
|
|
||||||
res.setHeader('content-type', 'application/pdf');
|
|
||||||
return contracts;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Get('/contracts')
|
||||||
|
@ContentType("application/pdf")
|
||||||
|
@OpenAPI({ description: "Generate Sponsoring contract pdfs from runner objects." })
|
||||||
|
async generateContracts() {
|
||||||
|
//TODO: Accept the real classes
|
||||||
|
const contracts = await this.pdf.generateSponsoringContract();
|
||||||
|
return await contracts.toBuffer();
|
||||||
|
}
|
||||||
|
}
|
@ -2,9 +2,6 @@ import { Get, JsonController } from 'routing-controllers';
|
|||||||
import { OpenAPI } from 'routing-controllers-openapi';
|
import { OpenAPI } from 'routing-controllers-openapi';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
|
|
||||||
/**
|
|
||||||
* The statuscontroller provides simple endpoints concerning basic information about the server.
|
|
||||||
*/
|
|
||||||
@JsonController()
|
@JsonController()
|
||||||
export class StatusController {
|
export class StatusController {
|
||||||
@Get('/version')
|
@Get('/version')
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"address": "Adresse",
|
|
||||||
"city": "Stadt",
|
|
||||||
"date": "Datum",
|
|
||||||
"firstname": "Vorname",
|
|
||||||
"group": "Team/Klasse",
|
|
||||||
"house_number": "Hausnummer",
|
|
||||||
"id": "ID",
|
|
||||||
"lastname": "Nachname",
|
|
||||||
"location": "Ort",
|
|
||||||
"please_use_blockletters": "Bitte in DRUCKBUCHSTABEN schreiben",
|
|
||||||
"postalcode": "Postleitzahl",
|
|
||||||
"signature": "Unterschrift",
|
|
||||||
"sponsor": "Sponsor",
|
|
||||||
"sponsoring_address_condition": "Muss ausgefüllt werden, wenn Sie eine Spendenquittung benötigen - Spendenquittungen können erst ab einem Gesamtbetrag von {{sponsoring_receipt_minimum_amount}}{{currency_symbol}} ausgestellt werden",
|
|
||||||
"sponsoring_amount_per_distance": "mit einem Betrag von _____{{currency_symbol}} pro gelaufenem Kilometer zu unterstützen.",
|
|
||||||
"sponsoring_subtitle": "Ich/Wir sind bereit anlässlich des {{eventname}}",
|
|
||||||
"sponsoring_title": "Sponsoringerklärung",
|
|
||||||
"street": "Straße"
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"address": "Address",
|
|
||||||
"city": "City",
|
|
||||||
"date": "date",
|
|
||||||
"firstname": "First name",
|
|
||||||
"group": "Team/class",
|
|
||||||
"house_number": "House number",
|
|
||||||
"lastname": "Last name",
|
|
||||||
"location": "Location",
|
|
||||||
"please_use_blockletters": "Please write in BLOCK LETTERS.",
|
|
||||||
"postalcode": "Postal code",
|
|
||||||
"signature": "Signature",
|
|
||||||
"sponsor": "sponsor",
|
|
||||||
"sponsoring_address_condition": "You have to provide an address if you want a donation receipt - Donation receipts can't be issued for total donation amounts under {{sponsoring_receipt_minimum_amount}}{{currency_symbol}}",
|
|
||||||
"sponsoring_amount_per_distance": "with the amount of _____{{currency_symbol}} per kilometer run.",
|
|
||||||
"sponsoring_subtitle": "On the ocation of the {{eventname}} I/We want to support",
|
|
||||||
"sponsoring_title": "Sponsoring contract",
|
|
||||||
"street": "Street"
|
|
||||||
}
|
|
@ -1,18 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
IsInt,
|
IsInt,
|
||||||
|
|
||||||
IsNotEmpty,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
IsObject,
|
IsObject,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
IsOptional,
|
|
||||||
|
|
||||||
IsPositive,
|
|
||||||
|
|
||||||
IsString
|
IsString
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { RunnerGroup } from './RunnerGroup';
|
import { RunnerGroup } from './RunnerGroup';
|
||||||
@ -25,28 +13,24 @@ export class Runner {
|
|||||||
* The runner's id.
|
* The runner's id.
|
||||||
*/
|
*/
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsPositive()
|
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The runner's first name.
|
* The runner's first name.
|
||||||
*/
|
*/
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
|
||||||
firstname: string;
|
firstname: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The runner's middle name.
|
* The runner's middle name.
|
||||||
*/
|
*/
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
|
||||||
middlename?: string;
|
middlename?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The runner's last name.
|
* The runner's last name.
|
||||||
*/
|
*/
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
|
||||||
lastname: string;
|
lastname: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { IsInt, IsNotEmpty, IsObject, IsOptional, IsPositive, IsString } from "class-validator";
|
import { IsInt, IsNotEmpty, IsObject, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the runner group class - a simplified version of the backend's ResponseRunnerTeam/-Organization
|
* Defines the runner group class - a simplified version of the backend's ResponseRunnerTeam/-Organization
|
||||||
*/
|
*/
|
||||||
export class RunnerGroup {
|
export abstract class RunnerGroup {
|
||||||
/**
|
/**
|
||||||
* The group's id.
|
* The group's id.
|
||||||
*/
|
*/
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsPositive()
|
@IsNotEmpty()
|
||||||
id: number;
|
id: number;;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The group's name.
|
* The group's name.
|
||||||
@ -24,5 +24,13 @@ export class RunnerGroup {
|
|||||||
*/
|
*/
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
parentGroup?: RunnerGroup;
|
parentGroup?: RunnerGroup
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the groups full name in the format: org.name/team.name (or just org).
|
||||||
|
*/
|
||||||
|
public get fullName(): string {
|
||||||
|
if (!this.parentGroup) { return this.name; }
|
||||||
|
return `${this.name}/${this.parentGroup.fullName}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,115 +1,45 @@
|
|||||||
<html>
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8">
|
||||||
|
<title>Sponsoring contract</title>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Sackers Gothic Std';
|
||||||
|
font-weight: 500;
|
||||||
|
background: rgb(241,241,241);
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
<head>
|
.page {
|
||||||
<meta charset="utf8">
|
position: relative;
|
||||||
<title>Sponsoring contract</title>
|
height: 148mm;
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
width: 210mm;
|
||||||
<style>
|
display: block;
|
||||||
.sheet {
|
background: white;
|
||||||
margin: 0;
|
page-break-after: auto;
|
||||||
overflow: hidden;
|
margin: 50px;
|
||||||
position: relative;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
}
|
||||||
page-break-after: always;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.A5.landscape .sheet {
|
@media print {
|
||||||
width: 210mm;
|
body {
|
||||||
height: 147mm
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
.page {
|
||||||
margin-bottom: -20;
|
margin: 0;
|
||||||
}
|
height: 100%;
|
||||||
</style>
|
width: 100%;
|
||||||
</head>
|
}
|
||||||
|
}
|
||||||
<body class="A5 landscape">
|
</style>
|
||||||
{{#each runners}}
|
</head>
|
||||||
<div class="sheet">
|
<body>
|
||||||
<img id="header_img" width="100%" src="sponsoringheader.png" />
|
<div class="page">
|
||||||
<div style=" padding: 0 1rem 0 1rem;">
|
<p style="font-size: 100vw;">{{Runner Name}}</p>
|
||||||
<div class="columns" style="padding-bottom: 0;">
|
|
||||||
<div class="column is-two-fifths">
|
|
||||||
<p style="font-size: large; font-weight: bold;">{{__ "sponsoring_title"}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<p style="font-size: x-small; vertical-align: revert; margin-top: auto;">{{__ "please_use_blockletters"}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p> {{__ "sponsoring_subtitle"}} </p>
|
|
||||||
<div class="columns" style="padding-top: 0;">
|
|
||||||
<div class="column is-8">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;">{{this.firstname}}
|
|
||||||
{{this.middlename}}</span>
|
|
||||||
<p style="font-size: x-small; display: block;">{{__ "firstname"}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="column is-2">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;">{{this.id}}</span>
|
|
||||||
<p style="font-size: x-small; display: block;">ID</p>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<!-- CODE Here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-6">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;">{{this.lastname}}</span>
|
|
||||||
<p style="font-size: x-small; display: block;">{{__ "lastname"}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="column is-6">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;">{{this.group.name}}</span>
|
|
||||||
<p style="font-size: x-small; display: block;">{{__ "group"}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>{{__ "sponsoring_amount_per_distance"}}</p>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-6">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;"> </span>
|
|
||||||
<p style="font-size: x-small; display: block;">{{__ "lastname"}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="column is-6">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;"> </span>
|
|
||||||
<p style="font-size: x-small; display: block;">{{__ "firstname"}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p style="font-size: medium;">{{__ "address"}} ({{__ "sponsor"}})</p>
|
|
||||||
<p style="font-size: x-small;">({{__ "sponsoring_address_condition"}})</p>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-8">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;"> </span>
|
|
||||||
<p style="font-size: x-small; display: block;">{{__ "street"}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="column is-4">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;"> </span>
|
|
||||||
<p style="font-size: x-small; display: block;">{{__ "house_number"}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-4">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;"> </span>
|
|
||||||
<p style="font-size: x-small; display: block;">{{__ "postalcode"}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="column is-8">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;"> </span>
|
|
||||||
<p style="font-size: x-small; display: block;">{{__ "city"}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-7">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;"> </span>
|
|
||||||
<p style="font-size: x-small; display: block;">{{__ "location"}}, {{__ "date"}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="column is-5">
|
|
||||||
<span style="border-bottom: 1px solid; width: 100%; display: block;"> </span>
|
|
||||||
<p style="font-size: x-small; display: block;">{{__ "signature"}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</body>
|
||||||
{{/each}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
Binary file not shown.
Before Width: | Height: | Size: 225 KiB |
Loading…
x
Reference in New Issue
Block a user