Add frontend 2.0
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
// This starter template is using Vue 3 <script setup> SFCs
|
||||
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
|
||||
import HelloWorld from './components/HelloWorld.vue';
|
||||
import Input from './components/Input.vue';
|
||||
import Nav from './components/Nav.vue';
|
||||
import Settings from './components/Settings.vue';
|
||||
import Renderer from './components/Renderer.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img alt="Vue logo" src="./assets/logo.png" />
|
||||
<HelloWorld msg="Hello Vue 3 + TypeScript + Vite" />
|
||||
<Input />
|
||||
<Nav />
|
||||
<div class="flex grow space-x-8">
|
||||
<div class="w-80">
|
||||
<Settings />
|
||||
</div>
|
||||
<Renderer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ msg: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
</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 type="text" class="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-7 pr-12 sm:text-sm border-gray-300 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
35
frontend/src/components/Nav.vue
Normal file
35
frontend/src/components/Nav.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
const downloads = [
|
||||
{ name: 'Linux', link: './assets/covergen.linux-amd64' },
|
||||
{ name: 'Darwin', link: './assets/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>
|
||||
25
frontend/src/components/Renderer.vue
Normal file
25
frontend/src/components/Renderer.vue
Normal 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>
|
||||
70
frontend/src/components/Settings.vue
Normal file
70
frontend/src/components/Settings.vue
Normal 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>
|
||||
24
frontend/src/components/form/Color.vue
Normal file
24
frontend/src/components/form/Color.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string;
|
||||
modelValue: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
</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
|
||||
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"
|
||||
:value="modelValue"
|
||||
v-bind="$attrs"
|
||||
@input="emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
24
frontend/src/components/form/Input.vue
Normal file
24
frontend/src/components/form/Input.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string;
|
||||
modelValue: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
</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
|
||||
type="text"
|
||||
class="focus:ring-blue-500 focus:border-blue-500 block w-full border-gray-300 rounded-md"
|
||||
:value="modelValue"
|
||||
v-bind="$attrs"
|
||||
@input="emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
23
frontend/src/components/form/TextArea.vue
Normal file
23
frontend/src/components/form/TextArea.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string;
|
||||
modelValue: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
</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
|
||||
class="focus:ring-blue-500 focus:border-blue-500 block w-full border-gray-300 rounded-md"
|
||||
:value="modelValue"
|
||||
v-bind="$attrs"
|
||||
@input="emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
6
frontend/src/env.d.ts
vendored
6
frontend/src/env.d.ts
vendored
@@ -1,8 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from '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
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
16
frontend/src/icons/SadFace.vue
Normal file
16
frontend/src/icons/SadFace.vue
Normal 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>
|
||||
@@ -1,3 +1,14 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-items: center;
|
||||
}
|
||||
51
frontend/src/lib/covergen.ts
Normal file
51
frontend/src/lib/covergen.ts
Normal 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' }),
|
||||
};
|
||||
}
|
||||
18
frontend/src/lib/randomlabel.ts
Normal file
18
frontend/src/lib/randomlabel.ts
Normal 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()),
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
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).mount('#app')
|
||||
createApp(App).use(createPinia()).mount('#app');
|
||||
|
||||
28
frontend/src/stores/cover.ts
Normal file
28
frontend/src/stores/cover.ts
Normal 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);
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user