Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/app.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { ToasterProps } from '@nuxt/ui'

useHead({
meta: [{ name: 'viewport', content: 'width=device-width, initial-scale=1' }],
link: [{ rel: 'icon', href: '/favicon.svg' }],
Expand All @@ -14,10 +16,12 @@ useSeoMeta({
ogTitle: title,
ogDescription: description
})

const toaster: ToasterProps = { position: 'top-center', expand: false, duration: 3000 }
</script>

<template>
<UApp>
<UApp :toaster="toaster">
<div class="flex flex-col h-screen w-screen max-w-screen max-h-screen overflow-hidden">
<LayoutHeader />
<main class="flex flex-col grow overflow-hidden w-full h-full">
Expand Down
14 changes: 7 additions & 7 deletions app/components/editor/EditorPanel.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'
import type { ActiveTab } from '~/composables/useCvEditor'
import type { MonacoEditorLang } from '~/components/utils/VMonacoEditor.vue'
import type { MonacoEditorLang } from '~/components/utils/monaco'
import type { ActiveTab } from '~/composables'

const { yamlContent, hbsContent, cssContent, htmlHeadContent, activeTab } = useCvEditor()
const { yaml, hbs, css, head, activeTab } = useCvEditor()

interface EditorSource {
label: string
Expand All @@ -13,10 +13,10 @@ interface EditorSource {
}

const sources = {
yaml: { label: 'Content', icon: 'i-lucide-file-text', lang: 'yaml', content: yamlContent },
hbs: { label: 'Template', icon: 'i-lucide-code', lang: 'handlebars', content: hbsContent },
css: { label: 'Styles', icon: 'i-lucide-palette', lang: 'css', content: cssContent },
head: { label: 'Head', icon: 'i-lucide-code-xml', lang: 'html', content: htmlHeadContent }
yaml: { label: 'Content', icon: 'i-lucide-file-text', lang: 'yaml', content: yaml },
hbs: { label: 'Template', icon: 'i-lucide-code', lang: 'handlebars', content: hbs },
css: { label: 'Styles', icon: 'i-lucide-palette', lang: 'css', content: css },
head: { label: 'Head', icon: 'i-lucide-code-xml', lang: 'html', content: head }
} satisfies Record<ActiveTab, EditorSource>

const tabKeys = Object.keys(sources) as ActiveTab[]
Expand Down
26 changes: 12 additions & 14 deletions app/components/editor/PreviewToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@ const { isPreviewMarkup } = useCvEditor()
</script>

<template>
<LayoutToolbar>
<div class="flex items-center justify-end w-full gap-2">
<UTooltip text="Toggle Preview Markup">
<UButton
icon="i-lucide-square-dashed"
:color="isPreviewMarkup ? 'primary' : 'neutral'"
variant="ghost"
size="sm"
aria-label="Toggle Preview Markup"
@click="isPreviewMarkup = !isPreviewMarkup"
/>
</UTooltip>
</div>
</LayoutToolbar>
<div class="flex items-center border-b border-default bg-background px-2 min-h-10 h-10 justify-end w-full gap-2">
<UTooltip text="Toggle Preview Markup">
<UButton
icon="i-lucide-square-dashed"
:color="isPreviewMarkup ? 'primary' : 'neutral'"
variant="ghost"
size="sm"
aria-label="Toggle Preview Markup"
@click="isPreviewMarkup = !isPreviewMarkup"
/>
</UTooltip>
</div>
</template>
19 changes: 19 additions & 0 deletions app/components/layout/LayoutHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,25 @@ const {
importHbs,
importCss,
importHtmlHead,
importBundle,
resetToDefault
} = useCvEditor()

const bundleInputRef = ref<HTMLInputElement | null>(null)
const yamlInputRef = ref<HTMLInputElement | null>(null)
const hbsInputRef = ref<HTMLInputElement | null>(null)
const cssInputRef = ref<HTMLInputElement | null>(null)
const htmlHeadInputRef = ref<HTMLInputElement | null>(null)

function handleBundleFileChange(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (file) {
importBundle(file)
input.value = ''
}
}

function handleYamlFileChange(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
Expand Down Expand Up @@ -58,6 +69,7 @@ function handleHbsFileChange(event: Event) {
}

const importItems: DropdownMenuItem[][] = [
[{ label: 'Import Bundle', icon: 'i-lucide-package', onSelect: () => bundleInputRef.value?.click() }],
[
{ label: 'Import Content', icon: 'i-lucide-file-text', onSelect: () => yamlInputRef.value?.click() },
{ label: 'Import Template', icon: 'i-lucide-code', onSelect: () => hbsInputRef.value?.click() },
Expand Down Expand Up @@ -194,6 +206,13 @@ const exportItems: DropdownMenuItem[][] = [
</UTooltip>
</div>

<input
ref="bundleInputRef"
type="file"
accept=".zip,application/zip,application/x-zip-compressed"
class="hidden"
@change="handleBundleFileChange"
>
<input
ref="yamlInputRef"
type="file"
Expand Down
9 changes: 0 additions & 9 deletions app/components/layout/LayoutToolbar.vue

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
<script lang="ts">
import type * as Monaco from 'monaco-editor'
import { KeyCode, KeyMod } from 'monaco-editor'

export type MonacoEditorOptions = Monaco.editor.IStandaloneEditorConstructionOptions
export type MonacoEditorLang = 'html' | 'css' | 'yaml' | 'handlebars'
export type MonacoCodeEditor = Monaco.editor.IStandaloneCodeEditor

export const MonacoEditorLang: Record<MonacoEditorLang, MonacoEditorLang> = {
html: 'html',
css: 'css',
yaml: 'yaml',
handlebars: 'handlebars'
} as const
<script setup lang="ts">
import type { MonacoCodeEditor, MonacoEditorLang, MonacoEditorOptions } from './model'

const formatters: Record<MonacoEditorLang, (code: string) => Promise<string>> = {
html: formatHtml,
css: formatCss,
yaml: formatYaml,
handlebars: formatHandlebars
}
</script>

<script setup lang="ts">
const props = withDefaults(defineProps<{
modelValue: string
lang: MonacoEditorLang
Expand Down Expand Up @@ -59,7 +45,9 @@ const monacoLang = computed(() => {
return props.lang
})

const handleLoad = (editor: MonacoCodeEditor) => {
const handleLoad = async (editor: MonacoCodeEditor) => {
const { KeyCode, KeyMod } = await import('monaco-editor')

editor.addAction({
id: `format-${props.lang}`,
label: `Format ${props.lang}`,
Expand Down
3 changes: 3 additions & 0 deletions app/components/utils/monaco/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { MonacoCodeEditor, MonacoEditorLang, MonacoEditorOptions } from './model'

export { default as VMonacoEditor } from './VMonacoEditor.client.vue'
12 changes: 12 additions & 0 deletions app/components/utils/monaco/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type * as Monaco from 'monaco-editor'

export type MonacoEditorOptions = Monaco.editor.IStandaloneEditorConstructionOptions
export type MonacoEditorLang = 'html' | 'css' | 'yaml' | 'handlebars'
export type MonacoCodeEditor = Monaco.editor.IStandaloneCodeEditor

export const MonacoEditorLang: Record<MonacoEditorLang, MonacoEditorLang> = {
html: 'html',
css: 'css',
yaml: 'yaml',
handlebars: 'handlebars'
} as const
16 changes: 16 additions & 0 deletions app/composables/editor/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const EDITOR_PREFIX = 'editor__'

export const STORAGE_KEY_YAML = EDITOR_PREFIX + 'yaml'
export const STORAGE_KEY_HBS = EDITOR_PREFIX + 'hbs'
export const STORAGE_KEY_CSS = EDITOR_PREFIX + 'css'
export const STORAGE_KEY_HTML_HEAD = EDITOR_PREFIX + 'html-head'

export const BUNDLE_FILES = {
yaml: 'content.yaml',
hbs: 'template.hbs',
css: 'styles.css',
htmlHead: 'head.html',
html: 'cv.html'
} as const

export const REQUIRED_BUNDLE_FILES = Object.values(BUNDLE_FILES)
3 changes: 3 additions & 0 deletions app/composables/editor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { useCvEditor } from './useCvEditor'

export type { EditorRefs, ActiveTab } from './model'
8 changes: 8 additions & 0 deletions app/composables/editor/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type ActiveTab = 'yaml' | 'hbs' | 'css' | 'head'

export type EditorRefs = {
yaml: Ref<string>
hbs: Ref<string>
css: Ref<string>
head: Ref<string>
}
144 changes: 144 additions & 0 deletions app/composables/editor/useCvEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import * as yamlJs from 'js-yaml'
import Handlebars from 'handlebars'

import type { ActiveTab, EditorRefs } from './model'
import { STORAGE_KEY_YAML, STORAGE_KEY_HBS, STORAGE_KEY_CSS, STORAGE_KEY_HTML_HEAD } from './const'
import { useEditorExport } from './useEditorExport'
import { useEditorImport } from './useEditorImport'
import { getErrorMessage } from './utils'

export type { ActiveTab } from './model'

export function useCvEditor() {
const toaster = useCommonToaster()

const head = useState<string>('cv-html-head', () => defaultHtmlHead)
const yaml = useState<string>('cv-yaml', () => defaultYaml)
const hbs = useState<string>('cv-hbs', () => defaultHbs)
const css = useState<string>('cv-css', () => defaultCss)

const activeTab = useState<ActiveTab>('cv-active-tab', () => 'yaml')
const isPreviewMarkup = useState<boolean>('cv-is-preview-markup', () => false)
const compileError = useState<string | null>('cv-compile-error', () => null)

const compiledHtml = computed(() => {
const [html, err] = trycatch(() => {
const cvData = yamlJs.load(yaml.value) || {}
const template = Handlebars.compile(hbs.value)
return template({ cv: cvData })
})

if (err) {
const msg = getErrorMessage(err)
compileError.value = msg
return `<div style="color:red;padding:20px;font-family:monospace"><b>Error:</b> ${msg}</div>`
}

compileError.value = null
return html
})

if (import.meta.client) {
const savedYaml = localStorage.getItem(STORAGE_KEY_YAML)
const savedHbs = localStorage.getItem(STORAGE_KEY_HBS)
const savedCss = localStorage.getItem(STORAGE_KEY_CSS)
const savedHtmlHead = localStorage.getItem(STORAGE_KEY_HTML_HEAD)

if (savedYaml !== null) yaml.value = savedYaml
if (savedHbs !== null) hbs.value = savedHbs
if (savedCss !== null) css.value = savedCss
if (savedHtmlHead !== null) head.value = savedHtmlHead

watchDebounced(
[yaml, hbs, css, head],
() => {
localStorage.setItem(STORAGE_KEY_YAML, yaml.value)
localStorage.setItem(STORAGE_KEY_HBS, hbs.value)
localStorage.setItem(STORAGE_KEY_CSS, css.value)
localStorage.setItem(STORAGE_KEY_HTML_HEAD, head.value)
},
{ deep: true, debounce: 500 }
)
}

const combinedDocument = computed(() => buildFullDocument(compiledHtml.value, css.value, head.value))

function buildFullDocument(html: string, css: string, htmlHead: string) {
return `
<!DOCTYPE html>
<html class="overflow-hidden min-h-auto">
<head>
${htmlHead}
<style>${css}</style>
</head>
${html}
</html>
`
}

const contentRefs: EditorRefs = { yaml, hbs, css, head }

const {
exportToPdf,
downloadHtml,
downloadYaml,
downloadHbs,
downloadCss,
downloadHtmlHead,
downloadBundle
} = useEditorExport({ ...contentRefs, combinedDocument })

const {
importYaml,
importHbs,
importCss,
importHtmlHead,
importBundle
} = useEditorImport(contentRefs)

function resetToDefault() {
yaml.value = defaultYaml
hbs.value = defaultHbs
css.value = defaultCss
head.value = defaultHtmlHead

if (import.meta.client) {
localStorage.removeItem(STORAGE_KEY_YAML)
localStorage.removeItem(STORAGE_KEY_HBS)
localStorage.removeItem(STORAGE_KEY_CSS)
localStorage.removeItem(STORAGE_KEY_HTML_HEAD)
}

toaster.success('Reset to default', 'All content has been reset to default.')
}

return {
yaml,
hbs,
css,
head,

activeTab,
isPreviewMarkup,

compiledHtml,
combinedDocument,
compileError,

exportToPdf,
resetToDefault,

downloadYaml,
downloadHbs,
downloadCss,
downloadHtmlHead,
downloadBundle,
downloadHtml,

importYaml,
importHbs,
importCss,
importHtmlHead,
importBundle
}
}
Loading