Skip to content

Commit

Permalink
feat: improved client UI
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Jan 25, 2023
1 parent 2e950ca commit 3e8f3d1
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 261 deletions.
269 changes: 95 additions & 174 deletions client/app.vue
Original file line number Diff line number Diff line change
@@ -1,61 +1,23 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/vue'
import { joinURL } from 'ufo'
import type { OgImageOptions, PlaygroundClientFunctions } from '../src/types'
const refreshTime = ref(Date.now())
const hostname = window.location.host as string
const host = `${window.location.protocol}//${hostname}`
const path = ref(useRoute().query.path || '/about')
const optionsPath = joinURL(path.value as string, '__og_image__/options')
const vnodePath = joinURL(path.value as string, '__og_image__/vnode')
function refreshSources() {
refreshTime.value = Date.now()
}
const clientFunctions: PlaygroundClientFunctions = {
refresh() {
// @todo this is pretty hacky, we should validate the file being changed is one we care about
refreshSources()
},
}
await connectWS(hostname)
const rpc = createBirpcClient(clientFunctions)
const config = await rpc.useServerConfig()
import { containerWidth, path, refreshSources, rpc } from './util/logic'
useHead({
title: 'OG Image Playground',
})
const width = config.value?.width || 1200
const height = config.value?.height || 630
const { data: options } = await useAsyncData<OgImageOptions>(() => {
return $fetch(optionsPath, {
baseURL: host,
watch: [path, refreshTime],
})
})
path.value = useRoute().query.path as string || '/'
const containerWidth = ref<number | null>(null)
const absoluteBasePath = `${host}${path.value === '/' ? '' : path.value}`
const OgImageTemplate = computed(() => resolveComponent(options.value?.component || 'OgImageTemplate'))
const hasSatori = computed(() => options.value?.provider === 'satori')
const config = await rpc.useServerConfig()
const options = await fetchOptions()
</script>

