@ -2,7 +2,9 @@
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import { getI18n } from '@/i18n/utils'
|
||||
import { href, ROUTES } from '@/lib/routes'
|
||||
|
||||
const { locale, t } = getI18n(Astro)
|
||||
|
||||
const titleLabel = t('my account')
|
||||
const pleaseLabel = t('please log in')
|
||||
const loginLabel = t('login')
|
||||
@ -14,46 +16,72 @@ const historyLabel = t('my history')
|
||||
const soonLabel = t('coming soon')
|
||||
---
|
||||
<Layout title={titleLabel}>
|
||||
<section class="mx-auto w-full max-w-2xl px-6 py-24" x-data="accountPage()" data-locale={locale}>
|
||||
<h1 class="font-serif text-4xl uppercase mb-2">{titleLabel}</h1>
|
||||
|
||||
<div x-show="!user" x-cloak class="mt-10 space-y-4">
|
||||
<p class="text-sm text-zinc-500">{pleaseLabel}</p>
|
||||
<a href={href(locale, ROUTES.login) + '?redirect=' + encodeURIComponent(href(locale, ROUTES.account))}
|
||||
class="inline-block border border-black px-6 py-3 text-[12px] font-bold uppercase tracking-[0.2em] transition-colors hover:bg-black hover:text-white">{loginLabel}</a>
|
||||
</div>
|
||||
|
||||
<div x-show="user" x-cloak class="mt-10">
|
||||
<dl class="space-y-4 text-sm">
|
||||
<div class="flex gap-4 border-b border-zinc-100 pb-3">
|
||||
<dt class="w-32 text-[11px] font-mono uppercase tracking-widest text-zinc-400 pt-1">{accountLabel}</dt>
|
||||
<dd class="font-medium" x-text="user?.username"></dd>
|
||||
<main class="mx-auto w-full max-w-6xl px-6 py-12 md:px-8 lg:px-12 lg:py-16" x-data="accountPage" data-locale={locale}>
|
||||
<div class="grid gap-8 lg:grid-cols-[0.9fr_1.5fr]">
|
||||
<aside class="rounded-[30px] border border-black/10 bg-black p-6 text-white shadow-[0_20px_60px_rgba(0,0,0,0.08)] md:p-8">
|
||||
<div class="mb-10 flex items-center justify-between gap-3">
|
||||
<span class="text-[10px] font-mono uppercase tracking-[0.38em] 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.32em] text-white">2026</span>
|
||||
</div>
|
||||
<div class="flex gap-4 border-b border-zinc-100 pb-3">
|
||||
<dt class="w-32 text-[11px] font-mono uppercase tracking-widest text-zinc-400 pt-1">{emailLabel}</dt>
|
||||
<dd class="font-medium" x-text="user?.email"></dd>
|
||||
|
||||
<div class="mb-10">
|
||||
<p class="mb-4 text-[10px] font-mono uppercase tracking-[0.38em] text-white/50">Member profile</p>
|
||||
<h1 class="font-serif text-4xl uppercase leading-none tracking-[-0.06em] md:text-5xl">{titleLabel}</h1>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<button @click="logout()" class="mt-8 border border-black px-6 py-3 text-[12px] font-bold uppercase tracking-[0.2em] transition-colors hover:bg-black hover:text-white">{signOutLabel}</button>
|
||||
<div class="space-y-3 text-sm text-white/70">
|
||||
<p class="text-[11px] font-mono uppercase tracking-[0.32em] text-white/45">Portfolio Studio</p>
|
||||
<p class="leading-relaxed">Curated access to runway archives, editorial notes, and private collections.</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="mt-16 space-y-10">
|
||||
<section>
|
||||
<h2 class="font-serif text-2xl uppercase mb-3">{collectionsLabel}</h2>
|
||||
<p class="text-sm text-zinc-400">{soonLabel}</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="font-serif text-2xl uppercase mb-3">{historyLabel}</h2>
|
||||
<p class="text-sm text-zinc-400">{soonLabel}</p>
|
||||
</section>
|
||||
</div>
|
||||
<section class="rounded-[30px] border border-black/10 bg-[#f7f5f1] p-6 shadow-[0_18px_50px_rgba(0,0,0,0.04)] md:p-8 lg:p-10">
|
||||
<div x-show="!user" x-cloak class="space-y-6">
|
||||
<p class="text-[10px] font-mono uppercase tracking-[0.32em] text-black/45">{pleaseLabel}</p>
|
||||
<a href={href(locale, ROUTES.login) + '?redirect=' + encodeURIComponent(href(locale, ROUTES.account))}
|
||||
class="inline-flex items-center justify-center border border-black bg-black px-5 py-3 text-[11px] font-bold uppercase tracking-[0.24em] text-white transition-colors hover:bg-white hover:text-black">{loginLabel}</a>
|
||||
</div>
|
||||
|
||||
<div x-show="user" x-cloak class="space-y-8">
|
||||
<div>
|
||||
<p class="mb-4 text-[10px] font-mono uppercase tracking-[0.32em] text-black/45">Account</p>
|
||||
<dl class="space-y-4">
|
||||
<div class="flex gap-5 border-b border-black/10 pb-4">
|
||||
<dt class="w-28 pt-1 text-[10px] font-mono uppercase tracking-[0.26em] text-black/45">{accountLabel}</dt>
|
||||
<dd class="flex-1 font-medium" x-text="user?.username"></dd>
|
||||
</div>
|
||||
<div class="flex gap-5 border-b border-black/10 pb-4">
|
||||
<dt class="w-28 pt-1 text-[10px] font-mono uppercase tracking-[0.26em] text-black/45">{emailLabel}</dt>
|
||||
<dd class="flex-1 font-medium break-all" x-text="user?.email"></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button @click="logout()" class="border border-black bg-black px-5 py-3 text-[11px] font-bold uppercase tracking-[0.24em] text-white transition-colors hover:bg-white hover:text-black">{signOutLabel}</button>
|
||||
<a href={href(locale, ROUTES.runwayLooks)} class="border border-black/15 bg-transparent px-5 py-3 text-[11px] font-bold uppercase tracking-[0.24em] text-black transition-colors hover:bg-black hover:text-white">{t('View archive')}</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8 pt-4">
|
||||
<section class="border-t border-black/10 pt-6">
|
||||
<h2 class="mb-3 font-serif text-2xl uppercase tracking-[-0.04em]">{collectionsLabel}</h2>
|
||||
<p class="text-sm text-black/45">{soonLabel}</p>
|
||||
</section>
|
||||
<section class="border-t border-black/10 pt-6">
|
||||
<h2 class="mb-3 font-serif text-2xl uppercase tracking-[-0.04em]">{historyLabel}</h2>
|
||||
<p class="text-sm text-black/45">{soonLabel}</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import { getUser, logout as logoutApi } from "@/lib/api"
|
||||
import { clientHref, ROUTES } from "@/lib/routes"
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("accountPage", () => ({
|
||||
user: null,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import { getI18n } from '@/i18n/utils'
|
||||
import { href, ROUTES } from '@/lib/routes'
|
||||
|
||||
const { locale, t } = getI18n(Astro)
|
||||
|
||||
const titleLabel = t('my account')
|
||||
const pleaseLabel = t('please log in')
|
||||
const loginLabel = t('login')
|
||||
@ -14,46 +16,72 @@ const historyLabel = t('my history')
|
||||
const soonLabel = t('coming soon')
|
||||
---
|
||||
<Layout title={titleLabel}>
|
||||
<section class="mx-auto w-full max-w-2xl px-6 py-24" x-data="accountPage()" data-locale={locale}>
|
||||
<h1 class="font-serif text-4xl uppercase mb-2">{titleLabel}</h1>
|
||||
|
||||
<div x-show="!user" x-cloak class="mt-10 space-y-4">
|
||||
<p class="text-sm text-zinc-500">{pleaseLabel}</p>
|
||||
<a href={href(locale, ROUTES.login) + '?redirect=' + encodeURIComponent(href(locale, ROUTES.account))}
|
||||
class="inline-block border border-black px-6 py-3 text-[12px] font-bold uppercase tracking-[0.2em] transition-colors hover:bg-black hover:text-white">{loginLabel}</a>
|
||||
</div>
|
||||
|
||||
<div x-show="user" x-cloak class="mt-10">
|
||||
<dl class="space-y-4 text-sm">
|
||||
<div class="flex gap-4 border-b border-zinc-100 pb-3">
|
||||
<dt class="w-32 text-[11px] font-mono uppercase tracking-widest text-zinc-400 pt-1">{accountLabel}</dt>
|
||||
<dd class="font-medium" x-text="user?.username"></dd>
|
||||
<main class="mx-auto w-full max-w-6xl px-6 py-12 md:px-8 lg:px-12 lg:py-16" x-data="accountPage" data-locale={locale}>
|
||||
<div class="grid gap-8 lg:grid-cols-[0.9fr_1.5fr]">
|
||||
<aside class="rounded-[30px] border border-black/10 bg-black p-6 text-white shadow-[0_20px_60px_rgba(0,0,0,0.08)] md:p-8">
|
||||
<div class="mb-10 flex items-center justify-between gap-3">
|
||||
<span class="text-[10px] font-mono uppercase tracking-[0.38em] 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.32em] text-white">2026</span>
|
||||
</div>
|
||||
<div class="flex gap-4 border-b border-zinc-100 pb-3">
|
||||
<dt class="w-32 text-[11px] font-mono uppercase tracking-widest text-zinc-400 pt-1">{emailLabel}</dt>
|
||||
<dd class="font-medium" x-text="user?.email"></dd>
|
||||
|
||||
<div class="mb-10">
|
||||
<p class="mb-4 text-[10px] font-mono uppercase tracking-[0.38em] text-white/50">Member profile</p>
|
||||
<h1 class="font-serif text-4xl uppercase leading-none tracking-[-0.06em] md:text-5xl">{titleLabel}</h1>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<button @click="logout()" class="mt-8 border border-black px-6 py-3 text-[12px] font-bold uppercase tracking-[0.2em] transition-colors hover:bg-black hover:text-white">{signOutLabel}</button>
|
||||
<div class="space-y-3 text-sm text-white/70">
|
||||
<p class="text-[11px] font-mono uppercase tracking-[0.32em] text-white/45">Portfolio Studio</p>
|
||||
<p class="leading-relaxed">Curated access to runway archives, editorial notes, and private collections.</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="mt-16 space-y-10">
|
||||
<section>
|
||||
<h2 class="font-serif text-2xl uppercase mb-3">{collectionsLabel}</h2>
|
||||
<p class="text-sm text-zinc-400">{soonLabel}</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="font-serif text-2xl uppercase mb-3">{historyLabel}</h2>
|
||||
<p class="text-sm text-zinc-400">{soonLabel}</p>
|
||||
</section>
|
||||
</div>
|
||||
<section class="rounded-[30px] border border-black/10 bg-[#f7f5f1] p-6 shadow-[0_18px_50px_rgba(0,0,0,0.04)] md:p-8 lg:p-10">
|
||||
<div x-show="!user" x-cloak class="space-y-6">
|
||||
<p class="text-[10px] font-mono uppercase tracking-[0.32em] text-black/45">{pleaseLabel}</p>
|
||||
<a href={href(locale, ROUTES.login) + '?redirect=' + encodeURIComponent(href(locale, ROUTES.account))}
|
||||
class="inline-flex items-center justify-center border border-black bg-black px-5 py-3 text-[11px] font-bold uppercase tracking-[0.24em] text-white transition-colors hover:bg-white hover:text-black">{loginLabel}</a>
|
||||
</div>
|
||||
|
||||
<div x-show="user" x-cloak class="space-y-8">
|
||||
<div>
|
||||
<p class="mb-4 text-[10px] font-mono uppercase tracking-[0.32em] text-black/45">Account</p>
|
||||
<dl class="space-y-4">
|
||||
<div class="flex gap-5 border-b border-black/10 pb-4">
|
||||
<dt class="w-28 pt-1 text-[10px] font-mono uppercase tracking-[0.26em] text-black/45">{accountLabel}</dt>
|
||||
<dd class="flex-1 font-medium" x-text="user?.username"></dd>
|
||||
</div>
|
||||
<div class="flex gap-5 border-b border-black/10 pb-4">
|
||||
<dt class="w-28 pt-1 text-[10px] font-mono uppercase tracking-[0.26em] text-black/45">{emailLabel}</dt>
|
||||
<dd class="flex-1 font-medium break-all" x-text="user?.email"></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button @click="logout()" class="border border-black bg-black px-5 py-3 text-[11px] font-bold uppercase tracking-[0.24em] text-white transition-colors hover:bg-white hover:text-black">{signOutLabel}</button>
|
||||
<a href={href(locale, ROUTES.runwayLooks)} class="border border-black/15 bg-transparent px-5 py-3 text-[11px] font-bold uppercase tracking-[0.24em] text-black transition-colors hover:bg-black hover:text-white">{t('View archive')}</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8 pt-4">
|
||||
<section class="border-t border-black/10 pt-6">
|
||||
<h2 class="mb-3 font-serif text-2xl uppercase tracking-[-0.04em]">{collectionsLabel}</h2>
|
||||
<p class="text-sm text-black/45">{soonLabel}</p>
|
||||
</section>
|
||||
<section class="border-t border-black/10 pt-6">
|
||||
<h2 class="mb-3 font-serif text-2xl uppercase tracking-[-0.04em]">{historyLabel}</h2>
|
||||
<p class="text-sm text-black/45">{soonLabel}</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import { getUser, logout as logoutApi } from "@/lib/api"
|
||||
import { clientHref, ROUTES } from "@/lib/routes"
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("accountPage", () => ({
|
||||
user: null,
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user