Sync Das project files for George collab
This commit is contained in:
parent
8e763ffa48
commit
95f4e0df03
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"lastUpdated": "2026-02-16T22:00:00-05:00",
|
||||
"updatedBy": "Buba (heartbeat 10PM 2/16: no changes. dec-004 still zero reactions (~5 days, 6 reminders sent). All gates unchanged: 6×Stage 19 blocked on dec-004, 31×Stage 6 held at design gate, 2×Stage 9 need creds, 1×Stage 7 design gate.)",
|
||||
"lastUpdated": "2026-02-17T04:00:00-05:00",
|
||||
"updatedBy": "Buba (heartbeat 4AM 2/17: no changes. dec-004 still zero reactions (~6 days). All gates unchanged: 6×Stage 19 blocked on dec-004, 31×Stage 6 held at design gate, 2×Stage 9 need creds, 1×Stage 7 design gate.)",
|
||||
"phases": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
16
memory/2026-02-17.md
Normal file
16
memory/2026-02-17.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Daily Log — 2026-02-17
|
||||
|
||||
## What We Worked On
|
||||
|
||||
### Upwork Proposal Submitted: Senior Full-Stack Engineer — Integration & Production Stabilisation
|
||||
- **1:31 AM** — Applied to reposted job by same UAE/Dubai client (5.0★, $4.5K spent)
|
||||
- Original job URL closed (~022023636271824508978), applied to repost (~022023642842201724978)
|
||||
- **Bid:** $500 fixed-price (matching client's budget)
|
||||
- **You'll receive:** $450 after 10% fee
|
||||
- **Connects spent:** 17 (126 remaining)
|
||||
- **Duration:** Less than 1 month
|
||||
- **Milestone:** "Full integration, production deployment, architecture docs, clean repo, all workflows end-to-end functional"
|
||||
- Cover letter included 3-phase deliverable: Architecture Audit → Integration/Debugging → Production Deployment/Handoff
|
||||
- 15-20 proposals already submitted by others, bid range $400-$500
|
||||
- No boost (not spending extra connects)
|
||||
- Proposal draft saved: `proposals/2026-02-17-fullstack-integration-stabilisation.md`
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
TheNicheQuiz.com — AI-Powered Niche Discovery for Health Insurance
|
||||
Rebuilt Feb 15, 2026 — Healthy Self-Employed Focus + Nano Banana Pro Images
|
||||
TheNicheQuiz.com — AI-Powered Niche Discovery for ANY Industry
|
||||
v2 — Broad multi-industry + Health Insurance vertical
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -9,6 +9,7 @@ import csv
|
||||
import io
|
||||
import uuid
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
@ -92,6 +93,26 @@ def generate_ad_image(prompt, campaign_id):
|
||||
import traceback; traceback.print_exc()
|
||||
return None
|
||||
|
||||
# --- Industries for Broad Mode ---
|
||||
INDUSTRIES = [
|
||||
"Real Estate", "E-commerce", "SaaS", "Health & Wellness", "Finance",
|
||||
"Legal", "Education", "Marketing", "Food & Beverage", "Travel",
|
||||
"Fitness", "Beauty", "Automotive", "Home Services", "Pet Care",
|
||||
"Tech/Software", "Manufacturing", "Nonprofit", "Construction", "Entertainment"
|
||||
]
|
||||
|
||||
# --- Health Insurance Segments (for self-employed healthy people) ---
|
||||
SEGMENTS = [
|
||||
{"id": "private-market", "title": "Private Health Plans (Off-Market)", "desc": "Skip the ACA marketplace entirely. Private plans that actually reward healthy people with lower premiums — no subsidies needed."},
|
||||
{"id": "aca-smart", "title": "ACA Plans — The Smart Way", "desc": "ACA plans aren't just for low-income. Learn how to navigate the marketplace like a pro and find plans that don't punish you for being healthy."},
|
||||
{"id": "private-vs-aca", "title": "Private vs. ACA — Side by Side", "desc": "Don't pick blindly. Compare private market plans against ACA options for YOUR situation. Healthy + self-employed = different math."},
|
||||
{"id": "hsa-hdhp", "title": "HSA + High-Deductible Strategy", "desc": "Turn your health into a tax-advantaged wealth-building tool. Pay less now, invest the rest. Works with both private and ACA plans."},
|
||||
{"id": "healthshare", "title": "Health Sharing Ministries", "desc": "Not insurance. Often 40-60% cheaper. Community-based cost sharing for healthy people who think differently about coverage."},
|
||||
{"id": "directprimary", "title": "Direct Primary Care + Catastrophic", "desc": "Skip the middleman. $80/mo gets you a real doctor. Pair with a private catastrophic plan for the what-ifs."},
|
||||
{"id": "freelancer", "title": "Freelancer & Gig Worker Plans", "desc": "1099 life means you're the HR department. Private plans, ACA tricks, and hybrid strategies that don't punish independence."},
|
||||
{"id": "group-of-one", "title": "Group Plans for Solo Businesses", "desc": "LLC or S-corp tricks that unlock group rates for a company of one. Often beats both ACA and private individual plans."},
|
||||
]
|
||||
|
||||
# --- CSS ---
|
||||
CSS = """
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
@ -108,6 +129,8 @@ a{color:var(--primary);text-decoration:none}
|
||||
.hero p{font-size:1.15rem;color:var(--muted);max-width:620px;margin-bottom:2rem;line-height:1.7}
|
||||
.hero-cta{display:inline-flex;align-items:center;gap:.5rem;padding:1rem 2.5rem;border-radius:12px;background:var(--gradient);color:#fff;font-size:1.1rem;font-weight:600;border:none;cursor:pointer;transition:all .3s;text-decoration:none}
|
||||
.hero-cta:hover{transform:translateY(-2px);box-shadow:0 8px 30px var(--primary-glow)}
|
||||
.hero-cta-secondary{display:inline-flex;align-items:center;gap:.5rem;padding:1rem 2.5rem;border-radius:12px;background:transparent;color:var(--primary);font-size:1rem;font-weight:600;border:2px solid var(--primary);cursor:pointer;transition:all .3s;text-decoration:none}
|
||||
.hero-cta-secondary:hover{background:var(--primary-glow);transform:translateY(-2px)}
|
||||
|
||||
.section{padding:5rem 2rem;max-width:1100px;margin:0 auto}
|
||||
.section-title{font-size:2rem;font-weight:700;text-align:center;margin-bottom:1rem}
|
||||
@ -136,6 +159,22 @@ a{color:var(--primary);text-decoration:none}
|
||||
.proof-stat .num{font-size:2.5rem;font-weight:800;background:var(--gradient);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.proof-stat .label{color:var(--muted);font-size:.9rem;margin-top:.25rem}
|
||||
|
||||
/* Industry picker */
|
||||
.industry-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.75rem;margin-bottom:2rem}
|
||||
.industry-pill{background:var(--card);border:2px solid var(--border);border-radius:12px;padding:.85rem 1rem;cursor:pointer;transition:all .25s;font-size:.95rem;text-align:center;font-weight:500}
|
||||
.industry-pill:hover{border-color:var(--primary);transform:translateY(-2px)}
|
||||
.industry-pill.selected{border-color:var(--primary);background:var(--primary-glow);color:var(--primary)}
|
||||
.custom-industry{margin-top:1rem}
|
||||
.custom-industry label{display:block;margin-bottom:.5rem;font-size:.9rem;color:var(--muted)}
|
||||
.custom-industry input{width:100%;padding:.85rem 1rem;border-radius:10px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-size:1rem;outline:none;transition:border .3s}
|
||||
.custom-industry input:focus{border-color:var(--primary)}
|
||||
|
||||
/* Vertical card */
|
||||
.vertical-card{background:linear-gradient(135deg,#00d4aa11,#7c5cff11);border:1px solid var(--primary);border-radius:20px;padding:2.5rem;margin:2rem auto;max-width:700px;text-align:center;transition:all .3s}
|
||||
.vertical-card:hover{transform:translateY(-3px);box-shadow:0 8px 30px var(--primary-glow)}
|
||||
.vertical-card h3{font-size:1.4rem;margin-bottom:.75rem;color:var(--primary)}
|
||||
.vertical-card p{color:var(--muted);margin-bottom:1.5rem;font-size:1rem;line-height:1.7}
|
||||
|
||||
/* Auth */
|
||||
.auth-wrap{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem}
|
||||
.auth-card{background:var(--card);border:1px solid var(--border);border-radius:20px;padding:3rem;width:100%;max-width:420px}
|
||||
@ -202,6 +241,11 @@ a{color:var(--primary);text-decoration:none}
|
||||
/* Image generation status */
|
||||
.img-generating{background:var(--surface);border:1px dashed var(--border);border-radius:8px;height:200px;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:.85rem}
|
||||
|
||||
/* Mode badge */
|
||||
.mode-badge{display:inline-block;padding:.25rem .75rem;border-radius:99px;font-size:.7rem;font-weight:600;margin-left:.5rem;vertical-align:middle}
|
||||
.mode-badge.broad{background:#7c5cff33;color:#7c5cff}
|
||||
.mode-badge.health{background:#00d4aa33;color:#00d4aa}
|
||||
|
||||
@media(max-width:768px){
|
||||
.hero h1{font-size:2rem}
|
||||
.steps,.features{grid-template-columns:1fr}
|
||||
@ -209,42 +253,166 @@ a{color:var(--primary);text-decoration:none}
|
||||
.campaigns-grid{grid-template-columns:1fr;padding:0 1rem 1rem}
|
||||
.dash-header{padding:1rem}
|
||||
.quiz-container{padding:0 1rem}
|
||||
.industry-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr))}
|
||||
}
|
||||
"""
|
||||
|
||||
# --- Health Insurance Segments (for self-employed healthy people) ---
|
||||
SEGMENTS = [
|
||||
{"id": "private-market", "title": "Private Health Plans (Off-Market)", "desc": "Skip the ACA marketplace entirely. Private plans that actually reward healthy people with lower premiums — no subsidies needed."},
|
||||
{"id": "aca-smart", "title": "ACA Plans — The Smart Way", "desc": "ACA plans aren't just for low-income. Learn how to navigate the marketplace like a pro and find plans that don't punish you for being healthy."},
|
||||
{"id": "private-vs-aca", "title": "Private vs. ACA — Side by Side", "desc": "Don't pick blindly. Compare private market plans against ACA options for YOUR situation. Healthy + self-employed = different math."},
|
||||
{"id": "hsa-hdhp", "title": "HSA + High-Deductible Strategy", "desc": "Turn your health into a tax-advantaged wealth-building tool. Pay less now, invest the rest. Works with both private and ACA plans."},
|
||||
{"id": "healthshare", "title": "Health Sharing Ministries", "desc": "Not insurance. Often 40-60% cheaper. Community-based cost sharing for healthy people who think differently about coverage."},
|
||||
{"id": "directprimary", "title": "Direct Primary Care + Catastrophic", "desc": "Skip the middleman. $80/mo gets you a real doctor. Pair with a private catastrophic plan for the what-ifs."},
|
||||
{"id": "freelancer", "title": "Freelancer & Gig Worker Plans", "desc": "1099 life means you're the HR department. Private plans, ACA tricks, and hybrid strategies that don't punish independence."},
|
||||
{"id": "group-of-one", "title": "Group Plans for Solo Businesses", "desc": "LLC or S-corp tricks that unlock group rates for a company of one. Often beats both ACA and private individual plans."},
|
||||
]
|
||||
|
||||
# --- Templates ---
|
||||
def page(content, script=''):
|
||||
def page(content, script='', title='TheNicheQuiz — AI-Powered Niche Discovery'):
|
||||
return render_template_string(
|
||||
'<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">'
|
||||
'<meta name="viewport" content="width=device-width,initial-scale=1.0">'
|
||||
'<title>TheNicheQuiz — Health Insurance for the Self-Employed</title>'
|
||||
'<title>{{ title }}</title>'
|
||||
'<style>{{ css|safe }}</style></head><body>{{ content|safe }}'
|
||||
'<script>{{ script|safe }}</script></body></html>',
|
||||
css=CSS, content=content, script=script
|
||||
css=CSS, content=content, script=script, title=title
|
||||
)
|
||||
|
||||
# --- Routes ---
|
||||
# ============ LANDING PAGES ============
|
||||
|
||||
@app.route('/')
|
||||
def landing():
|
||||
# Build industry pills for the picker section
|
||||
pills = ''
|
||||
for ind in INDUSTRIES:
|
||||
pills += f'<div class="industry-pill" onclick="selectIndustry(this, \'{ind}\')">{ind}</div>'
|
||||
|
||||
content = f"""
|
||||
<div class="hero">
|
||||
<div class="hero-badge">✨ Works for ANY industry</div>
|
||||
<h1>Find Your Most Profitable Micro-Niche in Under 2 Minutes</h1>
|
||||
<div class="subtitle">AI-powered niche discovery + 10 scroll-stopping ad campaigns</div>
|
||||
<p>Pick any industry. Our AI drills down through sub-niches and micro-niches to find the underserved audiences nobody is talking to — then generates 10 emotionally-charged ad campaigns with AI visuals, ready to launch on Meta.</p>
|
||||
<div style="display:flex;gap:1rem;flex-wrap:wrap;justify-content:center">
|
||||
<a href="/signup" class="hero-cta">Start the Quiz — Free →</a>
|
||||
<a href="/health-insurance" class="hero-cta-secondary">🏥 Health Insurance Vertical →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Pick Any Industry. We'll Find the Gold.</h2>
|
||||
<p class="section-sub">Choose from 20 popular industries or type your own. The AI does the rest — drilling down to hyper-specific micro-niches where the real money is.</p>
|
||||
<div class="industry-grid">{pills}</div>
|
||||
<div class="custom-industry" style="max-width:500px;margin:0 auto">
|
||||
<label>Or type your own industry:</label>
|
||||
<input type="text" id="custom-ind" placeholder="e.g., Sustainable Fashion, Crypto, Veterinary...">
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:2rem">
|
||||
<a href="/signup" class="hero-cta">Start Finding Niches →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="emotion-block">
|
||||
<h2>Why Micro-Niches Win</h2>
|
||||
<p>Everyone's fighting over the same broad audiences. Meanwhile, hyper-specific micro-niches have <strong>10x lower ad costs</strong> and <strong>5x higher conversion rates</strong>. The person who dominates "yoga mats" loses to the person who dominates "eco-friendly yoga mats for prenatal classes in Austin."</p>
|
||||
<p>Most people never go deep enough. They stop at the industry level and wonder why their ads don't convert. <strong>The money is in the micro-niche.</strong></p>
|
||||
<div class="callout">Industry → Sub-Niche → Micro-Niche → 10 Campaigns + AI Images. Done.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">How It Works</h2>
|
||||
<p class="section-sub">3 steps. Under 2 minutes. Walk away with 10 emotionally-charged ad campaigns + AI-generated visuals ready to launch.</p>
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<h3>Pick Your Industry</h3>
|
||||
<p>Choose from 20+ industries or type your own. Real Estate, SaaS, Fitness, Legal — whatever space you're in, we've got you.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<h3>AI Drills Down</h3>
|
||||
<p>Our AI generates sub-niches, then micro-niches — finding the specific underserved audiences with the highest opportunity and lowest competition.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<h3>Get 10 Campaigns + Visuals</h3>
|
||||
<p>Emotionally-resonant ad campaigns with headlines, copy, targeting, and AI-generated ad images. One-click CSV export to Meta Ads Manager.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">What You Get</h2>
|
||||
<p class="section-sub">Everything to launch a micro-niche ad campaign that actually makes people feel something.</p>
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<h3>→ Hyper-Specific Micro-Niches</h3>
|
||||
<p>Not "real estate marketing." Think "Instagram Reels strategy for luxury condo agents targeting remote workers relocating to Miami." That specific.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>→ 10 Emotionally-Charged Campaigns</h3>
|
||||
<p>Copy that makes your exact target audience stop scrolling. Each campaign speaks directly to their pain points, desires, and unspoken frustrations.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>→ AI-Generated Ad Visuals</h3>
|
||||
<p>Unique ad images for each campaign. No stock photos. No generic imagery. Real creative that matches your niche and grabs attention.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>→ Meta Ads CSV Export</h3>
|
||||
<p>One-click download formatted for Meta Ads Manager bulk upload. Quiz to running ads in under 10 minutes.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>→ Works for ANY Industry</h3>
|
||||
<p>From SaaS to pet care, finance to fitness. The AI adapts its niche-finding and copywriting to your specific market dynamics.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>→ Unlimited Runs</h3>
|
||||
<p>Run the quiz as many times as you want. Explore different industries, different angles, different micro-niches. Build a portfolio of campaigns.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="proof">
|
||||
<div class="proof-stat"><div class="num">20+</div><div class="label">Industries Supported</div></div>
|
||||
<div class="proof-stat"><div class="num">10</div><div class="label">Campaigns + Images</div></div>
|
||||
<div class="proof-stat"><div class="num"><2min</div><div class="label">Quiz Time</div></div>
|
||||
<div class="proof-stat"><div class="num">CSV</div><div class="label">Meta Ads Ready</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="vertical-card">
|
||||
<h3>🏥 Specialized: Health Insurance for the Self-Employed</h3>
|
||||
<p>Our flagship vertical. Pre-loaded with 8 health insurance strategies for healthy self-employed people who are tired of overpaying. Private market plans, ACA optimization, HSA hacks, health sharing — with emotionally-charged copy that speaks to the quiet frustration of subsidizing everyone else's healthcare.</p>
|
||||
<a href="/health-insurance" class="hero-cta">Explore Health Insurance Vertical →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align:center;padding:4rem 2rem">
|
||||
<a href="/signup" class="hero-cta">Find Your Micro-Niche — Free →</a>
|
||||
<p style="color:var(--muted);margin-top:1rem;font-size:.9rem">Free. No credit card. AI-powered niche discovery for any industry.</p>
|
||||
</div>
|
||||
|
||||
<footer style="text-align:center;padding:3rem 2rem;border-top:1px solid var(--border);color:var(--muted);font-size:.85rem">
|
||||
TheNicheQuiz.com — AI-Powered Niche Discovery<br>
|
||||
<a href="/login" style="color:var(--muted)">Login</a> · <a href="/signup" style="color:var(--muted)">Sign Up</a> · <a href="/health-insurance" style="color:var(--muted)">Health Insurance Vertical</a>
|
||||
</footer>
|
||||
"""
|
||||
script = """
|
||||
function selectIndustry(el, name) {
|
||||
document.querySelectorAll('.industry-pill').forEach(p => p.classList.remove('selected'));
|
||||
el.classList.add('selected');
|
||||
document.getElementById('custom-ind').value = '';
|
||||
}
|
||||
"""
|
||||
return page(content, script)
|
||||
|
||||
|
||||
@app.route('/health-insurance')
|
||||
def health_insurance_landing():
|
||||
content = """
|
||||
<div class="hero">
|
||||
<div class="hero-badge">Built for the self-employed</div>
|
||||
<h1>Stop Overpaying for Health Insurance You Barely Use</h1>
|
||||
<div class="subtitle">You're healthy. You're independent. You deserve better.</div>
|
||||
<p>You didn't quit your 9-to-5 to hand $800/month to a system designed for someone else. Whether it's private market plans, smarter ACA strategies, or alternatives most people don't know exist — our AI finds the micro-niches where healthy self-employed people save thousands. Then it builds you 10 scroll-stopping ad campaigns to own that space.</p>
|
||||
<a href="/signup" class="hero-cta">Find My Niche — Free →</a>
|
||||
<div style="display:flex;gap:1rem;flex-wrap:wrap;justify-content:center">
|
||||
<a href="/signup?mode=health" class="hero-cta">Find My Health Insurance Niche →</a>
|
||||
<a href="/" class="hero-cta-secondary">← All Industries</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
@ -293,7 +461,7 @@ def landing():
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>→ AI-Generated Ad Visuals</h3>
|
||||
<p>Nano Banana Pro creates unique ad images for each campaign. No stock photos. No generic healthcare imagery. Real creative that matches your niche.</p>
|
||||
<p>Unique ad images for each campaign. No stock photos. No generic healthcare imagery. Real creative that matches your niche.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>→ Meta Ads CSV Export</h3>
|
||||
@ -320,52 +488,66 @@ def landing():
|
||||
</div>
|
||||
|
||||
<div style="text-align:center;padding:4rem 2rem">
|
||||
<a href="/signup" class="hero-cta">Find My Health Insurance Niche →</a>
|
||||
<a href="/signup?mode=health" class="hero-cta">Find My Health Insurance Niche →</a>
|
||||
<p style="color:var(--muted);margin-top:1rem;font-size:.9rem">Free. No credit card. Built by people who also overpay for insurance.</p>
|
||||
</div>
|
||||
|
||||
<footer style="text-align:center;padding:3rem 2rem;border-top:1px solid var(--border);color:var(--muted);font-size:.85rem">
|
||||
TheNicheQuiz.com — AI-Powered Niche Discovery for Health Insurance<br>
|
||||
<a href="/login" style="color:var(--muted)">Login</a> · <a href="/signup" style="color:var(--muted)">Sign Up</a>
|
||||
<a href="/login" style="color:var(--muted)">Login</a> · <a href="/signup" style="color:var(--muted)">Sign Up</a> · <a href="/" style="color:var(--muted)">All Industries</a>
|
||||
</footer>
|
||||
"""
|
||||
return page(content)
|
||||
return page(content, title='TheNicheQuiz — Health Insurance for the Self-Employed')
|
||||
|
||||
# ============ AUTH ============
|
||||
|
||||
@app.route('/signup', methods=['GET'])
|
||||
def signup_page():
|
||||
content = """
|
||||
mode = request.args.get('mode', '')
|
||||
# Store mode in session for quiz redirect
|
||||
if mode == 'health':
|
||||
tagline = "Stop overpaying. Start owning a micro-market."
|
||||
redirect_url = '/quiz?mode=health'
|
||||
else:
|
||||
tagline = "AI-powered niche discovery for any industry. Let's go."
|
||||
redirect_url = '/quiz'
|
||||
|
||||
content = f"""
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-card">
|
||||
<h2>Let's Find Your Niche</h2>
|
||||
<div class="tagline">Stop overpaying. Start owning a micro-market.</div>
|
||||
<div class="tagline">{tagline}</div>
|
||||
<div id="error"></div>
|
||||
<form onsubmit="return doSignup(event)">
|
||||
<div class="form-group"><label>Email</label><input type="email" id="email" required placeholder="you@yourbusiness.com"></div>
|
||||
<div class="form-group"><label>Password</label><input type="password" id="password" required minlength="6" placeholder="6+ characters"></div>
|
||||
<button type="submit" class="btn">Create Account & Start Quiz</button>
|
||||
</form>
|
||||
<div class="auth-link">Already have an account? <a href="/login">Log in</a></div>
|
||||
<div class="auth-link">Already have an account? <a href="/login{'?mode=health' if mode == 'health' else ''}">Log in</a></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
script = """
|
||||
async function doSignup(e) {
|
||||
script = f"""
|
||||
async function doSignup(e) {{
|
||||
e.preventDefault();
|
||||
const resp = await fetch('/api/signup', {
|
||||
const resp = await fetch('/api/signup', {{
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({email: document.getElementById('email').value, password: document.getElementById('password').value})
|
||||
});
|
||||
headers: {{'Content-Type': 'application/json'}},
|
||||
body: JSON.stringify({{email: document.getElementById('email').value, password: document.getElementById('password').value}})
|
||||
}});
|
||||
const data = await resp.json();
|
||||
if (data.ok) { window.location.href = '/quiz'; }
|
||||
else { document.getElementById('error').innerHTML = '<div class="error-msg">' + data.error + '</div>'; }
|
||||
}
|
||||
if (data.ok) {{ window.location.href = '{redirect_url}'; }}
|
||||
else {{ document.getElementById('error').innerHTML = '<div class="error-msg">' + data.error + '</div>'; }}
|
||||
}}
|
||||
"""
|
||||
return page(content, script)
|
||||
|
||||
@app.route('/login', methods=['GET'])
|
||||
def login_page():
|
||||
content = """
|
||||
mode = request.args.get('mode', '')
|
||||
redirect_url = '/quiz?mode=health' if mode == 'health' else '/quiz'
|
||||
|
||||
content = f"""
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-card">
|
||||
<h2>Welcome Back</h2>
|
||||
@ -376,39 +558,88 @@ def login_page():
|
||||
<div class="form-group"><label>Password</label><input type="password" id="password" required></div>
|
||||
<button type="submit" class="btn">Log In</button>
|
||||
</form>
|
||||
<div class="auth-link">Don't have an account? <a href="/signup">Sign up free</a></div>
|
||||
<div class="auth-link">Don't have an account? <a href="/signup{'?mode=health' if mode == 'health' else ''}">Sign up free</a></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
script = """
|
||||
async function doLogin(e) {
|
||||
script = f"""
|
||||
async function doLogin(e) {{
|
||||
e.preventDefault();
|
||||
const resp = await fetch('/api/login', {
|
||||
const resp = await fetch('/api/login', {{
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({email: document.getElementById('email').value, password: document.getElementById('password').value})
|
||||
});
|
||||
headers: {{'Content-Type': 'application/json'}},
|
||||
body: JSON.stringify({{email: document.getElementById('email').value, password: document.getElementById('password').value}})
|
||||
}});
|
||||
const data = await resp.json();
|
||||
if (data.ok) { window.location.href = '/quiz'; }
|
||||
else { document.getElementById('error').innerHTML = '<div class="error-msg">' + data.error + '</div>'; }
|
||||
}
|
||||
if (data.ok) {{ window.location.href = '{redirect_url}'; }}
|
||||
else {{ document.getElementById('error').innerHTML = '<div class="error-msg">' + data.error + '</div>'; }}
|
||||
}}
|
||||
"""
|
||||
return page(content, script)
|
||||
|
||||
# ============ QUIZ PAGE ============
|
||||
|
||||
@app.route('/quiz')
|
||||
@login_required
|
||||
def quiz_page():
|
||||
opts = ''
|
||||
for s in SEGMENTS:
|
||||
opts += f'''<div class="option" onclick="selectOption(0, '{s["id"]}', '{s["title"]}')">\
|
||||
<div class="opt-title">{s["title"]}</div>\
|
||||
<div class="opt-desc">{s["desc"]}</div></div>'''
|
||||
mode = request.args.get('mode', 'broad') # 'health' or 'broad'
|
||||
|
||||
if mode == 'health':
|
||||
return _quiz_health()
|
||||
else:
|
||||
return _quiz_broad()
|
||||
|
||||
|
||||
def _quiz_broad():
|
||||
"""Broad mode quiz — pick industry first, then AI generates sub-niches and micro-niches."""
|
||||
pills = ''
|
||||
for ind in INDUSTRIES:
|
||||
safe = ind.replace("'", "\\'")
|
||||
pills += f'<div class="industry-pill" onclick="selectIndustryQuiz(this, \'{safe}\')">{ind}</div>'
|
||||
|
||||
content = f"""
|
||||
<div class="dash-header">
|
||||
<h1>TheNicheQuiz</h1>
|
||||
<div class="dash-nav">
|
||||
<a href="/dashboard">My Campaigns</a>
|
||||
<a href="/quiz?mode=health">Health Insurance Mode</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quiz-container">
|
||||
<div class="quiz-step active" id="step-0">
|
||||
<h2>What Industry Are You In?</h2>
|
||||
<p class="step-desc">Pick an industry or type your own. We'll use AI to find the most profitable, underserved micro-niches in your space.</p>
|
||||
<div class="industry-grid">{pills}</div>
|
||||
<div class="custom-industry">
|
||||
<label>Or type your own:</label>
|
||||
<input type="text" id="custom-industry" placeholder="e.g., Sustainable Fashion, Crypto, Veterinary..." oninput="onCustomIndustry(this)">
|
||||
</div>
|
||||
<div class="quiz-nav"><div></div><button class="quiz-btn primary" onclick="nextStep()">Find Sub-Niches →</button></div>
|
||||
</div>
|
||||
<div class="quiz-step" id="step-1"></div>
|
||||
<div class="quiz-step" id="step-2"></div>
|
||||
<div id="campaigns-area" style="display:none"></div>
|
||||
</div>
|
||||
"""
|
||||
return page(content, QUIZ_SCRIPT_BROAD, title='TheNicheQuiz — Find Your Niche')
|
||||
|
||||
|
||||
def _quiz_health():
|
||||
"""Health insurance mode quiz — pick from preset segments."""
|
||||
opts = ''
|
||||
for s in SEGMENTS:
|
||||
safe_title = s["title"].replace("'", "\\'")
|
||||
opts += f'''<div class="option" onclick="selectOption(0, '{s["id"]}', '{safe_title}')">\
|
||||
<div class="opt-title">{s["title"]}</div>\
|
||||
<div class="opt-desc">{s["desc"]}</div></div>'''
|
||||
|
||||
content = f"""
|
||||
<div class="dash-header">
|
||||
<h1>TheNicheQuiz <span class="mode-badge health">Health Insurance</span></h1>
|
||||
<div class="dash-nav">
|
||||
<a href="/dashboard">My Campaigns</a>
|
||||
<a href="/quiz">Broad Mode</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -424,11 +655,31 @@ def quiz_page():
|
||||
<div id="campaigns-area" style="display:none"></div>
|
||||
</div>
|
||||
"""
|
||||
return page(content, QUIZ_SCRIPT)
|
||||
return page(content, QUIZ_SCRIPT_HEALTH, title='TheNicheQuiz — Health Insurance Niches')
|
||||
|
||||
QUIZ_SCRIPT = """
|
||||
|
||||
# --- Quiz JS for BROAD mode ---
|
||||
QUIZ_SCRIPT_BROAD = """
|
||||
let currentStep = 0;
|
||||
let selections = {};
|
||||
let selectedIndustry = '';
|
||||
const QUIZ_MODE = 'broad';
|
||||
|
||||
function selectIndustryQuiz(el, name) {
|
||||
document.querySelectorAll('.industry-pill').forEach(p => p.classList.remove('selected'));
|
||||
el.classList.add('selected');
|
||||
selectedIndustry = name;
|
||||
document.getElementById('custom-industry').value = '';
|
||||
selections[0] = {value: name, title: name};
|
||||
}
|
||||
|
||||
function onCustomIndustry(input) {
|
||||
if (input.value.trim()) {
|
||||
document.querySelectorAll('.industry-pill').forEach(p => p.classList.remove('selected'));
|
||||
selectedIndustry = input.value.trim();
|
||||
selections[0] = {value: input.value.trim(), title: input.value.trim()};
|
||||
}
|
||||
}
|
||||
|
||||
function showStep(n) {
|
||||
document.querySelectorAll('.quiz-step').forEach(s => s.classList.remove('active'));
|
||||
@ -444,15 +695,23 @@ function selectOption(step, value, title) {
|
||||
}
|
||||
|
||||
function nextStep() {
|
||||
if (!selections[currentStep]) { alert('Pick one first!'); return; }
|
||||
if (currentStep === 0) loadSubNiches(selections[0].value, selections[0].title);
|
||||
else if (currentStep === 1) loadMicroNiches(selections[0].value, selections[1].value, selections[1].title);
|
||||
else if (currentStep === 2) generateCampaigns();
|
||||
if (currentStep === 0) {
|
||||
const custom = document.getElementById('custom-industry').value.trim();
|
||||
if (custom) { selections[0] = {value: custom, title: custom}; }
|
||||
if (!selections[0]) { alert('Pick an industry or type your own!'); return; }
|
||||
loadSubNiches_broad(selections[0].title);
|
||||
} else if (currentStep === 1) {
|
||||
if (!selections[1]) { alert('Pick one first!'); return; }
|
||||
loadMicroNiches_broad(selections[0].title, selections[1].value, selections[1].title);
|
||||
} else if (currentStep === 2) {
|
||||
if (!selections[2]) { alert('Pick one first!'); return; }
|
||||
generateCampaigns();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubNiches(segment, segmentTitle) {
|
||||
async function loadSubNiches_broad(industry) {
|
||||
const container = document.getElementById('step-1');
|
||||
container.innerHTML = '<div class="loading"><div class="spinner"></div><p>Finding underserved sub-niches in <strong>' + segmentTitle + '</strong>...</p></div>';
|
||||
container.innerHTML = '<div class="loading"><div class="spinner"></div><p>Finding underserved sub-niches in <strong>' + industry + '</strong>...</p></div>';
|
||||
container.classList.add('active');
|
||||
document.getElementById('step-0').classList.remove('active');
|
||||
currentStep = 1;
|
||||
@ -460,12 +719,12 @@ async function loadSubNiches(segment, segmentTitle) {
|
||||
const resp = await fetch('/api/sub-niches', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({segment, segment_title: segmentTitle})
|
||||
body: JSON.stringify({segment: industry, segment_title: industry, mode: 'broad'})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) { container.innerHTML = '<div class="loading"><p style="color:#ff6666">Error: ' + (data.error||'Unknown') + '</p><br><button class="quiz-btn" onclick="showStep(0)">← Try Again</button></div>'; return; }
|
||||
|
||||
let html = '<h2>Narrow It Down</h2><p class="step-desc">These are the underserved sub-niches where healthy self-employed people are being ignored.</p><div class="options">';
|
||||
let html = '<h2>Narrow It Down</h2><p class="step-desc">These are the most promising sub-niches in ' + industry + '. Pick the one that excites you most.</p><div class="options">';
|
||||
data.sub_niches.forEach(sn => {
|
||||
html += '<div class="option" onclick="selectOption(1, \\'' + sn.id + '\\', \\'' + sn.title.replace(/'/g, "\\\\'") + '\\')">';
|
||||
html += '<div class="opt-title">' + sn.title + '</div>';
|
||||
@ -476,7 +735,7 @@ async function loadSubNiches(segment, segmentTitle) {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
async function loadMicroNiches(segment, subNiche, subNicheTitle) {
|
||||
async function loadMicroNiches_broad(industry, subNiche, subNicheTitle) {
|
||||
const container = document.getElementById('step-2');
|
||||
container.innerHTML = '<div class="loading"><div class="spinner"></div><p>Drilling into <strong>' + subNicheTitle + '</strong>...</p><div class="sub">Finding the people nobody is talking to yet.</div></div>';
|
||||
container.classList.add('active');
|
||||
@ -486,12 +745,12 @@ async function loadMicroNiches(segment, subNiche, subNicheTitle) {
|
||||
const resp = await fetch('/api/micro-niches', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({segment, sub_niche: subNiche, sub_niche_title: subNicheTitle, segment_title: selections[0].title})
|
||||
body: JSON.stringify({segment: industry, sub_niche: subNiche, sub_niche_title: subNicheTitle, segment_title: industry, mode: 'broad'})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) { container.innerHTML = '<div class="loading"><p style="color:#ff6666">Error: ' + (data.error||'Unknown') + '</p><br><button class="quiz-btn" onclick="showStep(1)">← Try Again</button></div>'; return; }
|
||||
|
||||
let html = '<h2>Pick Your Micro-Niche</h2><p class="step-desc">These people need to hear from you. Pick the one that lights you up.</p><div class="options">';
|
||||
let html = '<h2>Pick Your Micro-Niche</h2><p class="step-desc">These are hyper-specific audiences with high opportunity. Pick the one that lights you up.</p><div class="options">';
|
||||
data.micro_niches.forEach(mn => {
|
||||
html += '<div class="option" onclick="selectOption(2, \\'' + mn.id + '\\', \\'' + mn.title.replace(/'/g, "\\\\'") + '\\')">';
|
||||
html += '<div class="opt-title">' + mn.title + '</div>';
|
||||
@ -514,7 +773,8 @@ async function generateCampaigns() {
|
||||
body: JSON.stringify({
|
||||
segment: selections[0].title,
|
||||
sub_niche: selections[1].title,
|
||||
micro_niche: selections[2].title
|
||||
micro_niche: selections[2].title,
|
||||
mode: QUIZ_MODE
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
@ -538,7 +798,6 @@ async function generateCampaigns() {
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
// Start image generation polling
|
||||
if (data.images_pending) {
|
||||
html += '<div id="img-poll" style="text-align:center;padding:1rem;color:var(--muted);font-size:.85rem"><div class="spinner" style="width:24px;height:24px;margin:0 auto .5rem"></div>Generating AI ad images (1 of 10)...</div>';
|
||||
}
|
||||
@ -581,6 +840,166 @@ async function pollImages(campaignId) {
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
# --- Quiz JS for HEALTH INSURANCE mode ---
|
||||
QUIZ_SCRIPT_HEALTH = """
|
||||
let currentStep = 0;
|
||||
let selections = {};
|
||||
const QUIZ_MODE = 'health';
|
||||
|
||||
function showStep(n) {
|
||||
document.querySelectorAll('.quiz-step').forEach(s => s.classList.remove('active'));
|
||||
const step = document.getElementById('step-' + n);
|
||||
if (step) step.classList.add('active');
|
||||
currentStep = n;
|
||||
}
|
||||
|
||||
function selectOption(step, value, title) {
|
||||
selections[step] = {value, title};
|
||||
document.querySelectorAll('#step-' + step + ' .option').forEach(o => o.classList.remove('selected'));
|
||||
event.currentTarget.classList.add('selected');
|
||||
}
|
||||
|
||||
function nextStep() {
|
||||
if (!selections[currentStep]) { alert('Pick one first!'); return; }
|
||||
if (currentStep === 0) loadSubNiches(selections[0].value, selections[0].title);
|
||||
else if (currentStep === 1) loadMicroNiches(selections[0].value, selections[1].value, selections[1].title);
|
||||
else if (currentStep === 2) generateCampaigns();
|
||||
}
|
||||
|
||||
async function loadSubNiches(segment, segmentTitle) {
|
||||
const container = document.getElementById('step-1');
|
||||
container.innerHTML = '<div class="loading"><div class="spinner"></div><p>Finding underserved sub-niches in <strong>' + segmentTitle + '</strong>...</p></div>';
|
||||
container.classList.add('active');
|
||||
document.getElementById('step-0').classList.remove('active');
|
||||
currentStep = 1;
|
||||
|
||||
const resp = await fetch('/api/sub-niches', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({segment, segment_title: segmentTitle, mode: 'health'})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) { container.innerHTML = '<div class="loading"><p style="color:#ff6666">Error: ' + (data.error||'Unknown') + '</p><br><button class="quiz-btn" onclick="showStep(0)">← Try Again</button></div>'; return; }
|
||||
|
||||
let html = '<h2>Narrow It Down</h2><p class="step-desc">These are the underserved sub-niches where healthy self-employed people are being ignored.</p><div class="options">';
|
||||
data.sub_niches.forEach(sn => {
|
||||
html += '<div class="option" onclick="selectOption(1, \\'' + sn.id + '\\', \\'' + sn.title.replace(/'/g, "\\\\'") + '\\')">';
|
||||
html += '<div class="opt-title">' + sn.title + '</div>';
|
||||
html += '<div class="opt-desc">' + sn.description + '</div></div>';
|
||||
});
|
||||
html += '</div><div class="quiz-nav"><button class="quiz-btn" onclick="showStep(0)">← Back</button>';
|
||||
html += '<button class="quiz-btn primary" onclick="nextStep()">Go Deeper →</button></div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
async function loadMicroNiches(segment, subNiche, subNicheTitle) {
|
||||
const container = document.getElementById('step-2');
|
||||
container.innerHTML = '<div class="loading"><div class="spinner"></div><p>Drilling into <strong>' + subNicheTitle + '</strong>...</p><div class="sub">Finding the people nobody is talking to yet.</div></div>';
|
||||
container.classList.add('active');
|
||||
document.getElementById('step-1').classList.remove('active');
|
||||
currentStep = 2;
|
||||
|
||||
const resp = await fetch('/api/micro-niches', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({segment, sub_niche: subNiche, sub_niche_title: subNicheTitle, segment_title: selections[0].title, mode: 'health'})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) { container.innerHTML = '<div class="loading"><p style="color:#ff6666">Error: ' + (data.error||'Unknown') + '</p><br><button class="quiz-btn" onclick="showStep(1)">← Try Again</button></div>'; return; }
|
||||
|
||||
let html = '<h2>Pick Your Micro-Niche</h2><p class="step-desc">These people need to hear from you. Pick the one that lights you up.</p><div class="options">';
|
||||
data.micro_niches.forEach(mn => {
|
||||
html += '<div class="option" onclick="selectOption(2, \\'' + mn.id + '\\', \\'' + mn.title.replace(/'/g, "\\\\'") + '\\')">';
|
||||
html += '<div class="opt-title">' + mn.title + '</div>';
|
||||
html += '<div class="opt-desc">' + mn.description + ' <strong style=\\'color:var(--primary)\\'>Opportunity: ' + mn.opportunity + '</strong></div></div>';
|
||||
});
|
||||
html += '</div><div class="quiz-nav"><button class="quiz-btn" onclick="showStep(1)">← Back</button>';
|
||||
html += '<button class="quiz-btn primary" onclick="nextStep()">Generate 10 Campaigns + Images →</button></div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
async function generateCampaigns() {
|
||||
const area = document.getElementById('campaigns-area');
|
||||
area.style.display = 'block';
|
||||
area.innerHTML = '<div class="loading"><div class="spinner"></div><p>Generating 10 emotionally-charged campaigns...</p><div class="sub">AI is writing copy + creating ad visuals. This takes 30-60 seconds.</div></div>';
|
||||
document.querySelectorAll('.quiz-step').forEach(s => s.classList.remove('active'));
|
||||
|
||||
const resp = await fetch('/api/generate-campaigns', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
segment: selections[0].title,
|
||||
sub_niche: selections[1].title,
|
||||
micro_niche: selections[2].title,
|
||||
mode: QUIZ_MODE
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) { area.innerHTML = '<div class="loading"><p style="color:#ff6666">Error: ' + (data.error||'Unknown') + '</p><br><a href="/quiz?mode=health" class="quiz-btn primary">Try Again</a></div>'; return; }
|
||||
|
||||
let html = '<div style="text-align:center;padding:2rem"><h2 style="font-size:1.6rem;margin-bottom:.5rem">Your 10 Campaigns</h2>';
|
||||
html += '<p style="color:var(--muted);margin-bottom:.5rem;font-size:.95rem">' + selections[0].title + ' → ' + selections[1].title + ' → ' + selections[2].title + '</p>';
|
||||
html += '<div style="display:flex;gap:1rem;justify-content:center;margin:1.5rem 0;flex-wrap:wrap">';
|
||||
html += '<a href="/api/export-csv/' + data.campaign_id + '" class="quiz-btn primary">Export CSV (Meta Ads) ↓</a>';
|
||||
html += '<a href="/dashboard" class="quiz-btn">My Campaigns →</a></div></div>';
|
||||
html += '<div class="campaigns-grid">';
|
||||
data.campaigns.forEach((c, i) => {
|
||||
let imgHtml = c.image_url ? '<img src="' + c.image_url + '" alt="' + c.headline + '">' : '<div class="img-generating">Image generating...</div>';
|
||||
let tags = (c.targeting || []).map(t => '<span class="tag">' + t + '</span>').join('');
|
||||
html += '<div class="campaign-card">' + imgHtml + '<div class="card-body">';
|
||||
html += '<h3>' + (i+1) + '. ' + c.headline + '</h3>';
|
||||
html += '<div class="meta">' + c.format + ' · ' + c.objective + '</div>';
|
||||
html += '<div class="body-text">' + c.body + '</div>';
|
||||
html += '<div class="tags">' + tags + '</div>';
|
||||
html += '<div class="cta-line">' + c.cta + '</div></div></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
if (data.images_pending) {
|
||||
html += '<div id="img-poll" style="text-align:center;padding:1rem;color:var(--muted);font-size:.85rem"><div class="spinner" style="width:24px;height:24px;margin:0 auto .5rem"></div>Generating AI ad images (1 of 10)...</div>';
|
||||
}
|
||||
area.innerHTML = html;
|
||||
|
||||
if (data.images_pending) {
|
||||
setTimeout(() => pollImages(data.campaign_id), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function pollImages(campaignId) {
|
||||
try {
|
||||
const resp = await fetch('/api/campaign-images/' + campaignId);
|
||||
const data = await resp.json();
|
||||
if (data.ok && data.images && data.images.length > 0) {
|
||||
const cards = document.querySelectorAll('.campaign-card');
|
||||
data.images.forEach((img, i) => {
|
||||
if (img && cards[i]) {
|
||||
const placeholder = cards[i].querySelector('.img-generating');
|
||||
if (placeholder) {
|
||||
const newImg = document.createElement('img');
|
||||
newImg.src = img;
|
||||
newImg.alt = 'Campaign ' + (i+1);
|
||||
placeholder.replaceWith(newImg);
|
||||
}
|
||||
}
|
||||
});
|
||||
const poll = document.getElementById('img-poll');
|
||||
if (data.all_done) {
|
||||
if (poll) poll.innerHTML = '<span style="color:var(--primary)">All 10 images generated!</span>';
|
||||
setTimeout(() => { if(poll) poll.remove(); }, 3000);
|
||||
} else {
|
||||
if (poll) poll.innerHTML = '<div class="spinner" style="width:24px;height:24px;margin:0 auto .5rem"></div>Generating AI ad images (' + data.count + ' of ' + data.total + ')...';
|
||||
setTimeout(() => pollImages(campaignId), 5000);
|
||||
}
|
||||
} else {
|
||||
setTimeout(() => pollImages(campaignId), 3000);
|
||||
}
|
||||
} catch(e) { console.error(e); setTimeout(() => pollImages(campaignId), 5000); }
|
||||
}
|
||||
"""
|
||||
|
||||
# ============ DASHBOARD & CAMPAIGN VIEW ============
|
||||
|
||||
@app.route('/dashboard')
|
||||
@login_required
|
||||
def dashboard():
|
||||
@ -595,8 +1014,10 @@ def dashboard():
|
||||
for c in campaigns:
|
||||
data = c['campaign_data'] if isinstance(c['campaign_data'], dict) else json.loads(c['campaign_data']) if c['campaign_data'] else {}
|
||||
count = len(data.get('campaigns', []))
|
||||
mode = data.get('mode', 'broad')
|
||||
badge = '<span class="mode-badge health">Health</span>' if mode == 'health' else '<span class="mode-badge broad">Broad</span>'
|
||||
cards += f"""<div class="campaign-card"><div class="card-body">
|
||||
<h3>{c['micro_niche'] or c['name'] or 'Untitled'}</h3>
|
||||
<h3>{c['micro_niche'] or c['name'] or 'Untitled'} {badge}</h3>
|
||||
<div class="meta">{c['industry'] or ''} → {c['sub_niche'] or ''} · {count} campaigns · {c['created_at'].strftime('%b %d') if c['created_at'] else ''}</div>
|
||||
</div><div class="campaign-actions">
|
||||
<a href="/campaign/{c['id']}" class="primary">View</a>
|
||||
@ -607,7 +1028,7 @@ def dashboard():
|
||||
cards = '<div style="text-align:center;padding:4rem;color:var(--muted)"><p>No campaigns yet.</p><a href="/quiz" class="hero-cta" style="margin-top:1rem;display:inline-flex">Take the Quiz →</a></div>'
|
||||
|
||||
content = f"""
|
||||
<div class="dash-header"><h1>TheNicheQuiz</h1><div class="dash-nav"><a href="/quiz">New Quiz</a><a href="/logout">Logout</a></div></div>
|
||||
<div class="dash-header"><h1>TheNicheQuiz</h1><div class="dash-nav"><a href="/quiz">New Quiz (Any Industry)</a><a href="/quiz?mode=health">Health Insurance Quiz</a><a href="/logout">Logout</a></div></div>
|
||||
<div style="padding:2rem;max-width:1100px;margin:0 auto">
|
||||
<h2 style="margin-bottom:1.5rem">Your Campaigns</h2>
|
||||
<div class="campaigns-grid" style="padding:0">{cards}</div>
|
||||
@ -621,7 +1042,6 @@ def view_campaign(cid):
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute("SELECT * FROM campaigns WHERE id = %s AND user_id = %s", (cid, session['user_id']))
|
||||
c = cur.fetchone()
|
||||
# Get images
|
||||
cur.execute("SELECT * FROM generated_images WHERE campaign_id = %s ORDER BY id", (cid,))
|
||||
images = cur.fetchall()
|
||||
cur.close()
|
||||
@ -705,11 +1125,15 @@ def api_login():
|
||||
session['email'] = email
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/api/sub-niches', methods=['POST'])
|
||||
@login_required
|
||||
def api_sub_niches():
|
||||
data = request.json
|
||||
segment_title = data.get('segment_title', '')
|
||||
mode = data.get('mode', 'broad')
|
||||
|
||||
if mode == 'health':
|
||||
prompt = f"""You are a health insurance strategist for HEALTHY SELF-EMPLOYED people who earn too much for ACA subsidies and don't have chronic conditions. They are independent, proud, and smart with money — but they're overpaying for coverage they barely use.
|
||||
|
||||
Given the strategy "{segment_title}", generate exactly 6 underserved sub-niches within this area.
|
||||
@ -726,6 +1150,24 @@ For each, return:
|
||||
- title: Clear specific sub-niche (include the TYPE of self-employed person)
|
||||
- description: 1-2 sentences. Why this specific group is underserved and how much they could save. Mention private vs ACA where relevant.
|
||||
|
||||
Return ONLY valid JSON array, no markdown:
|
||||
[{{"id":"example","title":"Sub-Niche Name","description":"Why underserved..."}}]"""
|
||||
else:
|
||||
prompt = f"""You are a niche marketing strategist who finds underserved, profitable micro-audiences.
|
||||
|
||||
Given the industry "{segment_title}", generate exactly 6 underserved sub-niches within this space.
|
||||
|
||||
CRITICAL RULES:
|
||||
- Each sub-niche should represent a distinct angle, audience segment, or strategy within {segment_title}
|
||||
- Focus on sub-niches where there is real demand but low competition — the gaps most marketers miss
|
||||
- Be specific: include audience types, use cases, business models, or market segments
|
||||
- Think about WHO the customer is, WHAT specific problem they have, and WHY nobody is solving it well yet
|
||||
|
||||
For each, return:
|
||||
- id: kebab-case
|
||||
- title: Clear, specific sub-niche name
|
||||
- description: 1-2 sentences. Why this sub-niche is underserved and what the opportunity looks like.
|
||||
|
||||
Return ONLY valid JSON array, no markdown:
|
||||
[{{"id":"example","title":"Sub-Niche Name","description":"Why underserved..."}}]"""
|
||||
|
||||
@ -738,13 +1180,16 @@ Return ONLY valid JSON array, no markdown:
|
||||
except Exception as e:
|
||||
return jsonify({'ok': False, 'error': str(e)[:200]})
|
||||
|
||||
|
||||
@app.route('/api/micro-niches', methods=['POST'])
|
||||
@login_required
|
||||
def api_micro_niches():
|
||||
data = request.json
|
||||
segment_title = data.get('segment_title', '')
|
||||
sub_niche_title = data.get('sub_niche_title', '')
|
||||
mode = data.get('mode', 'broad')
|
||||
|
||||
if mode == 'health':
|
||||
prompt = f"""You are a micro-niche expert for HEALTHY SELF-EMPLOYED people overpaying for health insurance.
|
||||
|
||||
Strategy: "{segment_title}"
|
||||
@ -766,6 +1211,29 @@ Return:
|
||||
- description: Why this micro-niche is profitable and emotionally charged. Mention whether private or ACA tends to win for this group.
|
||||
- opportunity: "HIGH", "VERY HIGH", or "EXTREME"
|
||||
|
||||
ONLY valid JSON array, no markdown:
|
||||
[{{"id":"ex","title":"Specific Niche","description":"Why...","opportunity":"HIGH"}}]"""
|
||||
else:
|
||||
prompt = f"""You are a micro-niche marketing expert who finds hyper-specific, profitable audiences.
|
||||
|
||||
Industry: "{segment_title}"
|
||||
Sub-niche: "{sub_niche_title}"
|
||||
|
||||
Generate exactly 6 HYPER-SPECIFIC micro-niches. These must be so specific that a Facebook ad targeting them would make the person say "this is literally about ME."
|
||||
|
||||
RULES:
|
||||
- Each micro-niche should be a very specific combination of: audience type + situation + pain point + geography/context
|
||||
- Think: "Solo Shopify store owners in Austin who sell handmade candles and struggle with Meta ad ROAS under 2x"
|
||||
- The emotional hook: these people feel like no one in the {segment_title} space truly understands their specific situation
|
||||
- Include specific professions, business sizes, locations, life situations, or use cases
|
||||
- Focus on groups where advertising would have low competition and high emotional resonance
|
||||
|
||||
Return:
|
||||
- id: kebab-case
|
||||
- title: Very specific micro-niche name
|
||||
- description: Why this micro-niche is profitable and emotionally charged. What makes them underserved.
|
||||
- opportunity: "HIGH", "VERY HIGH", or "EXTREME"
|
||||
|
||||
ONLY valid JSON array, no markdown:
|
||||
[{{"id":"ex","title":"Specific Niche","description":"Why...","opportunity":"HIGH"}}]"""
|
||||
|
||||
@ -778,6 +1246,7 @@ ONLY valid JSON array, no markdown:
|
||||
except Exception as e:
|
||||
return jsonify({'ok': False, 'error': str(e)[:200]})
|
||||
|
||||
|
||||
@app.route('/api/generate-campaigns', methods=['POST'])
|
||||
@login_required
|
||||
def api_generate_campaigns():
|
||||
@ -785,8 +1254,10 @@ def api_generate_campaigns():
|
||||
segment = data.get('segment', '')
|
||||
sub_niche = data.get('sub_niche', '')
|
||||
micro_niche = data.get('micro_niche', '')
|
||||
mode = data.get('mode', 'broad')
|
||||
campaign_name = f"{micro_niche[:200]}" if micro_niche else f"{sub_niche[:200]}"
|
||||
|
||||
if mode == 'health':
|
||||
prompt = f"""You are an elite Facebook/Instagram ad copywriter specializing in health insurance for HEALTHY SELF-EMPLOYED people.
|
||||
|
||||
Micro-niche: {micro_niche}
|
||||
@ -824,6 +1295,43 @@ For each campaign, include an "image_prompt" that describes a TEXT-HEAVY ad crea
|
||||
- The overall emotional tone of the visual (anger, empowerment, relief, curiosity)
|
||||
The text ON the image should be the STAR. It should feel like a personal callout that makes the ICP say "are they talking about ME?"
|
||||
|
||||
Return ONLY valid JSON array:
|
||||
[{{"headline":"...","body":"...","cta":"...","format":"...","objective":"...","targeting":["..."],"image_prompt":"..."}}]"""
|
||||
else:
|
||||
prompt = f"""You are an elite Facebook/Instagram ad copywriter who creates scroll-stopping campaigns for hyper-specific micro-niches.
|
||||
|
||||
Industry: {segment}
|
||||
Sub-niche: {sub_niche}
|
||||
Micro-niche: {micro_niche}
|
||||
Path: {segment} → {sub_niche} → {micro_niche}
|
||||
|
||||
AUDIENCE: People in this exact micro-niche who feel like no marketer truly understands their specific situation, challenges, and goals. They've seen generic ads a thousand times. They scroll past everything — until something speaks DIRECTLY to them.
|
||||
|
||||
CREATE 10 ad campaigns. Each must make the reader FEEL something powerful:
|
||||
- The frustration of being misunderstood by generic solutions in their space
|
||||
- The excitement of discovering something built specifically for THEM
|
||||
- The "wait, someone actually gets my situation?" moment
|
||||
- The fear of missing out on an opportunity their competitors might find first
|
||||
- The pride of being part of a specific community/niche
|
||||
- The relief of finally finding the right approach for their exact needs
|
||||
|
||||
RULES:
|
||||
- Headlines: Under 40 chars. Punchy. Personal. Make them stop scrolling.
|
||||
- Body: 2-3 sentences. Conversational. Like talking to a friend who's in the same industry and GETS IT. NOT corporate. NOT salesy. Real talk that resonates with this specific audience.
|
||||
- Every campaign must feel like it was written by someone who deeply understands the {segment} industry and specifically this micro-niche
|
||||
- Mix formats: Single Image, Carousel, Video, Story, Reel
|
||||
- Mix objectives: Lead Gen, Traffic, Conversions, Awareness
|
||||
- Targeting: 3-4 hyper-specific criteria per campaign
|
||||
- Use industry-specific language, pain points, and aspirations
|
||||
|
||||
IMAGE PROMPTS — THIS IS CRITICAL:
|
||||
For each campaign, include an "image_prompt" that describes a TEXT-HEAVY ad creative where 60-80% of the image is BOLD TEXT. The image should look like a finished, scroll-stopping Facebook ad. Describe:
|
||||
- The EXACT large text that should appear on the image (a direct callout to the target audience)
|
||||
- The color scheme (high contrast — dark bg with bright text, or bold color blocks)
|
||||
- A small supporting visual element (icon, silhouette, or lifestyle image taking up 20-30% of the space)
|
||||
- The overall emotional tone of the visual
|
||||
The text ON the image should be the STAR. It should feel like a personal callout that makes the target person say "are they talking about ME?"
|
||||
|
||||
Return ONLY valid JSON array:
|
||||
[{{"headline":"...","body":"...","cta":"...","format":"...","objective":"...","targeting":["..."],"image_prompt":"..."}}]"""
|
||||
|
||||
@ -833,18 +1341,18 @@ Return ONLY valid JSON array:
|
||||
if text.startswith('```'): text = text.split('\n',1)[1].rsplit('```',1)[0].strip()
|
||||
campaigns = json.loads(text)[:10]
|
||||
|
||||
# Save to DB (use existing schema with name column)
|
||||
# Save to DB
|
||||
conn = get_db()
|
||||
cur = conn.cursor()
|
||||
campaign_data = {'campaigns': campaigns, 'mode': mode}
|
||||
cur.execute(
|
||||
"INSERT INTO campaigns (user_id, name, industry, sub_niche, micro_niche, campaign_data) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id",
|
||||
(session['user_id'], campaign_name, segment, sub_niche, micro_niche, json.dumps({'campaigns': campaigns}))
|
||||
(session['user_id'], campaign_name, segment, sub_niche, micro_niche, json.dumps(campaign_data))
|
||||
)
|
||||
campaign_id = cur.fetchone()[0]
|
||||
conn.commit()
|
||||
cur.close(); conn.close()
|
||||
|
||||
# Return campaigns immediately — images generated on-demand via separate endpoint
|
||||
for c in campaigns:
|
||||
c['image_url'] = None
|
||||
|
||||
@ -855,17 +1363,23 @@ Return ONLY valid JSON array:
|
||||
traceback.print_exc()
|
||||
return jsonify({'ok': False, 'error': str(e)[:300]})
|
||||
|
||||
# Background image generation (one at a time)
|
||||
import threading
|
||||
|
||||
# ============ BACKGROUND IMAGE GENERATION ============
|
||||
|
||||
_image_gen_lock = threading.Lock()
|
||||
_image_gen_active = {} # campaign_id -> True if generating
|
||||
|
||||
def _bg_generate_image(campaign_id, user_id, camp_index, camps):
|
||||
def _bg_generate_image(campaign_id, user_id, camp_index, camps, mode='broad'):
|
||||
"""Generate one image in background thread."""
|
||||
try:
|
||||
camp = camps[camp_index]
|
||||
img_prompt = camp.get('image_prompt', 'Health insurance ad')
|
||||
img_prompt = camp.get('image_prompt', 'Bold ad creative')
|
||||
|
||||
if mode == 'health':
|
||||
full_prompt = f"Create a bold, eye-catching Facebook ad image that is 60-80% large text. The text should directly call out the target audience and make them stop scrolling. Style: modern, high contrast, vibrant colors (dark background with bright text OR bold color blocks). The text should be the MAIN element — big, punchy, impossible to ignore. Small lifestyle image or icon in the corner as supporting visual. Think Instagram quote card meets direct response ad. Here's the concept: {img_prompt}"
|
||||
else:
|
||||
full_prompt = f"Create a bold, eye-catching Facebook ad image that is 60-80% large text. The text should directly call out the target audience and make them stop scrolling. Style: modern, high contrast, vibrant colors (dark background with bright text OR bold color blocks). Professional and industry-appropriate. The text should be the MAIN element — big, punchy, impossible to ignore. Small supporting visual element (icon, silhouette, or relevant imagery) in the corner. Think Instagram quote card meets direct response ad. Here's the concept: {img_prompt}"
|
||||
|
||||
filename = generate_ad_image(full_prompt, campaign_id)
|
||||
if filename:
|
||||
conn = get_db()
|
||||
@ -882,6 +1396,7 @@ def _bg_generate_image(campaign_id, user_id, camp_index, camps):
|
||||
finally:
|
||||
_image_gen_active.pop(campaign_id, None)
|
||||
|
||||
|
||||
@app.route('/api/campaign-images/<int:cid>')
|
||||
@login_required
|
||||
def api_campaign_images(cid):
|
||||
@ -895,6 +1410,7 @@ def api_campaign_images(cid):
|
||||
|
||||
data = c['campaign_data'] if isinstance(c['campaign_data'], dict) else json.loads(c['campaign_data']) if c['campaign_data'] else {}
|
||||
camps = data.get('campaigns', [])
|
||||
mode = data.get('mode', 'broad')
|
||||
total = len(camps)
|
||||
|
||||
cur.execute("SELECT filename FROM generated_images WHERE campaign_id = %s ORDER BY id", (cid,))
|
||||
@ -906,12 +1422,13 @@ def api_campaign_images(cid):
|
||||
if existing_count < total and cid not in _image_gen_active:
|
||||
_image_gen_active[cid] = True
|
||||
uid = session['user_id']
|
||||
t = threading.Thread(target=_bg_generate_image, args=(cid, uid, existing_count, camps), daemon=True)
|
||||
t = threading.Thread(target=_bg_generate_image, args=(cid, uid, existing_count, camps, mode), daemon=True)
|
||||
t.start()
|
||||
|
||||
img_urls = [f"/images/{img['filename']}" for img in existing]
|
||||
return jsonify({'ok': True, 'images': img_urls, 'all_done': existing_count >= total, 'count': existing_count, 'total': total, 'generating': cid in _image_gen_active})
|
||||
|
||||
|
||||
@app.route('/api/export-csv/<int:cid>')
|
||||
@login_required
|
||||
def api_export_csv(cid):
|
||||
@ -943,6 +1460,9 @@ def api_export_csv(cid):
|
||||
resp.headers['Content-Disposition'] = f'attachment; filename=nichequiz-{cid}-meta-ads.csv'
|
||||
return resp
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f"TheNicheQuiz running on http://localhost:{PORT}")
|
||||
print(f"TheNicheQuiz v2 running on http://localhost:{PORT}")
|
||||
print(f" Broad mode: http://localhost:{PORT}/")
|
||||
print(f" Health Insurance: http://localhost:{PORT}/health-insurance")
|
||||
app.run(host='0.0.0.0', port=PORT, debug=False)
|
||||
|
||||
57
proposals/2026-02-17-fullstack-integration-stabilisation.md
Normal file
57
proposals/2026-02-17-fullstack-integration-stabilisation.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Senior Full-Stack Engineer – Integration & Production Stabilisation
|
||||
|
||||
**URL:** https://www.upwork.com/jobs/~022023642842201724978
|
||||
**Budget:** $500 fixed-price
|
||||
**Bid Range:** High $500 | Avg $490.63 | Low $400
|
||||
**Client:** UAE/Dubai, 5.0★, $4.5K spent, 15 jobs, 67% hire rate, Payment verified
|
||||
**Proposals:** 15-20
|
||||
**Connect Cost:** 17
|
||||
**Posted:** Feb 17, 2026
|
||||
|
||||
## Cover Letter
|
||||
|
||||
I can start immediately and deliver a fully working, production-deployed platform within 24-48 hours.
|
||||
|
||||
I've done exactly this kind of work many times — inheriting near-complete builds and getting them across the finish line. My recent projects include stabilising AI-enabled platforms with Node.js/React/TypeScript stacks, PostgreSQL integration, and production deployment on PaaS platforms like Render, Vercel, and Railway.
|
||||
|
||||
Here's what I'll deliver:
|
||||
|
||||
DELIVERABLE — Production Stabilisation Sprint:
|
||||
|
||||
Phase 1 — Architecture Audit (2-3 hours):
|
||||
• Full codebase review and dependency audit
|
||||
• Map all database → backend → frontend data flows
|
||||
• Validate environment variables and API key configurations
|
||||
• Document current state and identify all breaking points
|
||||
|
||||
Phase 2 — Integration & Debugging (6-10 hours):
|
||||
• Connect PostgreSQL ↔ backend ↔ frontend cleanly
|
||||
• Fix routing, authentication, and state management issues
|
||||
• Validate and repair all API integrations
|
||||
• Ensure end-to-end workflow functionality:
|
||||
- Task creation and persistence
|
||||
- Calendar management
|
||||
- Daily briefing generation
|
||||
- Document retrieval
|
||||
- Multi-project workload handling
|
||||
|
||||
Phase 3 — Production Deployment & Handoff (2-3 hours):
|
||||
• Stabilise Render deployment with proper build/start scripts
|
||||
• Clean up GitHub repository (remove dead code, organise structure)
|
||||
• Updated README with deployment notes and architecture summary
|
||||
• List of any known limitations or recommended next steps
|
||||
|
||||
Total: 12-16 focused hours → fully working deployed platform.
|
||||
|
||||
I'm available for live collaboration throughout and can communicate on your schedule (I work US Eastern time but am flexible for urgent sprints).
|
||||
|
||||
Portfolio: https://portfolio.mcpengage.com
|
||||
|
||||
## Screening Questions
|
||||
(None visible on this posting)
|
||||
|
||||
## Notes
|
||||
- Client avg hourly rate paid is $10/hr — but this is fixed-price so less relevant
|
||||
- Good reviews from freelancers — "pleasure to work with", "communicates clearly"
|
||||
- This is a repost of ~022023636271824508978 which already closed
|
||||
- Bid at $500 (match high end, match the posted budget)
|
||||
48
proposals/2026-02-17-tech-ops-retellai-n8n.md
Normal file
48
proposals/2026-02-17-tech-ops-retellai-n8n.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Technical Operator / Automation (RetellAI, n8n, React, AWS)
|
||||
|
||||
**Submitted:** 2026-02-17 ~2:00 AM ET
|
||||
**URL:** https://www.upwork.com/jobs/~022023607751121504259
|
||||
**Rate:** $40/hr ($36/hr after 10% fee)
|
||||
**Connects:** 32 spent (94 remaining)
|
||||
**Rate Increase:** Never
|
||||
|
||||
## Client Details
|
||||
- **Location:** Newport Beach, CA
|
||||
- **Rating:** 4.99★ (19 reviews)
|
||||
- **Total Spent:** $42K
|
||||
- **Avg Hourly Rate Paid:** $17.37 (FLAG - pays low)
|
||||
- **Jobs Posted:** 63 (59% hire rate)
|
||||
- **Active Hires:** 7
|
||||
- **Payment Verified:** Yes
|
||||
- **Member Since:** Oct 2023
|
||||
|
||||
## Job Details
|
||||
- **Type:** Hourly, 30+ hrs/week, 6+ months, Expert level
|
||||
- **Budget Range:** $25-47/hr
|
||||
- **Contract-to-hire** opportunity
|
||||
- **Skills:** AI Agent Dev, Full-Stack, Web Dev, Node.js, AWS Lambda, React, MySQL
|
||||
- **Competition:** 50+ proposals, 6 interviewing, 28 invites sent
|
||||
|
||||
## Cover Letter
|
||||
|
||||
TECH OPS
|
||||
|
||||
I run the technical operations for a lead gen and AI automation consultancy - this is literally what I do every day.
|
||||
|
||||
What I've built that's directly relevant:
|
||||
|
||||
- Full n8n automation suites connecting voice AI, CRMs, and call tracking platforms (Ringba included). Dozens of production workflows handling lead routing, webhook processing, and API orchestration.
|
||||
|
||||
- RetellAI voice agent integrations - built conversational AI pipelines that connect to CRM systems and trigger downstream automations based on call outcomes.
|
||||
|
||||
- React frontends for lead capture and internal dashboards. Clean, conversion-focused, shipped fast.
|
||||
|
||||
- AWS infrastructure management - Lambda functions, S3, EC2 instances, CloudFront distributions. Comfortable managing the full stack.
|
||||
|
||||
- 30+ API integrations across CRMs (GoHighLevel, HubSpot, Salesforce), communication platforms, and data tools. When something breaks at 2am, I'm the one fixing it.
|
||||
|
||||
I'm self-sufficient. You describe the outcome, I figure out the implementation. I document everything as I go so systems are repeatable and hand-offable. If something is blocked, I surface it early with options - not excuses.
|
||||
|
||||
Available 30+ hrs/week, US East timezone (flexible hours). Happy to start with a paid trial project to prove the fit.
|
||||
|
||||
Portfolio with case studies: https://portfolio.mcpengage.com
|
||||
@ -1 +1 @@
|
||||
["19c69493d1a38ec5", "19c68c765ca1c2af", "19c6989a6cdc98c1", "19c69abbb1bf0874", "19c69b4e707c0d80"]
|
||||
["19c68c765ca1c2af","19c6989a6cdc98c1","19c69493d1a38ec5","19c69b4e707c0d80","19c69abbb1bf0874","19c69c0810320213","19c69d313e5fb490","19c69f03c3aec7e8","19c6a378efb18ccf","19c6a29eb41670d1","19c6a28eabd1d032","19c6a24229758135","19c6975099d10fb7","19c68b5583211f02","19c6874ab5ff3245","19c6845dd170d7b4","19c6aeb88160bed4","19c6aaa9072ee05e","19c6a78a3287106b","19c6a64f2b0ed3ab","19c6a4ebbd227620"]
|
||||
Loading…
x
Reference in New Issue
Block a user