mirror of
https://github.com/reonokiy/blog.nokiy.net.git
synced 2025-06-17 12:01:33 +02:00
refactor: refactoring project structure and components, optimizing internationalization and page presentation
This commit is contained in:
parent
6674cb7072
commit
d6cff842e1
37 changed files with 156 additions and 146 deletions
77
src/components/Widgets/BackToTop.astro
Normal file
77
src/components/Widgets/BackToTop.astro
Normal 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>
|
67
src/components/Widgets/LanguageSwitcher.astro
Normal file
67
src/components/Widgets/LanguageSwitcher.astro
Normal 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>
|
75
src/components/Widgets/PhotoSwipe.astro
Normal file
75
src/components/Widgets/PhotoSwipe.astro
Normal 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>
|
64
src/components/Widgets/Scrollbar.astro
Normal file
64
src/components/Widgets/Scrollbar.astro
Normal 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>
|
64
src/components/Widgets/ThemeToggle.astro
Normal file
64
src/components/Widgets/ThemeToggle.astro
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue