Compare commits

...

9 Commits

Author SHA1 Message Date
b88208ba3d ci: revert trigger
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-22 20:03:24 +01:00
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
ff22127baa scaffold frontend 2022-01-21 20:18:48 +01:00
32 changed files with 4260 additions and 105 deletions

View File

@@ -36,23 +36,42 @@ trigger:
branch:
- main
volumes:
- name: wasm
temp: {}
steps:
- name: build
- 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 assets/wasm_exec.js"
- "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: assets/**/*
source: frontend/dist/**/*
target: /
strip_prefix: assets/
strip_prefix: frontend/dist/
path_style: true
endpoint: https://s3.blacknova.io

2
.gitignore vendored
View File

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

View File

@@ -6,4 +6,4 @@ build-cross-clis:
.PHONY: wasm
wasm:
GOOS=js GOARCH=wasm go build -o assets/covergen.wasm ./cmd/wasm/main.go
GOOS=js GOARCH=wasm go build -o covergen.wasm ./cmd/wasm/main.go

View File

@@ -1,99 +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.generateCover(args);
if (result.error) {
throw result.error;
}
return new File([result], 'cover.pdf', {type: 'application/pdf'});
}
function makeSplitCover(args) {
const result = window.generateSplitCover(args);
if (result.error) {
throw result.error;
}
return Object.fromEntries(['front', 'back'].map((side) => [side, new File([result[side]], `${side}.pdf`, {type: 'application/pdf'})]));
}
function letsfuckinggo() {
const covers = makeSplitCover({
customer: document.getElementById('customer').value,
number: document.getElementById('number').value,
numberPrefix: document.getElementById('prefix').value,
hlColor: document.getElementById('color').value,
})
document.getElementById('front').src = window.URL.createObjectURL(covers.front);
document.getElementById('back').src = window.URL.createObjectURL(covers.back);
}
</script>
<style>
html, body {
height: 100%;
width: 100%;
}
.covers {
height: 100%;
width: 90%;
display: flex;
flex-direction: row;
}
.covers iframe {
flex-grow: 1;
}
</style>
</head>
<body>
<div>
<label>
Customer:
<textarea id="customer"></textarea>
</label>
</div>
<div>
<label>
Prefix:
<input id="prefix" type="text" value="offerte">
</label>
</div>
<div>
<label>
Number:
<input id="number" type="text">
</label>
</div>
<div>
<label>
Color:
<input id="color" type="color" value="#FF69B4">
</label>
</div>
<div>
<button onclick="letsfuckinggo()">Fuck it!</button>
</div>
<div class="covers">
<iframe id="front"></iframe>
<iframe id="back"></iframe>
</div>
<div class="download-links">
Download the CLI:
<a href="./dist/covergen.linux-amd64">Linux amd64</a>
<a href="./dist/covergen.darwin-amd64">Darwin amd64</a>
</div>
</body>
</html>

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