update
Some checks failed
deploy / deploy (push) Has been cancelled

This commit is contained in:
toom1996
2026-09-01 00:08:05 +08:00
parent 97b023bbbd
commit 50e5775dc8
9 changed files with 434 additions and 141 deletions

View File

@ -1,59 +1,147 @@
---
import Layout from '@/layouts/Layout.astro'
import '@/styles/global.css'
import { getI18n } from '@/i18n/utils'
const { locale, t } = getI18n(Astro)
import { href, ROUTES } from '@/lib/routes'
import { LOCALE_NAMES } from '@/i18n/config'
import { i18n } from 'astro:config/client';
import { toCodes } from 'astro:i18n';
const { locale, t, getRelativeLocaleUrl } = getI18n(Astro)
const pathName = Astro.url.pathname
const locales = toCodes(i18n!.locales)
const pathWithoutLocale = locales.includes(pathName.split('/')[1])
? pathName.replace(`/${locale}`, '')
: pathName
const languages = locales.map((l) => ({ code: l, label: LOCALE_NAMES[l].nativeName }))
const loginLabel = t('login')
const promptLabel = t('login prompt')
const accountLabel = t('username or email')
const passwordLabel = t('password')
const failLabel = t('login failed')
const membersLabel = t('members')
const backLabel = t('back to site')
const homeUrl = href(locale, ROUTES.runwayLooks)
---
<Layout title={loginLabel}>
<section class="mx-auto w-full max-w-md px-6 py-24">
<h1 class="font-serif text-4xl tracking-tight uppercase mb-2">{loginLabel}</h1>
<p class="text-[11px] font-mono uppercase tracking-widest text-zinc-400 mb-10">{promptLabel}</p>
<body class="min-h-screen bg-[#f4f1eb] text-black antialiased selection:bg-black selection:text-white">
<div class="absolute right-0 top-0 z-30 p-6 md:p-10" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
<button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="flex items-center gap-1.5 rounded-full border border-black/10 bg-white/85 px-2.5 py-1.5 text-[10px] font-medium uppercase tracking-[0.26em] text-black backdrop-blur-sm transition-colors hover:bg-black hover:text-white" aria-label={t('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="h-[14px] w-[14px] transition-transform duration-200" :class="open ? 'rotate-180' : ''" 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].nativeName}</span>
</button>
<form x-data="loginForm()" data-locale={locale} data-fail-label={failLabel} @submit.prevent="submit()" class="space-y-8" novalidate>
<div>
<label class="block text-[11px] font-mono uppercase tracking-widest text-zinc-500 mb-2">{accountLabel}</label>
<input type="text" x-model="account" autocomplete="username" required
class="w-full bg-transparent border-0 border-b border-black/30 rounded-none px-0 py-2 text-sm outline-none focus:border-black focus:ring-0" />
</div>
<div>
<label class="block text-[11px] font-mono uppercase tracking-widest text-zinc-500 mb-2">{passwordLabel}</label>
<input type="password" x-model="password" autocomplete="current-password" required
class="w-full bg-transparent border-0 border-b border-black/30 rounded-none px-0 py-2 text-sm outline-none focus:border-black focus:ring-0" />
<ul x-show="open" x-transition.opacity.duration.150ms class="absolute right-0 top-full mt-2 min-w-[170px] border border-black/10 bg-white py-2 shadow-[0_18px_40px_rgba(0,0,0,0.08)]">
{languages.map((l) => (
<li>
<a href={getRelativeLocaleUrl(l.code, pathWithoutLocale)} @click="open = false" class:list={['block px-4 py-2 text-[10px] uppercase tracking-[0.22em] transition-colors hover:bg-black hover:text-white', l.code === locale ? 'font-bold text-black' : 'text-black/60']}>{l.label}</a>
</li>
))}
</ul>
</div>
<main class="grid min-h-screen md:grid-cols-[1.1fr_1.25fr]">
<section class="relative hidden overflow-hidden bg-black px-10 py-12 text-white md:flex md:flex-col md:justify-between lg:px-14 lg:py-16">
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.12),transparent_30%),linear-gradient(135deg,rgba(255,255,255,0.04),transparent_55%)]"></div>
<div class="pointer-events-none absolute -bottom-16 -right-10 select-none font-serif text-[220px] font-black leading-none text-white/[0.04]">PS</div>
<div class="relative z-10 flex items-center justify-between">
<span class="text-[10px] font-mono uppercase tracking-[0.42em] text-white/60">Archive</span>
<span class="border border-white/20 bg-white/5 px-2 py-1 text-[9px] font-mono uppercase tracking-[0.35em] text-white">2026</span>
</div>
<p x-show="error" x-text="error" x-cloak class="text-xs text-red-600 uppercase tracking-wide"></p>
<div class="relative z-10">
<p class="mb-6 text-[10px] font-mono uppercase tracking-[0.42em] text-white/50">Member access</p>
<h2 class="font-serif text-5xl uppercase leading-[0.88] tracking-[-0.06em] lg:text-[5.4rem]">Portfolio<br />Studio</h2>
<p class="mt-8 text-[11px] font-mono uppercase tracking-[0.34em] text-white/50">Runway · Street · Archive</p>
</div>
<button type="submit" :disabled="loading"
class="w-full border border-black py-3 text-[12px] font-bold uppercase tracking-[0.2em] transition-colors hover:bg-black hover:text-white disabled:opacity-40">
<span x-show="!loading">{loginLabel}</span>
<span x-show="loading" x-cloak>…</span>
</button>
</form>
</section>
</Layout>
<div class="relative z-10 max-w-md">
<div class="mb-8 h-px w-24 bg-white/20"></div>
<p class="text-sm leading-relaxed text-white/75">{t('minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. integrated with 2026 color forecast intelligence and ai palette generator.')}</p>
<div class="mt-8 flex flex-wrap gap-3 text-[10px] font-mono uppercase tracking-[0.28em] text-white/50">
<span>Paris</span>
<span>•</span>
<span>Milan</span>
<span>•</span>
<span>Tokyo</span>
<span>•</span>
<span>New York</span>
</div>
</div>
</section>
<section class="flex items-center justify-center px-6 py-12 md:px-8 lg:px-12">
<div class="w-full max-w-[440px] rounded-[32px] border border-black/10 bg-white/80 p-6 shadow-[0_24px_80px_rgba(0,0,0,0.06)] backdrop-blur-sm md:p-8 lg:p-10">
<div class="mb-8 flex items-center justify-between gap-4 border-b border-black/10 pb-5">
<div>
<p class="mb-2 text-[10px] font-mono uppercase tracking-[0.36em] text-black/45">{membersLabel}</p>
<h1 class="font-serif text-4xl uppercase leading-none tracking-[-0.06em] md:text-5xl">{loginLabel}</h1>
</div>
<a href={homeUrl} class="inline-flex items-center justify-center rounded-full border border-black/10 bg-black px-2.5 py-1.5 text-[9px] font-mono uppercase tracking-[0.28em] text-white transition-colors hover:bg-white hover:text-black">
Studio
</a>
</div>
<p class="mb-8 text-[10px] font-mono uppercase tracking-[0.28em] text-black/45">{promptLabel}</p>
<form x-data="loginForm" data-locale={locale} data-fail-label={failLabel} @submit.prevent="submit()" class="space-y-7" novalidate>
<div>
<label class="mb-2 block text-[10px] font-mono uppercase tracking-[0.34em] text-black/50">{accountLabel}</label>
<input type="text" x-model="account" autocomplete="username" required autofocus class="w-full border-0 border-b border-black/15 bg-transparent px-0 py-3 text-sm text-black outline-none transition-colors placeholder:text-black/30 focus:border-black" placeholder="" />
</div>
<div>
<label class="mb-2 block text-[10px] font-mono uppercase tracking-[0.34em] text-black/50">{passwordLabel}</label>
<input type="password" x-model="password" autocomplete="current-password" required class="w-full border-0 border-b border-black/15 bg-transparent px-0 py-3 text-sm text-black outline-none transition-colors placeholder:text-black/30 focus:border-black" placeholder="" />
</div>
<p x-show="error" x-text="error" x-cloak class="text-[10px] font-mono uppercase tracking-[0.18em] text-red-600"></p>
<button type="submit" :disabled="loading" class="flex w-full items-center justify-center border border-black bg-black px-4 py-3.5 text-[11px] font-bold uppercase tracking-[0.28em] text-white transition-all duration-200 hover:bg-white hover:text-black disabled:cursor-not-allowed disabled:opacity-60">
<span x-show="!loading">{loginLabel}</span>
<span x-show="loading" x-cloak class="inline-flex items-center gap-2">Loading</span>
</button>
</form>
<div class="mt-8 flex items-center justify-between border-t border-black/10 pt-5">
<a href={homeUrl} class="inline-flex items-center gap-2 text-[10px] font-mono uppercase tracking-[0.32em] text-black/55 transition-colors hover:text-black">
<span aria-hidden="true">←</span>
{backLabel}
</a>
<span class="text-[9px] font-mono uppercase tracking-[0.28em] text-black/35">Editorial access</span>
</div>
</div>
</section>
</main>
</body>
<script>
import { login, getUser } from "@/lib/api"
import { clientHref, ROUTES } from "@/lib/routes"
document.addEventListener("alpine:init", () => {
Alpine.data("loginForm", () => ({
account: "",
password: "",
error: "",
loading: false,
init() {
if (getUser()) {
const loc = this.$el.dataset.locale || "en"
window.location.href = clientHref(loc, ROUTES.account)
}
},
submit() {
this.error = ""
this.loading = true
login({ account: this.account, password: this.password })
.then(() => {
const params = new URLSearchParams(window.location.search)
@ -61,9 +149,13 @@ const failLabel = t('login failed')
const loc = this.$el.dataset.locale || "en"
window.location.href = redirect || clientHref(loc, ROUTES.account)
})
.catch(() => { this.error = this.$el.dataset.failLabel || "login failed" })
.finally(() => { this.loading = false })
}
.catch(() => {
this.error = this.$el.dataset.failLabel || "login failed"
})
.finally(() => {
this.loading = false
})
},
}))
})
</script>