Compare commits

22 Commits

Author SHA1 Message Date
5ceadd09dd Fix asset path in navbar
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-22 20:01:03 +01:00
7387f33678 fix basepath
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-22 19:57:36 +01:00
f4c704740a ci: uncommented wrong pipeline
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-22 19:49:41 +01:00
0c6cdcf1f5 Attempt at making drone work
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-22 19:48:47 +01:00
6bfa971b0a Merge branch 'main' into feat/frontend
* main:
  Force CI to repackage the wasm_exec.js support file
2022-01-22 19:42:17 +01:00
809fa354c4 Fix typing errors 2022-01-22 19:41:53 +01:00
e0efd9dc41 Add frontend 2.0 2022-01-22 19:28:06 +01:00
68756014be Force CI to repackage the wasm_exec.js support file
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-21 20:22:17 +01:00
ff22127baa scaffold frontend 2022-01-21 20:18:48 +01:00
79f8adaf1a I meant main
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-20 21:44:09 +01:00
4413fab9ad Limit CI to master 2022-01-20 21:43:40 +01:00
e89acf585f Add readme
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-20 21:40:09 +01:00
2a0bfd0e10 Fix download links
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-20 21:37:34 +01:00
ac4b86fd03 Update CI
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-20 21:35:37 +01:00
f05c4bbcc7 Update wasm
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-20 20:32:57 +01:00
d2ac081d5e Update pipeline 2022-01-20 20:32:40 +01:00
39af776d00 ci test 2
Some checks failed
continuous-integration/drone Build is failing
2022-01-20 17:10:11 +01:00
15130938ce ci test 1
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-01-20 16:58:49 +01:00
05ac24254c Generate that beautiful double page 2021-12-17 22:07:00 +01:00
72a245610a Remove sha256 debug output 2021-12-17 21:47:20 +01:00
7040909625 Add split-output to cli 2021-12-17 21:46:13 +01:00
ec9426b59c Remove compiled wasm 2021-12-17 21:37:29 +01:00
37 changed files with 4485 additions and 86 deletions

82
.drone.yml Normal file
View File

@@ -0,0 +1,82 @@
---
kind: pipeline
type: kubernetes
name: build-clis
trigger:
branch:
- main
steps:
- name: build
image: golang:1.17
commands:
- make build-cross-clis
- name: upload
image: plugins/s3
settings:
bucket: covergen
source: dist/*
target: /dist/
strip_prefix: dist/
path_style: true
endpoint: https://s3.blacknova.io
access_key:
from_secret: minio_access_key_id
secret_key:
from_secret: minio_secret_access_key
---
kind: pipeline
type: kubernetes
name: build-wasm
#trigger:
# branch:
# - main
volumes:
- name: wasm
temp: {}
steps:
- name: build wasm
image: golang:1.17
environment:
GOOS: js
GOARCH: wasm
volumes:
- name: wasm
path: /wasm
commands:
- make wasm
- "cp covergen.wasm /wasm/covergen.wasm"
# Grab the wasm shim from the docker image, otherwise there is a mismatch
- "cp /usr/local/go/misc/wasm/wasm_exec.js /wasm/wasm_exec.js"
- name: build frontend
image: node:16
volumes:
- name: wasm
path: /wasm
commands:
- cd frontend
- yarn install
- "cp /wasm/* public/"
- yarn build
- name: upload
image: plugins/s3
settings:
bucket: covergen
source: frontend/dist/**/*
target: /
strip_prefix: frontend/dist/
path_style: true
endpoint: https://s3.blacknova.io
access_key:
from_secret: minio_access_key_id
secret_key:
from_secret: minio_secret_access_key

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
covergen
!covergen/
*.pdf
*.wasm
dist/

View File

