refactor: refactoring project structure and components, optimizing internationalization and page presentation

This commit is contained in:
radishzzz 2025-03-08 21:38:08 +00:00
parent 6674cb7072
commit d6cff842e1
37 changed files with 156 additions and 146 deletions

View file

@ -0,0 +1,77 @@
<!-- Sentinel element for scroll detection -->
<div
id="top-sentinel"
class="pointer-events-none absolute left-0 top-0 h-px w-full"
aria-hidden="true"
/>
<button
id="back-to-top-button"
aria-label="Back to top"
class="fixed bottom-8 right-8 h-10 w-10 rounded-full bg-background transition-all duration-300 ease-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="m-auto h-60% w-60%"
fill="currentColor"
>
<path d="M18 15l-6-6-6 6" />
</svg>
</button>
<script>
let observer: IntersectionObserver | null = null
let backToTopButton: HTMLButtonElement | null = null
function initBackToTop() {
// Get elements
const sentinel = document.getElementById('top-sentinel')
backToTopButton = document.getElementById('back-to-top-button') as HTMLButtonElement
if (!sentinel || !backToTopButton)
return
// Initialize IntersectionObserver
observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
backToTopButton?.classList.add('opacity-0', 'pointer-events-none', 'translate-y-4')
}
else {
backToTopButton?.classList.remove('opacity-0', 'pointer-events-none', 'translate-y-4')
}
},
{
threshold: 0,
rootMargin: '30% 0% 0% 0%',
},
)
// Observe sentinel
observer.observe(sentinel)
// Add click handler
backToTopButton.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
})
})
}
function cleanup() {
// Cleanup observer
if (observer) {
observer.disconnect()
observer = null
}
// Remove event listeners
backToTopButton = null
}
// Handle page transitions
document.addEventListener('astro:page-load', initBackToTop)
document.addEventListener('astro:before-swap', cleanup)
</script>

View file