<template>
<div class="2xl:flex-row flex-col flex h-screen">
<header class="dark:(bg-dark-900 text-light) 2xl:(px-10 py-7) px-5 py-5 bg-light-200 text-dark-800 flex flex-col justify-between 2xl:h-full">
<div>
<div class="w-full flex items-start justify-between space-x-5 2xl:mb-8 mb-3">
<h1 class="text-sm">
<div class="flex-row flex h-screen">
<header class="border-r-1 border-light-400 dark:(border-dark-400 bg-dark-900 text-light) bg-white text-dark-800 flex flex-col justify-between h-screen z-5">
<div class="flex-grow">
<div class="py-5 w-full flex items-start px-5 justify-between space-x-5">
<h1 class="text-base hidden md:block">
<div>OG Image Playground</div>
<a href="https://github.com/harlan-zw/nuxt-og-image" class="underline text-xs opacity-50">nuxt-og-image</a>
</h1>
<NDarkToggle>
<template #default="{ toggle }">
Expand All @@ -65,54 +27,105 @@ const hasSatori = computed(() => options.value?.provider === 'satori')
</template>
</NDarkToggle>
</div>
<div class="2xl:(block space-y-4 space-x-0) space-x-6 flex justify-center">
<div class="text-sm">
<div class="text-xs opacity-60 mb-1">
Path
</div>
<div class="flex items-center space-x-1 mb-1">
<span>{{ path }}</span>
</div>
<hr class="border-1 border-light-400 dark:border-dark-400">
<div class="py-7 px-5 text-sm flex flex-col space-y-3">
<NuxtLink v-slot="{ isActive }" to="/" class="transition-all hover:(ml-1)">
<Icon name="carbon:image-search" class="mr-1" :class="[isActive ? 'opacity-90' : 'opacity-60']" />
<span :class="[isActive ? 'underline' : 'opacity-60']">
Preview
</span>
</NuxtLink>
<NuxtLink v-slot="{ isActive }" to="/options" class="transition-all hover:(ml-1)">
<Icon name="carbon:operations-record" class="mr-1" :class="[isActive ? 'opacity-90' : 'opacity-60']" />
<span :class="[isActive ? 'underline' : 'opacity-60']">
Options
</span>
</NuxtLink>
<NuxtLink v-slot="{ isActive }" to="/vnodes" class="transition-all hover:(ml-1)">
<Icon name="carbon:ibm-cloud-pak-manta-automated-data-lineage" class="mr-1" :class="[isActive ? 'opacity-90' : 'opacity-60']" />
<span :class="[isActive ? 'underline' : 'opacity-60']">
vNodes
</span>
</NuxtLink>
</div>
<hr class="border-1 border-light-400 dark:border-dark-400">
<div v-if="useRoute().path === '/'" class="py-7 px-5 text-sm flex flex-col space-y-3">
<div>
<NButton v-if="containerWidth !== 504" @click="containerWidth = 504">
Small
</NButton>
<NButton v-if="containerWidth !== null" @click="containerWidth = null">
Full width
</NButton>
</div>
<div class="text-sm">
<div class="text-xs opacity-60 mb-1">
Provider
</div>
<div class="flex items-center space-x-1">
<span :class="hasSatori ? 'logos-vercel-icon' : 'logos-chrome'" />
<span>{{ hasSatori ? 'Satori' : 'Browser' }}</span>
</div>
</div>
<hr class="border-1 2xl:block hidden border-light-400 dark:border-dark-400">
<div class="py-7 px-5 2xl:(block space-y-4 space-x-0) space-x-6 hidden justify-center">
<nav class="text-sm hidden 2xl:block" role="navigation">
<ul class="mb-5">
<li class="mb-2">
<a href="https://github.com/harlan-zw/nuxt-og-image" target="_blank">Docs</a>
</li>
<li>
<a href="https://github.com/sponsors/harlan-zw">Sponsor</a>
</li>
</ul>
<a class="hidden 2xl:flex items-center" href="https://harlanzw.com" title="View Harlan's site." target="_blank">
<div class="flex items-center">
<img src="https://avatars.githubusercontent.com/u/5326365?v=4" class="rounded-full h-7 w-7 mr-2">
<div class="flex flex-col">
<span class="opacity-60 text-xs">Created by</span>
<h1 class="text-sm opacity-80">harlanzw</h1>
</div>
</div>
</a>
</nav>
</div>
</header>
<main class="mx-auto flex-1 w-full bg-white dark:bg-black max-h-screen overflow-hidden">
<div class="py-9px dark:(bg-dark-800) bg-light-200 px-10 opacity-80 flex justify-center items-center block space-x-10">
<div class="text-sm">
<div class="text-xs opacity-40">
Path
</div>
<div v-if="options?.component" class="text-sm">
<div class="text-xs opacity-60 mb-1">
Component
</div>
<div class="flex items-center space-x-1">
<span class="logos-vue" />
<span>{{ options?.component }}</span>
</div>
<div class="flex items-center space-x-1">
<NTextInput v-model="path" placeholder="Search..." n="primary" @input="refreshSources" />
</div>
<div class="text-sm">
<div class="text-xs opacity-60 mb-1">
Debug
</div>
<div class="mb-1">
<a :href="optionsPath" target="_blank" class="underline text-xs">Options</a>
</div>
<div><a :href="vnodePath" target="_blank" class="underline text-xs">vNodes</a></div>
</div>
<div class="text-sm">
<div class="text-xs opacity-40 mb-1">
Provider
</div>
<div class="flex items-center space-x-1">
<span :class="options.provider === 'satori' ? 'logos-vercel-icon' : 'logos-chrome'" />
<span>{{ options.provider === 'satori' ? 'Satori' : 'Browser' }}</span>
</div>
</div>
<div v-if="options.component" class="text-sm">
<div class="text-xs opacity-40 mb-1">
Component
</div>
<div class="flex items-center space-x-1">
<span class="logos-vue" />
<span>{{ options.component }}.vue</span>
</div>
</div>
</div>
<nav class="text-sm hidden 2xl:block" role="navigation">
<ul class="mb-5">
<hr class="border-1 border-light-400 dark:border-dark-400">
<div class="h-full max-h-full overflow-auto dark:bg-dark-700 bg-light-200">
<NuxtPage />
</div>
<footer class="block 2xl:hidden space-x-5 flex justify-center items-center">
<ul class="flex space-x-5">
<li class="mb-2">
<a href="https://github.com/harlan-zw/nuxt-og-image" target="_blank">Docs</a>
</li>
<li>
<a href="https://github.com/sponsors/harlan-zw">Sponsor</a>
</li>
</ul>
<a class="hidden 2xl:flex items-center" href="https://harlanzw.com" title="View Harlan's site." target="_blank">
<a class="flex items-center" href="https://harlanzw.com" title="View Harlan's site." target="_blank">
<div class="flex items-center">
<img src="https://avatars.githubusercontent.com/u/5326365?v=4" class="rounded-full h-7 w-7 mr-2">
<div class="flex flex-col">
Expand All @@ -121,100 +134,8 @@ const hasSatori = computed(() => options.value?.provider === 'satori')
</div>
</div>
</a>
</nav>
</header>
<main class="mx-auto flex-1 w-full py-7 ">
<div class="max-h-full flex px-2 sm:px-0 2xl:(w-1205px mx-auto) mx-3 transition-all" :style="containerWidth ? { width: `${containerWidth}px` } : {}">
<div v-if="hasSatori" class="flex flex-col w-full">
<TabGroup>
<TabList class="p-1 dark:(bg-dark-900/20 border-none) border-2 border-dark-900/30 rounded-xl flex space-x-5">
<Tab
v-for="category in ['HTML - Vue', 'SVG - Satori', 'PNG - Satori + Resvg']"
:key="category"
v-slot="{ selected }"
as="template"
>
<button
class="w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-dark-700 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2"
:class="[
selected
? 'text-dark-200 bg-light-900 dark:(bg-dark-300 text-light-100) shadow'
: 'dark:(bg-dark-800 text-light-900) text-blue-900/70 hover:(bg-blue-200)',
]"
>
{{ category }}
</button>
</Tab>
</TabList>