@@ -1,3 +1,9 @@
wasm:
GOOS=js GOARCH=wasm go build -o assets/covergen.wasm ./cmd/wasm/main.go
.PHONY: build-cross-clis
build-cross-clis:
rm -rf ./dist && mkdir ./dist
GOOS=linux GOARCH=amd64 go build -o dist/covergen.linux-amd64 ./cmd/covergen
GOOS=darwin GOARCH=amd64 go build -o dist/covergen.darwin-amd64 ./cmd/covergen
.PHONY: wasm
wasm:
GOOS=js GOARCH=wasm go build -o covergen.wasm ./cmd/wasm/main.go

20
README.md Normal file
View File

@@ -0,0 +1,20 @@
# Covergen
Just generates some magic PDF invoice covers using straight up dark magic.
## Just be lazy
Go see it in action and download it from [https://s3.blacknova.io/covergen/index.html](https://s3.blacknova.io/covergen/index.html)
## Just build it
```
$ go build -o covergen ./cmd/covergen
```
## It has wasm!
```
$ make wasm
```

Binary file not shown.

View File

@@ -1,53 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("covergen.wasm"), go.importObject).then((result) => {
go.run(result.instance);
});
function makeCover(args) {
const result = window.covergen(args);
if (result.error) {
throw result.error;
}
return new File([result], 'cover.pdf', {type: 'application/pdf'});
}
function letsfuckinggo() {
document.getElementById('cover').src = window.URL.createObjectURL(makeCover({
customer: document.getElementById('customer').value,
number: document.getElementById('number').value,
}));
}
</script>
<style>
html, body, #cover {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div>
<label>
Customer:
<textarea id="customer"></textarea>
</label>
</div>
<div>
<label>
Number:
<input id="number" type="text">
</label>
</div>
<div>
<button onclick="letsfuckinggo()">Fuck it!</button>
</div>
<iframe id="cover"></iframe>
</body>
</html>

View File

