126 lines
3.9 KiB
Vue
126 lines
3.9 KiB
Vue
<script setup lang="ts">
|
|
import { formatDate } from 'date-fns'
|
|
const { slug } = useRoute().params
|
|
|
|
const config = useAppConfig()
|
|
const content = useContent()
|
|
|
|
const meta = {
|
|
title: `${content.page.title}`,
|
|
description: content.page.description,
|
|
image: content.page.thumbnail,
|
|
url: `${config.url}/blog`,
|
|
}
|
|
|
|
defineOgImage()
|
|
|
|
// Hydrate the rendered items.
|
|
onMounted(() => {
|
|
document.querySelectorAll('pre').forEach((pre) => {
|
|
const icon = document.createElement('iconify-icon')
|
|
|
|
icon.setAttribute('icon', 'mdi:content-copy')
|
|
icon.setAttribute('inline', 'true')
|
|
|
|
icon.classList.add('button')
|
|
|
|
pre.appendChild(icon)
|
|
})
|
|
|
|
document
|
|
.querySelectorAll('code:not(pre *), pre > iconify-icon.button')
|
|
.forEach((code) => {
|
|
if (code instanceof HTMLElement) {
|
|
code.onclick = () => {
|
|
const area = document.createElement('textarea')
|
|
|
|
area.textContent =
|
|
code.nodeName && code.nodeName.toLowerCase() === 'code'
|
|
? code.textContent
|
|
: code.parentElement!.textContent
|
|
|
|
// It's necessary to create the textarea element every time you copy to get access to the select() method.
|
|
area.setSelectionRange(0, 99999) // An iOS gotcha.
|
|
area.select()
|
|
|
|
// Copy the text inside the textarea.
|
|
navigator.clipboard.writeText(area.value)
|
|
|
|
// TODO: Alert the user text has been successfully copied.
|
|
|
|
// Remove the textarea element.
|
|
area.remove()
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.querySelectorAll('code').forEach((code) => {
|
|
code.onclick = null
|
|
})
|
|
|
|
document.querySelectorAll('pre').forEach((pre) => {
|
|
pre.querySelector('.clipboard')?.remove()
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<article
|
|
class="post scrollbar fade-mask-sm d-flex flex-column gap-3 flex-grow-1 overflow-x-hidden overflow-y-auto py-sm-4 pe-sm-3"
|
|
>
|
|
<ContentDoc :path="`/blog/${slug}`">
|
|
<template #default="{ doc }">
|
|
<div
|
|
class="post-preamble"
|
|
:style="{ backgroundImage: 'url(' + doc.thumbnail + ')' }"
|
|
>
|
|
<div class="p-2">
|
|
<h3 class="alex">{{ doc.title }}</h3>
|
|
<div class="d-flex flex-row row-gap-0 column-gap-2 flex-wrap">
|
|
<div class="d-flex flex-row gap-1 align-items-center">
|
|
<iconify-icon icon="mdi:calendar" />
|
|
<strong class="nobr font-small">
|
|
{{ formatDate(doc.created, 'LLLL do, y – HH:mm') }}
|
|
</strong>
|
|
</div>
|
|
<div class="d-flex flex-row gap-1 align-items-center">
|
|
<iconify-icon icon="mdi:clock-outline" />
|
|
<strong class="nobr font-small">
|
|
{{ doc.readingTime.text.split(' ')[0] + ' minutes to read' }}
|
|
</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<NuxtLink
|
|
:href="`https://twitter.com/share?url=${config.url}/blog/${slug}&text=${doc.title}&hashtags=${doc.tags.slice(0, 3).join(',').replace(/ /g, '')}`"
|
|
target="_blank"
|
|
class="link post-preamble-share px-2 py-1"
|
|
>
|
|
<iconify-icon icon="mdi:twitter"></iconify-icon>
|
|
</NuxtLink>
|
|
<NuxtLink to="/blog" class="link post-preamble-control px-2 py-1">
|
|
<iconify-icon icon="icon-park-solid:back"></iconify-icon>
|
|
</NuxtLink>
|
|
</div>
|
|
<div class="post-content">
|
|
<Separator class="mt-0" />
|
|
<ContentRenderer :value="doc" />
|
|
</div>
|
|
</template>
|
|
|
|
<template #not-found>
|
|
<div
|
|
class="d-flex flex-column justify-content-center align-items-center gap-3"
|
|
>
|
|
<span>Blog post not found 🙁</span>
|
|
<NuxtLink to="/blog" class="link-hi">Back to blog</NuxtLink>
|
|
</div>
|
|
</template>
|
|
</ContentDoc>
|
|
</article>
|
|
</template>
|
|
|
|
<style scoped lang="scss"></style>
|