First naive implementation (~ PHP version copy)

This commit is contained in:
yumauri
2019-10-15 15:21:41 +03:00
parent 37f9e9e6e7
commit 86b78c6ae5
31 changed files with 5003 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
/node_modules/
/lib/
yarn-error.log
.DS_Store
.project
.vscode
*.log
+6
View File
@@ -0,0 +1,6 @@
{
"transform": {
"^.+\\.ts$": "ts-jest"
},
"testRegex": ".+\\.spec\\.ts$"
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "gotenberg-js-client",
"version": "0.1.0",
"description": "A simple JS/TS for interacting with a Gotenberg API",
"keywords": [
"gotenberg",
"pdf",
"puppeteer"
],
"files": [
"lib/**/*"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"author": "Victor Didenko <yumaa.verdin@gmail.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/yumauri/gotenberg-js-client"
},
"bugs": {
"url": "https://github.com/yumauri/gotenberg-js-client/issues"
},
"homepage": "https://github.com/yumauri/gotenberg-js-client#readme",
"scripts": {
"dev": "ts-node src/index.ts",
"test": "jest --config jestconfig.json",
"build": "npm run clean && tsc",
"clean": "rimraf lib",
"format": "prettier --write \"src/**/*.ts\"",
"lint": "tslint -p tsconfig.json",
"prepare": "npm run build",
"prepublishOnly": "npm test && npm run lint",
"preversion": "npm run lint",
"version": "npm run format && git add -A src",
"postversion": "git push && git push --tags"
},
"dependencies": {
"@types/node": "^12.7.12",
"form-data": "^2.5.1"
},
"devDependencies": {
"@types/jest": "^24.0.19",
"jest": "^24.9.0",
"prettier": "^1.18.2",
"rimraf": "^3.0.0",
"ts-jest": "^24.1.0",
"ts-node": "^8.4.1",
"tslint": "^5.20.0",
"tslint-config-prettier": "^1.18.0",
"tslint-config-security": "^1.16.0",
"tslint-config-standard-plus": "^2.2.0",
"typescript": "^3.6.4"
}
}
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
quoteProps: 'as-needed',
jsxSingleQuote: true,
trailingComma: 'es5',
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: 'avoid',
}
+50
View File
@@ -0,0 +1,50 @@
import * as FromData from 'form-data'
import { HttpAdapter } from '../http/_Adapter'
import { NativeAdapter } from '../http/NativeAdapter'
import { Request } from '../request/_Request'
/**
* Helper function to convert Request to FormData
* https://github.com/form-data/form-data
*/
const formdata = (request: Request) => {
const data = new FromData()
// append all form values
const values = request.getFormValues()
for (const field in values) {
if (values.hasOwnProperty(field)) {
const value = values[field]
if (value !== undefined) {
data.append(field, String(value))
}
}
}
// append all form files
const files = request.getFormFiles()
for (const filename in files) {
if (files.hasOwnProperty(filename)) {
const document = files[filename]
if (document !== undefined) {
data.append(filename, document.getStream(), { filename })
}
}
}
return data
}
/**
* Gotenberg client
*/
export class Client {
constructor(
private readonly url: string,
private readonly client: HttpAdapter = new NativeAdapter()
) {}
public do(request: Request): Promise<NodeJS.ReadableStream> {
return this.client.post(this.url + request.getPostURL(), formdata(request))
}
}
+1
View File
@@ -0,0 +1 @@
export { Client } from './Client'
+17
View File
@@ -0,0 +1,17 @@
import { resolve } from 'path'
import { createReadStream } from 'fs'
import { GotenbergDocument } from './_GotenbergDocument'
import { Document } from './_Document'
/**
* Document from file, for gotenberg conversions
*/
export class FileDocument extends Document implements GotenbergDocument {
constructor(fileName: string, private readonly path: string) {
super(fileName)
}
public getStream(): NodeJS.ReadableStream {
return createReadStream(resolve(__dirname, this.path)) // FIXME: I guess __dirname will not work as I want
}
}
+52
View File
@@ -0,0 +1,52 @@
import { Readable } from 'stream'
import { GotenbergDocument } from './_GotenbergDocument'
import { Document } from './_Document'
/**
* Readable stream from string
* https://medium.com/@dupski/nodejs-creating-a-readable-stream-from-a-string-e0568597387f
*/
class ReadableBuffer extends Readable {
private sent = false
private readonly body: Buffer
constructor(body: string | Buffer) {
super()
this.body = body instanceof Buffer ? body : Buffer.from(body)
}
_read() {
if (!this.sent) {
this.push(this.body)
this.sent = true
} else {
this.push(null)
}
}
}
/**
* Document from string or Buffer, for gotenberg conversions
*/
export class InplaceDocument<T extends string | Buffer> extends Document
implements GotenbergDocument {
constructor(fileName: string, private readonly body: T) {
super(fileName)
}
public getStream(): NodeJS.ReadableStream {
return new ReadableBuffer(this.body)
}
}
/**
* Aliase for Document from string, for gotenberg conversions
* Use class/extends instead of type alias to preserve this identifier after compilation
*/
export class StringDocument extends InplaceDocument<string> {}
/**
* Aliase for Document from Buffer, for gotenberg conversions
* Use class/extends instead of type alias to preserve this identifier after compilation
*/
export class BufferDocument extends InplaceDocument<Buffer> {}
+18
View File
@@ -0,0 +1,18 @@
import { GotenbergDocument } from './_GotenbergDocument'
import { Document } from './_Document'
/**
* Document from stream.Readable, for gotenberg conversions
*/
export class StreamDocument extends Document implements GotenbergDocument {
constructor(
fileName: string,
private readonly stream: NodeJS.ReadableStream
) {
super(fileName)
}
public getStream(): NodeJS.ReadableStream {
return this.stream
}
}
+31
View File
@@ -0,0 +1,31 @@
import { GotenbergDocument } from './_GotenbergDocument'
/**
* Abstract Document for gotenberg conversions
*/
export abstract class Document implements GotenbergDocument {
constructor(private readonly fileName: string) {}
public getFileName(): string {
return this.fileName
}
abstract getStream(): NodeJS.ReadableStream
}
/**
* Reduce function, used in `reduce` header
*/
const reduceFn = (
map: { [filename: string]: Document },
document: Document
) => {
map[document.getFileName()] = document
return map
}
/**
* Helper function to reduce array of documents to map
*/
export const reduce = (documents: Document[]) =>
documents.reduce(reduceFn, Object.create(null))
+4
View File
@@ -0,0 +1,4 @@
export interface GotenbergDocument {
getFileName(): string
getStream(): NodeJS.ReadableStream
}
+7
View File
@@ -0,0 +1,7 @@
export { StreamDocument } from './StreamDocument'
export { FileDocument } from './FileDocument'
export {
BufferDocument,
InplaceDocument,
StringDocument,
} from './InplaceDocument'
+35
View File
@@ -0,0 +1,35 @@
import * as http from 'http'
import * as https from 'https'
import * as FormData from 'form-data'
import { HttpAdapter } from './_Adapter'
export class NativeAdapter extends HttpAdapter {
public post(
url: URL | string,
data: FormData
): Promise<NodeJS.ReadableStream> {
const _url = url instanceof URL ? url : new URL(url)
const request = _url.protocol === 'http:' ? http.request : https.request
return new Promise((resolve, reject) => {
const req = request(_url, {
method: 'POST',
headers: data.getHeaders(),
// auth <string> Basic authentication i.e. 'user:password' to compute an Authorization header.
// timeout <number>: A number specifying the socket timeout in milliseconds. This will set the timeout before the socket is connected.
})
req.on('error', reject)
req.on('response', res => {
if (res.statusCode === 200) {
resolve(res)
} else {
res.resume() // ignore response body
reject(new Error(res.statusCode + ' ' + res.statusMessage))
}
})
data.pipe(req) // pipe Form data to request
})
}
}
+8
View File
@@ -0,0 +1,8 @@
import * as FormData from 'form-data'
export abstract class HttpAdapter {
abstract post(
url: URL | string,
data: FormData
): Promise<NodeJS.ReadableStream>
}
+5
View File
@@ -0,0 +1,5 @@
// import { Greeter } from './index'
//
// test('My Greeter', () => {
// expect(Greeter('Carl')).toBe('Hello Carl')
// })
+35
View File
@@ -0,0 +1,35 @@
export * from './client'
export * from './request'
export * from './document'
export * from './page'
// import { Client } from './client'
// import { FileDocument } from './document'
// import { HTMLRequest } from './request'
// import { A4 } from './page'
// import { createWriteStream } from 'fs'
//
//
/*
const client = new Client('http://localhost:3000')
client
.do(
new HTMLRequest(new FileDocument('index.html', '../../test/index.html'))
.margins({
top: 0,
right: 0.2, // ~5mm
bottom: 0,
left: 0.2, // ~5mm
})
.paperSize(A4)
)
.then(res => {
res.pipe(createWriteStream('test/invoice_statement.pdf'))
})
.catch(e => {
console.log('Error:', e)
})
*/
+19
View File
@@ -0,0 +1,19 @@
export type PageSize = [number, number] | { width?: number; height?: number }
export type Margins =
| [number, number, number, number]
| { top?: number; right?: number; bottom?: number; left?: number }
// TODO: more sizes
// https://papersizes.io/
export const A3: PageSize = [11.7, 16.5]
export const A4: PageSize = [8.27, 11.7]
export const A5: PageSize = [5.8, 8.3]
export const A6: PageSize = [4.1, 5.8]
export const LETTER: PageSize = [8.5, 11]
export const LEGAL: PageSize = [8.5, 14]
export const TABLOID: PageSize = [11, 17]
export const NO_MARGINS: Margins = [0, 0, 0, 0]
export const NORMAL_MARGINS: Margins = [1, 1, 1, 1]
export const LARGE_MARGINS: Margins = [2, 2, 2, 2]
+43
View File
@@ -0,0 +1,43 @@
import { FormFiles, GotenbergRequest } from './_GotenbergRequest'
import { Document, reduce } from '../document/_Document'
import { ChromeRequest } from './_ChromeRequest'
const INDEX_TEMPLATE = 'index.html'
/**
* Request builder for HTML conversions
* https://thecodingmachine.github.io/gotenberg/#html
*/
export class HTMLRequest extends ChromeRequest implements GotenbergRequest {
private _assets: Document[]
constructor(private readonly index: Document, ...assets: Document[]) {
super()
this._assets = assets || []
}
//
// Methods from GotenbergRequest
//
public getPostURL() {
return '/convert/html'
}
public getFormFiles(): FormFiles {
return {
...super.getFormFiles(),
...reduce(this._assets),
[INDEX_TEMPLATE]: this.index,
}
}
//
// Setters for private fields
//
public assets(...assets: Document[]): HTMLRequest {
this._assets = assets
return this
}
}
+31
View File
@@ -0,0 +1,31 @@
import { FormFiles, GotenbergRequest } from './_GotenbergRequest'
import { Document, reduce } from '../document/_Document'
import { HTMLRequest } from './HTMLRequest'
/**
* Request builder for Markdown conversions
* https://thecodingmachine.github.io/gotenberg/#markdown
*/
export class MarkdownRequest extends HTMLRequest implements GotenbergRequest {
private readonly _markdowns: Document[]
constructor(index: Document, ...markdowns: Document[]) {
super(index)
this._markdowns = markdowns
}
//
// Methods from GotenbergRequest
//
public getPostURL() {
return '/convert/markdown'
}
public getFormFiles(): FormFiles {
return {
...super.getFormFiles(),
...reduce(this._markdowns),
}
}
}
+31
View File
@@ -0,0 +1,31 @@
import { FormFiles, GotenbergRequest } from './_GotenbergRequest'
import { Request } from './_Request'
import { Document, reduce } from '../document/_Document'
/**
* Request builder for merging PDFs
* https://thecodingmachine.github.io/gotenberg/#merge
*/
export class MergeRequest extends Request implements GotenbergRequest {
private readonly _files: Document[]
constructor(...files: Document[]) {
super()
this._files = files
}
//
// Methods from GotenbergRequest
//
public getPostURL(): string {
return '/merge'
}
public getFormFiles(): FormFiles {
return {
...super.getFormFiles(),
...reduce(this._files),
}
}
}
+50
View File
@@ -0,0 +1,50 @@
import { FormFiles, FormValues, GotenbergRequest } from './_GotenbergRequest'
import { Request } from './_Request'
import { Document, reduce } from '../document/_Document'
const LANDSCAPE = 'landscape'
/**
* Request builder for Office document conversions
* https://thecodingmachine.github.io/gotenberg/#office
*/
export class OfficeRequest extends Request implements GotenbergRequest {
private readonly _files: Document[]
private _landscape? = false
constructor(...files: Document[]) {
super()
this._files = files || []
}
//
// Methods from GotenbergRequest
//
public getPostURL(): string {
return '/convert/office'
}
public getFormValues(): FormValues {
return {
...super.getFormValues(),
[LANDSCAPE]: this._landscape,
}
}
public getFormFiles(): FormFiles {
return {
...super.getFormFiles(),
...reduce(this._files),
}
}
//
// Setters for private fields
//
public landscape(value: boolean): OfficeRequest {
this._landscape = value
return this
}
}
+29
View File
@@ -0,0 +1,29 @@
import { FormValues, GotenbergRequest } from './_GotenbergRequest'
import { Request } from './_Request'
const REMOTE_URL = 'remoteURL'
/**
* Request builder for remote URL conversions
* https://thecodingmachine.github.io/gotenberg/#url
*/
export class URLRequest extends Request implements GotenbergRequest {
constructor(private readonly url: string) {
super()
}
//
// Methods from GotenbergRequest
//
public getPostURL(): string {
return '/convert/url'
}
public getFormValues(): FormValues {
return {
...super.getFormValues(),
[REMOTE_URL]: this.url,
}
}
}
+149
View File
@@ -0,0 +1,149 @@
import { FormFiles, FormValues, GotenbergRequest } from './_GotenbergRequest'
import { Request } from './_Request'
import { Document } from '../document/_Document'
import { Margins, PageSize } from '../page'
const HEADER_TEMPLATE = 'header.html'
const FOOTER_TEMPLATE = 'footer.html'
const WAIT_DELAY = 'waitDelay'
const PAPER_WIDTH = 'paperWidth'
const PAPER_HEIGHT = 'paperHeight'
const MARGIN_TOP = 'marginTop'
const MARGIN_BOTTOM = 'marginBottom'
const MARGIN_LEFT = 'marginLeft'
const MARGIN_RIGHT = 'marginRight'
const LANDSCAPE = 'landscape'
const GOOGLE_CHROME_RPCC_BUFFER_SIZE = 'googleChromeRpccBufferSize'
/**
* Abstract ChromeRequest builder for gotenberg headless chrome (HTML and Markdown) conversions
*/
export abstract class ChromeRequest extends Request
implements GotenbergRequest {
private _header?: Document
private _footer?: Document
private _waitDelay?: number
private _paperWidth?: number
private _paperHeight?: number
private _marginTop?: number
private _marginBottom?: number
private _marginLeft?: number
private _marginRight?: number
private _landscape? = false
private _googleChromeRpccBufferSize?: number
//
// Methods from GotenbergRequest
//
public getFormValues(): FormValues {
return {
...super.getFormValues(),
[WAIT_DELAY]: this._waitDelay,
[PAPER_WIDTH]: this._paperWidth,
[PAPER_HEIGHT]: this._paperHeight,
[MARGIN_TOP]: this._marginTop,
[MARGIN_BOTTOM]: this._marginBottom,
[MARGIN_LEFT]: this._marginLeft,
[MARGIN_RIGHT]: this._marginRight,
[GOOGLE_CHROME_RPCC_BUFFER_SIZE]: this._googleChromeRpccBufferSize,
[LANDSCAPE]: this._landscape,
}
}
public getFormFiles(): FormFiles {
return {
...super.getFormFiles(),
[HEADER_TEMPLATE]: this._header,
[FOOTER_TEMPLATE]: this._footer,
}
}
//
// Setters for private fields
//
public header(header: Document): ChromeRequest {
this._header = header
return this
}
public footer(footer: Document): ChromeRequest {
this._footer = footer
return this
}
public waitDelay(value: number): ChromeRequest {
this._waitDelay = value
return this
}
public paperSize(size: PageSize): ChromeRequest {
if (Array.isArray(size)) {
;[this._paperWidth, this._paperHeight] = size
} else {
;({ width: this._paperWidth, height: this._paperHeight } = size)
}
return this
}
public paperWidth(value: number): ChromeRequest {
this._paperWidth = value
return this
}
public paperHeight(value: number): ChromeRequest {
this._paperHeight = value
return this
}
public margins(margins: Margins): ChromeRequest {
if (Array.isArray(margins)) {
;[
this._marginTop,
this._marginRight,
this._marginBottom,
this._marginLeft,
] = margins
} else {
;({
top: this._marginTop,
right: this._marginRight,
bottom: this._marginBottom,
left: this._marginLeft,
} = margins)
}
return this
}
public marginTop(value: number): ChromeRequest {
this._marginTop = value
return this
}
public marginRight(value: number): ChromeRequest {
this._marginRight = value
return this
}
public marginBottom(value: number): ChromeRequest {
this._marginBottom = value
return this
}
public marginLeft(value: number): ChromeRequest {
this._marginLeft = value
return this
}
public landscape(value: boolean): ChromeRequest {
this._landscape = value
return this
}
public googleChromeRpccBufferSize(value: number): ChromeRequest {
this._googleChromeRpccBufferSize = value
return this
}
}
+15
View File
@@ -0,0 +1,15 @@
import { Document } from '../document/_Document'
export type FormValues = {
[field: string]: number | string | boolean | undefined
}
export type FormFiles = {
[field: string]: Document | undefined
}
export interface GotenbergRequest {
getPostURL(): string
getFormValues(): FormValues | void
getFormFiles(): FormFiles | void
}
+59
View File
@@ -0,0 +1,59 @@
import { FormFiles, FormValues, GotenbergRequest } from './_GotenbergRequest'
const RESULT_FILENAME = 'resultFilename'
const WAIT_TIMEOUT = 'waitTimeout'
const WEBHOOK_URL = 'webhookURL'
const WEBHOOK_URL_TIMEOUT = 'webhookURLTimeout'
/**
* Abstract Request builder for gotenberg conversions
*/
export abstract class Request implements GotenbergRequest {
private _resultFilename?: string
private _waitTimeout?: number
private _webhookURL?: string
private _webhookURLTimeout?: number
//
// Methods from GotenbergRequest
//
abstract getPostURL(): string
public getFormValues(): FormValues {
return {
[RESULT_FILENAME]: this._resultFilename,
[WAIT_TIMEOUT]: this._waitTimeout,
[WEBHOOK_URL]: this._webhookURL,
[WEBHOOK_URL_TIMEOUT]: this._webhookURLTimeout,
}
}
public getFormFiles(): FormFiles {
return {}
}
//
// Setters for private fields
//
public resultFilename(value: string): Request {
this._resultFilename = value
return this
}
public waitTimeout(value: number): Request {
this._waitTimeout = value
return this
}
public webhookURL(value: string): Request {
this._webhookURL = value
return this
}
public webhookURLTimeout(value: number): Request {
this._webhookURLTimeout = value
return this
}
}
+5
View File
@@ -0,0 +1,5 @@
export { HTMLRequest } from './HTMLRequest'
export { MarkdownRequest } from './MarkdownRequest'
export { MergeRequest } from './MergeRequest'
export { OfficeRequest } from './OfficeRequest'
export { URLRequest } from './URLRequest'
+406
View File
@@ -0,0 +1,406 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Invoice Statement</title>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
margin: 0;
}
.gray {
color: #707070;
}
.noBorder {
border: none !important;
}
.noPadding {
padding: 0 !important;
}
.alignTop {
vertical-align: top;
}
.header {
height: 80px;
padding: 0 40px;
border-bottom: 1px solid #707070;
}
.logo .setplexlogo {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI1MCA1OSIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMjUwIDU5IiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxnIGNsaXAtcnVsZT0iZXZlbm9kZCIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJtMjE2LjI5IDM2LjQ0M2wtNy4xNDUgNS4zOTNjLTAuMzYzIDAuMzUzLTAuODA4IDAuNTM2LTEuMzE3IDAuNTM2LTAuNjYzIDAtMS4xOTctMC4zMjItMS42MDgtMC44My0wLjQwNS0wLjUwMi0wLjYzOS0xLjA3OS0wLjYzOS0xLjcyNyAwLTAuNjM5IDAuMzE5LTEuMTUgMC44MTQtMS41NDYgMi42NjEtMi4wNjEgNS40MjUtNC4wMTQgNy45NzktNi4yMTMgMi4xNjgtMS44NjggMi40MDYtMi4xNzEtMWUtMyAtMy43MjRsLTcuOTc1LTYuMDc5Yy0wLjYwMi0wLjQxNC0wLjk5Ni0wLjk1MS0wLjk5Ni0xLjY5OCAwLTAuNjcyIDAuMjI3LTEuMjY1IDAuNzIyLTEuNzI0IDAuNDY1LTAuNDMyIDEuMDI0LTAuNjU3IDEuNjU4LTAuNjU3IDAuMjQ4IDAgMC41MDcgMC4wNDQgMC43NDkgMC4wOTggMC4zMTcgMC4wNzEgMC42MDIgMC4yMTMgMC44MzUgMC40MzlsNi4xMiA0LjY4NC0wLjAxNCAwLjAxOWMxLjY5OSAxLjI5MSAzLjM5OSAyLjU4MiA1LjA5OCAzLjg3MyAwLjAxOCAwLjAxMyAwLjAzNiAwLjAyNyAwLjA1NCAwLjA0MSAzLjIzMiAyLjE4IDIuNDE5IDQuMTE4LTAuMDY1IDUuOTU1LTEuNDM2IDEuMDYyLTIuODQ2IDIuMTM0LTQuMjY5IDMuMTZ6IiBmaWxsPSIjMDA3MEJBIi8+PHBhdGggZD0ibTIyNy4zOCAzMi40MzFsNy41NzIgNS44NDFjMC40OSAwLjM5MiAwLjgxIDAuOTA0IDAuODEgMS41NDIgMCAwLjY2My0wLjIyNCAxLjI1Ni0wLjYzOSAxLjc3MS0wLjQxMSAwLjUwOS0wLjk0NSAwLjgzMS0xLjYwOSAwLjgzMS0wLjQ5MSAwLTAuOTM4LTAuMjY0LTEuMzA0LTAuNTcxbC04LjY3Mi02LjQ5NiAzLjg0Mi0yLjkxOHoiIGZpbGw9IiMxQjFCMUIiLz48cGF0aCBkPSJtMjIzLjUyIDI1LjExOGw4LjQxLTYuNDEyYzAuNDQ1LTAuNDA1IDAuOTc1LTAuNjIyIDEuNTc4LTAuNjIyIDAuNjkzIDAgMS4yODggMC4yNTUgMS43NSAwLjc3MSAwLjQzMiAwLjQ4MiAwLjY3NSAxLjA0OSAwLjY3NSAxLjY5OHYwLjAyM2wtM2UtMyAwLjAyNGMtMC4wMzcgMC4yOTItMC4xMjggMC41NzItMC4yNiAwLjgzNC0wLjE2NiAwLjMzLTAuNDM5IDAuNjA5LTAuNzMyIDAuODQ0bC03LjU4IDUuNzU0LTMuODM4LTIuOTE0eiIgZmlsbD0iIzFCMUIxQiIvPjxwYXRoIGQ9Im0xNzEuODMgMzIuNjMxdjIuNzU0aDAuMDE2YzAuMjggMi4zNTEgNC4zMTIgMi40NDcgNi4wMTUgMi40NDdoMTguMjA4YzEuMzczIDAgMi4xNTkgMC43MjYgMi4xNTkgMi4xMTUgMCAxLjM5Ni0wLjc2MyAyLjE1OS0yLjE1OSAyLjE1OWgtMTcuMTAxYy01LjEyOCAwLTEyLjA3OC0xLTEyLjA3OC03LjUxN3YtOC44NTVjMC02LjQ0MiA2Ljg3OC03LjQyOCAxMS45NDQtNy40MjhoOS40MzFjNC40NzUgMCAxMC4xMjkgMC45NjIgMTAuMTI5IDYuNTQzdjUuNDljMCAxLjQ4NC0wLjgwNyAyLjI5Mi0yLjI5MiAyLjI5MmgtMjEuNDc5di00LjM2M2gxOC45MjF2LTMuMDIyYzAtMi40MDEtMi41NS0yLjcxMS00LjQzOC0yLjcxMWgtMTEuMjQ3Yy0xLjk0NCAwLTYuMDMyIDAuMTU3LTYuMDMyIDIuOTMzdjcuMTYzaDNlLTN6IiBmaWxsPSIjMUIxQjFCIi8+PHBhdGggZD0ibTE1Ny44IDQwLjA4YzAgMS41NTgtMC45ODIgMi4yNDctMi40NjkgMi4yNDctMS40OTIgMC0yLjUxMy0wLjY3LTIuNTEzLTIuMjQ3di0yNi44MzJjMC0xLjU1OCAwLjk4Mi0yLjI0OCAyLjQ2OS0yLjI0OCAxLjQ5MiAwIDIuNTEzIDAuNjcgMi41MTMgMi4yNDh2MjYuODMyeiIgZmlsbD0iIzFCMUIxQiIvPjxwYXRoIGQ9Im0xMTcuMjEgNDIuMTA3djQuNzkyYzAgMS41OTctMC45NzIgMi4zMzYtMi41MTMgMi4zMzYtMS41MjggMC0yLjQyNS0wLjc4LTIuNDI1LTIuMzM2di0yMS41NjNjMC02LjExOCA2LjYyNS03LjAzIDExLjQxNC03LjAzaDguMDU4YzQuNjkzIDAgMTEuOTg5IDAuNzU0IDExLjk4OSA2Ljk0MXY5LjUyYzAgNi4xMzItNS43OTcgNy4zNC0xMC44MzggNy4zNGgtMTIuODg5di00LjI3NGgxMy41OThjMi4wNTMgMCA1LjEwMy0wLjMwMSA1LjEwMy0yLjk3OHYtOS41MTljMC0yLjctNC40OC0yLjgtNi4yOTgtMi44aC05LjIxYy0xLjk0NyAwLTUuOTg4IDAuMTYyLTUuOTg4IDIuOTMzdjE2LjYzOGgtMWUtM3oiIGZpbGw9IiMxQjFCMUIiLz48cGF0aCBkPSJtNTQuMjU3IDMyLjYzMXYyLjQ5YzAgMi42MDkgNC4yNjcgMi43MTEgNi4wMzIgMi43MTFoMTguMjA4YzEuMzc0IDAgMi4xNTkgMC43MjYgMi4xNTkgMi4xMTUgMCAxLjM5Ni0wLjc2MyAyLjE1OS0yLjE1OSAyLjE1OWgtMTcuMWMtNS4xMjggMC0xMi4wNzctMS0xMi4wNzctNy41MTd2LTguODU1YzAtNi40NDIgNi44NzctNy40MjggMTEuOTQ0LTcuNDI4aDkuNDMxYzQuNDc1IDAgMTAuMTI5IDAuOTYyIDEwLjEyOSA2LjU0M3Y1LjQ5YzAgMS40ODQtMC44MDcgMi4yOTItMi4yOTIgMi4yOTJoLTIxLjQ4di00LjM2M2gxOC45MjJ2LTMuMDIyYzAtMi40MDEtMi41NS0yLjcxMS00LjQzOC0yLjcxMWgtMTEuMjQ3Yy0xLjk0NCAwLTYuMDMyIDAuMTU3LTYuMDMyIDIuOTMzdjcuMTYzeiIgZmlsbD0iIzFCMUIxQiIvPjxwYXRoIGQ9Im05MS4xMSAzMy41MzhsMWUtMyAtMTEuMDAzaC0xLjY0OWMtMS4zOTcgMC0yLjE1OS0wLjc2My0yLjE1OS0yLjE1OSAwLTEuMzggMC44MDgtMi4wNyAyLjE1OS0yLjA3aDEuNjQ5di01LjA1OGMwLTEuNTk2IDEuMDYyLTIuMjQ4IDIuNTU4LTIuMjQ4IDEuNDc2IDAgMi4zOCAwLjcyOSAyLjM4IDIuMjQ4djUuMDU4aDcuOTgxYzEuMzY3IDAgMi4xMTUgMC43NDggMi4xMTUgMi4xMTUgMCAxLjM2Ni0wLjc0OCAyLjExNS0yLjExNSAyLjExNWgtNy45ODF2MTIuNTg2YzAgMi42MDkgNC4yNjcgMi43MTEgNi4wMzIgMi43MTFoMS40NjFjMS4zNzQgMCAyLjE1OSAwLjcyNiAyLjE1OSAyLjExNSAwIDEuMzk2LTAuNzYzIDIuMTU5LTIuMTU5IDIuMTU5aC0wLjM1NGMtNS4xMjggMC0xMi4wNzgtMS0xMi4wNzgtNy41MTd2LTEuMDUyeiIgZmlsbD0iIzFCMUIxQiIvPjxwYXRoIGQ9Im00MS4yMDMgMzUuNjA4YzAgNS4zMDMtNC42ODYgNi40OTktOS4xNTQgNi40OTloLTE3LjhjLTEuMzYzIDAtMi4yNDgtMC42NTEtMi4yNDgtMi4wNzEgMC0xLjQ0NyAwLjgxNS0yLjIwMyAyLjI0OC0yLjIwM2gxOC41MDhjMS41NzMgMCAzLjM3NS0wLjMwNiAzLjM3NS0yLjIyNHYtMS4zNzNjMC0xLjk4Ny0yLjM2MS0yLjE3OS0zLjg2Mi0yLjE3OWgtOS44NzRjLTQuMjQxIDAtMTAuMTI5LTAuODA5LTEwLjEyOS02LjIzM3YtMS41MDZjMC01LjIwMyA1LjU0Ni02LjAxMSA5LjY0Mi02LjAxMWgxNi40NzFjMS4zOCAwIDIuMjAzIDAuNzA1IDIuMjAzIDIuMTE1IDAgMS40MDktMC44MjQgMi4xMTUtMi4yMDMgMi4xMTVoLTE3LjQ0NmMtMS4xOCAwLTMuNzMgMC4wODUtMy43MyAxLjc4MXYxLjQxOGMwIDIuMDMzIDMuMDcxIDIuMTM1IDQuNDgyIDIuMTM1aDEwLjA5NWM0LjMyNiAwIDkuNDIxIDEuMDI1IDkuNDIxIDYuMzY1djEuMzcyaDFlLTN6IiBmaWxsPSIjMUIxQjFCIi8+PC9nPjwvc3ZnPg==);
background-repeat: no-repeat;
width: 170px;
height: 35px;
-webkit-print-color-adjust: exact;
}
.contactsHeader {
float: right;
font-size: 14px;
margin: 20px 0;
}
.contactsHeader p {
font-size: 12px;
line-height: 16px;
letter-spacing: 0.5px;
margin: 5px 0;
}
.main {
padding: 20px;
}
.tableInvoices:before {
content: 'PAID';
position: absolute;
font-size: 110px;
letter-spacing: 15px;
font-weight: bold;
color: #daf1ce;
top: 150px;
left: 50%;
z-index: -1;
transform: translateX(-50%) rotate(-45deg);
}
.invoiceData {
height: 140px;
}
.invoiceData table tr td {
padding-right: 20px;
font-size: 14px;
}
.invoiceData tr td:first-child {
color: #707070;
}
.innerDiv {
float: right;
}
.companyData {
height: 200px;
}
.companyData h4 {
margin-bottom: 5px;
}
.companyData p {
font-size: 14px;
margin-top: 0;
}
.companyData p:nth-child(4) {
margin: 0;
}
.tableInvoices {
position: relative;
height: 1px;
width: 100%;
border-collapse: collapse;
}
.tableInvoices td {
padding: 20px 10px;
border: 1px solid #707070;
min-width: 30px;
}
.tableInvoices th {
padding: 10px;
border: 1px solid #707070;
background-color: #e4f9ed;
min-width: 30px;
}
.tableInvoices tr:first-child th {
height: 56px;
}
/*remove border from first td from pre-last tr*/
.tableInvoices tr:nth-last-child(2) td:first-child {
vertical-align: top;
}
.tableInvoices tr:nth-last-child(2) td:first-child p {
margin: 0;
}
.tableInvoices tr:nth-last-child(2) h4 {
margin: 0 0 10px;
}
.tableInvoices tr:nth-last-child(2) p {
margin: 5px 0;
}
.dashed p {
padding: 15px 10px;
margin: 0 !important;
border-bottom: 1px dashed #707070;
}
.dashed p:last-child {
border-bottom: none;
}
.innerTable {
height: 100%;
width: 100%;
border-collapse: collapse;
}
.innerTable tr:first-child th {
border-top: 0;
}
.innerTable tr:last-child td {
border-bottom: 0;
}
.innerTable tr th:first-child, .innerTable tr td:first-child {
border-left: 0;
}
.innerTable tr th:last-child, .innerTable tr td:last-child {
border-right: 0;
}
.innerTable tr:first-child td {
border-top: 0;
}
.bold {
font-weight: bold;
}
.haveAnyQuestions {
font-size: 12px;
}
.footer {
width: 100%;
height: 141px;
padding: 0 0 0 40px;
box-sizing: border-box;
border-top: 1px solid #707070;
}
.contactsWrap {
float: right;
}
.contactsWrap div {
float: left;
margin: 30px 10px;
}
.contactsWrap div h4, .contactsWrap div p {
margin: 5px 0;
}
.contactsWrap div p {
font-size: 13px;
letter-spacing: 0.5px;
line-height: 16px;
}
.header:before, .footer:before {
display: inline-block;
height: 100%;
vertical-align: middle;
content: "";
}
.verticAlign {
display: inline-block;
vertical-align: middle;
}
.gorizontAlign {
text-align: center;
}
.lowercase {
text-transform: lowercase;
}
@media print {
@page {
size: A4;
}
body {
font-family: Arial, Helvetica, sans-serif;
}
.main {
-webkit-print-color-adjust: exact;
}
.gray {
color: #707070;
-webkit-print-color-adjust: exact;
}
.invoiceData tr td:first-child {
color: #707070;
-webkit-print-color-adjust: exact;
}
.tableInvoices th {
background-color: #e4f9ed;
-webkit-print-color-adjust: exact;
}
.footer {
position: fixed;
left: 0;
bottom: 0;
}
.footer .logo {
margin-right: 50px;
}
}
</style>
</head>
<body>
<div class="header">
<div class="logo verticAlign">
<div class="setplexlogo"></div>
</div>
<div class="contactsHeader">
<p>sales@setplex.com<br/>
tel.:+1-855-738-7539</p>
</div>
</div>
<div class="main">
<div class="invoiceData">
<div class="innerDiv">
<h2>INVOICE</h2>
<table>
<tr>
<td>INVOICE NUMBER</td>
<td>1</td>
</tr>
<tr>
<td>INVOICE DATE</td>
<td>17.07.2019</td>
</tr>
<!-- <tr>
<td>PAGE</td>
<td>1 FROM 1</td>
</tr> -->
</table>
</div>
</div>
<div class="companyData">
<h2>BILL TO</h2>
<h4>IIvanov</h4>
<p>Ivan Ivanov</p>
<p class="gray">Ohio / Columbus</p>
<p class="gray">926 Steensland Trail / United States of America</p>
<p class="gray"><b>ivan.ivanov99@gmail.com</b></p>
</div>
<div class="tableInvoicesWrap">
<table class="tableInvoices gray">
<tr>
<th></th>
<th>PROCESS DATE</th>
<td rowspan="2" class="noPadding alignTop">
<table class="innerTable">
<tr>
<th>SERVICE</th>
<th>AMOUNT</th>
<th>PRICE, USD</th>
</tr>
<tr>
<td>Nora &amp; Apps</td>
<td class="gorizontAlign">100 seats</td>
<td class="gorizontAlign">100</td>
</tr>
<tr>
<td>CDN Streaming</td>
<td class="gorizontAlign">100 seats</td>
<td class="gorizontAlign">250</td>
</tr>
<tr>
<td>VOD Storage</td>
<td class="gorizontAlign">150 GBs</td>
<td class="gorizontAlign">30</td>
</tr>
<tr>
<td>Catchup Channels 7D</td>
<td class="gorizontAlign">10 channels</td>
<td class="gorizontAlign">350</td>
</tr>
</table>
</td>
<th>TOTAL AMOUNT, USD</th>
<th>PAYMENT TYPE</th>
<th>TRANSACTION ID</th>
<th>STATUS</th>
</tr>
<tr>
<td class="gorizontAlign">1</td>
<td class="gorizontAlign">17.07.2019</td>
<td class="gorizontAlign">730</td>
<td class="gorizontAlign">
Credit Card - XXXX7654
</td>
<td class="gorizontAlign">60124519509</td>
<td class="gorizontAlign">PAID</td>
</tr>
<tr>
<td rowspan="2" colspan="5" class="noBorder">
<h4>COMMENTS</h4>
<p>1. Purpose of this invoice is <b class="lowercase">REGISTRATION</b> of services</p>
<p></p>
</td>
<td class="bold noPadding dashed"><p>SUBTOTAL</p>
<p>TAX RATE</p>
<p>TAX</p></td>
<td class="gorizontAlign bold noPadding dashed"><p>730</p>
<p>0%</p>
<p>0</p></td>
</tr>
<tr>
<th>TOTAL, USD</th>
<th>730</th>
</tr>
</table>
</div>
<div class="haveAnyQuestions gray">
If you have any questions about this invoice, please contact us<br/>
tel.: +1-855-738-7539<br/>
sales@setplex.com
</div>
</div>
<div class="footer">
<div class="logo verticAlign">
<div class="setplexlogo"></div>
</div>
<div class="contactsWrap verticAlign">
<div>
<h4>ADDRESS</h4>
<p>Setplex LLC<br/>
231 Central Ave 4th Floor<br/>
White Plains, NY 10606</p>
</div>
<div>
<h4>CONTACTS</h4>
<p>sales@setplex.com<br/>
tel.: +1-855-738-7539<br/>
fax.: +1-718-701-4407</p>
</div>
</div>
</div>
</body>
</html>
Binary file not shown.
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"declaration": true,
"strict": true,
"noImplicitAny": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": true,
"allowSyntheticDefaultImports": true,
"outDir": "./lib",
"baseUrl": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": [
"tslint-config-standard-plus",
"tslint-config-security",
"tslint-config-prettier"
]
}
+3798
View File
File diff suppressed because it is too large Load Diff