<TabPanels class="mt-2 flex tab-panels">
<TabPanel>
<IFrameLoader
:src="`${absoluteBasePath}/__og_image__/html?timestamp=${refreshTime}&scale=${!containerWidth ? 1 : (containerWidth - 12) / 1200}`"
:width="width"
:height="height"
description="[HTML] Generated in %sms."
@refresh="refreshSources"
/>
</TabPanel>
<TabPanel>
<ImageLoader
:src="`${absoluteBasePath}/__og_image__/svg?timestamp=${refreshTime}`"
:width="width"
:height="height"
description="[SVG] Generated in %sms using Satori."
@refresh="refreshSources"
/>
</TabPanel>
<TabPanel>
<ImageLoader
:src="`${absoluteBasePath}/__og_image__/og.png?timestamp=${refreshTime}`"
:width="width"
:height="height"
description="[PNG] Generated in %sms using Satori & Resvg."
@refresh="refreshSources"
/>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
<ImageLoader
v-else
:src="`${absoluteBasePath}/__og_image__/og.png?timestamp=${refreshTime}`"
:width="width"
:height="height"
description="[PNG] Generated in %sms using browser screenshot."
@refresh="refreshSources"
/>
</div>
<div class="flex items-center justify-center mt-5">
<NButton v-if="containerWidth !== 504" @click="containerWidth = 504">
Small
</NButton>
<NButton v-if="containerWidth !== null" @click="containerWidth = null">
Reset width
</NButton>
</div>
</footer>
</main>
<footer class="block 2xl:hidden space-x-5 flex justify-center items-center pb-7">
<ul class="flex space-x-5">
<li class="mb-2">
<a href="https://github.com/harlan-zw/nuxt-og-image" target="_blank">Docs</a>
</li>
<li>
<a href="https://github.com/sponsors/harlan-zw">Sponsor</a>
</li>
</ul>
<a class="flex items-center" href="https://harlanzw.com" title="View Harlan's site." target="_blank">
<div class="flex items-center">
<img src="https://avatars.githubusercontent.com/u/5326365?v=4" class="rounded-full h-7 w-7 mr-2">
<div class="flex flex-col">
<span class="opacity-60 text-xs">Created by</span>
<h1 class="text-sm opacity-80">harlanzw</h1>
</div>
</div>
</a>
</footer>
</div>
</template>

Expand Down
23 changes: 23 additions & 0 deletions client/composables/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { OgImageOptions } from '../../src/types'
import { host, optionsPath, path, refreshTime, vnodePath } from '~/util/logic'
export async function fetchOptions() {
const { data: options } = await useAsyncData<OgImageOptions>(() => {
return $fetch(optionsPath.value, {
baseURL: host,
})
}, {
watch: [path, refreshTime],
})
return options
}

export async function fetchVNodes() {
const { data: options } = await useAsyncData<OgImageOptions>(() => {
return $fetch(vnodePath.value, {
baseURL: host,
})
}, {
watch: [path, refreshTime],
})
return options
}
28 changes: 28 additions & 0 deletions client/composables/shiki.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Highlighter, Lang } from 'shiki-es'
import { getHighlighter } from 'shiki-es'

export const shiki = ref<Highlighter>()

// TODO: Only loading when needed
getHighlighter({
themes: [
'vitesse-dark',
'vitesse-light',
],
langs: [
'css',
'json',
'javascript',
'typescript',
],
}).then((i) => { shiki.value = i })

export function highlight(code: string, lang: Lang) {
const mode = useColorMode()
if (!shiki.value)
return code
return shiki.value.codeToHtml(code, {
lang,
theme: mode.value === 'dark' ? 'vitesse-dark' : 'vitesse-light',
})
}
4 changes: 4 additions & 0 deletions client/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export default defineNuxtConfig({
baseURL: '/__nuxt_og_image__/client',
},
vite: {
// fixes shiki bug
define: {
'process.env.VSCODE_TEXTMATE_DEBUG': 'false',
},
build: {
target: 'esnext',
},
Expand Down
3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
},
"dependencies": {
"flatted": "^3.2.7",
"nuxt-icon": "^0.2.5"
"nuxt-icon": "^0.2.5",
"shiki-es": "^0.2.0"
},
"devDependencies": {
"@headlessui/vue": "^1.7.7",
Expand Down
Loading

0 comments on commit 3e8f3d1

Please sign in to comment.