Skip to content
This repository has been archived by the owner on Jun 22, 2024. It is now read-only.

feat: props editor #25

Merged
merged 9 commits into from
Feb 4, 2024
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
81 changes: 77 additions & 4 deletions client/components/CodeContainer.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<script lang="ts" setup>
import { camelCase } from 'scule'
import JsonEditorVue from 'json-editor-vue'
import { copyTextToClipboard } from '@/util/copy-text-to-clipboard'
import 'vanilla-jsoneditor/themes/jse-theme-dark.css'

defineEmits(['setlang'])

const toast = useToast()
const { editorCode } = useTool()
const { template, email } = useEmail()
const { template, email, renderEmail } = useEmail()

const emailProps = ref(JSON.parse(JSON.stringify(email.value.props)))

function handleDownload(lang: 'html' | 'txt' | 'vue') {
const content = template.value[lang]
Expand Down Expand Up @@ -85,14 +89,25 @@ const items = computed(() => {
})
}

if (emailProps.value.length) {
arr.push({
key: 'props',
label: 'Props',
icon: 'i-ph-code-duotone',
} as any)
}

return arr
})

const tab = ref(0)

watchEffect(() => {
emailProps.value = JSON.parse(JSON.stringify(email.value.props))
})
</script>

<template>
{{ email.props }}
<UTabs
v-model="tab" :items="items" :ui="{
wrapper: 'relative space-y-0',
Expand All @@ -103,7 +118,7 @@ const tab = ref(0)
<UIcon :name="item.icon" class="w-7 h-7 flex-shrink-0" />

<span class="truncate">{{ item.label }}</span>
<template v-if="selected">
<template v-if="selected && item.code">
<UTooltip text="Copy to clipboard">
<UButton class="ml-6" icon="i-ph-copy-duotone" size="xs" square color="gray" variant="solid" @click="handleClipboard(item.key)" />
</UTooltip>
Expand All @@ -112,12 +127,49 @@ const tab = ref(0)
</UTooltip>
</template>

<UBadge v-if="item.key === 'props'" size="xs" label="Beta" variant="subtle" />

<span v-if="selected" class="absolute -right-4 w-2 h-2 rounded-full bg-primary-500 dark:bg-primary-400" />
</div>
</template>

<template #item="{ item }">
<div class="w-full h-full" v-html="highlight(item.code, item.key)" />
<div v-if="item.code" class="w-full h-full" v-html="highlight(item.code, item.key)" />
<div v-else-if="item.key === 'props' && email.props && email.props.length" class="w-full h-full">
<UContainer class="py-5 flex flex-col gap-y-4 relative">
<template v-for="prop in email.props" :key="prop.label">
<UFormGroup v-if="prop.type === 'string'" size="lg" :label="prop.label" :description="prop.description">
<UInput v-model="prop.value" type="text" />
</UFormGroup>
<UFormGroup v-if="prop.type === 'number'" size="lg" :label="prop.label" :description="prop.description">
<UInput v-model.number="prop.value" type="number" />
</UFormGroup>
<UFormGroup v-if="prop.type === 'date'" size="lg" :label="prop.label" :description="prop.description">
<UInput v-model="prop.value" type="datetime-local" :value="prop.value" />
</UFormGroup>
<UFormGroup v-if="prop.type === 'boolean'" size="lg" :label="prop.label" :description="prop.description">
<UToggle v-model="prop.value" />
</UFormGroup>
<UFormGroup v-if="prop.type === 'object'" size="lg" :label="prop.label" :description="prop.description">
<JsonEditorVue
v-model="prop.value"
:class="[$colorMode.value === 'dark' ? 'jse-theme-dark' : 'light']"
class="json-editor-vue of-auto text-sm outline-none"
mode="tree" :navigation-bar="false" :indentation="2" :tab-size="2"
/>
</UFormGroup>
<UFormGroup v-if="prop.type === 'array'" size="lg" :label="prop.label" :description="prop.description">
<JsonEditorVue
v-model="prop.value"
:class="[$colorMode.value === 'dark' ? 'jse-theme-dark' : 'light']"
class="json-editor-vue of-auto text-sm outline-none"
mode="tree" :navigation-bar="false" :indentation="2" :tab-size="2"
/>
</UFormGroup>
</template>
<UButton size="lg" icon="i-ph-floppy-disk" block label="Update Props" @click="renderEmail(emailProps)" />
</UContainer>
</div>
</template>
</UTabs>
</template>
Expand All @@ -135,4 +187,25 @@ const tab = ref(0)
overflow: auto;
white-space: break-spaces;
}

.dark,
.jse-theme-dark {
--jse-panel-background: #111 !important;
--jse-theme-color: #111 !important;
--jse-text-color-inverse: #fff !important;
--jse-main-border: none !important;
}

.json-editor-vue .no-main-menu {
border: none !important;
}

.json-editor-vue .jse-main {
min-height: 1em !important;
}

.json-editor-vue .jse-contents {
border-width: 0 !important;
border-radius: 5px !important;
}
</style>
4 changes: 2 additions & 2 deletions client/composables/shiki.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Highlighter } from 'shikiji'
import { getHighlighter } from 'shikiji'
import type { Highlighter } from 'shiki'
import { getHighlighter } from 'shiki'
import { ref } from 'vue'

export const shiki = ref<Highlighter>()
Expand Down
25 changes: 10 additions & 15 deletions client/composables/useEmail.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import pretty from 'pretty'
import type { Result } from '@vue-email/compiler'
import type { Email } from '@/types/email'
import type { Email, Template } from '@/types/email'

