writekit/internal/server/templates/signup.html

473 lines
26 KiB
HTML
Raw Normal View History

2026-01-09 00:16:46 +02:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Sign Up — WriteKit</title>
<link rel="icon" type="image/x-icon" href="/assets/writekit-icon.ico"/>
<link rel="icon" type="image/svg+xml" href="/assets/writekit-icon.svg"/>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0a0a0a;--bg-elevated:#111111;--bg-subtle:#1a1a1a;--text:#fafafa;--text-muted:#737373;--text-dim:#525252;--border:#262626;--border-focus:#404040;--accent:{{ACCENT}};--emerald:#10b981;--cyan:#06b6d4;--red:#ef4444}
html,body{height:100%}
body{font-family:'SF Mono','Fira Code','Consolas',monospace;background:var(--bg);color:var(--text);line-height:1.6;overflow:hidden}
.layout{display:grid;grid-template-columns:280px 1fr;height:100vh}
.sidebar{background:var(--bg);border-right:1px solid var(--border);padding:2.5rem 2rem;display:flex;flex-direction:column}
.sidebar-header{margin-bottom:3rem}
.logo{font-size:15px;font-weight:600;letter-spacing:-0.02em;margin-bottom:0.35rem}
.tagline{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.1em}
.sidebar-content{flex:1;display:flex;flex-direction:column;justify-content:center}
.step-indicator{display:flex;flex-direction:column;gap:1rem}
.step-item{display:flex;align-items:center;gap:1rem;font-size:12px;color:var(--text-dim);transition:all 0.4s ease}
.step-item.active{color:var(--text)}
.step-item.completed{color:var(--emerald)}
.step-dot{width:8px;height:8px;border:1px solid var(--border);background:transparent;transition:all 0.4s ease}
.step-item.active .step-dot{background:var(--text);border-color:var(--text);box-shadow:0 0 12px rgba(250,250,250,0.3)}
.step-item.completed .step-dot{background:var(--emerald);border-color:var(--emerald)}
.sidebar-footer{margin-top:auto}
.env-badge{display:inline-block;padding:0.35rem 0.75rem;border:1px solid var(--accent);font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:var(--accent)}
.main{display:flex;align-items:center;justify-content:center;padding:2rem;position:relative;overflow:hidden}
.main::before{content:'';position:absolute;inset:0;background-image:linear-gradient(var(--border) 1px,transparent 1px),linear-gradient(90deg,var(--border) 1px,transparent 1px);background-size:60px 60px;opacity:0.3;mask-image:radial-gradient(ellipse at center,black 0%,transparent 70%)}
.step-container{position:relative;width:100%;max-width:480px;z-index:1}
.step{position:absolute;width:100%;opacity:0;visibility:hidden;transform:translateY(30px);transition:all 0.5s cubic-bezier(0.16,1,0.3,1)}
.step.active{position:relative;opacity:1;visibility:visible;transform:translateY(0)}
.step.exit-up{transform:translateY(-30px)}
.step-header{margin-bottom:2.5rem}
.step-label{font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.15em;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem}
.step-label::before{content:'>';color:var(--emerald)}
.step-title{font-size:clamp(1.75rem,3vw,2.25rem);font-weight:500;letter-spacing:-0.03em;line-height:1.2;margin-bottom:0.75rem}
.step-desc{font-size:13px;color:var(--text-muted);line-height:1.7}
.auth-buttons{display:flex;flex-direction:column;gap:0.75rem}
.auth-btn{display:flex;align-items:center;justify-content:center;gap:0.75rem;padding:1rem 1.5rem;border:1px solid var(--border);background:var(--bg-elevated);color:var(--text);font-family:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:all 0.2s ease;text-decoration:none}
.auth-btn:hover{border-color:var(--border-focus);background:var(--bg-subtle)}
.auth-btn.primary{background:var(--text);color:var(--bg);border-color:var(--text)}
.auth-btn.primary:hover{background:#e5e5e5;border-color:#e5e5e5;transform:translateY(-2px);box-shadow:0 8px 24px rgba(250,250,250,0.15)}
.auth-btn svg{width:20px;height:20px;flex-shrink:0}
.auth-divider{display:flex;align-items:center;gap:1rem;margin:0.5rem 0}
.auth-divider::before,.auth-divider::after{content:'';flex:1;height:1px;background:var(--border)}
.auth-divider span{font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em}
.user-greeting{display:flex;flex-direction:column}
.user-avatar{width:64px;height:64px;border-radius:50%;margin-bottom:1.5rem;border:2px solid var(--border);display:none;object-fit:cover}
.user-avatar.loaded{display:block;animation:avatarPop 0.5s cubic-bezier(0.34,1.56,0.64,1)}
@keyframes avatarPop{0%{transform:scale(0);opacity:0}100%{transform:scale(1);opacity:1}}
.subdomain-form{margin-bottom:1.5rem}
.input-row{display:flex;align-items:stretch;border:1px solid var(--border);background:var(--bg-elevated);transition:all 0.2s ease}
.input-row:focus-within{border-color:var(--border-focus)}
.input-row.valid{border-color:var(--emerald)}
.input-row.invalid{border-color:var(--red)}
.subdomain-input{flex:1;padding:1rem 1.25rem;background:transparent;border:none;color:var(--text);font-family:inherit;font-size:15px;outline:none}
.subdomain-input::placeholder{color:var(--text-dim)}
.subdomain-suffix{padding:1rem 1.25rem;background:var(--bg-subtle);color:var(--text-muted);font-size:15px;display:flex;align-items:center;border-left:1px solid var(--border)}
.input-status{height:1.5rem;margin-top:0.75rem;font-size:12px;display:flex;align-items:center;gap:0.5rem}
.input-status.available{color:var(--emerald)}
.input-status.unavailable{color:var(--red)}
.input-status.checking{color:var(--text-muted)}
.input-status .dot{width:6px;height:6px;background:currentColor;animation:pulse 1s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
.btn-row{display:flex;gap:0.75rem}
.btn{padding:1rem 2rem;font-family:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:all 0.2s ease;border:1px solid var(--border);background:var(--bg-elevated);color:var(--text)}
.btn:hover:not(:disabled){border-color:var(--border-focus);background:var(--bg-subtle)}
.btn:disabled{opacity:0.4;cursor:not-allowed}
.btn.primary{flex:1;background:linear-gradient(135deg,var(--emerald),var(--cyan));border:none;color:white}
.btn.primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 24px rgba(16,185,129,0.3)}
.btn.primary:disabled{background:var(--border);transform:none;box-shadow:none}
.btn-back{width:48px;padding:1rem;display:flex;align-items:center;justify-content:center}
.btn-back svg{width:16px;height:16px}
.progress-steps{background:var(--bg-elevated);border:1px solid var(--border);padding:1.5rem}
.progress-step{display:flex;align-items:center;gap:1rem;padding:0.75rem 0;font-size:13px;color:var(--text-dim);transition:all 0.4s ease}
.progress-step.active{color:var(--text)}
.progress-step.done{color:var(--emerald)}
.progress-step .dot{width:8px;height:8px;background:var(--border);flex-shrink:0;transition:all 0.3s ease}
.progress-step.active .dot{background:var(--text);animation:progressPulse 1s infinite}
.progress-step.done .dot{background:var(--emerald)}
@keyframes progressPulse{0%,100%{transform:scale(1);box-shadow:0 0 0 0 rgba(250,250,250,0.4)}50%{transform:scale(1.2);box-shadow:0 0 12px 2px rgba(250,250,250,0.2)}}
.success-content{text-align:center}
.success-icon{width:72px;height:72px;margin:0 auto 2rem;background:linear-gradient(135deg,var(--emerald),var(--cyan));display:flex;align-items:center;justify-content:center;animation:successPop 0.6s cubic-bezier(0.34,1.56,0.64,1)}
@keyframes successPop{0%{transform:scale(0);opacity:0}50%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}
.success-icon svg{width:32px;height:32px;color:white;animation:checkDraw 0.4s 0.3s ease-out both}
@keyframes checkDraw{0%{stroke-dashoffset:24;opacity:0}100%{stroke-dashoffset:0;opacity:1}}
.success-icon svg path{stroke-dasharray:24;stroke-dashoffset:24;animation:checkDraw 0.4s 0.3s ease-out forwards}
.success-url{font-size:18px;font-weight:600;margin-bottom:0.5rem;background:linear-gradient(135deg,var(--emerald),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.success-redirect{font-size:12px;color:var(--text-muted);display:flex;align-items:center;justify-content:center;gap:0.5rem}
.success-redirect .dot{width:6px;height:6px;background:var(--text-muted);animation:pulse 1s infinite}
.back-link{position:absolute;top:2rem;left:2rem;font-size:12px;color:var(--text-muted);text-decoration:none;display:flex;align-items:center;gap:0.5rem;transition:color 0.2s;z-index:10}
.back-link:hover{color:var(--text)}
.back-link svg{width:14px;height:14px}
.keyboard-hint{position:absolute;bottom:2rem;left:50%;transform:translateX(-50%);font-size:11px;color:var(--text-dim);display:flex;align-items:center;gap:0.5rem}
.key{padding:0.25rem 0.5rem;background:var(--bg-subtle);border:1px solid var(--border);font-size:10px}
.color-picker{margin-bottom:2rem}
.color-options{display:flex;gap:0.75rem;flex-wrap:wrap;margin-bottom:1.5rem}
.color-option{width:48px;height:48px;border:2px solid transparent;background:var(--color);cursor:pointer;transition:all 0.2s ease;position:relative}
.color-option:hover{transform:scale(1.1)}
.color-option.selected{border-color:var(--text);box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--color)}
.color-option.selected::after{content:'✓';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:white;font-size:18px;text-shadow:0 1px 2px rgba(0,0,0,0.3)}
.color-preview{display:flex;align-items:center;gap:1rem}
.preview-label{font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em}
.preview-box{flex:1;height:8px;background:var(--bg-subtle);border:1px solid var(--border);overflow:hidden}
.preview-accent{height:100%;width:60%;background:var(--emerald);transition:background 0.3s ease,width 0.3s ease}
@media(max-width:900px){.layout{grid-template-columns:1fr}.sidebar{display:none}.main{padding:1.5rem}.back-link{position:relative;top:auto;left:auto;margin-bottom:2rem}.keyboard-hint{display:none}}
@media(max-width:480px){.step-title{font-size:1.5rem}.auth-btn{padding:0.875rem 1.25rem}}
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<div class="logo">WriteKit</div>
<div class="tagline">Blogging Platform</div>
</div>
<div class="sidebar-content">
<div class="step-indicator">
<div class="step-item active" data-step="1"><span class="step-dot"></span><span>Sign in</span></div>
<div class="step-item" data-step="2"><span class="step-dot"></span><span>Personalize</span></div>
<div class="step-item" data-step="3"><span class="step-dot"></span><span>Choose subdomain</span></div>
<div class="step-item" data-step="4"><span class="step-dot"></span><span>Launch</span></div>
</div>
</div>
<div class="sidebar-footer">
<div class="env-badge">ALPHA</div>
</div>
</aside>
<main class="main">
<a href="/" class="back-link">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
Back to home
</a>
<div class="step-container">
<div class="step active" id="step-auth">
<div class="step-header">
<div class="step-label">Step 1</div>
<h1 class="step-title">Start your blog</h1>
<p class="step-desc">Sign in to create your WriteKit instance. Your blog will be ready in seconds.</p>
</div>
<div class="auth-buttons">
<a href="/auth/github?callback=/signup/complete" class="auth-btn primary">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
Continue with GitHub
</a>
<div class="auth-divider"><span>or</span></div>
<a href="/auth/google?callback=/signup/complete" class="auth-btn">
<svg viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
Continue with Google
</a>
<a href="/auth/discord?callback=/signup/complete" class="auth-btn">
<svg viewBox="0 0 24 24" fill="#5865F2"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
Continue with Discord
</a>
</div>
</div>
<div class="step" id="step-personalize">
<div class="step-header">
<div class="user-greeting">
<img class="user-avatar" id="personalize-avatar" alt=""/>
<div class="step-label">Step 2</div>
<h1 class="step-title" id="personalize-title">Pick your style</h1>
</div>
<p class="step-desc">Choose an accent color for your blog. You can change this anytime in settings.</p>
</div>
<div class="color-picker">
<div class="color-options">
<button type="button" class="color-option" data-color="#10b981" style="--color:#10b981" title="Emerald"></button>
<button type="button" class="color-option" data-color="#06b6d4" style="--color:#06b6d4" title="Cyan"></button>
<button type="button" class="color-option" data-color="#8b5cf6" style="--color:#8b5cf6" title="Violet"></button>
<button type="button" class="color-option" data-color="#ec4899" style="--color:#ec4899" title="Pink"></button>
<button type="button" class="color-option" data-color="#f97316" style="--color:#f97316" title="Orange"></button>
<button type="button" class="color-option" data-color="#eab308" style="--color:#eab308" title="Yellow"></button>
<button type="button" class="color-option" data-color="#ef4444" style="--color:#ef4444" title="Red"></button>
<button type="button" class="color-option" data-color="#64748b" style="--color:#64748b" title="Slate"></button>
</div>
<div class="color-preview">
<span class="preview-label">Preview</span>
<div class="preview-box" id="color-preview"><div class="preview-accent"></div></div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-back" id="btn-personalize-back" type="button" title="Use different account">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
</button>
<button class="btn primary" id="btn-personalize-next">Continue</button>
</div>
</div>
<div class="step" id="step-subdomain">
<div class="step-header">
<div class="user-greeting" id="user-greeting">
<img class="user-avatar" id="user-avatar" alt=""/>
<div class="step-label">Step 3</div>
<h1 class="step-title" id="greeting-title">Choose your subdomain</h1>
</div>
<p class="step-desc">Pick a memorable address for your blog. You can add a custom domain later.</p>
</div>
<div class="subdomain-form">
<div class="input-row" id="input-row">
<input type="text" class="subdomain-input" id="subdomain" placeholder="myblog" autocomplete="off" spellcheck="false" autofocus/>
<span class="subdomain-suffix" id="domain-suffix">.writekit.dev</span>
</div>
<div class="input-status" id="status"></div>
</div>
<div class="btn-row">
<button class="btn btn-back" id="btn-back" type="button">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
</button>
<button class="btn primary" id="btn-launch" disabled>Create my blog</button>
</div>
</div>
<div class="step" id="step-provisioning">
<div class="step-header">
<div class="step-label">Step 4</div>
<h1 class="step-title">Launching your blog</h1>
<p class="step-desc">Setting everything up. This only takes a few seconds.</p>
</div>
<div class="progress-steps" id="progress-steps">
<div class="progress-step" data-step="1"><span class="dot"></span><span>Reserving subdomain...</span></div>
<div class="progress-step" data-step="2"><span class="dot"></span><span>Spinning up container...</span></div>
<div class="progress-step" data-step="3"><span class="dot"></span><span>Configuring SSL...</span></div>
<div class="progress-step" data-step="4"><span class="dot"></span><span>Almost ready...</span></div>
</div>
</div>
<div class="step" id="step-success">
<div class="success-content">
<div class="success-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
</div>
<div class="step-title">You're all set!</div>
<div class="success-url" id="success-url"></div>
<div class="success-redirect"><span class="dot"></span><span>Redirecting to your studio...</span></div>
</div>
</div>
</div>
<div class="keyboard-hint">Press <span class="key">Enter</span> to continue</div>
</main>
</div>
<script>
const $ = s => document.getElementById(s)
const $$ = s => document.querySelectorAll(s)
const steps = { auth: $('step-auth'), personalize: $('step-personalize'), subdomain: $('step-subdomain'), provisioning: $('step-provisioning'), success: $('step-success') }
const stepIndicators = $$('.step-item')
const subdomainInput = $('subdomain')
const inputRow = $('input-row')
const status = $('status')
const btnLaunch = $('btn-launch')
const btnBack = $('btn-back')
const successUrl = $('success-url')
const userAvatar = $('user-avatar')
const greetingTitle = $('greeting-title')
const personalizeAvatar = $('personalize-avatar')
const personalizeTitle = $('personalize-title')
const colorOptions = $$('.color-option')
const previewAccent = document.querySelector('.preview-accent')
const btnPersonalizeNext = $('btn-personalize-next')
const btnPersonalizeBack = $('btn-personalize-back')
let currentStep = 'auth'
let currentSubdomain = ''
let isAvailable = false
let debounceTimer
let currentUser = null
let selectedColor = '#10b981'
const sleep = ms => new Promise(r => setTimeout(r, ms))
const fetchUserInfo = async token => {
try {
const res = await fetch(`/api/auth/user?token=${encodeURIComponent(token)}`)
if (!res.ok) {
sessionStorage.removeItem('signup_token')
return false
}
const user = await res.json()
currentUser = user
const firstName = user.name?.split(' ')[0] ?? ''
if (user.avatar_url) {
userAvatar.src = user.avatar_url
userAvatar.onload = () => userAvatar.classList.add('loaded')
personalizeAvatar.src = user.avatar_url
personalizeAvatar.onload = () => personalizeAvatar.classList.add('loaded')
}
if (firstName) {
greetingTitle.textContent = `Hey ${firstName}!`
personalizeTitle.textContent = `Pick your style, ${firstName}`
}
return true
} catch {
sessionStorage.removeItem('signup_token')
return false
}
}
const goToStep = stepName => {
const currentEl = steps[currentStep]
const nextEl = steps[stepName]
const stepOrder = ['auth', 'personalize', 'subdomain', 'provisioning', 'success']
const nextIndex = stepOrder.indexOf(stepName)
stepIndicators.forEach((indicator, i) => {
indicator.classList.remove('active', 'completed')
if (i < nextIndex) indicator.classList.add('completed')
else if (i === nextIndex) indicator.classList.add('active')
})
currentEl.classList.add('exit-up')
currentEl.classList.remove('active')
setTimeout(() => {
currentEl.classList.remove('exit-up')
nextEl.classList.add('active')
currentStep = stepName
if (stepName === 'subdomain') setTimeout(() => subdomainInput.focus(), 100)
}, 150)
}
const checkAvailability = async subdomain => {
if (subdomain !== currentSubdomain) return
try {
const res = await fetch(`/api/demo/check?subdomain=${encodeURIComponent(subdomain)}`)
const data = await res.json()
if (subdomain !== currentSubdomain) return
if (data.domain) $('domain-suffix').textContent = '.' + data.domain
if (data.available) {
status.textContent = `${subdomain}.${data.domain} is available`
status.className = 'input-status available'
inputRow.className = 'input-row valid'
btnLaunch.disabled = false
isAvailable = true
} else {
status.textContent = data.reason || 'Not available'
status.className = 'input-status unavailable'
inputRow.className = 'input-row invalid'
btnLaunch.disabled = true
isAvailable = false
}
} catch {
status.textContent = 'Error checking availability'
status.className = 'input-status unavailable'
btnLaunch.disabled = true
isAvailable = false
}
}
const animateProgress = async url => {
const progressSteps = $$('#progress-steps .progress-step')
let ready = false
let redirecting = false
const pollUrl = async () => {
const start = Date.now()
while (!ready && Date.now() - start < 30000) {
try {
const res = await fetch(url + '/health', { method: 'HEAD' })
if (res.ok || res.status === 307 || res.status === 302) ready = true
} catch { await sleep(300) }
}
}
const doRedirect = () => {
if (redirecting) return
redirecting = true
progressSteps.forEach(s => { s.classList.remove('active'); s.classList.add('done') })
successUrl.textContent = url.replace('https://', '')
goToStep('success')
const token = sessionStorage.getItem('signup_token')
setTimeout(() => { window.location.href = url + '/auth/callback?token=' + encodeURIComponent(token) + '&redirect=/studio' }, 400)
}
pollUrl().then(() => { if (ready) doRedirect() })
for (let i = 0; i < progressSteps.length; i++) {
if (ready) return doRedirect()
progressSteps[i].classList.add('active')
await sleep(300)
progressSteps[i].classList.remove('active')
progressSteps[i].classList.add('done')
}
while (!ready) await sleep(100)
doRedirect()
}
const launchBlog = async () => {
if (!isAvailable || !currentSubdomain) return
const token = sessionStorage.getItem('signup_token')
if (!token) { goToStep('auth'); return }
btnLaunch.disabled = true
goToStep('provisioning')
try {
const res = await fetch('/api/signup/tenant', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ subdomain: currentSubdomain, accent_color: selectedColor })
})
if (res.status === 401) { sessionStorage.removeItem('signup_token'); goToStep('auth'); return }
const data = await res.json()
if (res.ok && data.url) {
await animateProgress(data.url)
} else {
goToStep('subdomain')
status.textContent = data.error || 'Failed to create blog'
status.className = 'input-status unavailable'
}
} catch {
goToStep('subdomain')
status.textContent = 'Error creating blog'
status.className = 'input-status unavailable'
}
}
;(async () => {
const urlParams = new URLSearchParams(window.location.search)
const urlToken = urlParams.get('token')
const storedToken = sessionStorage.getItem('signup_token')
if (urlToken) {
sessionStorage.setItem('signup_token', urlToken)
window.history.replaceState({}, '', '/signup')
if (await fetchUserInfo(urlToken)) goToStep('personalize')
} else if (storedToken) {
if (await fetchUserInfo(storedToken)) goToStep('personalize')
}
colorOptions[0]?.classList.add('selected')
})()
subdomainInput.addEventListener('input', () => {
const value = subdomainInput.value.toLowerCase().replace(/[^a-z0-9-]/g, '')
if (value !== subdomainInput.value) subdomainInput.value = value
currentSubdomain = value
if (!value) {
status.textContent = ''
status.className = 'input-status'
inputRow.className = 'input-row'
btnLaunch.disabled = true
isAvailable = false
return
}
clearTimeout(debounceTimer)
status.innerHTML = '<span class="dot"></span> Checking...'
status.className = 'input-status checking'
inputRow.className = 'input-row'
btnLaunch.disabled = true
debounceTimer = setTimeout(() => checkAvailability(value), 300)
})
colorOptions.forEach(option => {
option.addEventListener('click', () => {
colorOptions.forEach(o => o.classList.remove('selected'))
option.classList.add('selected')
selectedColor = option.dataset.color
previewAccent.style.background = selectedColor
})
})
btnPersonalizeBack.addEventListener('click', () => { sessionStorage.removeItem('signup_token'); goToStep('auth') })
btnPersonalizeNext.addEventListener('click', () => goToStep('subdomain'))
btnBack.addEventListener('click', () => goToStep('personalize'))
btnLaunch.addEventListener('click', launchBlog)
document.addEventListener('keydown', e => { if (e.key === 'Enter' && currentStep === 'subdomain' && isAvailable) launchBlog() })
</script>
</body>
</html>