@ -0,0 +1,67 @@
---
import themeConfig from '@/config'
// Language array with empty string as default locale
const langs = ['', ...themeConfig.global.moreLocale]
const currentLocale = themeConfig.global.locale
function getLanguageDisplayName(code: string) {
if (!code) {
return 'Default'
}
return new Intl.DisplayNames(['en'], { type: 'language' }).of(code) || code
}
---
<button
type="button"
id="language-switcher"
class="aspect-square w-4 c-secondary active:scale-90"
aria-label={`Current Language: ${getLanguageDisplayName(currentLocale)}. Click to switch to next language.`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-full w-full"
aria-hidden="true"
>
<path d="M19 21 12.3 2h-1L4.7 21l-2.5.2v.8h6.3v-.8L5.7 21l2-5.9h7.5l2 5.9-3.3.2v.8h7.9v-.8zM8 14.3l3.4-10.1 3.5 10.1z" fill="currentColor" />
</svg>
</button>
<script is:inline define:vars={{ langs }}>
document.addEventListener('astro:page-load', () => {
const langSwitch = document.getElementById('language-switcher')
langSwitch?.addEventListener('click', () => {
const { pathname, search, hash } = window.location
const segments = pathname.split('/').filter(Boolean)
const firstSegment = segments[0] || ''
// Get current language or empty string if invalid
const currentLang = langs.includes(firstSegment)
? firstSegment
: ''
const currentIndex = langs.indexOf(currentLang)
const nextLang = langs[(currentIndex + 1) % langs.length]
const newPath = buildNewPath(currentLang, nextLang, segments, pathname) || '/'
window.location.href = `${newPath}${search}${hash}`
})
})
function buildNewPath(currentLang, nextLang, segments, pathname) {
if (currentLang) {
segments[0] = nextLang || segments[0]
return nextLang
? `/${segments.join('/')}`
: `/${segments.slice(1).join('/')}`
}
return nextLang
? `/${nextLang}${pathname}`
: pathname
}
</script>

View file

@ -0,0 +1,75 @@
<script>
import PhotoSwipeLightbox from 'photoswipe/lightbox'
import 'photoswipe/style.css'
let lightbox: PhotoSwipeLightbox | null = null
const pswp = import('photoswipe')
function cleanup() {
if (lightbox) {
lightbox.destroy()
lightbox = null
}
document.removeEventListener('astro:page-load', createPhotoSwipe)
document.removeEventListener('astro:before-swap', cleanup)
}
function createPhotoSwipe() {
// Clean up existing instance if any
cleanup()
lightbox = new PhotoSwipeLightbox({
gallery: 'article img',
pswpModule: () => pswp,
closeSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="2.4rem" viewBox="0 -960 960 960" width="3.8rem" fill="#A0A09F"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></svg>',
zoomSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="2.4rem" viewBox="0 -960 960 960" width="3.8rem" fill="#A0A09F"><path d="M340-540h-40q-17 0-28.5-11.5T260-580q0-17 11.5-28.5T300-620h40v-40q0-17 11.5-28.5T380-700q17 0 28.5 11.5T420-660v40h40q17 0 28.5 11.5T500-580q0 17-11.5 28.5T460-540h-40v40q0 17-11.5 28.5T380-460q-17 0-28.5-11.5T340-500v-40Zm40 220q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>',
padding: { top: window.innerHeight * 0.1, bottom: window.innerHeight * 0.1, left: window.innerWidth * 0.06, right: window.innerWidth * 0.06 },
wheelToZoom: true,
arrowPrev: false,
arrowNext: false,
imageClickAction: 'close',
tapAction: 'close',
doubleTapAction: 'zoom',
})
// Automatically add image dimensions
lightbox.addFilter('domItemData', (itemData: any, element: Element) => {
if (element instanceof HTMLImageElement) {
itemData.src = element.src
itemData.w = Number(element.naturalWidth || window.innerWidth)
itemData.h = Number(element.naturalHeight || window.innerHeight)
itemData.msrc = element.src
}
return itemData
})
lightbox.init()
}
document.addEventListener('astro:page-load', createPhotoSwipe)
document.addEventListener('astro:before-swap', cleanup)
</script>
<style is:global>
.pswp__button {
--at-apply: 'flex items-center justify-center transition';
}
.pswp__button--zoom,
.pswp__button--close {
--at-apply: 'mt-2 lg:mt-4 active:scale-90';
}
.pswp__button--zoom svg:hover,
.pswp__button--close svg:hover {
fill: #BEBEBE;
}
.pswp__button--close {
--at-apply: 'mr-4 lg:mr-8';
}
.pswp__button--zoom {
--at-apply: 'mr--6 lg:mr-0';
}
</style>

View file

@ -0,0 +1,64 @@
<script>
import { OverlayScrollbars } from 'overlayscrollbars'
function initScrollbar() {
const bodyElement = document.body
const scrollbarTheme = document.documentElement.classList.contains('dark') ? 'scrollbar-dark' : 'scrollbar-light'
if (!bodyElement.hasAttribute('data-scrollbar-initialized')) {
OverlayScrollbars({
target: bodyElement,
cancel: {
// don't initialize the overlay scrollbar if there is a native one
nativeScrollbarsOverlaid: true,
},
}, {
scrollbars: {
theme: scrollbarTheme,
autoHide: 'scroll',
autoHideDelay: 800,
},
overflow: {
x: 'hidden',
},
})
bodyElement.setAttribute('data-scrollbar-initialized', 'true')
}
}
initScrollbar()
document.addEventListener('astro:after-swap', initScrollbar)
</script>
<style is:global>
@import 'overlayscrollbars/overlayscrollbars.css';
.scrollbar-light,
.scrollbar-dark {
--os-size: 1rem;
--os-padding-perpendicular: 0.2rem;
--os-padding-axis: 0.4rem;
--os-handle-border-radius: 0.7rem;
--os-handle-perpendicular-size-hover: 160%;
--os-handle-perpendicular-size-active: 160%;
--os-handle-interactive-area-offset: 3px;
}
.scrollbar-light {
--os-handle-bg: #CFC5BD;
--os-handle-bg-hover: #ADA49E;
--os-handle-bg-active: #ADA49E;
}
.scrollbar-dark {
--os-handle-bg: #2C2C2C;
--os-handle-bg-hover: #3C3C3C;
--os-handle-bg-active: #3C3C3C;
}
@media (max-width: 1023px) {
.os-scrollbar {
display: none !important;
}
}
</style>

View file

@ -0,0 +1,64 @@
---
import { themeConfig } from '@/config'
const { light: { background: lightMode }, dark: { background: darkMode } } = themeConfig.color
---
<button
id="theme-toggle"
aria-label="Theme Toggle Button"
class="aspect-square w-4.2 c-secondary active:scale-90"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="m12 1c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-4.9 11-11-4.9-11-11-11m0 20c-5.8 0-10.5-4-10.5-9s4.7-9 10.5-9 10.5 4 10.5 9-4.7 9-10.5 9" />
</svg>
</button>
<script define:vars={{ lightMode, darkMode }}>
// Update theme
function updateTheme() {
// Toggle website theme
document.documentElement.classList.toggle('dark')
// Get current theme
const isDark = document.documentElement.classList.contains('dark')
// Update meta theme color
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
if (metaThemeColor) {
metaThemeColor.setAttribute('content', isDark ? darkMode : lightMode)
}
// Update theme configuration in local storage
localStorage.setItem('theme', isDark ? 'dark' : 'light')
document.dispatchEvent(new Event('theme-changed'))
}
// Bind click event to the button
function setupThemeToggle() {
// Locate theme toggle button
const themeToggleButton = document.getElementById('theme-toggle')
// Add click listener
themeToggleButton.addEventListener('click', () => {
// If browser doesn't support View Transitions API, update theme directly
if (!document.startViewTransition) {
updateTheme()
return
}
// Temporarily add markers during animation to implement view transition and disable CSS transitions
document.documentElement.style.setProperty('view-transition-name', 'theme-transition')
document.documentElement.setAttribute('data-theme-transition', '')
// If browser supports View Transitions API, use it to update theme
const themeTransition = document.startViewTransition(updateTheme)
// Remove markers after animation
themeTransition.finished.then(() => {
document.documentElement.style.removeProperty('view-transition-name')
document.documentElement.removeAttribute('data-theme-transition')
})
})
}
// Initialize click event (on first load or page transition)
setupThemeToggle()
document.addEventListener('astro:after-swap', setupThemeToggle)
</script>