@@ -26,6 +26,7 @@ var customer = pflag.StringP("customer", "c", "", "Customer name for cover")
var numberPrefix = pflag.String("number-prefix", "offerte", "Prefix to use for number")
var color = pflag.String("color", "random", "Selects a color to use for the grid highlight. Valid choices: ['random', 'red', 'blue', 'yellow', 'green']")
var output = pflag.StringP("output", "o", "cover.pdf", "File to output to")
var output2 = pflag.StringP("output-back", "b", "", "Output front and back separately")
func init() {
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger()
@@ -69,19 +70,39 @@ func main() {
}
*customer = strings.ReplaceAll(*customer, "\\n", "\n")
pdf, err := covergen.GenerateInvoice(covergen.CoverSettings{
settings := covergen.CoverSettings{
Number: *number,
NumberPrefix: *numberPrefix,
CustomerName: *customer,
HLColor: chosenColor,
})
if err != nil {
log.Fatal().Err(err).Msg("Failed to render invoice")
}
if err := pdf.OutputFileAndClose(*output); err != nil {
log.Fatal().Err(err).Msg("Failed to write invoice to disk")
if *output2 == "" {
pdf, err := covergen.GenerateInvoice(settings)
if err != nil {
log.Fatal().Err(err).Msg("Failed to render invoice")
}
if err := pdf.OutputFileAndClose(*output); err != nil {
log.Fatal().Err(err).Msg("Failed to write invoice to disk")
}
} else {
front, err := covergen.GenerateFrontCover(settings)
if err != nil {
log.Fatal().Err(err).Msg("Failed to render front")
}
if err := front.OutputFileAndClose(*output); err != nil {
log.Fatal().Err(err).Msg("Failed to write front to disk")
}
back, err := covergen.GenerateBackCover(settings)
if err != nil {
log.Fatal().Err(err).Msg("Failed to render back")
}
if err := back.OutputFileAndClose(*output2); err != nil {
log.Fatal().Err(err).Msg("Failed to write back to disk")
}
}
}

View File

@@ -2,12 +2,11 @@ package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"syscall/js"
"github.com/go-pdf/fpdf"
"github.com/thegrumpylion/jsref"
"covergen/pkg/covergen"
@@ -72,6 +71,18 @@ func settingsFromValue(arg js.Value) (*covergen.CoverSettings, error) {
}, nil
}
func pdfToJs(pdf *fpdf.Fpdf) (js.Value, error) {
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return js.Null(), fmt.Errorf("failed to write pdf: %w", err)
}
s := buf.Bytes()
ta := js.Global().Get("Uint8Array").New(len(s))
js.CopyBytesToJS(ta, s)
return ta, nil
}
func generateCover(this js.Value, args []js.Value) interface{} {
defer func() {
if r := recover(); r != nil {
@@ -98,21 +109,61 @@ func generateCover(this js.Value, args []js.Value) interface{} {
return jsmap{"error": err.Error()}
}
var buf bytes.Buffer
if err = pdf.Output(&buf); err != nil {
jsBytes, err := pdfToJs(pdf)
if err != nil {
return jsmap{"error": err.Error()}
}
return jsBytes
}
func generateSplitCover(this js.Value, args []js.Value) interface{} {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered", r)
}
}()
if len(args) != 1 {
return jsmap{"error": "missing argument"}
}
arg := args[0]
if arg.Type() != js.TypeObject {
return jsmap{"error": "expected object"}
}
settings, err := settingsFromValue(arg)
if err != nil {
return jsmap{"error": err.Error()}
}
s := buf.Bytes()
sum := sha256.Sum256(s)
fmt.Println("shasum", hex.EncodeToString(sum[:]))
front, err := covergen.GenerateFrontCover(*settings)
if err != nil {
return jsmap{"error": fmt.Sprintf("failed to render front: %s", err.Error())}
}
ta := js.Global().Get("Uint8Array").New(len(s))
js.CopyBytesToJS(ta, s)
return ta
back, err := covergen.GenerateBackCover(*settings)
if err != nil {
return jsmap{"error": fmt.Sprintf("failed to render back: %s", err.Error())}
}
frontJSBytes, err := pdfToJs(front)
if err != nil {
return jsmap{"error": fmt.Sprintf("failed to render front: %s", err.Error())}
}
backJSBytes, err := pdfToJs(back)
if err != nil {
return jsmap{"error": fmt.Sprintf("failed to render back: %s", err.Error())}
}
return jsmap{
"front": frontJSBytes,
"back": backJSBytes,
}
}
func main() {
js.Global().Set("covergen", js.FuncOf(generateCover))
js.Global().Set("generateCover", js.FuncOf(generateCover))
js.Global().Set("generateSplitCover", js.FuncOf(generateSplitCover))
<-make(chan bool)
}

12
frontend/.eslintrc Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": [
"plugin:vue/vue3-recommended",
"plugin:prettier-vue/recommended",
"@vue/typescript/recommended",
"prettier"
],
"rules": {
"@typescript-eslint/explicit-module-boundary-types": [0],
"vue/multi-word-component-names": 0
}
}