export function useEmail() {
const emails = useState<Email[]>('emails')
const email = useState<Email>('email')
const sending = useState<boolean>('sending', () => false)
const refresh = useState<boolean>('refresh', () => false)
const template = useState<{
vue: string
html: string
txt: string
}>('template')
const template = useState<Template>('template')

const { host } = useWindow()

Expand All @@ -29,23 +25,25 @@ export function useEmail() {
emails.value = data.value
}

const renderEmail = async () => {
const renderEmail = async (props?: Email['props']) => {
if (!email.value)
return null

const { data } = await useFetch<Result>(`/api/render/${email.value.filename}`, {
method: 'POST',
baseURL: host.value,
body: {
props,
},
})

if (data.value) {
return {
template.value = {
vue: email.value.content,
html: pretty(data.value.html),
txt: data.value.text,
}
} as Template
}

return null
}

const getEmail = async (filename: string) => {
Expand All @@ -55,10 +53,7 @@ export function useEmail() {
if (found) {
email.value = found

await renderEmail().then((value) => {
if (value)
template.value = value
})
await renderEmail()
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion client/emails/code-components.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const box = {
padding: '0 48px',
}

const code = `import { codeToThemedTokens } from 'shikiji'
const code = `import { codeToThemedTokens } from 'shiki'

const tokens = await codeToThemedTokens('<div class="foo">bar</div>', {
lang: 'html',
Expand Down
33 changes: 33 additions & 0 deletions client/emails/github-access-token.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ defineProps({
type: String,
default: 'John Doe',
},
string: {
type: String,
},
number: {
type: Number,
default: 0,
},
boolean: {
type: Boolean,
default: true,
},
array: {
type: Array,
default: () => [
{
key: 'value',
},
],
},
object: {
type: Object,
default: () => ({
key: 'value',
}),
},
})

const main = {
Expand Down Expand Up @@ -74,6 +99,14 @@ const footer = {
<strong>@{{ username }}</strong>, a personal access was created on your account.
</EText>

<p>
{{ string }}
{{ number }}
{{ boolean }}
{{ array }}
{{ object }}
</p>

<ESection :style="section">
<EText :style="text">
Hey <strong>{{ username }}</strong>!
Expand Down
5 changes: 4 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@
"@types/splitpanes": "^2.2.6",
"@vueuse/core": "^10.7.2",
"@vueuse/nuxt": "^10.7.2",
"destr": "^2.0.2",
"html-to-text": "^9.0.5",
"json-editor-vue": "^0.12.0",
"json5": "^2.2.3",
"nuxt": "^3.9.3",
"pretty": "^2.0.0",
"scule": "^1.2.0",
"shikiji": "^0.9.19",
"shiki": "^1.0.0-beta.3",
"splitpanes": "^3.1.5",
"vue-component-meta": "^1.8.27"
}
Expand Down
4 changes: 2 additions & 2 deletions client/pages/email/[file].vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ const route = useRoute()
const { getEmail, template } = useEmail()
const { horizontalSplit, previewMode } = useTool({
async onReload() {
await getEmail(`${route.params.file}`)
await getEmail(route.params.file as string)
},
})

onMounted(async () => {
await getEmail(`${route.params.file}`)
await getEmail(route.params.file as string)
})

const showBoth = computed(() => previewMode.value.id === 'both')
Expand Down
65 changes: 64 additions & 1 deletion client/server/api/emails.get.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import path from 'node:path'
import { kebabCase, pascalCase } from 'scule'
import { createComponentMetaCheckerByJsonConfig } from 'vue-component-meta'
import { destr } from 'destr'
import JSON5 from 'json5'
import type { Email } from '~/types/email'
import { createError, defineEventHandler, useStorage } from '#imports'

Expand Down Expand Up @@ -82,6 +84,65 @@ export default defineEventHandler(async () => {
return 0
})
emailProps = emailProps.map(stripeTypeScriptInternalTypesSchema)
const destructuredProps = emailProps.map((prop) => {
const destructuredType = prop.type.split('|').map((type) => {
type = type.trim()
const value = prop.default

if (type === 'string') {
return {
type: 'string',
value: destr(value) ?? '',
}
}

if (type === 'number') {
return {
type: 'number',
value: destr(value) || 0,
}
}

if (type === 'boolean') {
return {
type: 'boolean',
value: destr(value) || false,
}
}

if (type === 'object' || type.includes('Record') || type.includes('Record<')) {
return {
type: 'object',
value: value ? JSON5.parse(value) : {},
}
}

if (type === 'array' || type.includes('[]') || type.includes('Array') || type.includes('Array<')) {
return {
type: 'array',
value: value ? JSON5.parse(value) : [],
}
}

if (type === 'Date') {
return {
type: 'date',
value: value ? eval(value) : new Date().toISOString(),
}
}

return {
type: 'string',
value: value ?? '',
}
})

return {
label: prop.name,
type: destructuredType[0].type,
value: destructuredType[0].value,
}
})

const content = (await useStorage('assets:emails').getItem(
email,
Expand All @@ -99,7 +160,7 @@ export default defineEventHandler(async () => {
size: emailData.size,
created: emailData.birthtime,
modified: emailData.mtime,
props: emailProps,
props: destructuredProps,
}
}),
)
Expand All @@ -114,6 +175,8 @@ export default defineEventHandler(async () => {
return emails
}
catch (error) {
console.error(error)

throw createError({
statusCode: 500,
statusMessage: 'Internal Server Error',
Expand Down
Loading
Loading