@ -1,8 +1,11 @@
|
||||
---
|
||||
import '@/styles/global.css'
|
||||
import { href, PATH } from '@/lib/routes'
|
||||
import { href, ROUTES } from '@/lib/routes'
|
||||
import { getI18n } from '@/i18n/utils'
|
||||
import { LOCALES, LOCALE_NAMES } from '@/i18n/config'
|
||||
import { i18n } from 'astro:config/client';
|
||||
import { toCodes } from 'astro:i18n';
|
||||
|
||||
import LanguageDetector from '@/components/LanguageDetector.astro'
|
||||
|
||||
const {
|
||||
@ -12,7 +15,11 @@ const {
|
||||
activeThemePalette = null,
|
||||
} = Astro.props;
|
||||
|
||||
const { locale, t } = getI18n(Astro)
|
||||
const { locale, t, getRelativeLocaleUrl } = getI18n(Astro)
|
||||
|
||||
// 脚本内(打包型 <script>)无法用 {t()} 插值,需经 define:vars 把已翻译字符串传入
|
||||
const hiLabel = t('Hi')
|
||||
const logoutLabel = t('Logout')
|
||||
|
||||
// 当前路径(去掉语言前缀),供语言切换器「换前缀、留路径」用,例如 /cn/women → /women
|
||||
const pathSegments = Astro.url.pathname.split('/');
|
||||
@ -20,8 +27,10 @@ const firstSegment = pathSegments[1] || '';
|
||||
const currentPath = (LOCALES as readonly string[]).includes(firstSegment)
|
||||
? '/' + pathSegments.slice(2).filter(Boolean).join('/') || '/'
|
||||
: Astro.url.pathname;
|
||||
const queryString = Astro.url.search;
|
||||
|
||||
const pathName = Astro.url.pathname;
|
||||
const queryString = Astro.url.search;
|
||||
const locales = toCodes(i18n!.locales);
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang={locale === 'cn' ? 'zh-CN' : 'en'}>
|
||||
@ -39,13 +48,26 @@ const queryString = Astro.url.search;
|
||||
<!-- 顶部主导航栏 -->
|
||||
<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={href(locale, PATH.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={href(locale, ROUTES.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={href(locale, PATH.shows)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('All Shows')}</a>
|
||||
<a href={href(locale, PATH.latestProjects)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Latest Projects')}</a>
|
||||
<a href={href(locale, PATH.about)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('About')}</a>
|
||||
<a href={href(locale, PATH.contact)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Contact Us')}</a>
|
||||
<!-- 全部的 runway:header 下拉(假数据预览) -->
|
||||
<div class="relative" x-data="{ open: false, current: '全部的 runway', menu: ['全部的 runway','Spring / Summer 2026','Fall / Winter 2025','Resort 2026','Pre-Fall 2025','Couture FW25','Menswear SS26','Menswear FW25'] }" @mouseenter="open = true" @mouseleave="open = false" @click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<div class="flex items-center gap-1">
|
||||
<a href={href(locale, ROUTES.shows)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('全部的 runway')}</a>
|
||||
<button type="button" :aria-expanded="open" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 runway 列表">
|
||||
<svg class="w-2.5 h-2.5 transition-transform duration-200" :class="open ? 'rotate-180' : ''" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div x-show="open" style="display:none" class="absolute left-0 top-full mt-2 z-50 min-w-[240px] bg-white border border-black/10 shadow-sm py-1 before:content-[''] before:absolute before:-top-2 before:left-0 before:right-0 before:h-2">
|
||||
<template x-for="item in menu" :key="item">
|
||||
<a href={href(locale, ROUTES.shows)} @click="current = item; open = false" class="block px-4 py-2 text-[12px] uppercase tracking-[0.4px] hover:bg-black/5 transition-colors" :class="item === current ? 'font-bold' : ''" x-text="item"></a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<a href={href(locale, ROUTES.latestProjects)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Latest Projects')}</a>
|
||||
<a href={href(locale, ROUTES.about)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('About')}</a>
|
||||
<a href={href(locale, ROUTES.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">
|
||||
@ -63,7 +85,7 @@ const queryString = Astro.url.search;
|
||||
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-[13px] tracking-[0.5px]"
|
||||
aria-label="Language"
|
||||
aria-label={t('Language')}
|
||||
aria-haspopup="listbox"
|
||||
:aria-expanded="open"
|
||||
>
|
||||
@ -85,23 +107,23 @@ const queryString = Astro.url.search;
|
||||
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) => (
|
||||
{locales.map((l) => (
|
||||
<li>
|
||||
<a
|
||||
href={href(l, currentPath) + queryString}
|
||||
href={getRelativeLocaleUrl(l, pathName)}
|
||||
@click="open = false"
|
||||
class:list={[
|
||||
'block px-4 py-1.5 text-[13px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors',
|
||||
l === locale ? 'font-bold' : 'opacity-60',
|
||||
]}
|
||||
>{LOCALE_NAMES[l]}</a>
|
||||
>{LOCALE_NAMES[l].name}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex items-center gap-2" id="auth-slot">
|
||||
<a href={href(locale, PATH.login)} class="hover:opacity-60 transition-opacity duration-200">{t('Login')}</a>
|
||||
<a href={href(locale, PATH.register)} class="hover:opacity-60 transition-opacity duration-200">{t('Signup')}</a>
|
||||
<a href={href(locale, ROUTES.login)} class="hover:opacity-60 transition-opacity duration-200">{t('Login')}</a>
|
||||
<a href={href(locale, ROUTES.register)} class="hover:opacity-60 transition-opacity duration-200">{t('Signup')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@ -111,12 +133,12 @@ const queryString = Astro.url.search;
|
||||
</div>
|
||||
|
||||
<footer class="relative z-[1] bg-white mt-20 pt-10 border-t-2 border-black text-black w-full font-sans">
|
||||
<script>
|
||||
<script define:vars={{ hiLabel, logoutLabel }}>
|
||||
import { getUser, fetchMe, clearSession } from "@/lib/api"
|
||||
const slot = document.getElementById("auth-slot")
|
||||
function render(user) {
|
||||
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>`
|
||||
slot.innerHTML = `<span class="opacity-70">${hiLabel}, ${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]">${logoutLabel}</button>`
|
||||
const btn = slot.querySelector("#logout-btn")
|
||||
if (btn) btn.addEventListener("click", () => { clearSession(); location.reload() })
|
||||
}
|
||||
@ -139,7 +161,7 @@ const queryString = Astro.url.search;
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-zinc-600 leading-relaxed max-w-md">
|
||||
Minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. Integrated with 2026 color forecast intelligence and AI palette generator.
|
||||
{t('Minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. Integrated with 2026 color forecast intelligence and AI palette generator.')}
|
||||
</p>
|
||||
|
||||
{/* Location Badge */}
|
||||
@ -157,7 +179,7 @@ const queryString = Astro.url.search;
|
||||
{/* 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">
|
||||
Color Studio Tools
|
||||
{t('Color Studio Tools')}
|
||||
</span>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
@ -167,7 +189,7 @@ const queryString = Astro.url.search;
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- <Sparkles class="w-3.5 h-3.5 text-amber-300" /> -->
|
||||
<span>AI Color Trend Generator</span>
|
||||
<span>{t('AI Color Trend Generator')}</span>
|
||||
</div>
|
||||
<span class="text-[10px] text-zinc-400 font-normal">Gemini 2026</span>
|
||||
</button>
|
||||
@ -178,9 +200,9 @@ const queryString = Astro.url.search;
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- <Layers class="w-3.5 h-3.5 text-zinc-600" /> -->
|
||||
<span>Photo Swatch Extractor</span>
|
||||
<span>{t('Photo Swatch Extractor')}</span>
|
||||
</div>
|
||||
<span class="text-[10px] text-zinc-500 font-normal">Upload</span>
|
||||
<span class="text-[10px] text-zinc-500 font-normal">{t('Upload')}</span>
|
||||
</button>
|
||||
|
||||
{activeThemePalette && (
|
||||
@ -190,7 +212,7 @@ const queryString = Astro.url.search;
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
{/* <RefreshCw class="w-3.5 h-3.5" /> */}
|
||||
<span>Reset B&W Theme</span>
|
||||
<span>{t('Reset B&W Theme')}</span>
|
||||
</div>
|
||||
<span class="text-[10px] opacity-80">Default</span>
|
||||
</button>
|
||||
@ -203,18 +225,18 @@ const queryString = Astro.url.search;
|
||||
{/* Bottom Legal & Back To Top */}
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center text-[12px] font-mono text-zinc-500 gap-4">
|
||||
<div>
|
||||
<span>© 2026 PORTFOLIO STUDIO. ALL RIGHTS RESERVED.</span>
|
||||
<span>{t('© 2026 PORTFOLIO STUDIO. ALL RIGHTS RESERVED.')}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-6">
|
||||
<a
|
||||
href={href(locale, PATH.about)}
|
||||
href={href(locale, ROUTES.about)}
|
||||
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
||||
>
|
||||
{t('About')}
|
||||
</a>
|
||||
<a
|
||||
href={href(locale, PATH.contact)}
|
||||
href={href(locale, ROUTES.contact)}
|
||||
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
||||
>
|
||||
{t('Contact')}
|
||||
|
||||
Reference in New Issue
Block a user