Restore to commit 74e578279624c6045ca440a3459ebfa1f8d54191
This commit is contained in:
502
chat/public/signup.html
Normal file
502
chat/public/signup.html
Normal file
@@ -0,0 +1,502 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sign Up | Plugin Compass</title>
|
||||
<link rel="icon" type="image/png" href="/assets/Plugin.png">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
brand: {
|
||||
50: '#eef2ff',
|
||||
100: '#e0e7ff',
|
||||
200: '#c7d2fe',
|
||||
300: '#a5b4fc',
|
||||
400: '#818cf8',
|
||||
500: '#6366f1',
|
||||
600: '#4f46e5',
|
||||
700: '#4338ca',
|
||||
800: '#3730a3',
|
||||
900: '#312e81',
|
||||
950: '#1e1b4b',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #fdf6ed;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
.glass-nav {
|
||||
background: rgba(251, 246, 239, 0.9);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(0, 66, 37, 0.1);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(0, 66, 37, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- PostHog Analytics -->
|
||||
<script src="/posthog.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-amber-50 text-gray-900 font-sans antialiased min-h-screen flex flex-col">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="fixed w-full z-50 glass-nav transition-all duration-300" id="navbar">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-20">
|
||||
<!-- Logo -->
|
||||
<a href="/" class="flex-shrink-0 flex items-center gap-2 cursor-pointer">
|
||||
<img src="/assets/Plugin.png" alt="Plugin Compass" class="w-8 h-8">
|
||||
<span class="font-bold text-xl tracking-tight text-gray-800">Plugin<span
|
||||
class="text-green-700">Compass</span></span>
|
||||
</a>
|
||||
|
||||
<!-- Desktop Menu -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="/features"
|
||||
class="text-gray-700 hover:text-gray-900 transition-colors text-sm font-medium">Features</a>
|
||||
<a href="/pricing"
|
||||
class="text-gray-700 hover:text-gray-900 transition-colors text-sm font-medium">Pricing</a>
|
||||
<a href="/docs"
|
||||
class="text-gray-700 hover:text-gray-900 transition-colors text-sm font-medium">Docs</a>
|
||||
</div>
|
||||
|
||||
<!-- CTA Buttons -->
|
||||
<div class="hidden md:flex items-center gap-4">
|
||||
<a href="/login"
|
||||
class="text-gray-700 hover:text-gray-900 font-medium text-sm transition-colors">Sign In</a>
|
||||
<a href="/signup"
|
||||
class="bg-green-700 hover:bg-green-600 text-white px-5 py-2.5 rounded-full font-medium text-sm transition-all shadow-lg shadow-green-700/20 hover:shadow-green-700/40 transform hover:-translate-y-0.5">
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-btn" class="text-gray-700 hover:text-gray-900 focus:outline-none">
|
||||
<i class="fa-solid fa-bars text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Panel -->
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-amber-50 border-b border-amber-200">
|
||||
<div class="px-4 pt-2 pb-6 space-y-1">
|
||||
<a href="/features"
|
||||
class="block px-3 py-3 text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-amber-100 rounded-md">Features</a>
|
||||
<a href="/pricing"
|
||||
class="block px-3 py-3 text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-amber-100 rounded-md">Pricing</a>
|
||||
<a href="/docs"
|
||||
class="block px-3 py-3 text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-amber-100 rounded-md">Docs</a>
|
||||
<div class="pt-4 flex flex-col gap-3">
|
||||
<a href="/login" class="w-full text-center py-2 text-gray-700 font-medium">Sign In</a>
|
||||
<a href="/signup" class="w-full bg-green-700 text-white text-center py-3 rounded-lg font-medium">Get
|
||||
Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="flex-grow flex items-center justify-center px-4 pt-32 pb-12">
|
||||
<div class="max-w-md w-full">
|
||||
<div class="glass-panel p-8 rounded-3xl shadow-2xl shadow-green-900/10">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Create an account</h1>
|
||||
<p class="text-gray-600">Start building your WordPress plugin today.</p>
|
||||
</div>
|
||||
|
||||
<form id="signup-form" class="space-y-6">
|
||||
<!-- Honeypot field - hidden from real users -->
|
||||
<input type="text" name="website" id="website" tabindex="-1" autocomplete="off"
|
||||
style="position: absolute; left: -9999px;">
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="first-name" class="block text-sm font-medium text-gray-700 mb-1">First
|
||||
Name</label>
|
||||
<input type="text" id="first-name"
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-green-700/20 focus:border-green-700 outline-none transition-all bg-white/50"
|
||||
placeholder="John">
|
||||
</div>
|
||||
<div>
|
||||
<label for="last-name" class="block text-sm font-medium text-gray-700 mb-1">Last
|
||||
Name</label>
|
||||
<input type="text" id="last-name"
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-green-700/20 focus:border-green-700 outline-none transition-all bg-white/50"
|
||||
placeholder="Doe">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email Address</label>
|
||||
<input type="email" id="email"
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-green-700/20 focus:border-green-700 outline-none transition-all bg-white/50"
|
||||
placeholder="john@example.com">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Password</label>
|
||||
<input type="password" id="password"
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-green-700/20 focus:border-green-700 outline-none transition-all bg-white/50"
|
||||
placeholder="••••••••">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input id="terms" name="terms" type="checkbox"
|
||||
class="h-4 w-4 text-green-700 focus:ring-green-600 border-gray-300 rounded">
|
||||
<label for="terms" class="ml-2 block text-sm text-gray-600">
|
||||
I agree to the <a href="/terms" class="text-green-700 hover:underline">Terms of Service</a>
|
||||
and <a href="/privacy" class="text-green-700 hover:underline">Privacy Policy</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="w-full bg-green-700 hover:bg-green-600 text-white font-bold py-4 rounded-xl transition-all shadow-lg shadow-green-700/20 transform hover:-translate-y-0.5">
|
||||
Create Account
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-8">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-gray-200"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-amber-50 text-gray-500">Or sign up with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-2 gap-4">
|
||||
<button type="button" data-oauth-provider="google"
|
||||
class="flex items-center justify-center gap-2 px-4 py-3 border border-gray-200 rounded-xl hover:bg-gray-50 transition-all bg-white/50">
|
||||
<i class="fa-brands fa-google text-red-500"></i>
|
||||
<span class="text-sm font-medium text-gray-700">Google</span>
|
||||
</button>
|
||||
<button type="button" data-oauth-provider="github"
|
||||
class="flex items-center justify-center gap-2 px-4 py-3 border border-gray-200 rounded-xl hover:bg-gray-50 transition-all bg-white/50">
|
||||
<i class="fa-brands fa-github text-gray-900"></i>
|
||||
<span class="text-sm font-medium text-gray-700">GitHub</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-white border-t border-green-200 pt-16 pb-8">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-12 mb-16">
|
||||
<div class="col-span-2 md:col-span-1">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<img src="/assets/Plugin.png" alt="Plugin Compass" class="w-8 h-8">
|
||||
<span class="font-bold text-xl tracking-tight text-gray-800">Plugin<span
|
||||
class="text-green-700">Compass</span></span>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm leading-relaxed">
|
||||
The smart way for WordPress site owners to replace expensive plugin subscriptions with custom
|
||||
solutions. Save thousands monthly.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-bold text-gray-900 mb-6">Product</h4>
|
||||
<ul class="space-y-4 text-sm">
|
||||
<li><a href="/features" class="text-gray-600 hover:text-green-700">Features</a></li>
|
||||
<li><a href="/pricing" class="text-gray-600 hover:text-green-700">Pricing</a></li>
|
||||
<li><a href="#" class="text-gray-600 hover:text-green-700">Templates</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-bold text-gray-900 mb-6">Resources</h4>
|
||||
<ul class="space-y-4 text-sm">
|
||||
<li><a href="/docs" class="text-gray-600 hover:text-green-700">Documentation</a></li>
|
||||
<li><a href="/faq" class="text-gray-600 hover:text-green-700">FAQ</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-bold text-gray-900 mb-6">Legal</h4>
|
||||
<ul class="space-y-4 text-sm">
|
||||
<li><a href="/privacy" class="text-gray-600 hover:text-green-700">Privacy Policy</a></li>
|
||||
<li><a href="/terms" class="text-gray-600 hover:text-green-700">Terms of Service</a></li>
|
||||
<li><a href="/contact" class="text-gray-600 hover:text-green-700">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-100 pt-8 flex justify-center">
|
||||
<p class="text-gray-500 text-xs text-center">© 2026 Plugin Compass. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<script>
|
||||
// Navigation functionality
|
||||
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
if (mobileMenuBtn && mobileMenu) {
|
||||
mobileMenuBtn.addEventListener('click', () => {
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// Navbar scroll effect
|
||||
window.addEventListener('scroll', () => {
|
||||
const navbar = document.getElementById('navbar');
|
||||
if (!navbar) return;
|
||||
if (window.scrollY > 20) {
|
||||
navbar.classList.add('shadow-md', 'h-16');
|
||||
navbar.classList.remove('h-20');
|
||||
} else {
|
||||
navbar.classList.remove('shadow-md', 'h-16');
|
||||
navbar.classList.add('h-20');
|
||||
}
|
||||
});
|
||||
function cyrb53(str, seed = 0) {
|
||||
let h1 = 0xdeadbeef ^ seed;
|
||||
let h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
}
|
||||
|
||||
function computeAccountId(email) {
|
||||
const normalized = (email || '').trim().toLowerCase();
|
||||
if (!normalized) return '';
|
||||
const hash = cyrb53(normalized);
|
||||
return `acct-${hash.toString(16)}`;
|
||||
}
|
||||
|
||||
function getDeviceUserId() {
|
||||
try {
|
||||
const existing = localStorage.getItem('wordpress_plugin_ai_user_id');
|
||||
if (existing) return existing;
|
||||
const uuidPart = crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 12);
|
||||
const generated = `user-${uuidPart}`;
|
||||
localStorage.setItem('wordpress_plugin_ai_user_id', generated);
|
||||
return generated;
|
||||
} catch (_) {
|
||||
const fallbackPart = (typeof crypto !== 'undefined' && crypto.randomUUID)
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
||||
return `user-${fallbackPart}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getNextPath() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const next = params.get('next');
|
||||
if (next && next.startsWith('/') && !next.startsWith('//')) return next;
|
||||
return '/apps';
|
||||
}
|
||||
|
||||
function startOAuth(provider) {
|
||||
if (!provider) return;
|
||||
const next = encodeURIComponent(getNextPath());
|
||||
window.location.href = `/auth/${provider}?next=${next}`;
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-oauth-provider]').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const provider = btn.getAttribute('data-oauth-provider');
|
||||
startOAuth(provider);
|
||||
});
|
||||
});
|
||||
|
||||
async function claimDeviceApps(accountId, previousUserId) {
|
||||
try {
|
||||
await fetch('/api/account/claim', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-User-Id': accountId,
|
||||
},
|
||||
body: JSON.stringify({ previousUserId })
|
||||
});
|
||||
} catch (_) {
|
||||
// Best-effort; app data is not deleted.
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('signup-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const statusEl = document.createElement('div');
|
||||
statusEl.id = 'signup-status';
|
||||
statusEl.className = 'text-sm text-red-600 mt-2 min-h-[1.25rem]';
|
||||
|
||||
const form = document.getElementById('signup-form');
|
||||
const existingStatus = document.getElementById('signup-status');
|
||||
if (existingStatus) {
|
||||
existingStatus.remove();
|
||||
}
|
||||
form.appendChild(statusEl);
|
||||
|
||||
function setStatus(msg, isError = true) {
|
||||
statusEl.textContent = msg || '';
|
||||
statusEl.style.color = isError ? 'rgb(185 28 28)' : 'rgb(34 197 94)';
|
||||
}
|
||||
|
||||
const emailEl = document.getElementById('email');
|
||||
const email = (emailEl && emailEl.value || '').trim().toLowerCase();
|
||||
const passwordEl = document.getElementById('password');
|
||||
const password = (passwordEl && passwordEl.value) || '';
|
||||
const termsEl = document.getElementById('terms');
|
||||
|
||||
if (!email) {
|
||||
if (emailEl) emailEl.focus();
|
||||
setStatus('Email address is required', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
if (passwordEl) passwordEl.focus();
|
||||
setStatus('Password is required', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordRequirements = {
|
||||
hasMinLength: password.length >= 8,
|
||||
hasUppercase: /[A-Z]/.test(password),
|
||||
hasLowercase: /[a-z]/.test(password),
|
||||
hasNumber: /[0-9]/.test(password),
|
||||
hasSpecial: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)
|
||||
};
|
||||
|
||||
const allRequirementsMet = Object.values(passwordRequirements).every(req => req);
|
||||
|
||||
if (!allRequirementsMet) {
|
||||
const missingRequirements = [];
|
||||
if (!passwordRequirements.hasMinLength) missingRequirements.push('at least 8 characters');
|
||||
if (!passwordRequirements.hasUppercase) missingRequirements.push('an uppercase letter');
|
||||
if (!passwordRequirements.hasLowercase) missingRequirements.push('a lowercase letter');
|
||||
if (!passwordRequirements.hasNumber) missingRequirements.push('a number');
|
||||
if (!passwordRequirements.hasSpecial) missingRequirements.push('a special character (!@#$%^&*...)');
|
||||
|
||||
const requirementText = missingRequirements.join(', ');
|
||||
if (passwordEl) passwordEl.focus();
|
||||
setStatus(`Password must include: ${requirementText}`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!termsEl || !termsEl.checked) {
|
||||
setStatus('You must agree to the Terms of Service and Privacy Policy', true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try server-side registration first
|
||||
const resp = await fetch('/api/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
|
||||
if (!resp.ok) {
|
||||
setStatus(data.error || 'Registration failed', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.ok && data.user) {
|
||||
if (data.verificationRequired) {
|
||||
window.location.href = '/signup-success';
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.token) {
|
||||
try {
|
||||
localStorage.setItem('wordpress_plugin_ai_user', JSON.stringify({
|
||||
email: data.user.email,
|
||||
accountId: data.user.id,
|
||||
sessionToken: data.token
|
||||
}));
|
||||
} catch (_) { }
|
||||
|
||||
const deviceUserId = getDeviceUserId();
|
||||
await claimDeviceApps(data.user.id, deviceUserId);
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
let next = params.get('next') || '/apps';
|
||||
if (!next || !next.startsWith('/') || next.startsWith('//')) next = '/apps';
|
||||
|
||||
// Check if user has selected a plan
|
||||
const hasPlan = data.user.plan && ['hobby', 'starter', 'business', 'enterprise'].includes(data.user.plan.toLowerCase());
|
||||
if (!hasPlan && next === '/apps') {
|
||||
next = '/select-plan';
|
||||
}
|
||||
|
||||
window.location.href = next;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus('Registration request failed, please try again', true);
|
||||
console.warn('Registration request failed:', String(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to old behavior if server registration not available
|
||||
const deviceUserId = getDeviceUserId();
|
||||
const accountId = computeAccountId(email);
|
||||
try {
|
||||
localStorage.setItem('wordpress_plugin_ai_user', JSON.stringify({ email, accountId }));
|
||||
} catch (_) { }
|
||||
try {
|
||||
document.cookie = `chat_user=${encodeURIComponent(accountId)}; path=/; SameSite=Lax`;
|
||||
} catch (_) { }
|
||||
|
||||
await claimDeviceApps(accountId, deviceUserId);
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
let next = params.get('next') || '/apps';
|
||||
if (!next || !next.startsWith('/') || next.startsWith('//')) next = '/apps';
|
||||
window.location.href = next;
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user