-
-
-
Dashboard
- Loading...
+
+ Prompt AI β Write better prompts, get better results
-
-
-
-
-
-
-
+
-
-
+
-
+
+ β‘
+ Admin
+ Overview
+
+
+ Dashboard
+
+
+
+ Activity
+
+ Users
+
+
+ Accounts
+
+
+
+ Waitlist
+
+
+
-
-
-
-
-
-
-
- Total Users
β
Registered accounts
Waitlist
β
Awaiting launch
Total Prompts
β
All time
Today
β
Generations today
-
-
- Top Categories
- π
No data yet
-
- Recent Sign-ups0
- π€
No sign-ups yet
-
+
-
-
-
- Activity Feed
Live log from Supabase
-
-
- Events0
- π‘
No events yet
+
+
-
-
-
+
+
diff --git a/api/generate.js b/api/generate.js
index 6f43a59..0d63de3 100644
--- a/api/generate.js
+++ b/api/generate.js
@@ -2,7 +2,8 @@ export default async function handler(req, res) {
const prompt = req.body.prompt;
const response = await fetch(
- "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=" + process.env.GEMINI_API_KEY,
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=" +
+ process.env.GEMINI_API_KEY,
{
method: "POST",
headers: {
@@ -11,11 +12,11 @@ export default async function handler(req, res) {
body: JSON.stringify({
contents: [
{
- parts: [{ text: prompt }]
- }
- ]
+ parts: [{ text: prompt }],
+ },
+ ],
}),
- }
+ },
);
const data = await response.json();
diff --git a/index.html b/index.html
index 97a18a0..1fc691c 100644
--- a/index.html
+++ b/index.html
@@ -1,1036 +1,421 @@
-
+
-
-
-
-
+
-
-
+
+
+
+
+ Dashboard
+ Loading...
+
+
+
+
+ Total Users
+ β
+ Registered accounts
+
+
+ Waitlist
+ β
+ Awaiting launch
+
+
+ Total Prompts
+ β
+ All time
+
+
+ Today
+ β
+ Generations today
+
+
+
+ Top Categories
+
+
+
+
+
+ π
+ No data yet
+
+
+
+ Recent Sign-ups0
+
+
+
+
+
+ π€
+ No sign-ups yet
+
-
-
-
-
-
- Registered Users
Everyone who has signed up
-
-
- Accounts0
-
-
-
+
+
- | Name | Provider | Joined | |
|---|---|---|---|
π€ No users yet | |||
+
-
+
+
+
+
+ Activity Feed
+ Live log from Supabase
+
+
+ Events0
+
+
+
+
+
+ π‘
+ No events yet
+
-
-
-
-
-
- Waitlist
People waiting for Prompt AI Pro
-
Entries0
-
-
-
+
+
| Signed Up | Status | ||
|---|---|---|---|
π Waitlist is empty | |||
+
+
+
+
+
+
+
+
+ Registered Users
+ Everyone who has signed up
+
+
+
+ Accounts0
+
+
+
+
+
+
+ | Name | +Provider | +Joined | +|
|---|---|---|---|
|
+
+
+ π€
+ No users yet
+ |
+ |||
+
+
+
+
+
+ Waitlist
+ People waiting for Prompt AI Pro
+
+
+ Entries0
+
+
+
+
+
+
+ | Signed Up | +Status | +||
|---|---|---|---|
|
+
+
+ π
+ Waitlist is empty
+ |
+ |||
-
-
- Prompt AI β Write better prompts, get better results
+
+
+
+
+
+
+
-
-
-
+
+
+
+ β‘
- Prompt AI
-
+
-
+
-
+
+
-
- β‘
+ Prompt AI
+
- β οΈ You're running this as a local file. Auth won't work until the file is hosted. Upload to Netlify Drop or GitHub Pages first β it takes 2 minutes.
-
+
+
+ β οΈ You're running this as a local file. Auth won't
+ work until the file is hosted. Upload to
+ Netlify Drop
+ or GitHub Pages first β it takes 2 minutes.
+
-
-
-
+
+ Good to see you back
- Sign in and pick up right where you left off.
-
-
-
-
- or
-
- Don't have an account? Create one β it's free
-
+
-
-
Good to see you back
+
+ Sign in and pick up right where you left off.
+
+
+
+
+
+
+
+
+
+ or
+
+
+ Don't have an account?
+ Create one β it's free
+
+
-
-
-
-
- β‘
- Prompt AI
-
-
-
- 3 left
-
-
-
-
?
- Account
-
+
+
+
+
-
-
+
-
+
- β‘
+ Prompt AI
-
-
-
- β
- β
+
+
-
+
+ 3 left
-
-
- Join Waitlist
-
-
-
- Sign Out
+
+
+
+ ?
+ Account
+
+
+
+
+ β
+ β
+
+
+ Join Waitlist
+
+
+
+ Sign Out
+
+
- Stop guessing.
-
+ Stop guessing.
Write prompts that work.
- Most people write vague prompts and wonder why the AI keeps missing the mark. Prompt AI fixes that β just tell us what you need, and we'll build a prompt that actually delivers.
-
+ Stop guessing.
+
- Stop guessing.
Write prompts that work.
+ + Most people write vague prompts and wonder why the AI keeps missing + the mark. Prompt AI fixes that β just tell us what you need, and + we'll build a prompt that actually delivers. +
+
-
+ Category
-
-
-
-
-
-
-
-
-
-
- What do you need?
-
- Tone
-
-
-
-
-
-
-
-
-
- π« That one's off limits. Prompt AI doesn't generate prompts for building competing prompt tools.
-
+
- Category
+
+
+
+
+
+
+
+
+
+
+ What do you need?
+
+ Tone
+
+
+
+
+
+
+
+
+
+
+ π« That one's off limits. Prompt AI doesn't generate prompts for
+ building competing prompt tools.
+
+
-
-
- β¦ Your Prompt
-
+
-
-
+
+ β¦ Your Prompt
+
+
+
+
+ π‘ Quick tip:
+
+ π‘ Quick tip:
-
-
-
-π
- You've hit your limit
- You've used all 3 free prompts for this period. Upgrade to Pro for unlimited access and early beta perks β or wait for your reset below.
-
-
-
-Resets in
- --:--:--
-
+
+
+
-
+
π
+ You've hit your limit
+
+ You've used all 3 free prompts for this period.
+ Upgrade to Pro for unlimited access and early beta perks β or wait for
+ your reset below.
+
+
+
+
+ Resets in
+ --:--:--
+
+
-
-
- ';
+
+ // ββ STEP 1: Analyse what kind of goal this actually is ββββββββββββββ
+ // Each category has distinct failure modes and prompt patterns that work.
+ // A real engineer thinks about WHY prompts fail before writing one.
+
+ const catPlaybook = {
+ general: {
+ desc: "a general task",
+ failureModes:
+ "vague outputs, AI makes too many assumptions, no clear success criteria",
+ mustHave:
+ 'a precise deliverable, explicit success criteria, scope boundaries, and a "think step by step" anchor where reasoning helps',
+ formatNote:
+ 'Use clear sections if the output has multiple parts. End with a self-check instruction like "Before responding, verify your answer covers X, Y, Z."',
+ },
+ writing: {
+ desc: "a writing task",
+ failureModes:
+ "generic voice, wrong tone, wrong length, AI writes for itself not the audience, clichΓ©d openings, weak structure",
+ mustHave:
+ "exact audience definition, publication/platform context, precise word count or length range, structural blueprint, voice guidance with a concrete example sentence showing the style, explicit list of what NOT to do",
+ formatNote:
+ 'Include: "Do not start with [X]", "Avoid clichΓ©s like [Y]", "The ideal opening sentence does [Z]". Give the AI a negative example so it knows what to dodge.',
+ },
+ coding: {
+ desc: "a coding / technical task",
+ failureModes:
+ "wrong language version, missing error handling, no edge cases, unexplained code, insecure patterns, untested assumptions",
+ mustHave:
+ "language + exact version, runtime/environment, input/output spec with concrete examples, edge cases to handle explicitly, error handling requirements, whether to include comments/docs, performance constraints if any",
+ formatNote:
+ "Use this structure in the prompt: (1) Problem statement with example input/output, (2) Constraints and requirements, (3) What the response must include β code + explanation + test cases.",
+ },
+ business: {
+ desc: "a business task",
+ failureModes:
+ "generic advice that ignores company context, no actionability, missing numbers/metrics, outputs that sound smart but change nothing",
+ mustHave:
+ 'company stage/size/industry context, specific metrics or KPIs involved, decision-maker audience, time constraints, what "done" looks like (a deck? a memo? a decision?), what constraints exist (budget, team size, time)',
+ formatNote:
+ 'Prompt should ask for: executive summary first, then supporting detail. Include "List specific, implementable next steps with owners and deadlines."',
+ },
+ image: {
+ desc: "an AI image generation task (Midjourney, DALL-E, Stable Diffusion, Flux)",
+ failureModes:
+ "muddy composition, wrong mood, inconsistent style, AI guesses lighting and fails, no negative prompts so unwanted elements appear",
+ mustHave:
+ 'subject + action + setting, art style (be specific: "oil painting in the style of Edward Hopper" not "painterly"), lighting setup, colour palette (name specific colours or reference a mood like "golden hour warmth"), camera angle/composition, mood/atmosphere, aspect ratio, negative prompts to exclude',
+ formatNote:
+ "For Midjourney: format as a comma-separated visual description ending with --ar 16:9 --style raw --v 6. For DALL-E: write as a flowing descriptive paragraph. Specify which tool the prompt targets.",
+ },
+ marketing: {
+ desc: "a marketing task",
+ failureModes:
+ "copy that sounds like every other brand, no hook, wrong channel format, missing CTA, ignores the target customer's actual pain points",
+ mustHave:
+ "target customer with specific demographics AND psychographics (what keeps them up at night?), channel (Instagram caption vs Google Ad vs email subject line are completely different), brand voice with 3 adjectives, the ONE thing the customer should feel/do after reading, word/character limits if applicable, what competitors do that we must NOT sound like",
+ formatNote:
+ 'For copy tasks: ask for 3 variations so the user can pick. Include "Write a version that leads with the pain, one that leads with the benefit, one that leads with social proof."',
+ },
+ education: {
+ desc: "an education or learning task",
+ failureModes:
+ "explanation pitched at wrong level, too abstract, no analogies, no checks for understanding, info dumping without structure",
+ mustHave:
+ "learner's exact knowledge level (what do they already know?), learning objective (what should they be able to DO after?), preferred explanation style (analogy-heavy? step-by-step? example-first?), whether to include practice questions, any known misconceptions to proactively address",
+ formatNote:
+ "Structure the prompt to produce: (1) Core concept in one sentence, (2) Analogy or real-world example, (3) Detailed explanation, (4) Common mistakes to avoid, (5) 2-3 practice questions with answers.",
+ },
+ research: {
+ desc: "a research or analysis task",
+ failureModes:
+ "surface-level summaries, confident hallucinations, no citation awareness, missing counterarguments, conclusions not grounded in evidence",
+ mustHave:
+ 'research question stated precisely, scope (time range, geography, domain), required depth (overview vs deep analysis), format for citing uncertainty ("If you are not certain, say so explicitly"), whether counterarguments are needed, output format (report? bullet points? executive brief?)',
+ formatNote:
+ 'Always include: "Flag any claims you are less than 90% confident about with [UNCERTAIN]." and "Present the strongest counterargument to your main conclusion."',
+ },
+ };
+
+ const toneInstruction = {
+ professional:
+ "Write in a formal, authoritative register. Sentences are complete and precise. No slang, no contractions in formal sections. The reader is a professional making a real decision.",
+ casual:
+ "Write as a brilliant, knowledgeable friend β warm, direct, no corporate stiffness. Use contractions, be conversational, but never sacrifice accuracy for friendliness.",
+ persuasive:
+ "Every sentence should move the reader toward a decision or action. Lead with what they care about, not what you want to say. Build momentum. Make saying yes feel obvious.",
+ technical:
+ "Be exact. Use correct domain terminology without over-explaining it to experts. Prioritise precision over readability. Include specifics β versions, parameters, thresholds.",
+ creative:
+ "Prioritise originality and surprise. Avoid the first idea β it's probably a clichΓ©. Use vivid, concrete sensory language. The unexpected detail beats the expected one every time.",
+ concise:
+ "Every word must earn its place. Cut adjectives. Cut throat-clearing. Cut hedging. State the thing directly. If a sentence can be shorter, it must be shorter.",
+ };
+
+ const pb = catPlaybook[selectedCat] || catPlaybook.general;
+ const toneInst =
+ toneInstruction[selectedTone] || toneInstruction.professional;
+
+ const sys = `You are a prompt engineer with 5+ years of hands-on experience building prompts for production AI systems across dozens of industries. You have personally written thousands of prompts, studied what makes them fail, iterated obsessively, and developed strong opinions about what actually works vs what sounds good in theory.
+
+You are not building prompts from a template. You are diagnosing what the user actually needs, identifying the specific ways a naive prompt would fail for this request, and engineering around those failure modes precisely.
+
+THE USER'S REQUEST IS: ${pb.desc}
+
+KNOWN FAILURE MODES FOR THIS TYPE: ${pb.failureModes}
+
+WHAT THIS PROMPT MUST CONTAIN: ${pb.mustHave}
+
+FORMAT STRATEGY: ${pb.formatNote}
+
+TONE INSTRUCTION (apply throughout the generated prompt): ${toneInst}
+
+YOUR PROCESS β think through this before writing:
+1. What is the user ACTUALLY trying to accomplish? (Look past their words to their real goal)
+2. What would a naive AI do wrong with a vague version of this request?
+3. What context is the AI missing that would cause it to guess wrong?
+4. What does "done well" look like for this task β what are the specific quality signals?
+5. What should the AI explicitly NOT do?
+
+Now write the prompt.
+
+HARD RULES:
+- Output ONLY the finished, ready-to-paste prompt. No preamble. No "Here is your prompt:". No markdown fences around it.
+- The prompt opens directly with a specific role/persona for the AI β make it precise and contextually appropriate, not generic.
+- Every instruction must be actionable and unambiguous. Remove any instruction the AI could interpret more than one way.
+- No vague quality words: never write "high quality", "comprehensive", "detailed" without defining what that means in concrete terms.
+- The user pastes this with zero edits and gets an excellent result. If they'd need to fill in a blank or guess at anything, you've failed.
+- Length calibration: short focused tasks = 150-250 words. Medium complexity = 250-450 words. Deep/multi-part tasks = 450-650 words. Never pad. Never truncate important constraints.
+- After the complete prompt, leave exactly two blank lines, then write: TIP: followed by one specific, non-obvious tip (under 20 words) that meaningfully improves results when using this prompt.`;
+
+ // ββ GEMINI API KEY β paste yours here ββββββββββββββββββββββββββββββ
+ // Get a FREE key at: aistudio.google.com β Get API Key
+ const GEMINI_KEY = "AIzaSyCYiAFeTN6zh2lg3hJzXAAADGuCaNDbqKQ";
+
+ // Build the full prompt by combining system + user message
+ // Gemini Flash doesn't use a separate system role β we combine them
+ const fullUserMessage = `${sys}
+
+---
+
+USER GOAL: "${goal}"
+
+Think through the 5-step process silently, then output ONLY the final prompt. Nothing before it, nothing after it except the TIP line.`;
+
+ try {
+ const res = await fetch(
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${GEMINI_KEY}`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ contents: [
+ {
+ parts: [{ text: fullUserMessage }],
+ },
+ ],
+ generationConfig: {
+ temperature: 0.7,
+ maxOutputTokens: 1800,
+ topP: 0.95,
+ },
+ safetySettings: [
+ {
+ category: "HARM_CATEGORY_HARASSMENT",
+ threshold: "BLOCK_NONE",
+ },
+ {
+ category: "HARM_CATEGORY_HATE_SPEECH",
+ threshold: "BLOCK_NONE",
+ },
+ {
+ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
+ threshold: "BLOCK_NONE",
+ },
+ {
+ category: "HARM_CATEGORY_DANGEROUS_CONTENT",
+ threshold: "BLOCK_NONE",
+ },
+ ],
+ }),
+ },
+ );
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ const errMsg = data.error?.message || "API error " + res.status;
+ throw new Error(errMsg);
+ }
+
+ // Extract text from Gemini response
+ const txt = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
+ if (!txt) throw new Error("No response received. Try again.");
+
+ const tipMatch =
+ txt.match(/\n\nTIP:\s*(.+)/i) || txt.match(/\nTIP:\s*(.+)/i);
+ if (tipMatch) {
+ fullPromptText = txt.substring(0, txt.indexOf(tipMatch[0])).trim();
+ document.getElementById("tipText").textContent = tipMatch[1].trim();
+ tipBox.style.display = "block";
+ } else {
+ fullPromptText = txt.trim();
+ }
+ outputEl.textContent = fullPromptText;
+
+ const newUsage = incrementUsage();
+ updateUsageUI();
+
+ // Log to Supabase (fire and forget β don't await, don't block UX)
+ sbInsert("events", {
+ type: "generation",
+ user_id: currentUser?.id || null,
+ email: currentUser?.email || null,
+ category: selectedCat,
+ tone: selectedTone,
+ });
+
+ if (newUsage.count >= FREE_LIMIT) {
+ setTimeout(() => showGate(newUsage.resetAt), 1600);
+ }
+ } catch (err) {
+ outputCard.classList.remove("visible");
+ showToast("Something went wrong: " + err.message, "error");
+ }
+
+ btn.disabled = false;
+ btn.innerHTML = "Build My Prompt β‘";
+}
+
+// βββ GATE + TIMER ββββββββββββββββββββββββββββββββββββββββββββββββββ
+function showGate(resetAt) {
+ document.getElementById("gateOverlay").classList.add("active");
+ if (resetAt) startTimer(resetAt);
+}
+function closeGate() {
+ document.getElementById("gateOverlay").classList.remove("active");
+ if (timerInterval) {
+ clearInterval(timerInterval);
+ timerInterval = null;
+ }
+}
+function startTimer(resetAt) {
+ if (timerInterval) clearInterval(timerInterval);
+ const tick = () => {
+ const diff = Math.max(0, resetAt - Date.now());
+ const h = Math.floor(diff / 3600000);
+ const m = Math.floor((diff % 3600000) / 60000);
+ const s = Math.floor((diff % 60000) / 1000);
+ document.getElementById("timerDisplay").textContent =
+ pad(h) + ":" + pad(m) + ":" + pad(s);
+ if (diff <= 0) {
+ clearInterval(timerInterval);
+ timerInterval = null;
+ closeGate();
+ updateUsageUI();
+ showToast("You're back! Prompts reset. β‘", "success");
+ }
+ };
+ tick();
+ timerInterval = setInterval(tick, 1000);
+}
+const pad = (n) => String(n).padStart(2, "0");
+
+// βββ WAITLIST ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function showWaitlist() {
+ document.getElementById("waitlistFormView").style.display = "";
+ document.getElementById("waitlistSuccess").style.display = "none";
+ document.getElementById("wlError").style.display = "none";
+ if (currentUser)
+ document.getElementById("wlEmail").value = currentUser.email || "";
+ document.getElementById("waitlistOverlay").classList.add("active");
+}
+function closeWaitlist() {
+ document.getElementById("waitlistOverlay").classList.remove("active");
+}
+async function joinWaitlist() {
+ const email = document
+ .getElementById("wlEmail")
+ .value.trim()
+ .toLowerCase();
+ const whatsapp = document.getElementById("wlWhatsapp").value.trim();
+ const err = document.getElementById("wlError");
+ const btn = document.getElementById("wlBtn");
+ err.style.display = "none";
+
+ if (!email || !whatsapp) {
+ showErr(err, "Fill in both fields.");
+ return;
+ }
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+ showErr(err, "That doesn't look like a valid email.");
+ return;
+ }
+
+ btn.disabled = true;
+ btn.innerHTML = ' Joining...';
+
+ try {
+ await sbUpsert(
+ "waitlist",
+ {
+ email,
+ whatsapp,
+ user_id: currentUser?.id || null,
+ },
+ "email",
+ );
+
+ sbInsert("events", {
+ type: "waitlist",
+ email,
+ user_id: currentUser?.id || null,
+ });
+ } catch (e) {
+ /* silent fail β still show success */
+ }
+
+ btn.disabled = false;
+ btn.innerHTML = "Count Me In π―";
+ document.getElementById("waitlistFormView").style.display = "none";
+ document.getElementById("waitlistSuccess").style.display = "block";
+}
+
+// βββ COPY ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function copyPrompt() {
+ const text =
+ fullPromptText ||
+ document.getElementById("promptOutput").innerText ||
+ "";
+ if (!text || text.includes("On it")) return;
+ if (navigator.clipboard?.writeText) {
+ navigator.clipboard
+ .writeText(text)
+ .then(showCopied)
+ .catch(() => fallbackCopy(text));
+ } else {
+ fallbackCopy(text);
+ }
+}
+function fallbackCopy(text) {
+ const ta = document.createElement("textarea");
+ ta.value = text;
+ ta.setAttribute("readonly", "");
+ ta.style.cssText =
+ "position:fixed;top:0;left:0;width:1px;height:1px;opacity:0";
+ document.body.appendChild(ta);
+ ta.focus();
+ ta.select();
+ let ok = false;
+ try {
+ ok = document.execCommand("copy");
+ } catch (e) {}
+ document.body.removeChild(ta);
+ if (ok) showCopied();
+ else showToast("Long-press the text above to copy manually.", "error");
+}
+function showCopied() {
+ const btn = document.getElementById("copyBtn");
+ btn.classList.add("copied");
+ btn.innerHTML =
+ ' Copied!';
+ showToast("Copied β", "success");
+ setTimeout(() => {
+ btn.classList.remove("copied");
+ btn.innerHTML =
+ ' Copy';
+ }, 2500);
+}
+
+// βββ HELPERS βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function showErr(el, msg) {
+ el.textContent = msg;
+ el.style.display = msg ? "block" : "none";
+}
+function showToast(msg, type) {
+ const t = document.getElementById("toast");
+ t.textContent = msg;
+ t.className = "toast " + (type || "");
+ t.classList.add("show");
+ setTimeout(() => t.classList.remove("show"), 3200);
+}
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..459fd36
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,1000 @@
+@import url("https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,400&display=swap");
+:root {
+ --bg: #070b14;
+ --surface: #0d1422;
+ --card: #111929;
+ --border: #1e2d47;
+ --accent: #3b82f6;
+ --accent2: #a78bfa;
+ --text: #e2eaf8;
+ --muted: #5a7299;
+ --success: #34d399;
+ --danger: #f87171;
+ --warning: #fbbf24;
+}
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: "DM Sans", sans-serif;
+ min-height: 100vh;
+ overflow-x: hidden;
+}
+body::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ background-image:
+ linear-gradient(rgba(59, 130, 246, 0.04) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(59, 130, 246, 0.04) 1px, transparent 1px);
+ background-size: 52px 52px;
+ pointer-events: none;
+ z-index: 0;
+}
+.glow {
+ position: fixed;
+ width: 700px;
+ height: 700px;
+ background: radial-gradient(
+ circle,
+ rgba(59, 130, 246, 0.1) 0%,
+ transparent 70%
+ );
+ top: -300px;
+ left: 50%;
+ transform: translateX(-50%);
+ pointer-events: none;
+ z-index: 0;
+}
+.page {
+ display: none;
+ position: relative;
+ z-index: 1;
+}
+.page.active {
+ display: block;
+}
+
+/* AUTH */
+.auth-wrap {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 24px;
+}
+.auth-card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 24px;
+ padding: 44px 36px;
+ width: 100%;
+ max-width: 420px;
+ box-shadow: 0 40px 80px rgba(0, 0, 0, 0.5);
+}
+.auth-logo {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 28px;
+ justify-content: center;
+}
+.auth-logo-icon {
+ width: 40px;
+ height: 40px;
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+}
+.auth-logo-text {
+ font-family: "Syne", sans-serif;
+ font-weight: 800;
+ font-size: 22px;
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+.auth-title {
+ font-family: "Syne", sans-serif;
+ font-weight: 700;
+ font-size: 22px;
+ margin-bottom: 6px;
+ text-align: center;
+}
+.auth-sub {
+ color: var(--muted);
+ font-size: 13px;
+ text-align: center;
+ margin-bottom: 28px;
+ line-height: 1.6;
+}
+.field {
+ margin-bottom: 14px;
+}
+.field label {
+ display: block;
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 1.5px;
+ text-transform: uppercase;
+ color: var(--muted);
+ margin-bottom: 7px;
+}
+.field input {
+ width: 100%;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 12px 15px;
+ color: var(--text);
+ font-family: "DM Sans", sans-serif;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.2s;
+}
+.field input:focus {
+ border-color: var(--accent);
+}
+.field input::placeholder {
+ color: var(--muted);
+}
+.btn-primary {
+ width: 100%;
+ padding: 13px;
+ border-radius: 10px;
+ border: none;
+ background: linear-gradient(135deg, #3b82f6, #7c3aed);
+ color: #fff;
+ font-family: "Syne", sans-serif;
+ font-weight: 700;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s;
+ margin-top: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+}
+.btn-primary:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 8px 30px rgba(59, 130, 246, 0.4);
+}
+.btn-primary:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+}
+.auth-divider {
+ text-align: center;
+ color: var(--muted);
+ font-size: 11px;
+ margin: 14px 0;
+ position: relative;
+}
+.auth-divider::before,
+.auth-divider::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ width: 40%;
+ height: 1px;
+ background: var(--border);
+}
+.auth-divider::before {
+ left: 0;
+}
+.auth-divider::after {
+ right: 0;
+}
+.btn-google {
+ width: 100%;
+ padding: 12px 16px;
+ border-radius: 10px;
+ border: 1px solid var(--border);
+ background: var(--surface);
+ color: var(--text);
+ font-family: "DM Sans", sans-serif;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+}
+.btn-google:hover {
+ border-color: var(--accent);
+ background: #0f1d35;
+}
+.auth-switch {
+ text-align: center;
+ margin-top: 16px;
+ font-size: 13px;
+ color: var(--muted);
+}
+.auth-switch a {
+ color: var(--accent);
+ cursor: pointer;
+ text-decoration: none;
+}
+.auth-switch a:hover {
+ text-decoration: underline;
+}
+.inline-error {
+ color: var(--danger);
+ font-size: 12px;
+ margin-top: 8px;
+ padding: 10px 12px;
+ background: rgba(248, 113, 113, 0.08);
+ border: 1px solid rgba(248, 113, 113, 0.2);
+ border-radius: 8px;
+ display: none;
+ line-height: 1.5;
+}
+
+/* APP */
+.app-wrap {
+ max-width: 820px;
+ margin: 0 auto;
+ padding: 36px 20px 80px;
+}
+.app-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 44px;
+ position: relative;
+}
+.app-logo {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+}
+.app-logo-icon {
+ width: 33px;
+ height: 33px;
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+}
+.app-logo-text {
+ font-family: "Syne", sans-serif;
+ font-weight: 800;
+ font-size: 17px;
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+.header-right {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+.usage-badge {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 6px 13px;
+ border-radius: 100px;
+ background: rgba(59, 130, 246, 0.1);
+ border: 1px solid rgba(59, 130, 246, 0.25);
+ font-size: 12px;
+ color: var(--accent);
+ font-weight: 500;
+}
+.usage-badge.low {
+ background: rgba(251, 191, 36, 0.1);
+ border-color: rgba(251, 191, 36, 0.3);
+ color: var(--warning);
+}
+.usage-badge.empty {
+ background: rgba(248, 113, 113, 0.1);
+ border-color: rgba(248, 113, 113, 0.3);
+ color: var(--danger);
+}
+.user-wrap {
+ position: relative;
+}
+.user-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 13px;
+ border-radius: 100px;
+ border: 1px solid var(--border);
+ background: var(--card);
+ color: var(--muted);
+ font-size: 13px;
+ cursor: pointer;
+ transition: all 0.2s;
+ user-select: none;
+}
+.user-btn:hover {
+ border-color: var(--accent);
+ color: var(--text);
+}
+.user-avatar {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ font-weight: 700;
+ color: #fff;
+ flex-shrink: 0;
+}
+.user-dropdown {
+ position: absolute;
+ top: calc(100% + 8px);
+ right: 0;
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 8px;
+ min-width: 210px;
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
+ z-index: 300;
+ display: none;
+}
+.user-dropdown.open {
+ display: block;
+ animation: fadeUp 0.15s ease;
+}
+.dropdown-header {
+ padding: 10px 12px 12px;
+ border-bottom: 1px solid var(--border);
+ margin-bottom: 6px;
+}
+.dropdown-name {
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--text);
+}
+.dropdown-email {
+ font-size: 11px;
+ color: var(--muted);
+ margin-top: 2px;
+ word-break: break-all;
+}
+.dropdown-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ font-size: 13px;
+ color: var(--muted);
+ cursor: pointer;
+ transition: all 0.15s;
+}
+.dropdown-item:hover {
+ background: rgba(59, 130, 246, 0.08);
+ color: var(--text);
+}
+.dropdown-item.danger:hover {
+ background: rgba(248, 113, 113, 0.08);
+ color: var(--danger);
+}
+
+/* HERO */
+.hero {
+ text-align: center;
+ margin-bottom: 40px;
+}
+.hero h1 {
+ font-family: "Syne", sans-serif;
+ font-size: clamp(28px, 5vw, 48px);
+ font-weight: 800;
+ line-height: 1.1;
+ letter-spacing: -1.5px;
+ margin-bottom: 14px;
+}
+.hero h1 span {
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+.hero p {
+ color: var(--muted);
+ font-size: 15px;
+ line-height: 1.65;
+ max-width: 460px;
+ margin: 0 auto;
+}
+
+/* CARDS */
+.main-card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 20px;
+ padding: 28px;
+ margin-bottom: 20px;
+}
+.section-label {
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ color: var(--accent);
+ margin-bottom: 10px;
+}
+.categories {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 7px;
+ margin-bottom: 24px;
+}
+.pill {
+ padding: 6px 14px;
+ border-radius: 100px;
+ border: 1px solid var(--border);
+ background: transparent;
+ color: var(--muted);
+ font-family: "DM Sans", sans-serif;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.pill:hover {
+ border-color: var(--accent);
+ color: var(--text);
+}
+.pill.active {
+ background: rgba(59, 130, 246, 0.15);
+ border-color: var(--accent);
+ color: var(--accent);
+}
+textarea {
+ width: 100%;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 15px 17px;
+ color: var(--text);
+ font-family: "DM Sans", sans-serif;
+ font-size: 15px;
+ line-height: 1.6;
+ resize: none;
+ min-height: 110px;
+ transition: border-color 0.2s;
+ outline: none;
+ margin-bottom: 18px;
+ display: block;
+}
+textarea:focus {
+ border-color: var(--accent);
+}
+textarea::placeholder {
+ color: var(--muted);
+}
+.tone-row {
+ display: flex;
+ gap: 7px;
+ flex-wrap: wrap;
+ margin-bottom: 22px;
+}
+.tone-btn {
+ padding: 5px 13px;
+ border-radius: 8px;
+ border: 1px solid var(--border);
+ background: transparent;
+ color: var(--muted);
+ font-family: "DM Sans", sans-serif;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.tone-btn:hover,
+.tone-btn.active {
+ background: rgba(167, 139, 250, 0.12);
+ border-color: var(--accent2);
+ color: var(--accent2);
+}
+.btn-generate {
+ width: 100%;
+ padding: 15px;
+ border-radius: 12px;
+ border: none;
+ background: linear-gradient(135deg, #3b82f6, #7c3aed);
+ color: #fff;
+ font-family: "Syne", sans-serif;
+ font-weight: 700;
+ font-size: 15px;
+ cursor: pointer;
+ transition: all 0.25s;
+}
+.btn-generate:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 12px 40px rgba(59, 130, 246, 0.4);
+}
+.btn-generate:active {
+ transform: translateY(0);
+}
+.btn-generate:disabled {
+ opacity: 0.65;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+}
+
+/* OUTPUT */
+.output-card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 20px;
+ padding: 28px;
+ display: none;
+}
+.output-card.visible {
+ display: block;
+ animation: fadeUp 0.35s ease;
+}
+@keyframes fadeUp {
+ from {
+ opacity: 0;
+ transform: translateY(14px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+.output-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16px;
+}
+.output-title {
+ font-family: "Syne", sans-serif;
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--accent2);
+ letter-spacing: 1.5px;
+ text-transform: uppercase;
+}
+.copy-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 7px 14px;
+ border-radius: 8px;
+ border: 1px solid var(--border);
+ background: transparent;
+ color: var(--muted);
+ font-family: "DM Sans", sans-serif;
+ font-size: 13px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.copy-btn:hover {
+ border-color: var(--accent2);
+ color: var(--accent2);
+}
+.copy-btn.copied {
+ border-color: var(--success);
+ color: var(--success);
+}
+.prompt-output {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 18px;
+ font-size: 14px;
+ line-height: 1.8;
+ color: var(--text);
+ white-space: pre-wrap;
+ word-break: break-word;
+ min-height: 80px;
+}
+.tip-box {
+ margin-top: 14px;
+ padding: 13px 15px;
+ background: rgba(59, 130, 246, 0.07);
+ border: 1px solid rgba(59, 130, 246, 0.2);
+ border-radius: 10px;
+ font-size: 13px;
+ color: var(--muted);
+ line-height: 1.6;
+ display: none;
+}
+.tip-box strong {
+ color: var(--accent);
+}
+.loading-indicator {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ color: var(--muted);
+ font-size: 14px;
+}
+.dots {
+ display: flex;
+ gap: 5px;
+}
+.dots span {
+ width: 7px;
+ height: 7px;
+ background: var(--accent);
+ border-radius: 50%;
+ animation: dotb 1.2s ease-in-out infinite;
+}
+.dots span:nth-child(2) {
+ animation-delay: 0.2s;
+}
+.dots span:nth-child(3) {
+ animation-delay: 0.4s;
+}
+@keyframes dotb {
+ 0%,
+ 80%,
+ 100% {
+ opacity: 0.2;
+ transform: scale(0.7);
+ }
+ 40% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+/* GATE */
+.gate-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(7, 11, 20, 0.88);
+ backdrop-filter: blur(8px);
+ z-index: 100;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+}
+.gate-overlay.active {
+ display: flex;
+}
+.gate-card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 24px;
+ padding: 44px 36px;
+ max-width: 440px;
+ width: 100%;
+ text-align: center;
+ box-shadow: 0 40px 80px rgba(0, 0, 0, 0.6);
+ animation: fadeUp 0.3s ease;
+}
+.gate-icon {
+ font-size: 44px;
+ margin-bottom: 18px;
+}
+.gate-title {
+ font-family: "Syne", sans-serif;
+ font-weight: 800;
+ font-size: 24px;
+ margin-bottom: 12px;
+ letter-spacing: -0.5px;
+}
+.gate-sub {
+ color: var(--muted);
+ font-size: 14px;
+ line-height: 1.7;
+ margin-bottom: 28px;
+}
+.gate-sub strong {
+ color: var(--text);
+}
+.gate-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.btn-waitlist-cta {
+ padding: 15px;
+ border-radius: 12px;
+ border: none;
+ background: linear-gradient(135deg, #3b82f6, #7c3aed);
+ color: #fff;
+ font-family: "Syne", sans-serif;
+ font-weight: 700;
+ font-size: 15px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.btn-waitlist-cta:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 8px 30px rgba(59, 130, 246, 0.4);
+}
+.btn-close-gate {
+ padding: 13px;
+ border-radius: 12px;
+ border: 1px solid var(--border);
+ background: transparent;
+ color: var(--muted);
+ font-family: "DM Sans", sans-serif;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.btn-close-gate:hover {
+ border-color: var(--accent);
+ color: var(--text);
+}
+.timer-label {
+ font-size: 12px;
+ color: var(--muted);
+ margin-top: 4px;
+}
+.timer-display {
+ font-family: "Syne", sans-serif;
+ font-size: 32px;
+ font-weight: 800;
+ color: var(--accent);
+ letter-spacing: 3px;
+ margin: 6px 0 14px;
+}
+
+/* WAITLIST */
+.waitlist-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(7, 11, 20, 0.92);
+ backdrop-filter: blur(10px);
+ z-index: 200;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+ overflow-y: auto;
+}
+.waitlist-overlay.active {
+ display: flex;
+}
+.waitlist-card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 24px;
+ padding: 44px 36px;
+ max-width: 460px;
+ width: 100%;
+ text-align: center;
+ box-shadow: 0 40px 100px rgba(0, 0, 0, 0.7);
+ animation: fadeUp 0.3s ease;
+ position: relative;
+ margin: auto;
+}
+.waitlist-close {
+ position: absolute;
+ top: 18px;
+ right: 18px;
+ width: 30px;
+ height: 30px;
+ border-radius: 8px;
+ border: 1px solid var(--border);
+ background: transparent;
+ color: var(--muted);
+ font-size: 16px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s;
+}
+.waitlist-close:hover {
+ border-color: var(--danger);
+ color: var(--danger);
+}
+.waitlist-badge {
+ display: inline-block;
+ padding: 4px 13px;
+ border-radius: 100px;
+ background: rgba(251, 191, 36, 0.1);
+ border: 1px solid rgba(251, 191, 36, 0.3);
+ color: var(--warning);
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+ margin-bottom: 16px;
+}
+.waitlist-title {
+ font-family: "Syne", sans-serif;
+ font-weight: 800;
+ font-size: 26px;
+ letter-spacing: -0.5px;
+ margin-bottom: 10px;
+}
+.waitlist-sub {
+ color: var(--muted);
+ font-size: 13px;
+ line-height: 1.65;
+ margin-bottom: 22px;
+}
+.perks {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin: 0 0 22px;
+ text-align: left;
+}
+.perk {
+ display: flex;
+ align-items: center;
+ gap: 11px;
+ padding: 10px 14px;
+ background: rgba(59, 130, 246, 0.06);
+ border: 1px solid rgba(59, 130, 246, 0.15);
+ border-radius: 9px;
+ font-size: 13px;
+ color: var(--muted);
+}
+.perk-icon {
+ font-size: 16px;
+ flex-shrink: 0;
+}
+.waitlist-form .field {
+ margin-bottom: 12px;
+ text-align: left;
+}
+.waitlist-form .field label {
+ display: block;
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 1.5px;
+ text-transform: uppercase;
+ color: var(--muted);
+ margin-bottom: 6px;
+}
+.waitlist-form .field input {
+ width: 100%;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 12px 15px;
+ color: var(--text);
+ font-family: "DM Sans", sans-serif;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.2s;
+}
+.waitlist-form .field input:focus {
+ border-color: var(--accent);
+}
+.waitlist-form .field input::placeholder {
+ color: var(--muted);
+}
+.btn-join-waitlist {
+ width: 100%;
+ padding: 13px;
+ border-radius: 10px;
+ border: none;
+ background: linear-gradient(135deg, #3b82f6, #7c3aed);
+ color: #fff;
+ font-family: "Syne", sans-serif;
+ font-weight: 700;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s;
+ margin-top: 6px;
+}
+.btn-join-waitlist:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 8px 30px rgba(59, 130, 246, 0.4);
+}
+.btn-join-waitlist:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+}
+.waitlist-success {
+ display: none;
+ animation: fadeUp 0.3s ease;
+}
+.waitlist-success-icon {
+ font-size: 48px;
+ margin-bottom: 14px;
+}
+.waitlist-success-title {
+ font-family: "Syne", sans-serif;
+ font-weight: 800;
+ font-size: 22px;
+ margin-bottom: 10px;
+}
+.waitlist-success-sub {
+ color: var(--muted);
+ font-size: 13px;
+ line-height: 1.65;
+}
+
+.blocked-msg {
+ margin-top: 12px;
+ padding: 14px 16px;
+ background: rgba(248, 113, 113, 0.07);
+ border: 1px solid rgba(248, 113, 113, 0.2);
+ border-radius: 10px;
+ font-size: 13px;
+ color: var(--danger);
+ line-height: 1.6;
+ display: none;
+}
+.spinner {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-top-color: #fff;
+ border-radius: 50%;
+ animation: spin 0.7s linear infinite;
+ vertical-align: middle;
+}
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+.toast {
+ position: fixed;
+ bottom: 28px;
+ left: 50%;
+ transform: translateX(-50%) translateY(80px);
+ background: #1a2540;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 11px 22px;
+ font-size: 13px;
+ color: var(--text);
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.4);
+ z-index: 999;
+ transition: transform 0.3s ease;
+ white-space: nowrap;
+ pointer-events: none;
+}
+.toast.show {
+ transform: translateX(-50%) translateY(0);
+}
+.toast.success {
+ border-color: rgba(52, 211, 153, 0.4);
+ color: var(--success);
+}
+.toast.error {
+ border-color: rgba(248, 113, 113, 0.4);
+ color: var(--danger);
+}
+
+@media (max-width: 600px) {
+ .auth-card {
+ padding: 28px 20px;
+ }
+ .app-wrap {
+ padding: 20px 14px 60px;
+ }
+ .main-card,
+ .output-card {
+ padding: 18px;
+ }
+ .gate-card,
+ .waitlist-card {
+ padding: 28px 20px;
+ }
+ .hero h1 {
+ font-size: 26px;
+ }
+ .user-name-label {
+ display: none;
+ }
+}
-
-
-
-
-
-
+
+
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..5b49cf3
--- /dev/null
+++ b/script.js
@@ -0,0 +1,922 @@
+"use strict";
+
+// βββ SUPABASE CONFIG βββββββββββββββββββββββββββββββββββββββββββββββ
+const SB_URL = "https://sdrmbrrhgovkzzlmdqul.supabase.co";
+const SB_KEY =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNkcm1icnJoZ292a3p6bG1kcXVsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM2MTcwNjEsImV4cCI6MjA4OTE5MzA2MX0.YIcN9PQRcKAvCq-3_wpom3Ir-uD8tCZh2efg7Xasyyg";
+
+// Direct REST helper β no SDK needed
+async function sbPost(path, body) {
+ const r = await fetch(SB_URL + path, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ apikey: SB_KEY,
+ Authorization: "Bearer " + SB_KEY,
+ },
+ body: JSON.stringify(body),
+ });
+ const data = await r.json();
+ return { ok: r.ok, data, status: r.status };
+}
+
+async function sbGet(path) {
+ const r = await fetch(SB_URL + path, {
+ headers: { apikey: SB_KEY, Authorization: "Bearer " + SB_KEY },
+ });
+ const data = await r.json();
+ return { ok: r.ok, data };
+}
+
+async function sbUpsert(table, row, onConflict) {
+ const url = `${SB_URL}/rest/v1/${table}?on_conflict=${onConflict}`;
+ const r = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ apikey: SB_KEY,
+ Authorization: "Bearer " + SB_KEY,
+ Prefer: "resolution=merge-duplicates,return=minimal",
+ },
+ body: JSON.stringify(row),
+ });
+ return { ok: r.ok, status: r.status };
+}
+
+async function sbInsert(table, row) {
+ const url = `${SB_URL}/rest/v1/${table}`;
+ const r = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ apikey: SB_KEY,
+ Authorization: "Bearer " + SB_KEY,
+ Prefer: "return=minimal",
+ },
+ body: JSON.stringify(row),
+ });
+ return { ok: r.ok, status: r.status };
+}
+
+// βββ CONSTANTS βββββββββββββββββββββββββββββββββββββββββββββββββββββ
+const FREE_LIMIT = 3;
+const RESET_MS = 12 * 3600 * 1000;
+
+const BANNED = [
+ /prompt\s*(ai|generator|tool|maker|builder|engine|app|website|platform)/i,
+ /build\s*(a\s+)?(ai|prompt)\s*(generator|tool|app|website|clone)/i,
+ /create\s*(a\s+)?prompt\s*(generator|tool|app|system)/i,
+ /make\s*(a\s+)?prompt\s*(generator|tool|app)/i,
+ /ai\s*that\s*(generates?|creates?|makes?)\s*prompts/i,
+ /clone\s*(of|this)\s*prompt/i,
+];
+const isBanned = (t) => BANNED.some((p) => p.test(t));
+
+let currentUser = null;
+let selectedCat = "general";
+let selectedTone = "professional";
+let fullPromptText = "";
+let timerInterval = null;
+let dropdownOpen = false;
+
+const LS = {
+ get: (k) => {
+ try {
+ return localStorage.getItem(k);
+ } catch (e) {
+ return null;
+ }
+ },
+ set: (k, v) => {
+ try {
+ localStorage.setItem(k, v);
+ } catch (e) {}
+ },
+ remove: (k) => {
+ try {
+ localStorage.removeItem(k);
+ } catch (e) {}
+ },
+};
+
+// βββ INIT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+window.addEventListener("DOMContentLoaded", () => {
+ setupPills();
+
+ // Show local file warning if opened via file://
+ if (isLocalFile()) {
+ document.getElementById("localWarning").style.display = "block";
+ }
+
+ // Restore session from localStorage
+ const saved = LS.get("pai_user");
+ if (saved) {
+ try {
+ currentUser = JSON.parse(saved);
+ showApp();
+ } catch (e) {
+ showAuth();
+ }
+ } else {
+ // Check URL for Google OAuth token (hash fragment)
+ const hash = window.location.hash;
+ if (hash && hash.includes("access_token")) {
+ handleOAuthCallback(hash);
+ } else {
+ showAuth();
+ }
+ }
+
+ document.addEventListener("click", (e) => {
+ const btn = document.getElementById("userBtn");
+ const dd = document.getElementById("userDropdown");
+ if (
+ dropdownOpen &&
+ btn &&
+ dd &&
+ !btn.contains(e.target) &&
+ !dd.contains(e.target)
+ ) {
+ closeDropdown();
+ }
+ });
+});
+
+function setupPills() {
+ document.querySelectorAll(".pill").forEach((p) => {
+ p.addEventListener("click", () => {
+ document
+ .querySelectorAll(".pill")
+ .forEach((x) => x.classList.remove("active"));
+ p.classList.add("active");
+ selectedCat = p.getAttribute("data-cat");
+ });
+ });
+ document.querySelectorAll(".tone-btn").forEach((b) => {
+ b.addEventListener("click", () => {
+ document
+ .querySelectorAll(".tone-btn")
+ .forEach((x) => x.classList.remove("active"));
+ b.classList.add("active");
+ selectedTone = b.getAttribute("data-tone");
+ });
+ });
+}
+
+// βββ ROUTING βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function showAuth() {
+ document.getElementById("authPage").classList.add("active");
+ document.getElementById("appPage").classList.remove("active");
+}
+function showApp() {
+ document.getElementById("authPage").classList.remove("active");
+ document.getElementById("appPage").classList.add("active");
+ updateUserUI();
+ updateUsageUI();
+}
+function showLogin() {
+ document.getElementById("loginForm").style.display = "";
+ document.getElementById("registerForm").style.display = "none";
+ document.getElementById("loginError").style.display = "none";
+}
+function showRegister() {
+ document.getElementById("loginForm").style.display = "none";
+ document.getElementById("registerForm").style.display = "";
+ document.getElementById("regError").style.display = "none";
+}
+
+// βββ AUTH HEADERS ββββββββββββββββββββββββββββββββββββββββββββββββββ
+function authHeaders(extra) {
+ return Object.assign(
+ {
+ "Content-Type": "application/json",
+ apikey: SB_KEY,
+ "X-Client-Info": "prompt-ai/1.0",
+ },
+ extra || {},
+ );
+}
+
+function isLocalFile() {
+ return window.location.protocol === "file:";
+}
+
+function corsError(errEl) {
+ showErr(
+ errEl,
+ "β οΈ This file is opened locally (file://). Supabase requires a real URL. " +
+ "Please host the file first β upload to Netlify Drop (netlify.com/drop) " +
+ "or GitHub Pages, then try again.",
+ );
+}
+
+// βββ AUTH β EMAIL/PASSWORD via Supabase Auth REST ββββββββββββββββββ
+async function doLogin() {
+ const email = document
+ .getElementById("loginEmail")
+ .value.trim()
+ .toLowerCase();
+ const pass = document.getElementById("loginPass").value;
+ const err = document.getElementById("loginError");
+ err.style.display = "none";
+
+ if (!email || !pass) {
+ showErr(err, "Please fill in all fields.");
+ return;
+ }
+ if (isLocalFile()) {
+ corsError(err);
+ return;
+ }
+
+ const btn = document.getElementById("loginBtn");
+ btn.disabled = true;
+ btn.innerHTML = ' Signing in...';
+
+ try {
+ const r = await fetch(`${SB_URL}/auth/v1/token?grant_type=password`, {
+ method: "POST",
+ headers: authHeaders(),
+ body: JSON.stringify({ email, password: pass }),
+ });
+ const data = await r.json();
+
+ if (!r.ok) {
+ const msg =
+ data.error_description ||
+ data.msg ||
+ data.error ||
+ "Invalid email or password.";
+ showErr(
+ err,
+ msg === "Invalid login credentials"
+ ? "Wrong email or password. Try again."
+ : msg,
+ );
+ } else {
+ await saveSession(data);
+ }
+ } catch (e) {
+ showErr(
+ err,
+ "Could not reach the server. Make sure you're on a hosted URL, not a local file.",
+ );
+ }
+
+ btn.disabled = false;
+ btn.innerHTML = "Sign In";
+}
+
+async function doRegister() {
+ const name = document.getElementById("regName").value.trim();
+ const email = document
+ .getElementById("regEmail")
+ .value.trim()
+ .toLowerCase();
+ const pass = document.getElementById("regPass").value;
+ const err = document.getElementById("regError");
+ err.style.display = "none";
+
+ if (!name || !email || !pass) {
+ showErr(err, "Please fill in all fields.");
+ return;
+ }
+ if (pass.length < 6) {
+ showErr(err, "Password needs at least 6 characters.");
+ return;
+ }
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+ showErr(err, "That doesn't look like a valid email.");
+ return;
+ }
+ if (isLocalFile()) {
+ corsError(err);
+ return;
+ }
+
+ const btn = document.getElementById("regBtn");
+ btn.disabled = true;
+ btn.innerHTML = ' Creating account...';
+
+ try {
+ const r = await fetch(`${SB_URL}/auth/v1/signup`, {
+ method: "POST",
+ headers: authHeaders(),
+ body: JSON.stringify({
+ email,
+ password: pass,
+ data: { full_name: name },
+ }),
+ });
+ const data = await r.json();
+
+ if (!r.ok) {
+ const msg =
+ data.error_description ||
+ data.msg ||
+ data.error ||
+ "Registration failed.";
+ showErr(err, msg);
+ btn.disabled = false;
+ btn.innerHTML = "Create Account";
+ return;
+ }
+
+ if (data.access_token) {
+ await saveSession(data, name);
+ } else {
+ // Email confirmation required β show success
+ err.style.cssText =
+ "display:block;color:var(--success);background:rgba(52,211,153,0.08);border-color:rgba(52,211,153,0.2);";
+ err.textContent =
+ "β Account created! Check your email to confirm, then sign in.";
+ setTimeout(showLogin, 3500);
+ }
+ } catch (e) {
+ showErr(
+ err,
+ "Could not reach the server. Make sure you're on a hosted URL, not a local file.",
+ );
+ }
+
+ btn.disabled = false;
+ btn.innerHTML = "Create Account";
+}
+
+async function saveSession(authData, overrideName) {
+ const user = authData.user;
+ if (!user) return;
+
+ const name =
+ overrideName ||
+ user.user_metadata?.full_name ||
+ user.user_metadata?.name ||
+ user.email.split("@")[0];
+ const provider = user.app_metadata?.provider || "email";
+
+ // Save to our users table
+ await sbUpsert(
+ "users",
+ {
+ id: user.id,
+ name: name,
+ email: user.email,
+ provider: provider,
+ },
+ "id",
+ );
+
+ currentUser = { id: user.id, name, email: user.email, provider };
+ LS.set("pai_user", JSON.stringify(currentUser));
+ showApp();
+ showToast("Welcome, " + name.split(" ")[0] + "! β‘", "success");
+}
+
+async function handleOAuthCallback(hash) {
+ // Parse access_token from URL hash after Google OAuth redirect
+ const params = new URLSearchParams(hash.replace("#", ""));
+ const accessToken = params.get("access_token");
+ if (!accessToken) {
+ showAuth();
+ return;
+ }
+
+ try {
+ const r = await fetch(`${SB_URL}/auth/v1/user`, {
+ headers: { apikey: SB_KEY, Authorization: "Bearer " + accessToken },
+ });
+ const user = await r.json();
+ if (r.ok && user.id) {
+ // Clean URL
+ window.history.replaceState(
+ {},
+ document.title,
+ window.location.pathname,
+ );
+ await saveSession({ access_token: accessToken, user });
+ } else {
+ showAuth();
+ }
+ } catch (e) {
+ showAuth();
+ }
+}
+
+function doGoogleLogin() {
+ // Redirect to Supabase Google OAuth
+ const redirectTo = encodeURIComponent(
+ window.location.href.split("#")[0],
+ );
+ window.location.href = `${SB_URL}/auth/v1/authorize?provider=google&redirect_to=${redirectTo}`;
+}
+
+function doLogout() {
+ closeDropdown();
+ LS.remove("pai_user");
+ currentUser = null;
+ document.getElementById("userGoal").value = "";
+ document.getElementById("outputCard").classList.remove("visible");
+ document.getElementById("blockedMsg").style.display = "none";
+ if (timerInterval) {
+ clearInterval(timerInterval);
+ timerInterval = null;
+ }
+ showAuth();
+ showLogin();
+ showToast("Signed out. See you soon!", "");
+}
+
+// βββ USER UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function updateUserUI() {
+ if (!currentUser) return;
+ const initials = currentUser.name
+ .split(" ")
+ .map((w) => w[0])
+ .join("")
+ .toUpperCase()
+ .slice(0, 2);
+ document.getElementById("userAvatar").textContent = initials;
+ document.getElementById("userLabel").textContent =
+ currentUser.name.split(" ")[0];
+ document.getElementById("dropdownName").textContent = currentUser.name;
+ document.getElementById("dropdownEmail").textContent =
+ currentUser.email || "";
+}
+function toggleDropdown() {
+ dropdownOpen = !dropdownOpen;
+ document
+ .getElementById("userDropdown")
+ .classList.toggle("open", dropdownOpen);
+}
+function closeDropdown() {
+ dropdownOpen = false;
+ document.getElementById("userDropdown").classList.remove("open");
+}
+
+// βββ USAGE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function usageKey() {
+ return "pai_usage_" + (currentUser ? currentUser.id : "anon");
+}
+
+function getUsage() {
+ const r = LS.get(usageKey());
+ if (!r) return { count: 0, resetAt: null };
+ try {
+ return JSON.parse(r);
+ } catch (e) {
+ return { count: 0, resetAt: null };
+ }
+}
+
+function incrementUsage() {
+ let u = getUsage();
+ const now = Date.now();
+ if (!u.resetAt || now >= u.resetAt) {
+ u = { count: 1, resetAt: now + RESET_MS };
+ } else {
+ u.count++;
+ }
+ LS.set(usageKey(), JSON.stringify(u));
+ return u;
+}
+
+function checkUsage() {
+ const u = getUsage();
+ const now = Date.now();
+ if (!u.resetAt || now >= u.resetAt)
+ return { allowed: true, remaining: FREE_LIMIT, resetAt: null };
+ if (u.count >= FREE_LIMIT)
+ return { allowed: false, remaining: 0, resetAt: u.resetAt };
+ return {
+ allowed: true,
+ remaining: FREE_LIMIT - u.count,
+ resetAt: u.resetAt,
+ };
+}
+
+function updateUsageUI() {
+ const s = checkUsage();
+ const badge = document.getElementById("usageBadge");
+ document.getElementById("usageText").textContent =
+ s.remaining + " left";
+ badge.className = "usage-badge";
+ if (s.remaining === 1) badge.classList.add("low");
+ if (s.remaining === 0) badge.classList.add("empty");
+}
+
+// βββ GENERATE ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+async function generatePrompt() {
+ const goal = document.getElementById("userGoal").value.trim();
+ const blockedEl = document.getElementById("blockedMsg");
+ blockedEl.style.display = "none";
+
+ if (!goal) {
+ showToast("Tell us what you need first.", "error");
+ return;
+ }
+ if (isBanned(goal)) {
+ blockedEl.style.display = "block";
+ document.getElementById("outputCard").classList.remove("visible");
+ return;
+ }
+
+ const status = checkUsage();
+ if (!status.allowed) {
+ showGate(status.resetAt);
+ return;
+ }
+
+ const btn = document.getElementById("generateBtn");
+ const outputCard = document.getElementById("outputCard");
+ const outputEl = document.getElementById("promptOutput");
+ const tipBox = document.getElementById("tipBox");
+
+ tipBox.style.display = "none";
+ fullPromptText = "";
+ btn.disabled = true;
+ btn.innerHTML = ' Building...';
+ outputCard.classList.add("visible");
+ outputEl.innerHTML =
+ 'π Coming Soon
- Prompt AI Pro
- We're building the paid plan. Join now to lock in early bird pricing before we go public.
-
-
- β‘Unlimited generations, no daily cap
- πSave and revisit your best prompts
- π―Advanced modes, custom personas & more
- πEarly bird pricing β locked in at signup
-
-
-
-
-
+
+
-
+
-
+
+
+
+ π Coming Soon
+ Prompt AI Pro
+
+ We're building the paid plan. Join now to lock in early bird pricing
+ before we go public.
+
+
+
+
+ β‘Unlimited generations, no daily cap
+
+
+ πSave and revisit your best prompts
+
+
+ π―Advanced modes, custom personas & more
+
+
+ πEarly bird pricing β locked in at signup
+
+
+
+
+
+
+
+
+
+
+
+
+
π
+ You're on the list!
+
+ We'll hit you on WhatsApp and email the moment we launch. Early bird
+ pricing is yours. π
+
+
+
-
- π
- You're on the list!
- We'll hit you on WhatsApp and email the moment we launch. Early bird pricing is yours. π
-
- On it...