Restore to commit 74e578279624c6045ca440a3459ebfa1f8d54191
This commit is contained in:
442
chat/public/login.html
Normal file
442
chat/public/login.html
Normal file
@@ -0,0 +1,442 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sign In | 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="/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">Welcome back</h1>
|
||||
<p class="text-gray-600">Enter your details to access your account.</p>
|
||||
</div>
|
||||
|
||||
<form id="login-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>
|
||||
<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>
|
||||
<div class="flex justify-between mb-1">
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<a href="/reset-password" class="text-xs text-green-700 hover:underline">Forgot
|
||||
password?</a>
|
||||
</div>
|
||||
<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="remember" name="remember" type="checkbox"
|
||||
class="h-4 w-4 text-green-700 focus:ring-green-600 border-gray-300 rounded">
|
||||
<label for="remember" class="ml-2 block text-sm text-gray-600">
|
||||
Remember me for 30 days
|
||||
</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">
|
||||
Sign In
|
||||
</button>
|
||||
|
||||
<div id="login-status" class="text-sm text-red-600 mt-2 min-h-[1.25rem]"></div>
|
||||
</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 continue 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('login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const statusEl = document.getElementById('login-status');
|
||||
function setStatus(msg, isError = true) {
|
||||
if (!statusEl) return;
|
||||
statusEl.textContent = msg || '';
|
||||
statusEl.style.color = isError ? 'rgb(185 28 28)' : '';
|
||||
}
|
||||
const emailEl = document.getElementById('email');
|
||||
const email = (emailEl && emailEl.value || '').trim().toLowerCase();
|
||||
if (!email) {
|
||||
if (emailEl) emailEl.focus();
|
||||
return;
|
||||
}
|
||||
const passwordEl = document.getElementById('password');
|
||||
const password = (passwordEl && passwordEl.value) || '';
|
||||
|
||||
const remember = document.getElementById('remember').checked;
|
||||
|
||||
// If a password was provided, try server-side authentication first.
|
||||
if (password) {
|
||||
try {
|
||||
const resp = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password, remember }),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) {
|
||||
setStatus(data.error || 'Login failed', true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Server authentication successful
|
||||
if (data.ok && data.user && data.token) {
|
||||
// Store session information
|
||||
try {
|
||||
localStorage.setItem('wordpress_plugin_ai_user', JSON.stringify({
|
||||
email: data.user.email,
|
||||
accountId: data.user.id,
|
||||
sessionToken: data.token
|
||||
}));
|
||||
} catch (_) { }
|
||||
|
||||
// Continue with account claiming
|
||||
const deviceUserId = getDeviceUserId();
|
||||
await claimDeviceApps(data.user.id, deviceUserId);
|
||||
|
||||
// Redirect to appropriate page based on plan selection
|
||||
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
|
||||
// If data includes plan info, use it; otherwise fetch from /api/me
|
||||
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('Login request failed, please try again', true);
|
||||
console.warn('Login request failed:', String(err));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to old behavior if no password or server auth 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