init
This commit is contained in:
commit
d69342b2e9
160 changed files with 28681 additions and 0 deletions
64
internal/server/templates/404.html
Normal file
64
internal/server/templates/404.html
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>404 — 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:#fafafa;--text:#0a0a0a;--muted:#737373;--border:#e5e5e5;--primary:#10b981}
|
||||
body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}
|
||||
.mono{font-family:'SF Mono','Fira Code','Consolas',monospace}
|
||||
.layout{display:grid;grid-template-columns:200px 1fr;min-height:100vh}
|
||||
aside{padding:2rem 1.5rem;border-right:1px solid var(--border);position:sticky;top:0;height:100vh;display:flex;flex-direction:column}
|
||||
.sidebar-logo{font-family:'SF Mono','Fira Code',monospace;font-weight:600;font-size:14px;letter-spacing:-0.02em;margin-bottom:0.25rem}
|
||||
.sidebar-tagline{font-family:'SF Mono','Fira Code',monospace;font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:2.5rem}
|
||||
.sidebar-nav{display:flex;flex-direction:column;gap:0.25rem}
|
||||
.sidebar-nav a{font-family:'SF Mono','Fira Code',monospace;font-size:12px;color:var(--muted);text-decoration:none;padding:0.5rem 0;transition:color 0.15s}
|
||||
.sidebar-nav a:hover{color:var(--text)}
|
||||
.sidebar-footer{margin-top:auto}
|
||||
.env-badge{font-family:'SF Mono','Fira Code',monospace;display:inline-block;padding:0.35rem 0.75rem;border:1px solid var(--primary);font-size:10px;text-transform:uppercase;letter-spacing:0.05em;color:var(--primary)}
|
||||
main{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:2rem}
|
||||
.error-content{text-align:center;max-width:400px}
|
||||
.error-code{font-family:'SF Mono','Fira Code',monospace;font-size:120px;font-weight:600;letter-spacing:-0.05em;line-height:1;color:var(--border);margin-bottom:1rem}
|
||||
.error-title{font-size:1.5rem;font-weight:500;letter-spacing:-0.02em;margin-bottom:0.75rem}
|
||||
.error-message{font-family:'SF Mono','Fira Code',monospace;font-size:13px;color:var(--muted);margin-bottom:2rem;line-height:1.7}
|
||||
.error-actions{display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}
|
||||
.btn-primary{padding:12px 24px;background:var(--text);color:var(--bg);text-decoration:none;font-family:'SF Mono','Fira Code',monospace;font-size:13px;transition:all 0.2s}
|
||||
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.15)}
|
||||
.btn-secondary{padding:12px 24px;background:transparent;color:var(--text);border:1px solid var(--border);text-decoration:none;font-family:'SF Mono','Fira Code',monospace;font-size:13px;transition:all 0.15s}
|
||||
.btn-secondary:hover{border-color:var(--text)}
|
||||
@media(max-width:900px){.layout{grid-template-columns:1fr}aside{display:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
<aside>
|
||||
<div>
|
||||
<div class="sidebar-logo">WriteKit</div>
|
||||
<div class="sidebar-tagline">Blogging Platform</div>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/">Home</a>
|
||||
<a href="/signup" style="color:var(--primary)">Create Blog →</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="env-badge">ALPHA</div>
|
||||
</div>
|
||||
</aside>
|
||||
<main>
|
||||
<div class="error-content">
|
||||
<div class="error-code">404</div>
|
||||
<h1 class="error-title">Page not found</h1>
|
||||
<p class="error-message">The page you're looking for doesn't exist or may have been moved.</p>
|
||||
<div class="error-actions">
|
||||
<a href="/" class="btn-primary">Go Home</a>
|
||||
<a href="/signup" class="btn-secondary">Create a Blog</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
56
internal/server/templates/expired.html
Normal file
56
internal/server/templates/expired.html
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Demo Expired — 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:#fafafa;--text:#0a0a0a;--muted:#737373;--border:#e5e5e5;--primary:#10b981}
|
||||
body{font-family:'SF Mono','Fira Code','Consolas',monospace;background:var(--bg);color:var(--text);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem}
|
||||
.container{max-width:480px;width:100%}
|
||||
.header{margin-bottom:3rem}
|
||||
.logo{font-weight:600;font-size:14px;letter-spacing:-0.02em;margin-bottom:0.5rem}
|
||||
.subdomain{font-size:12px;color:var(--muted)}
|
||||
.title{font-family:system-ui,-apple-system,sans-serif;font-size:2rem;font-weight:400;letter-spacing:-0.03em;margin-bottom:1rem}
|
||||
.description{color:var(--muted);line-height:1.6;margin-bottom:2.5rem}
|
||||
.section{margin-bottom:2rem}
|
||||
.signup-btn{display:block;width:100%;padding:1rem 1.5rem;background:linear-gradient(135deg,#10b981,#06b6d4);color:white;text-decoration:none;font-family:inherit;font-size:14px;font-weight:500;text-align:center;border:none;cursor:pointer;transition:transform 0.2s,box-shadow 0.2s}
|
||||
.signup-btn:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(16,185,129,0.3)}
|
||||
.signup-note{font-size:11px;color:var(--muted);margin-top:0.75rem;text-align:center}
|
||||
.divider{display:flex;align-items:center;gap:1rem;margin:2rem 0;color:var(--muted);font-size:12px}
|
||||
.divider::before,.divider::after{content:'';flex:1;height:1px;background:var(--border)}
|
||||
.secondary-actions{display:flex;gap:1rem}
|
||||
.secondary-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:0.5rem;padding:0.875rem 1rem;background:transparent;color:var(--text);text-decoration:none;font-family:inherit;font-size:13px;border:1px solid var(--border);transition:all 0.15s}
|
||||
.secondary-btn:hover{border-color:var(--text);background:var(--border)}
|
||||
.secondary-btn svg{width:16px;height:16px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">WriteKit</div>
|
||||
<div class="subdomain">{{SUBDOMAIN_TEXT}}</div>
|
||||
</div>
|
||||
<h1 class="title">Demo Expired</h1>
|
||||
<p class="description">Your demo session has ended. Want to keep your blog? Sign up to make it permanent — it's free.</p>
|
||||
<div class="section">
|
||||
<a href="/signup" class="signup-btn">Keep My Blog — Sign Up Free</a>
|
||||
<p class="signup-note">No credit card required</p>
|
||||
</div>
|
||||
<div class="divider">or</div>
|
||||
<div class="secondary-actions">
|
||||
<a href="/discord" class="secondary-btn">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><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>
|
||||
Join Discord
|
||||
</a>
|
||||
<a href="/" class="secondary-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
Try Again
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
484
internal/server/templates/index.html
Normal file
484
internal/server/templates/index.html
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WriteKit — Full Blogging Platform. Lightweight. Yours.</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" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png" />
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--bg:#fafafa;--text:#0a0a0a;--muted:#737373;--border:#e5e5e5;--accent:{{ACCENT}};--primary:#10b981}
|
||||
body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}
|
||||
.mono{font-family:'SF Mono','Fira Code','Consolas',monospace}
|
||||
.layout{display:grid;grid-template-columns:200px 1fr;min-height:100vh}
|
||||
aside{padding:2rem 1.5rem;border-right:1px solid var(--border);position:sticky;top:0;height:100vh;display:flex;flex-direction:column}
|
||||
.sidebar-logo{font-family:'SF Mono','Fira Code',monospace;font-weight:600;font-size:14px;letter-spacing:-0.02em;margin-bottom:0.25rem}
|
||||
.sidebar-tagline{font-family:'SF Mono','Fira Code',monospace;font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:2.5rem}
|
||||
.sidebar-nav{display:flex;flex-direction:column;gap:0.25rem}
|
||||
.sidebar-nav a{font-family:'SF Mono','Fira Code',monospace;font-size:12px;color:var(--muted);text-decoration:none;padding:0.5rem 0;transition:color 0.15s}
|
||||
.sidebar-nav a:hover{color:var(--text)}
|
||||
.sidebar-divider{height:1px;background:var(--border);margin:1rem 0}
|
||||
.sidebar-label{font-family:'SF Mono','Fira Code',monospace;font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.5rem}
|
||||
.sidebar-footer{margin-top:auto}
|
||||
.env-badge{font-family:'SF Mono','Fira Code',monospace;display:inline-block;padding:0.35rem 0.75rem;border:1px solid var(--accent);font-size:10px;text-transform:uppercase;letter-spacing:0.05em;color:var(--accent)}
|
||||
.version-badge{font-family:'SF Mono','Fira Code',monospace;font-size:10px;color:var(--muted);opacity:0.5;margin-top:0.5rem;cursor:default;transition:opacity 0.15s}
|
||||
.version-badge:hover{opacity:1}
|
||||
main{min-width:0}
|
||||
.hero{position:relative;padding:5rem 3rem 4rem;border-bottom:1px solid var(--border);overflow:hidden}
|
||||
.hero-canvas{position:absolute;top:0;right:0;width:45%;height:100%;opacity:0.6;mask-image:linear-gradient(to left,black,transparent);-webkit-mask-image:linear-gradient(to left,black,transparent)}
|
||||
.hero>*:not(.hero-canvas){position:relative;z-index:1}
|
||||
.hero-label{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.15em;margin-bottom:1.5rem}
|
||||
.hero h1{font-family:system-ui,-apple-system,sans-serif;font-size:clamp(2rem,4vw,3rem);font-weight:500;letter-spacing:-0.03em;line-height:1.15;margin-bottom:1.5rem;max-width:600px}
|
||||
.hero-sub{font-family:'SF Mono','Fira Code',monospace;font-size:13px;color:var(--muted);max-width:550px;line-height:1.7}
|
||||
.hero-sub code{color:var(--text);background:var(--border);padding:0.15em 0.4em;font-size:0.95em}
|
||||
.section{padding:4rem 3rem;border-bottom:1px solid var(--border)}
|
||||
.section-label{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.15em;margin-bottom:2rem}
|
||||
.hero-cta{margin-top:2.5rem}
|
||||
.hero-cta .demo-btn{width:auto;padding:14px 32px;font-size:14px}
|
||||
.hero-cta .demo-note{text-align:left}
|
||||
.section-features{border-top:1px solid var(--border);border-bottom:1px solid var(--border);margin-top:-1px}
|
||||
.features-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:var(--border)}
|
||||
.feature{background:var(--bg);padding:2rem}
|
||||
.feature-num{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);margin-bottom:0.75rem}
|
||||
.feature h3{font-size:1rem;font-weight:500;margin-bottom:0.5rem;letter-spacing:-0.01em}
|
||||
.feature p{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);line-height:1.6}
|
||||
.demo-btn{width:100%;margin-top:16px;padding:12px 24px;background:var(--text);color:var(--bg);border:none;font-family:'SF Mono','Fira Code',monospace;font-size:13px;cursor:pointer;transition:all 0.2s}
|
||||
.demo-btn:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.15)}
|
||||
.demo-btn:disabled{opacity:0.4;cursor:not-allowed}
|
||||
.demo-note{margin-top:1rem;font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);text-align:center}
|
||||
.mission{max-width:650px}
|
||||
.mission h2{font-size:1.5rem;font-weight:400;letter-spacing:-0.02em;line-height:1.5;margin-bottom:1.5rem}
|
||||
.mission p{font-family:'SF Mono','Fira Code',monospace;font-size:13px;color:var(--muted);line-height:1.7}
|
||||
.section-cta{text-align:center;padding:5rem 3rem;margin-top:-1px}
|
||||
.cta-content h2{font-size:1.75rem;font-weight:500;letter-spacing:-0.02em;margin-bottom:0.75rem}
|
||||
.cta-content p{font-family:'SF Mono','Fira Code',monospace;font-size:13px;color:var(--muted);margin-bottom:2rem}
|
||||
.cta-buttons{display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}
|
||||
.cta-primary{padding:14px 32px;background:var(--text);color:var(--bg);text-decoration:none;font-family:'SF Mono','Fira Code',monospace;font-size:14px;transition:all 0.2s}
|
||||
.cta-primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.15)}
|
||||
.cta-secondary{padding:14px 32px;background:transparent;color:var(--text);border:1px solid var(--border);font-family:'SF Mono','Fira Code',monospace;font-size:14px;cursor:pointer;transition:all 0.2s}
|
||||
.cta-secondary:hover{border-color:var(--text)}
|
||||
footer{padding:4rem 3rem;background:var(--text);color:var(--bg);margin-top:-1px}
|
||||
.footer-content{display:flex;justify-content:space-between;align-items:flex-start;gap:3rem;max-width:900px}
|
||||
.footer-brand{flex:1}
|
||||
.footer-logo{font-family:'SF Mono','Fira Code',monospace;font-weight:600;font-size:16px;margin-bottom:0.5rem}
|
||||
.footer-tagline{font-family:'SF Mono','Fira Code',monospace;font-size:12px;color:var(--muted);margin-bottom:1.5rem}
|
||||
.footer-copy{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted)}
|
||||
.footer-links{display:flex;gap:3rem}
|
||||
.footer-col h4{font-family:'SF Mono','Fira Code',monospace;font-size:11px;text-transform:uppercase;letter-spacing:0.1em;color:var(--muted);margin-bottom:1rem;font-weight:normal}
|
||||
.footer-col a{display:flex;align-items:center;gap:0.5rem;font-family:'SF Mono','Fira Code',monospace;font-size:13px;color:var(--bg);text-decoration:none;padding:0.35rem 0;transition:opacity 0.15s}
|
||||
.footer-col a:hover{opacity:0.7}
|
||||
.footer-col a svg{width:16px;height:16px}
|
||||
.demo-modal{display:none;position:fixed;inset:0;z-index:1000;align-items:center;justify-content:center}
|
||||
.demo-modal.active{display:flex}
|
||||
.demo-modal-backdrop{position:absolute;inset:0;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px)}
|
||||
.demo-modal-content{position:relative;background:white;border:1px solid var(--border);width:100%;max-width:420px;margin:1rem;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25)}
|
||||
.demo-step{display:none;padding:2rem}
|
||||
.demo-step.active{display:block;animation:fadeIn 0.3s ease}
|
||||
.demo-step-header{margin-bottom:1.5rem}
|
||||
.demo-step-indicator{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;display:block;margin-bottom:0.75rem}
|
||||
.demo-step-header h2{font-size:1.25rem;font-weight:500;margin-bottom:0.5rem}
|
||||
.demo-step-header p{font-family:'SF Mono','Fira Code',monospace;font-size:12px;color:var(--muted)}
|
||||
.demo-name-input{width:100%;padding:14px 16px;border:1px solid var(--border);font-family:inherit;font-size:15px;outline:none;transition:border-color 0.15s}
|
||||
.demo-name-input:focus{border-color:var(--text)}
|
||||
.demo-step-footer{display:flex;justify-content:flex-end;gap:0.75rem;margin-top:1.5rem}
|
||||
.demo-next,.demo-launch{padding:12px 24px;background:var(--text);color:var(--bg);border:none;font-family:'SF Mono','Fira Code',monospace;font-size:13px;cursor:pointer;transition:all 0.2s}
|
||||
.demo-next:hover:not(:disabled),.demo-launch:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.15)}
|
||||
.demo-next:disabled,.demo-launch:disabled{opacity:0.4;cursor:not-allowed}
|
||||
.demo-back{padding:12px 24px;background:transparent;color:var(--muted);border:1px solid var(--border);font-family:'SF Mono','Fira Code',monospace;font-size:13px;cursor:pointer;transition:all 0.15s}
|
||||
.demo-back:hover{border-color:var(--text);color:var(--text)}
|
||||
.demo-launch{background:linear-gradient(135deg,#10b981,#06b6d4)}
|
||||
.demo-launch:hover:not(:disabled){box-shadow:0 8px 24px rgba(16,185,129,0.3)}
|
||||
.color-picker{display:flex;gap:0.75rem;flex-wrap:wrap}
|
||||
.color-swatch{width:48px;height:48px;border:2px solid transparent;cursor:pointer;transition:all 0.15s;position:relative}
|
||||
.color-swatch:hover{transform:scale(1.1)}
|
||||
.color-swatch.selected{border-color:var(--text)}
|
||||
.color-swatch.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)}
|
||||
.launch-progress{margin-top:1.5rem;padding:1rem;background:#f5f5f5;border:1px solid var(--border)}
|
||||
.launch-progress.active{display:block;animation:fadeIn 0.3s ease}
|
||||
.progress-step{display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;font-family:'SF Mono','Fira Code',monospace;font-size:12px;color:var(--muted)}
|
||||
.progress-step.active{color:var(--text)}
|
||||
.progress-step.done{color:var(--primary)}
|
||||
.progress-dot{width:8px;height:8px;background:var(--border);transition:all 0.3s}
|
||||
.progress-step.active .progress-dot{background:var(--text);animation:pulse 1s infinite}
|
||||
.progress-step.done .progress-dot{background:var(--primary)}
|
||||
.launch-success{text-align:center;padding:2rem}
|
||||
.launch-success.active{display:block;animation:successPop 0.5s cubic-bezier(0.34,1.56,0.64,1)}
|
||||
.success-icon{width:48px;height:48px;background:linear-gradient(135deg,#10b981,#06b6d4);display:flex;align-items:center;justify-content:center;margin:0 auto 1rem}
|
||||
.success-icon svg{width:24px;height:24px;color:white}
|
||||
.success-url{font-family:'SF Mono','Fira Code',monospace;font-size:14px;font-weight:500;margin-bottom:0.5rem}
|
||||
.success-redirect{font-family:'SF Mono','Fira Code',monospace;font-size:12px;color:var(--muted)}
|
||||
.demo-modal .launch-progress{display:block;margin-top:0;padding:0;background:transparent;border:none}
|
||||
.demo-modal .launch-success{display:block;padding:1rem 0}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.3)}}
|
||||
@keyframes successPop{0%{transform:scale(0.8);opacity:0}100%{transform:scale(1);opacity:1}}
|
||||
@media(max-width:1024px){.features-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@media(max-width:900px){.layout{grid-template-columns:1fr}aside{position:relative;height:auto;border-right:none;border-bottom:1px solid var(--border);flex-direction:row;align-items:center;justify-content:space-between;padding:1rem 1.5rem}.sidebar-tagline{margin-bottom:0}.sidebar-nav,.sidebar-divider,.sidebar-label,.sidebar-footer{display:none}.hero,.section{padding:3rem 2rem}footer{padding:3rem 2rem}.footer-content{flex-direction:column}}
|
||||
@media(max-width:768px){.features-grid{grid-template-columns:1fr}}
|
||||
@media(max-width:480px){.hero,.section{padding:2rem 1.5rem}.footer-links{flex-direction:column;gap:2rem}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
<aside>
|
||||
<div>
|
||||
<div class="sidebar-logo">WriteKit</div>
|
||||
<div class="sidebar-tagline">Blogging Platform</div>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="#why">Why WriteKit</a>
|
||||
<a href="#features">Features</a>
|
||||
<a href="/signup" style="color:var(--primary)">Create Blog →</a>
|
||||
</nav>
|
||||
<div class="sidebar-divider"></div>
|
||||
<div class="sidebar-label">Resources</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/docs">Documentation</a>
|
||||
<a href="/discord">Discord</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="env-badge">ALPHA</div>
|
||||
<div class="version-badge" title="{{COMMIT}}">{{VERSION}}</div>
|
||||
</div>
|
||||
</aside>
|
||||
<main>
|
||||
<section class="hero">
|
||||
<canvas class="hero-canvas" id="dither-canvas"></canvas>
|
||||
<p class="hero-label">Blog Hosting for Developers / 2025</p>
|
||||
<h1>Your Words,<br>Your Platform</h1>
|
||||
<p class="hero-sub">Spin up a beautiful, fast blog in seconds. <code>SQLite</code>-powered, <code>markdown</code>-native, infinitely customizable.</p>
|
||||
<div class="hero-cta">
|
||||
<button class="demo-btn" id="try-demo">Try Demo</button>
|
||||
<p class="demo-note">{{DEMO_MINUTES}} minute demo. <a href="/signup" style="color:var(--primary)">Create a real blog</a> instead.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section" id="why">
|
||||
<p class="section-label">Why WriteKit</p>
|
||||
<div class="mission">
|
||||
<h2>We built WriteKit because blogging platforms got complicated.</h2>
|
||||
<p>Ghost is heavy. Hashnode is bloated. Medium doesn't care about developers. Hugo outputs static sites — great, until you want comments, logins and analytics without bolting on five services.<br><br>WriteKit is a fully featured platform for developers. Comments, reactions, search, analytics, monetization, API — everything works out of the box. Deploy in seconds, own your data forever.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section-features" id="features">
|
||||
<div class="features-grid">
|
||||
<div class="feature"><p class="feature-num">01</p><h3>Comments & Reactions</h3><p>Threaded comments and emoji reactions. No Disqus, no third-party scripts.</p></div>
|
||||
<div class="feature"><p class="feature-num">02</p><h3>Full-text Search</h3><p>SQLite FTS5 powers instant search. Fast, local, no external service.</p></div>
|
||||
<div class="feature"><p class="feature-num">03</p><h3>Privacy-first Analytics</h3><p>Views, referrers, browsers — no cookies, no tracking pixels.</p></div>
|
||||
<div class="feature"><p class="feature-num">04</p><h3>REST API</h3><p>Full programmatic access. Create posts, manage content, build integrations.</p></div>
|
||||
<div class="feature"><p class="feature-num">05</p><h3>Markdown Native</h3><p>Write how you already write. YAML frontmatter, syntax highlighting.</p></div>
|
||||
<div class="feature"><p class="feature-num">06</p><h3>Custom Domains</h3><p>Your domain or *.writekit.dev. SSL included automatically.</p></div>
|
||||
<div class="feature"><p class="feature-num">07</p><h3>Own Your Data</h3><p>Export anytime. JSON, Markdown, full backup. No lock-in ever.</p></div>
|
||||
<div class="feature"><p class="feature-num">08</p><h3>One-click Deploy</h3><p>No DevOps required. One button, your instance is live.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section section-cta" id="cta">
|
||||
<div class="cta-content">
|
||||
<h2>Ready to start writing?</h2>
|
||||
<p>Deploy your blog in seconds. No credit card required.</p>
|
||||
<div class="cta-buttons">
|
||||
<a href="/signup" class="cta-primary">Create Your Blog</a>
|
||||
<button class="cta-secondary" id="try-demo-bottom">Try Demo First</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="demo-modal" id="demo-modal">
|
||||
<div class="demo-modal-backdrop"></div>
|
||||
<div class="demo-modal-content">
|
||||
<div class="demo-step active" data-step="1">
|
||||
<div class="demo-step-header">
|
||||
<span class="demo-step-indicator">1 / 2</span>
|
||||
<h2>What's your name?</h2>
|
||||
<p>We'll use this to personalize your blog.</p>
|
||||
</div>
|
||||
<input type="text" id="demo-name" class="demo-name-input" placeholder="Your name" autofocus autocomplete="off">
|
||||
<div class="demo-step-footer">
|
||||
<button class="demo-back" id="demo-skip">Skip</button>
|
||||
<button class="demo-next" id="demo-next-1" disabled>Next →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-step" data-step="2">
|
||||
<div class="demo-step-header">
|
||||
<span class="demo-step-indicator">2 / 2</span>
|
||||
<h2>Pick a color</h2>
|
||||
<p>Choose an accent color for your blog.</p>
|
||||
</div>
|
||||
<div class="color-picker">
|
||||
<button class="color-swatch selected" data-color="#10b981" style="background:#10b981" title="Emerald"></button>
|
||||
<button class="color-swatch" data-color="#3b82f6" style="background:#3b82f6" title="Blue"></button>
|
||||
<button class="color-swatch" data-color="#8b5cf6" style="background:#8b5cf6" title="Purple"></button>
|
||||
<button class="color-swatch" data-color="#f97316" style="background:#f97316" title="Orange"></button>
|
||||
<button class="color-swatch" data-color="#ef4444" style="background:#ef4444" title="Rose"></button>
|
||||
<button class="color-swatch" data-color="#64748b" style="background:#64748b" title="Slate"></button>
|
||||
</div>
|
||||
<div class="demo-step-footer">
|
||||
<button class="demo-back" id="demo-back-2">← Back</button>
|
||||
<button class="demo-launch" id="demo-launch">Launch Demo</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-step" data-step="3">
|
||||
<div class="demo-step-header"><h2>Launching your demo...</h2></div>
|
||||
<div class="launch-progress active">
|
||||
<div class="progress-step" data-step="1"><span class="progress-dot"></span><span>Creating database...</span></div>
|
||||
<div class="progress-step" data-step="2"><span class="progress-dot"></span><span>Configuring settings...</span></div>
|
||||
<div class="progress-step" data-step="3"><span class="progress-dot"></span><span>Adding welcome post...</span></div>
|
||||
<div class="progress-step" data-step="4"><span class="progress-dot"></span><span>Ready!</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-step" data-step="4">
|
||||
<div class="launch-success active">
|
||||
<div class="success-icon"><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="M5 13l4 4L19 7"/></svg></div>
|
||||
<div class="success-url" id="success-url"></div>
|
||||
<div class="success-redirect">Redirecting you now...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<div class="footer-logo">WriteKit</div>
|
||||
<div class="footer-tagline">Your Words, Your Platform</div>
|
||||
<div class="footer-copy">© 2025 WriteKit. All rights reserved.</div>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<div class="footer-col">
|
||||
<h4>Product</h4>
|
||||
<a href="#features">Features</a>
|
||||
<a href="/signup">Create Blog</a>
|
||||
<a href="/docs">Documentation</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>Community</h4>
|
||||
<a href="/discord"><svg viewBox="0 0 24 24" fill="currentColor"><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>Discord</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
<script>
|
||||
const $ = s => document.querySelector(s)
|
||||
const $$ = s => document.querySelectorAll(s)
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
|
||||
const modal = $('#demo-modal')
|
||||
const backdrop = modal.querySelector('.demo-modal-backdrop')
|
||||
const nameInput = $('#demo-name')
|
||||
const nextBtn = $('#demo-next-1')
|
||||
const backBtn = $('#demo-back-2')
|
||||
const launchBtn = $('#demo-launch')
|
||||
const successUrl = $('#success-url')
|
||||
const colorSwatches = $$('.color-swatch')
|
||||
|
||||
let demoName = ''
|
||||
let demoColor = '#10b981'
|
||||
|
||||
const openDemoModal = () => {
|
||||
modal.classList.add('active')
|
||||
setTimeout(() => nameInput.focus(), 100)
|
||||
}
|
||||
|
||||
const resetModal = () => {
|
||||
nameInput.value = ''
|
||||
demoName = ''
|
||||
nextBtn.disabled = true
|
||||
goToModalStep(1)
|
||||
colorSwatches.forEach(s => s.classList.remove('selected'))
|
||||
colorSwatches[0].classList.add('selected')
|
||||
demoColor = '#10b981'
|
||||
}
|
||||
|
||||
const goToModalStep = step => {
|
||||
modal.querySelectorAll('.demo-step').forEach(el => el.classList.remove('active'))
|
||||
modal.querySelector(`.demo-step[data-step="${step}"]`).classList.add('active')
|
||||
if (step === 1) setTimeout(() => nameInput.focus(), 100)
|
||||
}
|
||||
|
||||
const setProgressStep = n => {
|
||||
modal.querySelectorAll('.progress-step').forEach(el => {
|
||||
const step = parseInt(el.dataset.step)
|
||||
el.classList.remove('active', 'done')
|
||||
if (step < n) el.classList.add('done')
|
||||
if (step === n) el.classList.add('active')
|
||||
})
|
||||
}
|
||||
|
||||
const launchDemo = async () => {
|
||||
if (!demoName) return
|
||||
launchBtn.disabled = true
|
||||
launchBtn.textContent = 'Launching...'
|
||||
goToModalStep(3)
|
||||
setProgressStep(1)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/demo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: demoName, color: demoColor })
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
if (res.ok && data.url) {
|
||||
setProgressStep(2)
|
||||
await sleep(150)
|
||||
setProgressStep(3)
|
||||
await sleep(150)
|
||||
setProgressStep(4)
|
||||
await sleep(150)
|
||||
goToModalStep(4)
|
||||
successUrl.textContent = data.url
|
||||
await sleep(300)
|
||||
location.href = data.url
|
||||
} else {
|
||||
goToModalStep(2)
|
||||
launchBtn.disabled = false
|
||||
launchBtn.textContent = 'Launch Demo'
|
||||
alert(data.error || 'Failed to create demo')
|
||||
}
|
||||
} catch {
|
||||
goToModalStep(2)
|
||||
launchBtn.disabled = false
|
||||
launchBtn.textContent = 'Launch Demo'
|
||||
alert('Error creating demo')
|
||||
}
|
||||
}
|
||||
|
||||
$('#try-demo').addEventListener('click', openDemoModal)
|
||||
$('#try-demo-bottom').addEventListener('click', openDemoModal)
|
||||
|
||||
backdrop.addEventListener('click', () => {
|
||||
if (!launchBtn.disabled || modal.querySelector('.demo-step[data-step="1"].active') || modal.querySelector('.demo-step[data-step="2"].active')) {
|
||||
modal.classList.remove('active')
|
||||
resetModal()
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && modal.classList.contains('active')) {
|
||||
if (modal.querySelector('.demo-step[data-step="1"].active') || modal.querySelector('.demo-step[data-step="2"].active')) {
|
||||
modal.classList.remove('active')
|
||||
resetModal()
|
||||
}
|
||||
}
|
||||
if (e.key === 'Enter' && modal.classList.contains('active')) {
|
||||
if (modal.querySelector('.demo-step[data-step="1"].active') && !nextBtn.disabled) goToModalStep(2)
|
||||
else if (modal.querySelector('.demo-step[data-step="2"].active')) launchDemo()
|
||||
}
|
||||
})
|
||||
|
||||
nameInput.addEventListener('input', () => {
|
||||
demoName = nameInput.value.trim()
|
||||
nextBtn.disabled = !demoName.length
|
||||
})
|
||||
|
||||
nextBtn.addEventListener('click', () => goToModalStep(2))
|
||||
backBtn.addEventListener('click', () => goToModalStep(1))
|
||||
launchBtn.addEventListener('click', launchDemo)
|
||||
|
||||
$('#demo-skip').addEventListener('click', () => {
|
||||
demoName = 'Demo User'
|
||||
launchDemo()
|
||||
})
|
||||
|
||||
colorSwatches.forEach(swatch => {
|
||||
swatch.addEventListener('click', () => {
|
||||
colorSwatches.forEach(s => s.classList.remove('selected'))
|
||||
swatch.classList.add('selected')
|
||||
demoColor = swatch.dataset.color
|
||||
})
|
||||
})
|
||||
|
||||
$$('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
document.querySelector(anchor.getAttribute('href'))?.scrollIntoView({ behavior: 'smooth' })
|
||||
})
|
||||
})
|
||||
|
||||
;(() => {
|
||||
const settings = { pixelSize: 8, gridSize: 4, speed: 0.05, colorShift: 0.6, colors: { bright: [0.063, 0.725, 0.506], mid: [0.047, 0.545, 0.38], dark: [0.031, 0.363, 0.253], bg: [0.2, 0.2, 0.2] } }
|
||||
const canvas = $('#dither-canvas')
|
||||
if (!canvas) return
|
||||
const gl = canvas.getContext('webgl2')
|
||||
if (!gl) return
|
||||
|
||||
const vs = `#version 300 es
|
||||
in vec2 a_position;
|
||||
out vec2 v_uv;
|
||||
void main() { v_uv = a_position * 0.5 + 0.5; gl_Position = vec4(a_position, 0.0, 1.0); }`
|
||||
|
||||
const fs = `#version 300 es
|
||||
precision highp float;
|
||||
in vec2 v_uv;
|
||||
out vec4 fragColor;
|
||||
uniform vec2 u_resolution;
|
||||
uniform float u_time, u_pixelSize, u_gridSize, u_speed, u_colorShift;
|
||||
uniform vec3 u_color1, u_color2, u_color3, u_bgColor;
|
||||
float bayer8(vec2 p) { ivec2 P = ivec2(mod(floor(p), 8.0)); int i = P.x + P.y * 8; int b[64] = int[64](0,32,8,40,2,34,10,42,48,16,56,24,50,18,58,26,12,44,4,36,14,46,6,38,60,28,52,20,62,30,54,22,3,35,11,43,1,33,9,41,51,19,59,27,49,17,57,25,15,47,7,39,13,45,5,37,63,31,55,23,61,29,53,21); return float(b[i]) / 64.0; }
|
||||
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
||||
float noise(vec2 p) { vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f); return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x), f.y); }
|
||||
float fbm(vec2 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 5; i++) { v += a * noise(p); p *= 2.0; a *= 0.5; } return v; }
|
||||
void main() {
|
||||
vec2 pixelUV = floor(v_uv * u_resolution / u_pixelSize) * u_pixelSize / u_resolution;
|
||||
vec2 p = pixelUV * 3.0; float t = u_time * u_speed;
|
||||
float pattern = fbm(p + vec2(t * 0.5, t * 0.3)) * 0.5 + fbm(p * 1.5 - vec2(t * 0.4, -t * 0.2)) * 0.3 + fbm(p * 0.5 + vec2(-t * 0.3, t * 0.5)) * 0.2 + 0.1 * sin(p.x * 2.0 + t) * sin(p.y * 2.0 - t * 0.7);
|
||||
float luma = clamp(smoothstep(0.1, 0.9, pow(pattern, 0.7)) + u_colorShift * sin(u_time * 0.3) * 0.3, 0.0, 1.0);
|
||||
float threshold = bayer8(floor(v_uv * u_resolution / u_gridSize));
|
||||
float level = luma * 3.0; int band = int(floor(level)); float frac = fract(level);
|
||||
vec3 result = band >= 2 ? (frac > threshold ? u_color1 : u_color2) : band == 1 ? (frac > threshold ? u_color2 : u_color3) : (frac > threshold ? u_color3 : u_bgColor);
|
||||
fragColor = vec4(result, 1.0);
|
||||
}`
|
||||
|
||||
const createShader = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); return gl.getShaderParameter(s, gl.COMPILE_STATUS) ? s : null }
|
||||
const vertexShader = createShader(gl.VERTEX_SHADER, vs)
|
||||
const fragmentShader = createShader(gl.FRAGMENT_SHADER, fs)
|
||||
const program = gl.createProgram()
|
||||
gl.attachShader(program, vertexShader)
|
||||
gl.attachShader(program, fragmentShader)
|
||||
gl.linkProgram(program)
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return
|
||||
|
||||
const vao = gl.createVertexArray()
|
||||
gl.bindVertexArray(vao)
|
||||
const buf = gl.createBuffer()
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf)
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW)
|
||||
const posLoc = gl.getAttribLocation(program, 'a_position')
|
||||
gl.enableVertexAttribArray(posLoc)
|
||||
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0)
|
||||
|
||||
const u = { resolution: gl.getUniformLocation(program, 'u_resolution'), time: gl.getUniformLocation(program, 'u_time'), pixelSize: gl.getUniformLocation(program, 'u_pixelSize'), gridSize: gl.getUniformLocation(program, 'u_gridSize'), speed: gl.getUniformLocation(program, 'u_speed'), colorShift: gl.getUniformLocation(program, 'u_colorShift'), color1: gl.getUniformLocation(program, 'u_color1'), color2: gl.getUniformLocation(program, 'u_color2'), color3: gl.getUniformLocation(program, 'u_color3'), bgColor: gl.getUniformLocation(program, 'u_bgColor') }
|
||||
|
||||
gl.useProgram(program)
|
||||
gl.uniform3fv(u.color1, settings.colors.bright)
|
||||
gl.uniform3fv(u.color2, settings.colors.mid)
|
||||
gl.uniform3fv(u.color3, settings.colors.dark)
|
||||
gl.uniform3fv(u.bgColor, settings.colors.bg)
|
||||
|
||||
const resize = () => { const dpr = Math.min(devicePixelRatio, 2); canvas.width = canvas.offsetWidth * dpr; canvas.height = canvas.offsetHeight * dpr }
|
||||
resize()
|
||||
addEventListener('resize', resize)
|
||||
|
||||
const render = time => {
|
||||
time *= 0.001
|
||||
gl.viewport(0, 0, canvas.width, canvas.height)
|
||||
gl.useProgram(program)
|
||||
gl.bindVertexArray(vao)
|
||||
gl.uniform2f(u.resolution, canvas.width, canvas.height)
|
||||
gl.uniform1f(u.time, time)
|
||||
gl.uniform1f(u.pixelSize, settings.pixelSize)
|
||||
gl.uniform1f(u.gridSize, settings.gridSize)
|
||||
gl.uniform1f(u.speed, settings.speed)
|
||||
gl.uniform1f(u.colorShift, settings.colorShift)
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
requestAnimationFrame(render)
|
||||
}
|
||||
requestAnimationFrame(render)
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
472
internal/server/templates/signup.html
Normal file
472
internal/server/templates/signup.html
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
<!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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue