<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Clemelopy GEO Linking Map</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
<style>
:root{
--geo-bg:#ffffff; --geo-text:#1f2937; --geo-muted:#6b7280; --geo-border:#e5e7eb;
--geo-line:#eceff3; --geo-teal:#00A99D; --geo-orange:#FAA819;
--geo-shadow:0 8px 30px rgba(0,0,0,.06); --geo-radius:16px; --geo-focus:0 0 0 3px rgba(0,169,157,.25);
}
body{
margin:0;
padding:40px 0 60px;
background:#f5f5f7;
font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;
color:var(--geo-text);
}
.geo-wrap{max-width:880px;margin:0 auto;padding:0 24px;}
.geo-card{
background:var(--geo-bg);
border:1px solid var(--geo-border);
border-radius:var(--geo-radius);
box-shadow:var(--geo-shadow);
padding:28px;
margin-bottom:24px;
}
.geo-header{
border-bottom:1px solid var(--geo-line);
padding-bottom:14px;
margin-bottom:16px;
}
.geo-header h1{
margin:0 0 6px;
font-size:26px;
font-weight:700;
letter-spacing:.2px;
}
.geo-sub{
margin:0;
font-size:14px;
color:var(--geo-muted);
}
textarea{
width:100%;
min-height:180px;
resize:vertical;
border:1px solid var(--geo-border);
border-radius:12px;
padding:12px 14px;
font-size:15px;
line-height:1.6;
background:#fff;
transition:all .15s ease;
}
textarea:focus{
outline:none;
border-color:var(--geo-teal);
box-shadow:var(--geo-focus);
}
.geo-btn-wrap{display:flex;justify-content:center;margin-top:14px;}
.geo-btn{
display:inline-flex;
align-items:center;
justify-content:center;
background:var(--geo-teal);
color:#fff;
border:none;
cursor:pointer;
border-radius:999px;
padding:12px 26px;
min-width:200px;
font-size:15px;
font-weight:600;
letter-spacing:.3px;
transition:all .2s ease;
box-shadow:0 3px 12px rgba(0,169,157,.25);
}
.geo-btn:hover{background:#008c83;transform:translateY(-2px);}
.geo-btn:disabled{opacity:.7;cursor:not-allowed;transform:none;}
#status{
margin-top:10px;
color:#777;
font-size:.9em;
text-align:center;
min-height:1em;
}
.geo-project-card{
display:flex;
justify-content:space-between;
align-items:center;
padding:10px 0;
border-bottom:1px solid var(--geo-line);
}
.geo-project-title{font-weight:600;font-size:14px;}
.geo-project-date{font-size:12px;color:#9ca3af;}
.geo-project-link{
font-size:13px;
color:#00A99D;
text-decoration:none;
font-weight:600;
}
.geo-project-link:hover{text-decoration:underline;}
.billing-portal-wrap{margin-top:12px;}
.billing-portal-btn{
background-color:#FAA81A;
color:#ffffff;
font-weight:600;
font-size:13px;
padding:10px 22px;
border:none;
border-radius:999px;
cursor:pointer;
transition:all .25s ease;
box-shadow:0 3px 12px rgba(250,168,26,0.25);
}
.billing-portal-btn:hover{
background-color:#e69a15;
transform:translateY(-2px);
}
.billing-portal-btn:disabled{
opacity:.7;
cursor:not-allowed;
box-shadow:none;
transform:none;
}
.auth-email, .auth-password{
width:100%;
padding:9px 10px;
margin-bottom:6px;
border-radius:8px;
border:1px solid var(--geo-border);
font-size:14px;
}
.auth-email:focus,
.auth-password:focus{
outline:none;
border-color:var(--geo-teal);
box-shadow:var(--geo-focus);
}
.auth-label{font-size:12px;color:#6b7280;margin-bottom:2px;}
#authStatus{margin-top:6px;font-size:12px;color:#6b7280;min-height:1em;}
.tagline{font-size:12px;color:#9ca3af;margin:0 0 6px;}
</style>
</head>
<body>
<div class="geo-wrap">
<!-- Auth -->
<div class="geo-card" id="authCard">
<p class="tagline">Clemelopy GEO Suite</p>
<h3 style="margin:0 0 8px;">Sign in to generate your GEO Linking Map</h3>
<div class="auth-label">Email</div>
<input id="authEmail" class="auth-email" placeholder="you@example.com" />
<div class="auth-label">Password</div>
<input id="authPassword" class="auth-password" type="password" placeholder="Create or use an existing password" />
<button id="loginBtn" class="geo-btn" style="min-width:170px;margin-top:4px;">Sign in / Create account</button>
<div id="authStatus"></div>
</div>
<!-- Generator -->
<div class="geo-card" id="generatorCard" style="display:none;">
<header class="geo-header">
<h1>Generate your GEO Linking Map</h1>
<h3 class="geo-sub">
Paste your full content below and queue the confetti! Once generated, you’ll receive a downloadable, GEO-optimized PDF.
</h3>
</header>
<textarea id="content" placeholder="Paste your full content here"></textarea>
<div class="geo-btn-wrap">
<button id="generateBtn" class="geo-btn">Generate my map</button>
</div>
<p id="status"></p>
</div>
<!-- My Projects -->
<div class="geo-card" id="projectsCard" style="display:none;">
<h3 style="margin:0 0 10px;">My GEO Maps</h3>
<div id="geoProjects"></div>
<div class="billing-portal-wrap">
<button id="manageSubscriptionBtn" class="billing-portal-btn">
Manage my subscription
</button>
</div>
</div>
</div>
<script>
// ===== CONFIG =====
const SUPABASE_URL = "YOUR_SUPABASE_URL";
const SUPABASE_ANON_KEY = "YOUR_SUPABASE_ANON_KEY";
const WORKER_ORIGIN = "YOUR_WORKER_URL"; // e.g. https://linking-strategy-map.jen-86f.workers.dev
const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// ===== AUTH UI =====
async function refreshUI() {
const { data } = await supabase.auth.getSession();
const session = data.session;
const authed = !!session;
document.getElementById("authCard").style.display = authed ? "none" : "block";
document.getElementById("generatorCard").style.display = authed ? "block" : "none";
document.getElementById("projectsCard").style.display = authed ? "block" : "none";
if (authed) {
loadProjects();
}
}
document.getElementById("loginBtn").onclick = async () => {
const email = document.getElementById("authEmail").value.trim();
const password = document.getElementById("authPassword").value.trim();
const status = document.getElementById("authStatus");
status.textContent = "";
if (!email || !password) {
status.textContent = "Enter email and password.";
return;
}
// Try sign-in, if invalid then sign-up
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
if (error && /Invalid login/i.test(error.message)) {
const { error: signUpError } = await supabase.auth.signUp({ email, password });
status.textContent = signUpError
? signUpError.message
: "Check your email to confirm your account, then sign in.";
return;
}
if (error) {
status.textContent = error.message;
return;
}
status.textContent = "";
await refreshUI();
};
async function getAccessToken() {
const { data } = await supabase.auth.getSession();
return data.session?.access_token || null;
}
// ===== GENERATE =====
document.getElementById("generateBtn").onclick = async () => {
const content = document.getElementById("content").value.trim();
const status = document.getElementById("status");
const btn = document.getElementById("generateBtn");
status.textContent = "";
if (content.length < 300) {
status.textContent = "Please paste at least 300 characters of content.";
return;
}
const token = await getAccessToken();
if (!token) {
status.textContent = "Please log in again.";
return;
}
btn.disabled = true;
btn.textContent = "Generating...";
try {
const res = await fetch(WORKER_ORIGIN + "/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
},
body: JSON.stringify({ content })
});
const data = await res.json().catch(() => ({}));
if (res.status === 402 && (data.code === "PAYWALL" || /paywall/i.test(data.error || ""))) {
status.textContent = data.error || "You’ve hit your limit. Please upgrade to continue.";
} else if (!res.ok) {
status.textContent = data.error || `Server error (${res.status})`;
} else if (data.pdf_url) {
window.open(data.pdf_url, "_blank", "noopener");
status.textContent = "Your GEO Linking Map is ready.";
document.getElementById("content").value = "";
loadProjects();
} else {
status.textContent = "Completed, but no PDF URL returned.";
}
} catch (e) {
console.error(e);
status.textContent = "Network error. Please try again.";
} finally {
btn.disabled = false;
btn.textContent = "Generate my map";
}
};
// ===== MY PROJECTS =====
async function loadProjects() {
const root = document.getElementById("geoProjects");
root.textContent = "Loading...";
const token = await getAccessToken();
if (!token) {
root.textContent = "Please log in.";
return;
}
try {
const res = await fetch(WORKER_ORIGIN + "/my-projects", {
method: "GET",
headers: {
"Authorization": "Bearer " + token
}
});
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.ok) {
console.error("My Projects error", data);
root.textContent = "We couldn’t load your maps.";
return;
}
const projects = data.projects || [];
if (!projects.length) {
root.textContent = "You don’t have any maps yet. Generate one to see it here.";
return;
}
root.innerHTML = projects.map(p => {
const created = p.created_at
? new Date(p.created_at).toLocaleDateString()
: "";
const title = p.title || "Untitled Project";
const url = p.pdf_url || "#";
return `
<div class="geo-project-card">
<div>
<div class="geo-project-title">${title}</div>
${created ? `<div class="geo-project-date">${created}</div>` : ""}
</div>
${url && url !== "#" ? `
<a href="${url}" target="_blank" rel="noopener" class="geo-project-link">
Download PDF
</a>` : ""}
</div>
`;
}).join("");
} catch (e) {
console.error(e);
root.textContent = "Network error loading maps.";
}
}
// ===== BILLING PORTAL =====
document.getElementById("manageSubscriptionBtn").onclick = async () => {
const btn = document.getElementById("manageSubscriptionBtn");
const token = await getAccessToken();
if (!token) {
alert("Please log in to manage your subscription.");
return;
}
btn.disabled = true;
btn.textContent = "Opening portal...";
try {
const res = await fetch(WORKER_ORIGIN + "/billing/portal", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
},
body: JSON.stringify({})
});
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.url) {
console.error("Billing portal error", data);
alert("We couldn’t open your billing portal. Please contact support.");
} else {
window.open(data.url, "_blank", "noopener");
}
} catch (e) {
console.error(e);
alert("Network error. Please try again.");
} finally {
btn.disabled = false;
btn.textContent = "Manage my subscription";
}
};
// init
refreshUI();
supabase.auth.onAuthStateChange((_event, _session) => { refreshUI(); });
</script>
</body>
</html>