8
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.yarn/install-state.gz
public/*.wasm

3
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,3 @@
{
"singleQuote": true
}

11
frontend/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Vue 3 + Typescript + Vite
This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's `.vue` type support plugin by running `Volar: Switch TS Plugin on/off` from VSCode command palette.

16
frontend/index.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<title>NVLS | Covergen</title>
</head>
<body class="bg-gray-200">
<div id="app"></div>
<script src="/wasm_exec.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

37
frontend/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "frontend",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"lint": "yarn lint:ts && yarn lint:style",
"lint:ts": "vue-tsc --noEmit",
"lint:style": "eslint --ext .ts,.vue --ignore-path .gitignore .",
"lint:fix": "yarn lint:style --fix"
},
"dependencies": {
"@tailwindcss/forms": "^0.4.0",
"pinia": "^2.0.9",
"tailwindcss": "^3.0.15",
"vue": "^3.2.25"
},
"devDependencies": {
"@types/golang-wasm-exec": "^1.15.0",
"@types/node": "^17.0.10",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"@vitejs/plugin-vue": "^2.0.0",
"@vue/eslint-config-typescript": "^10.0.0",
"autoprefixer": "^10.4.2",
"eslint": "^8.7.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier-vue": "^3.1.0",
"eslint-plugin-vue": "^8.3.0",
"postcss": "^8.4.5",
"prettier": "^2.5.1",
"typescript": "^4.4.4",
"vite": "^2.7.2",
"vue-tsc": "^0.29.8"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

18
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import Nav from './components/Nav.vue';
import Settings from './components/Settings.vue';
import Renderer from './components/Renderer.vue';
</script>
<template>
<Nav />
<div class="flex grow space-x-8">
<div class="w-80">
<Settings />
</div>
<Renderer />
</div>
</template>
<style scoped>
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
const downloads = [
{ name: 'Linux', link: './dist/covergen.linux-amd64' },
{ name: 'Darwin', link: './dist/covergen.darwin-amd64' },
];
</script>
<template>
<div class="w-full mx-auto px-8">
<div class="relative flex items-center justify-between h-16">
<div class="flex-1 flex items-stretch justify-between">
<div class="flex-shrink-0 flex items-center">
<img
src="../assets/logo.png"
alt="covergen logo"
class="h-8 w-auto mr-2"
/>
<h1>CoverGen</h1>
</div>
<div class="ml-6">
<div class="flex space-x-4 items-center text-sm font-medium">
<span class="text-base"> Download me: </span>
<a
v-for="dl in downloads"
:key="dl.link"
:href="dl.link"
class="text-gray-800 hover:bg-gray-300 px-3 py-2 rounded-md"
>{{ dl.name }}</a
>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import { useCover } from '@/stores/cover';
import { storeToRefs } from 'pinia';
const store = useCover();
const { frontUri, backUri } = storeToRefs(store);
</script>
<template>
<div class="flex flex-row grow">
<div class="grow p-4">
<h2 class="text-lg font-medium pb-4">Front</h2>
<iframe
:src="frontUri"
class="w-full aspect-A4 border border-slate-500 rounded"
/>
</div>
<div class="grow p-4">
<h2 class="text-lg font-medium pb-4">Back</h2>
<iframe
:src="backUri"
class="w-full aspect-A4 border border-slate-500 rounded"
/>
</div>
</div>
</template>

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import Input from '@/components/form/Input.vue';
import TextArea from '@/components/form/TextArea.vue';
import Color from '@/components/form/Color.vue';
import SadFace from '@/icons/SadFace.vue';
import { randomLabel } from '@/lib/randomlabel';
import { useCover } from '@/stores/cover';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
const store = useCover();
const { customer, prefix, color, number } = storeToRefs(store);
const possibleLabels = [
'Hit it!',
'Let it rain!',
'💰💰💰',
'🤑🤑🤑',
'Make Rutte Proud',
'Kaching!',
'Rosebud',
'You show that customer',
'Do it for Berend',
];
const label = randomLabel(possibleLabels);
const renderError = ref<string | null>(null);
function doRender() {
try {
renderError.value = null;
store.render();
} catch (e) {
if (e instanceof Error) {
renderError.value = e.message;
} else if (typeof e === 'string') {
renderError.value = e;
} else {
renderError.value = 'An unknown error occurred';
console.error(e);
}
}
}
</script>
<template>
<div class="p-4 flex flex-col gap-4">
<h2 class="text-lg font-medium">Settings</h2>
<TextArea v-model="customer" label="Customer" />
<Input v-model="prefix" label="Prefix" />
<Input v-model="number" label="Number" />
<Color v-model="color" label="Highlight Color" />
<div
v-if="renderError !== null"
class="flex bg-red-100 rounded-lg p-4 mb-4 text-sm text-red-700 items-center gap-2"
role="alert"
>
<SadFace />
<div><span class="font-medium">Error:</span> {{ renderError }}</div>
</div>
<button
class="px-4 py-2 bg-blue-500 rounded-lg hover:bg-blue-600 text-white border-2 active:border-blue-500 focus:outline focus:outline-2 focus:outline-blue-500"
@click="label.update() && doRender()"
>
{{ label.label.value }}
</button>
</div>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
label: string;
modelValue: string;
}>();
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void;
}>();
const localValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
</script>
<template>
<div>
<label class="block text-sm font-medium text-gray-700">{{ label }}</label>
<div class="mt-1 relative rounded-md shadow-sm">
<input
v-model="localValue"
type="color"
class="block w-full py-2 px-4 h-10 bg-white border border-gray-300 hover:border-2 hover:border-blue-500 hover:cursor-pointer focus:border-blue-500 rounded-md"
v-bind="$attrs"
/>
</div>
</div>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
label: string;
modelValue: string;
}>();
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void;
}>();
const localValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
</script>
<template>
<div>
<label class="block text-sm font-medium text-gray-700">{{ label }}</label>
<div class="mt-1 relative rounded-md shadow-sm">
<input
v-model="localValue"
type="text"
class="focus:ring-blue-500 focus:border-blue-500 block w-full border-gray-300 rounded-md"
v-bind="$attrs"
/>
</div>
</div>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
label: string;
modelValue: string;
}>();
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void;
}>();
const localValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
</script>
<template>
<div>
<label class="block text-sm font-medium text-gray-700">{{ label }}</label>
<div class="mt-1 relative rounded-md shadow-sm">
<textarea
v-model="localValue"
class="focus:ring-blue-500 focus:border-blue-500 block w-full border-gray-300 rounded-md"
v-bind="$attrs"
/>
</div>
</div>
</template>

8
frontend/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue';
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -0,0 +1,16 @@
<template>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</template>

14
frontend/src/index.css Normal file
View File

@@ -0,0 +1,14 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html, body {
height: 100%;
}
#app {
min-height: 100%;
display: flex;
flex-direction: column;
justify-items: center;
}

View File

@@ -0,0 +1,51 @@
interface CoverArgs {
customer: string;
number: string;
numberPrefix?: string;
hlColor?: string;
}
interface CoverError {
error: string;
}
declare global {
interface Window {
generateCover(args: CoverArgs): Uint8Array | CoverError;
generateSplitCover(
args: CoverArgs
): { front: Uint8Array; back: Uint8Array } | CoverError;
}
}
const go = new Go();
WebAssembly.instantiateStreaming(fetch('covergen.wasm'), go.importObject).then(
(result) => {
go.run(result.instance);
}
);
export function generateCover(args: CoverArgs): File {
const result = window.generateCover(args);
if ('error' in result) {
throw result.error;
}
return new File([result], 'cover.pdf', { type: 'application/pdf' });
}
export function generateSplitCover(args: CoverArgs): {
front: File;
back: File;
} {
const result = window.generateSplitCover(args);
if ('error' in result) {
throw result.error;
}
return {
front: new File([result.front], 'front.pdf', { type: 'application/pdf' }),
back: new File([result.back], 'back.pdf', { type: 'application/pdf' }),
};
}

View File

@@ -0,0 +1,18 @@
import { readonly, ref } from 'vue';
export function randomLabel(options: string[]) {
const label = ref<string>();
const randomLabel = () => {
let newLabel;
do {
newLabel = options[Math.floor(Math.random() * options.length)];
} while (newLabel === label.value);
return newLabel;
};
label.value = randomLabel();
return {
label: readonly(label),
update: () => (label.value = randomLabel()),
};
}

7
frontend/src/main.ts Normal file
View File

@@ -0,0 +1,7 @@
import { createApp } from 'vue';
import App from './App.vue';
import './index.css';
import { createPinia } from 'pinia';
import './lib/covergen'; // Get that go app booting
createApp(App).use(createPinia()).mount('#app');

View File

@@ -0,0 +1,28 @@
import { defineStore } from 'pinia';
import { generateSplitCover } from '@/lib/covergen';
export const useCover = defineStore('coverSettings', {
state() {
return {
customer: '',
prefix: 'offerte',
number: '',
color: '#ff00ff',
frontUri: '',
backUri: '',
};
},
actions: {
render() {
const { front, back } = generateSplitCover({
customer: this.customer,
numberPrefix: this.prefix,
number: this.number,
hlColor: this.color,
});
this.frontUri = URL.createObjectURL(front);
this.backUri = URL.createObjectURL(back);
},
},
});

View File

@@ -0,0 +1,21 @@
const defaultTheme = require('tailwindcss/defaultTheme');
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts}",
],
theme: {
extend: {
fontFamily: {
sans: ['Inter var', ...defaultTheme.fontFamily.sans],
},
aspectRatio: {
'A4': '1 / 1.4142',
}
},
},
plugins: [
require('@tailwindcss/forms'),
],
}

18
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

14
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
plugins: [vue()],
base: mode === 'production' ? '/covergen/' : '/',
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
}));

3710
frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ import (
"strings"
"github.com/go-pdf/fpdf"
"github.com/rs/zerolog/log"
)
const margin = 20
@@ -44,17 +43,58 @@ type CoverSettings struct {
}
func GenerateInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := GenerateFrontCover(settings)
if err != nil {
return nil, err
}
pdf.AddPage()
drawCustomerName(pdf, settings)
if pdf.Err() {
return nil, pdf.Error()
}
return pdf, nil
}
func GenerateFrontCover(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := generateBaseInvoice(settings)
if err != nil {
return nil, err
}
drawInvoiceNumber(pdf, settings)
drawMiwebbLink(pdf)
if pdf.Err() {
return nil, pdf.Error()
}
return pdf, nil
}
func GenerateBackCover(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := generateBaseInvoice(settings)
if err != nil {
return nil, err
}
return pdf, err
}
func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf := fpdf.New("P", scaleUnit, "A4", "")
if err := addEmbeddedFont(pdf, "Mark Medium", "", "Mark-Medium"); err != nil {
log.Fatal().Err(err).Msg("Error adding font")
return nil, fmt.Errorf("error adding font: %w", err)
}
if err := addEmbeddedFont(pdf, "Mark Light", "", "Mark-Light"); err != nil {
log.Fatal().Err(err).Msg("Error adding font")
return nil, fmt.Errorf("error adding font: %w", err)
}
miwebbLogo, err := addEmbeddedLogo(pdf, miwebbLogoName, miwebbLogoPath)
miwebbLogo, err := addEmbeddedLogo(pdf, miwebbLogoName, "miwebb.white.png")
if err != nil {
log.Fatal().Err(err).Msg("Error adding logo")
return nil, fmt.Errorf("error adding logo: %w", err)
}
pdf.SetMargins(margin, margin, margin)
@@ -67,21 +107,16 @@ func GenerateInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf.Image(miwebbLogoName, margin-5, margin, miwebbLogo.Width()*logoScale, miwebbLogo.Height()*logoScale, false, "", 0, "")
})
pdf.AddPage()
drawCustomerName(pdf, settings)
drawInvoiceNumber(pdf, settings)
drawMiwebbLink(pdf)
pdf.AddPage()
drawCustomerName(pdf, settings)
if pdf.Err() {
return nil, pdf.Error()
return nil, fmt.Errorf("error generating invoice: %w", pdf.Error())
}
return pdf, nil
}
func drawInvoiceNumber(pdf *fpdf.Fpdf, settings CoverSettings) {
_, pageHeight := pdf.GetPageSize()