Files
kazoottt-blog/src/layouts/BlogPost.astro
2024-12-15 22:29:32 +08:00

229 lines
6.2 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import type { CollectionEntry } from 'astro:content'
import BlogHero from '@/components/blog/Hero.astro'
import TOC from '@/components/blog/TOC.astro'
import Button from '@/components/Button.astro'
import PageLayout from './BaseLayout.astro'
interface Props {
post: CollectionEntry<'post'>
simple?: boolean
backHref: string
}
const { post, simple = false, backHref = '/blog' } = Astro.props
const {
data: { description, ogImage, title, date },
slug
} = post
const socialImage = ogImage ?? `/og-image/${slug}.png`
const articleDate = date?.toISOString()
const { headings } = await post.render()
---
<PageLayout meta={{ articleDate, description: description ?? '', ogImage: socialImage, title }}>
<div class='w-full'>
<Button title='Back' href={backHref} style='button'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='14'
height='14'
viewBox='0 0 24 24'
slot='icon-before'
>
<path
fill='currentColor'
d='m6.921 12.5l5.792 5.792L12 19l-7-7l7-7l.713.708L6.921 11.5H19v1z'
>
</path>
</svg>
</Button>
<div class='mt-8 gap-x-10 lg:flex lg:items-start'>
{!!headings.length && <TOC headings={headings} />}
<article class='flex-1 flex-grow break-words' data-pagefind-body>
<div id='blog-hero'>
<BlogHero content={post} simple={simple} />
</div>
<div
id='blog-gallery'
class='prose prose-base prose-zinc mt-12 text-muted-foreground dark:prose-invert prose-headings:font-medium prose-headings:text-foreground prose-headings:before:absolute prose-headings:before:-ms-4 prose-th:before:content-none prose-img:shadow'
>
<slot />
</div>
</article>
</div>
<button
aria-label='Back to Top'
class='z-90 fixed bottom-8 end-4 flex h-8 w-8 translate-y-28 items-center justify-center rounded-full border-2 border-border/50 bg-primary-foreground text-3xl opacity-0 transition-all duration-300 hover:border-border/75 data-[show=true]:translate-y-0 data-[show=true]:opacity-100 sm:end-8 sm:h-12 sm:w-12'
data-show='false'
id='to-top-btn'
><svg
aria-hidden='true'
class='h-4 w-4'
fill='none'
focusable='false'
stroke='currentColor'
stroke-width='2'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M4.5 15.75l7.5-7.5 7.5 7.5' stroke-linecap='round' stroke-linejoin='round'></path>
</svg>
</button>
</div>
<!-- Image Preview Modal -->
<div
id='image-modal'
class='pointer-events-none fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 opacity-0 backdrop-blur-md transition-opacity duration-300 ease-in-out'
>
<div class='relative flex h-full w-full items-center justify-center p-4' id='modal-container'>
<img
id='modal-image'
alt='modal-image'
class='h-auto max-h-full w-auto max-w-full object-contain transition-transform duration-300 ease-in-out'
/>
<button
id='close-modal'
class='absolute right-4 top-4 text-white transition-colors duration-300 hover:text-gray-300'
>
<svg
xmlns='http://www.w3.org/2000/svg'
class='h-6 w-6'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='2'
d='M6 18L18 6M6 6l12 12'></path>
</svg>
</button>
</div>
</div>
</PageLayout>
<script>
const scrollBtn = document.getElementById('to-top-btn') as HTMLButtonElement
const targetHeader = document.getElementById('blog-hero') as HTMLDivElement
function callback(entries: IntersectionObserverEntry[]) {
entries.forEach((entry) => {
// only show the scroll to top button when the heading is out of view
scrollBtn.dataset.show = (!entry.isIntersecting).toString()
})
}
scrollBtn.addEventListener('click', () => {
document.documentElement.scrollTo({ behavior: 'smooth', top: 0 })
})
const observer = new IntersectionObserver(callback)
observer.observe(targetHeader)
// Image preview functionality
const imageModal = document.getElementById('image-modal')
const modalImage = document.getElementById('modal-image') as HTMLImageElement
const modalContainer = document.getElementById('modal-container')
function openModal() {
if (imageModal) {
imageModal.classList.remove('opacity-0', 'pointer-events-none')
modalImage.style.transform = 'scale(1)'
isZoomed = false
document.body.style.overflow = 'hidden'
}
}
function closeModal() {
if (imageModal) {
imageModal.classList.add('opacity-0', 'pointer-events-none')
modalImage.src = ''
modalImage.alt = ''
modalImage.style.transform = 'scale(1)'
isZoomed = false
document.body.style.overflow = ''
}
}
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement
if (target.tagName === 'IMG' && target.closest('.prose')) {
const img = target as HTMLImageElement
modalImage.src = img.src
modalImage.alt = img.alt
openModal()
}
})
// 处理点击事件
if (imageModal) {
imageModal.addEventListener('click', (e) => {
const clickedElement = e.target as HTMLElement
// 如果点击的是 modal 背景或 modal-container不是图片和关闭按钮
if (clickedElement === imageModal || clickedElement === modalContainer) {
closeModal()
}
})
}
// 关闭按钮事件
const closeButton = document.getElementById('close-modal')
if (closeButton) {
closeButton.addEventListener('click', closeModal)
}
// ESC 键关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && imageModal && !imageModal.classList.contains('opacity-0')) {
closeModal()
}
})
// 图片缩放功能
let isZoomed = false
modalImage.addEventListener('click', (e) => {
e.stopPropagation() // 阻止事件冒泡到 modal
if (isZoomed) {
modalImage.style.transform = 'scale(1)'
modalImage.classList.remove('zoomed')
} else {
modalImage.style.transform = 'scale(1.5)'
modalImage.classList.add('zoomed')
}
isZoomed = !isZoomed
})
</script>
<style>
#image-modal {
transition: opacity 300ms ease-in-out;
}
#modal-image {
transition: transform 300ms ease-in-out;
cursor: zoom-in;
}
#modal-image.zoomed {
cursor: zoom-out;
}
#close-modal {
opacity: 0.7;
transition: opacity 300ms ease-in-out;
}
#close-modal:hover {
opacity: 1;
}
body.modal-open {
overflow: hidden;
}
</style>