@ -1,6 +1,7 @@
|
||||
---
|
||||
import '../styles/global.css'
|
||||
import { getLocaleFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils'
|
||||
import { getRelativeLocaleUrl } from 'astro:i18n'
|
||||
import { useTranslations, DEFAULT_LOCALE, type Locale } from '../i18n/utils'
|
||||
import { LOCALES, LOCALE_NAMES, isLocaleSegment } from '../i18n/config'
|
||||
|
||||
const {
|
||||
@ -10,24 +11,19 @@ const {
|
||||
activeThemePalette = null,
|
||||
} = Astro.props;
|
||||
|
||||
const locale = getLocaleFromUrl(Astro.url)
|
||||
const locale = (Astro.currentLocale as Locale) ?? DEFAULT_LOCALE
|
||||
const { t } = useTranslations(locale)
|
||||
const translatePath = useTranslatedPath(locale)
|
||||
|
||||
// 当前路径去掉语言前缀,用于语言切换器拼出「同页面、另一语言」的链接
|
||||
const _segs = Astro.url.pathname.split('/').filter(Boolean);
|
||||
const _stripped = _segs.length > 0 && isLocaleSegment(_segs[0])
|
||||
? '/' + _segs.slice(1).join('/')
|
||||
: Astro.url.pathname;
|
||||
const currentPath = _stripped === '' ? '/' : _stripped;
|
||||
|
||||
const categories: { label: string; value: PortfolioCategory | 'Trending Colors' }[] = [
|
||||
{ label: 'x', value: 'x' },
|
||||
{ label: 'Backstage', value: 'Backstage' },
|
||||
{ label: 'Events', value: 'Events' },
|
||||
{ label: 'Street', value: 'Street' },
|
||||
{ label: 'Trending Colors', value: 'Trending Colors' },
|
||||
];
|
||||
// 当前路径去掉语言前缀与尾随 .html(format:'file' 产物如 cn.html / shows.html),
|
||||
// 用于语言切换器拼出「同页面、另一语言」的链接(逻辑路径,不带 .html)
|
||||
// 例:/cn.html → / ,/cn/shows.html → /shows ,/shows.html → /shows
|
||||
let _segs = Astro.url.pathname.split('/').filter(Boolean);
|
||||
if (_segs[0] && isLocaleSegment(_segs[0])) _segs = _segs.slice(1);
|
||||
const _last = _segs[_segs.length - 1];
|
||||
if (_last && _last.endsWith('.html')) _segs = _segs.slice(0, -1);
|
||||
const currentPath = _segs.length === 0 ? '/' : '/' + _segs.join('/');
|
||||
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang={locale === 'cn' ? 'zh-CN' : 'en'}>
|
||||
@ -61,31 +57,70 @@ const categories: { label: string; value: PortfolioCategory | 'Trending Colors'
|
||||
<!-- 顶部主导航栏 -->
|
||||
<nav class="flex justify-between items-center text-[13px] font-bold tracking-[0.5px] uppercase">
|
||||
<div class="flex flex-wrap items-center gap-6 md:gap-8">
|
||||
<a href={translatePath('/portfolio')} class="relative pb-[2px] cursor-pointer transition-opacity duration-200 flex items-center gap-1.5 hover:opacity-60 opacity-80 border-b-2 border-transparent">{t('Portfolio')}<span class="text-[10px] bg-black text-white px-1.5 py-0.5 rounded-full font-mono tracking-tight leading-none">
|
||||
<a href={getRelativeLocaleUrl(locale, '/portfolio')} class="relative pb-[2px] cursor-pointer transition-opacity duration-200 flex items-center gap-1.5 hover:opacity-60 opacity-80 border-b-2 border-transparent">{t('Portfolio')}<span class="text-[10px] bg-black text-white px-1.5 py-0.5 rounded-full font-mono tracking-tight leading-none">
|
||||
2026
|
||||
</span></a>
|
||||
<a href={translatePath('/shows')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('All Shows')}</a>
|
||||
<a href={translatePath('/latest-projects')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Latest Projects')}</a>
|
||||
<a href={translatePath('/about')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('About')}</a>
|
||||
<a href={translatePath('/contact')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Contact Us')}</a>
|
||||
<a href={getRelativeLocaleUrl(locale, '/shows')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('All Shows')}</a>
|
||||
<a href={getRelativeLocaleUrl(locale, '/latest-projects')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Latest Projects')}</a>
|
||||
<a href={getRelativeLocaleUrl(locale, '/about')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('About')}</a>
|
||||
<a href={getRelativeLocaleUrl(locale, '/contact')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Contact Us')}</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3" id="auth-slot">
|
||||
<div class="flex items-center gap-2 normal-case" aria-label="Language">
|
||||
{LOCALES.map((l) => (
|
||||
<a
|
||||
href={translatePath(currentPath, l)}
|
||||
aria-current={l === locale ? 'true' : undefined}
|
||||
class:list={[
|
||||
'cursor-pointer hover:opacity-60 transition-opacity duration-200',
|
||||
l === locale ? 'opacity-100 underline underline-offset-4' : 'opacity-40',
|
||||
]}
|
||||
>{LOCALE_NAMES[l]}</a>
|
||||
))}
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 语言切换器:Alpine 下拉,hover 展开;独立容器避免被 auth 脚本 innerHTML 重写误伤 -->
|
||||
<div
|
||||
x-data="{ open: false }"
|
||||
@mouseenter="open = true"
|
||||
@mouseleave="open = false"
|
||||
@click.outside="open = false"
|
||||
@keydown.escape.window="open = false"
|
||||
class="relative"
|
||||
id="lang-switch"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="open = !open"
|
||||
class="flex items-center gap-1.5 cursor-pointer hover:opacity-60 transition-opacity duration-200 p-1 -m-1 normal-case text-[12px] tracking-[0.5px]"
|
||||
aria-label="Language"
|
||||
aria-haspopup="listbox"
|
||||
:aria-expanded="open"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-[18px] h-[18px]" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
<span>{LOCALE_NAMES[locale]}</span>
|
||||
</button>
|
||||
<ul
|
||||
x-show="open"
|
||||
x-cloak
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 -translate-y-1"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-1"
|
||||
class="absolute right-0 top-full min-w-[160px] bg-white border border-black/10 shadow-sm z-50 py-2"
|
||||
>
|
||||
{LOCALES.map((l) => (
|
||||
<li>
|
||||
<a
|
||||
href={getRelativeLocaleUrl(l, currentPath)}
|
||||
@click="open = false"
|
||||
class:list={[
|
||||
'block px-4 py-1.5 text-[12px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors',
|
||||
l === locale ? 'font-bold' : 'opacity-60',
|
||||
]}
|
||||
>{LOCALE_NAMES[l]}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex items-center gap-2" id="auth-slot">
|
||||
<a href={getRelativeLocaleUrl(locale, '/login')} class="hover:opacity-60 transition-opacity duration-200">{t('Login')}</a>
|
||||
<a href={getRelativeLocaleUrl(locale, '/register')} class="hover:opacity-60 transition-opacity duration-200">{t('Signup')}</a>
|
||||
</div>
|
||||
<span class="opacity-25">/</span>
|
||||
<a href={translatePath('/login')} class="hover:opacity-60 transition-opacity duration-200">{t('Login')}</a>
|
||||
<a href={translatePath('/register')} class="hover:opacity-60 transition-opacity duration-200">{t('Signup')}</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@ -98,13 +133,10 @@ const categories: { label: string; value: PortfolioCategory | 'Trending Colors'
|
||||
import { getUser, fetchMe, clearSession } from "../lib/auth.ts"
|
||||
const slot = document.getElementById("auth-slot")
|
||||
function render(user) {
|
||||
if (user) {
|
||||
slot.innerHTML = `<span class="opacity-70">Hi, ${user.username}</span><button id="logout-btn" class="hover:opacity-60 transition-opacity cursor-pointer bg-transparent border-0 p-0 font-bold text-[13px] uppercase tracking-[0.5px]">Logout</button>`
|
||||
const btn = slot.querySelector("#logout-btn")
|
||||
if (btn) btn.addEventListener("click", () => { clearSession(); location.reload() })
|
||||
} else {
|
||||
slot.innerHTML = `<a href="/login" class="hover:opacity-60 transition-opacity duration-200">Login</a><a href="/register" class="hover:opacity-60 transition-opacity duration-200">Signup</a>`
|
||||
}
|
||||
if (!user) return // 未登录:保留 SSR 渲染的 Login/Signup(已带语言前缀,无需重写)
|
||||
slot.innerHTML = `<span class="opacity-70">Hi, ${user.username}</span><button id="logout-btn" class="hover:opacity-60 transition-opacity cursor-pointer bg-transparent border-0 p-0 font-bold text-[13px] uppercase tracking-[0.5px]">Logout</button>`
|
||||
const btn = slot.querySelector("#logout-btn")
|
||||
if (btn) btn.addEventListener("click", () => { clearSession(); location.reload() })
|
||||
}
|
||||
const cached = getUser()
|
||||
if (cached) render(cached)
|
||||
@ -140,30 +172,6 @@ const categories: { label: string; value: PortfolioCategory | 'Trending Colors'
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Column 2: Quick Navigation */}
|
||||
<div class="md:col-span-3 space-y-3">
|
||||
<span class="text-[11px] font-mono uppercase tracking-widest text-zinc-400 font-bold block mb-2">
|
||||
Categories
|
||||
</span>
|
||||
<ul class="space-y-2 list-none p-0 m-0 text-xs font-mono font-bold uppercase tracking-wider">
|
||||
{categories.map((cat) => (
|
||||
<li>
|
||||
<button
|
||||
data-category={cat.value}
|
||||
class={`category-btn hover:opacity-60 transition-opacity cursor-pointer flex items-center gap-2 ${
|
||||
activeCategory === cat.value ? 'underline font-black text-black' : 'text-zinc-600'
|
||||
}`}
|
||||
>
|
||||
<span>{cat.label}</span>
|
||||
{cat.value === 'Trending Colors' && (
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 3: Color Studio & Actions */}
|
||||
<div class="md:col-span-4 space-y-3">
|
||||
<span class="text-[11px] font-mono uppercase tracking-widest text-zinc-400 font-bold block mb-2">
|
||||
@ -239,13 +247,13 @@ const categories: { label: string; value: PortfolioCategory | 'Trending Colors'
|
||||
|
||||
<div class="flex items-center gap-6">
|
||||
<a
|
||||
href={translatePath('/about')}
|
||||
href={getRelativeLocaleUrl(locale, '/about')}
|
||||
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
||||
>
|
||||
{t('About')}
|
||||
</a>
|
||||
<a
|
||||
href={translatePath('/contact')}
|
||||
href={getRelativeLocaleUrl(locale, '/contact')}
|
||||
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
||||
>
|
||||
{t('Contact')}
|
||||
|
||||
Reference in New Issue
Block a user