feat: add landing page with beta signup + raise KB node limit to 100
Landing page at /landing with full marketing content: hero, features, pricing, testimonials, and beta email signup form. Beta signups email beta@resolutionflow.com via new public endpoint. Unauthenticated users redirect to landing instead of login. Also raises KB Accelerator node limit from 50 to 100 to accommodate dense troubleshooting articles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
31
backend/app/api/endpoints/beta_signup.py
Normal file
31
backend/app/api/endpoints/beta_signup.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Public beta signup endpoint — no auth required."""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from app.core.email import EmailService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/beta-signup", tags=["beta"])
|
||||
|
||||
|
||||
class BetaSignupRequest(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
|
||||
class BetaSignupResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
@router.post("", response_model=BetaSignupResponse)
|
||||
async def beta_signup(data: BetaSignupRequest):
|
||||
"""Collect beta interest — sends notification to beta@resolutionflow.com."""
|
||||
sent = await EmailService.send_beta_signup_notification(data.email)
|
||||
if not sent:
|
||||
logger.warning("Beta signup recorded (email delivery skipped): %s", data.email)
|
||||
return BetaSignupResponse(
|
||||
success=True,
|
||||
message="Thanks! We'll be in touch with beta access details.",
|
||||
)
|
||||
@@ -15,6 +15,7 @@ from app.api.endpoints import admin_survey
|
||||
from app.api.endpoints import tree_transfer
|
||||
from app.api.endpoints import ai_suggestions
|
||||
from app.api.endpoints import kb_accelerator
|
||||
from app.api.endpoints import beta_signup
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -54,3 +55,4 @@ api_router.include_router(admin_survey.router)
|
||||
api_router.include_router(tree_transfer.router)
|
||||
api_router.include_router(ai_suggestions.router)
|
||||
api_router.include_router(kb_accelerator.router)
|
||||
api_router.include_router(beta_signup.router)
|
||||
|
||||
@@ -151,9 +151,9 @@ def validate_generated_tree(tree: dict[str, Any]) -> list[str]:
|
||||
errors.append(
|
||||
f"Tree has only {node_count} nodes. Minimum 5 required for a useful tree."
|
||||
)
|
||||
if node_count > 50:
|
||||
if node_count > 100:
|
||||
errors.append(
|
||||
f"Tree has {node_count} nodes. Maximum 50 allowed."
|
||||
f"Tree has {node_count} nodes. Maximum 100 allowed."
|
||||
)
|
||||
if solution_count < 2:
|
||||
errors.append(
|
||||
|
||||
@@ -418,6 +418,72 @@ class EmailService:
|
||||
logger.exception("Failed to send survey copy email to %s", to_email)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def send_beta_signup_notification(
|
||||
signup_email: str,
|
||||
notify_email: str = "beta@resolutionflow.com",
|
||||
) -> bool:
|
||||
"""Notify beta@resolutionflow.com about a new beta signup. Fire-and-forget."""
|
||||
if not settings.email_enabled:
|
||||
logger.warning("Beta signup email not sent — RESEND_API_KEY not configured")
|
||||
return False
|
||||
|
||||
try:
|
||||
import resend
|
||||
import html as html_mod
|
||||
from datetime import datetime, timezone
|
||||
|
||||
resend.api_key = settings.RESEND_API_KEY
|
||||
|
||||
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||
safe_email = html_mod.escape(signup_email)
|
||||
subject = f"[ResolutionFlow Beta] New signup — {safe_email}"
|
||||
|
||||
email_html = f"""<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
||||
<body style="margin:0;padding:0;background:#101114;font-family:'Inter',Helvetica,Arial,sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background:#101114;padding:40px 0;">
|
||||
<tr><td align="center">
|
||||
<table width="560" cellpadding="0" cellspacing="0" style="background:#14161a;border:1px solid rgba(255,255,255,0.06);border-radius:16px;">
|
||||
<tr><td style="padding:40px 40px 24px;text-align:center;">
|
||||
<h1 style="margin:0;color:#f8fafc;font-size:24px;font-weight:600;">Resolution<span style="color:#06b6d4;">Flow</span></h1>
|
||||
<p style="margin:8px 0 0;color:#5a6170;font-size:14px;">New Beta Signup</p>
|
||||
</td></tr>
|
||||
<tr><td style="padding:0 40px 24px;">
|
||||
<p style="margin:0;color:#8891a0;font-size:16px;line-height:1.6;">
|
||||
A new user has requested beta access:
|
||||
</p>
|
||||
</td></tr>
|
||||
<tr><td style="padding:0 40px 24px;text-align:center;">
|
||||
<div style="background:rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:20px;">
|
||||
<p style="margin:0 0 4px;color:#5a6170;font-size:12px;text-transform:uppercase;letter-spacing:1px;">Email</p>
|
||||
<p style="margin:0;color:#22d3ee;font-size:18px;font-weight:600;">{safe_email}</p>
|
||||
</div>
|
||||
</td></tr>
|
||||
<tr><td style="padding:0 40px 32px;">
|
||||
<p style="margin:0;color:#5a6170;font-size:12px;text-align:center;">
|
||||
Submitted at {date_str}
|
||||
</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body></html>"""
|
||||
|
||||
resend.Emails.send({
|
||||
"from": settings.FROM_EMAIL,
|
||||
"to": [notify_email],
|
||||
"reply_to": signup_email,
|
||||
"subject": subject,
|
||||
"html": email_html,
|
||||
})
|
||||
logger.info("Beta signup notification sent for %s", signup_email)
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to send beta signup notification for %s", signup_email)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def send_survey_invite_email(
|
||||
to_email: str,
|
||||
|
||||
@@ -139,7 +139,7 @@ Return a JSON object with this structure:
|
||||
4. The first node is the root of the decision tree.
|
||||
5. All `next_node_id` and option `next_node_id` references must point to existing node IDs.
|
||||
6. Detect implicit branching logic (e.g., "If X, do Y; otherwise Z") and create decision nodes.
|
||||
7. Produce at least 3 nodes. Maximum 50 nodes.
|
||||
7. Produce at least 3 nodes. Maximum 100 nodes.
|
||||
8. Use high confidence (0.9+) for directly stated steps, medium (0.7-0.89) for reasonable inferences, low (<0.7) for significant interpretation.
|
||||
9. Return ONLY valid JSON — no markdown fences, no explanation text."""
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps)
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" state={{ from: location }} replace />
|
||||
return <Navigate to="/landing" state={{ from: location }} replace />
|
||||
}
|
||||
|
||||
// Enforce must_change_password — redirect unless already on /change-password
|
||||
|
||||
514
frontend/src/pages/LandingPage.tsx
Normal file
514
frontend/src/pages/LandingPage.tsx
Normal file
@@ -0,0 +1,514 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import '@/styles/landing.css'
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
||||
|
||||
export default function LandingPage() {
|
||||
const [navScrolled, setNavScrolled] = useState(false)
|
||||
const [betaEmail, setBetaEmail] = useState('')
|
||||
const [betaStatus, setBetaStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
|
||||
|
||||
// Nav scroll effect
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setNavScrolled(window.scrollY > 40)
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
// Scroll reveal
|
||||
useEffect(() => {
|
||||
const els = document.querySelectorAll('.landing-reveal')
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) entry.target.classList.add('visible')
|
||||
})
|
||||
},
|
||||
{ threshold: 0.15 }
|
||||
)
|
||||
els.forEach(el => observer.observe(el))
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
const handleBetaSubmit = useCallback(async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!betaEmail.trim() || betaStatus === 'sending') return
|
||||
setBetaStatus('sending')
|
||||
try {
|
||||
const resp = await fetch(`${API_URL}/api/v1/beta-signup`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: betaEmail }),
|
||||
})
|
||||
if (!resp.ok) throw new Error('Signup failed')
|
||||
setBetaStatus('sent')
|
||||
setBetaEmail('')
|
||||
} catch {
|
||||
setBetaStatus('error')
|
||||
setTimeout(() => setBetaStatus('idle'), 3000)
|
||||
}
|
||||
}, [betaEmail, betaStatus])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="ResolutionFlow — From Issue to Resolution, Documented"
|
||||
description="AI-guided decision trees that walk your engineers through troubleshooting and automatically document every step."
|
||||
/>
|
||||
|
||||
<div className="landing-page">
|
||||
<div className="landing-ambient-glow" />
|
||||
<div className="landing-grid-pattern" />
|
||||
|
||||
<div className="landing-page-content">
|
||||
{/* Navigation */}
|
||||
<nav className={`landing-nav ${navScrolled ? 'scrolled' : ''}`}>
|
||||
<div className="landing-nav-inner">
|
||||
<a href="#" className="landing-nav-logo">
|
||||
<div className="landing-nav-logo-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#000" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="5" r="2"/>
|
||||
<line x1="12" y1="7" x2="12" y2="11"/>
|
||||
<circle cx="6" cy="15" r="2"/>
|
||||
<circle cx="18" cy="15" r="2"/>
|
||||
<line x1="12" y1="11" x2="6" y2="13"/>
|
||||
<line x1="12" y1="11" x2="18" y2="13"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="landing-nav-wordmark">Resolution<span>Flow</span></div>
|
||||
</a>
|
||||
<ul className="landing-nav-links">
|
||||
<li><a href="#features">Features</a></li>
|
||||
<li><a href="#how-it-works">How It Works</a></li>
|
||||
<li><a href="#pricing">Pricing</a></li>
|
||||
</ul>
|
||||
<div className="landing-nav-cta">
|
||||
<Link to="/login" className="landing-btn-ghost">Sign In</Link>
|
||||
<Link to="/register" className="landing-btn-primary">Get Started Free</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero */}
|
||||
<section className="landing-hero">
|
||||
<div className="landing-hero-badge">Now in Beta — Join early access</div>
|
||||
<h1>
|
||||
Stop writing ticket notes.<br />
|
||||
<span className="landing-gradient-text">Start generating them.</span>
|
||||
</h1>
|
||||
<p className="landing-hero-sub">
|
||||
AI-guided decision trees that walk your engineers through troubleshooting — and automatically document every step, ready for your PSA ticket.
|
||||
</p>
|
||||
<div className="landing-hero-actions">
|
||||
<Link to="/register" className="landing-btn-hero-primary">Start Free</Link>
|
||||
<a href="#how-it-works" className="landing-btn-hero-secondary">See How It Works</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Social Proof Bar */}
|
||||
<div className="landing-social-proof-bar">
|
||||
<p>Built by MSP engineers, for MSP engineers</p>
|
||||
<div className="landing-proof-stats">
|
||||
<div className="landing-proof-stat">
|
||||
<div className="number">15+</div>
|
||||
<div className="label">Years MSP Experience</div>
|
||||
</div>
|
||||
<div className="landing-proof-stat">
|
||||
<div className="number">70%</div>
|
||||
<div className="label">Less Time on Documentation</div>
|
||||
</div>
|
||||
<div className="landing-proof-stat">
|
||||
<div className="number">0</div>
|
||||
<div className="label">Ticket Notes Written by Hand</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* App Preview */}
|
||||
<div className="landing-app-preview">
|
||||
<div className="landing-preview-window">
|
||||
<div className="landing-preview-titlebar">
|
||||
<div className="landing-preview-tab">
|
||||
<div className="landing-tab-icon" />
|
||||
ResolutionFlow
|
||||
<span className="landing-tab-close">×</span>
|
||||
</div>
|
||||
<div className="landing-preview-url-bar">
|
||||
<div className="landing-preview-url">
|
||||
<span className="landing-lock-icon">🔒</span>
|
||||
app.resolutionflow.com/editor
|
||||
</div>
|
||||
</div>
|
||||
<div className="landing-preview-window-controls">
|
||||
<div className="landing-win-btn">
|
||||
<svg viewBox="0 0 12 12"><line x1="2" y1="6" x2="10" y2="6"/></svg>
|
||||
</div>
|
||||
<div className="landing-win-btn">
|
||||
<svg viewBox="0 0 12 12"><rect x="2" y="2" width="8" height="8" rx="0.5"/></svg>
|
||||
</div>
|
||||
<div className="landing-win-btn close">
|
||||
<svg viewBox="0 0 12 12"><line x1="2" y1="2" x2="10" y2="10"/><line x1="10" y1="2" x2="2" y2="10"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="landing-preview-body">
|
||||
<div className="landing-preview-sidebar">
|
||||
<div className="landing-preview-sidebar-item active">
|
||||
<div className="dot" style={{ background: 'var(--cyan-400)' }} />
|
||||
Flow Editor
|
||||
</div>
|
||||
<div className="landing-preview-sidebar-item">
|
||||
<div className="dot" style={{ background: '#3b82f6' }} />
|
||||
Session Runner
|
||||
</div>
|
||||
<div className="landing-preview-sidebar-item">
|
||||
<div className="dot" style={{ background: '#22c55e' }} />
|
||||
Flow Library
|
||||
</div>
|
||||
<div className="landing-preview-sidebar-item">
|
||||
<div className="dot" style={{ background: '#f59e0b' }} />
|
||||
Session History
|
||||
</div>
|
||||
<div className="landing-preview-sidebar-item">
|
||||
<div className="dot" style={{ background: '#8b5cf6' }} />
|
||||
Team Analytics
|
||||
</div>
|
||||
</div>
|
||||
<div className="landing-preview-canvas">
|
||||
<div className="landing-mini-tree">
|
||||
<div className="landing-tree-node root">Outlook Not Syncing</div>
|
||||
<div className="landing-tree-connector" />
|
||||
<div className="landing-tree-branch">
|
||||
<div className="landing-tree-branch-arm">
|
||||
<div className="landing-tree-label">Yes</div>
|
||||
<div className="landing-tree-connector" />
|
||||
<div className="landing-tree-node decision">Check profile config</div>
|
||||
</div>
|
||||
<div className="landing-tree-branch-arm">
|
||||
<div className="landing-tree-label">No</div>
|
||||
<div className="landing-tree-connector" />
|
||||
<div className="landing-tree-node decision">Verify credentials</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ height: '5rem' }} />
|
||||
<div className="landing-section-divider" />
|
||||
|
||||
{/* Problem Section */}
|
||||
<section id="problem" className="landing-reveal">
|
||||
<div className="landing-section-inner">
|
||||
<div className="landing-section-label">The Problem</div>
|
||||
<div className="landing-section-title">Documentation is broken.<br />Everyone knows it.</div>
|
||||
<div className="landing-section-desc">
|
||||
Engineers don't want to write it. Managers hate chasing it. Clients never see it. The same issues get solved from scratch every time.
|
||||
</div>
|
||||
<div className="landing-problem-grid">
|
||||
<ProblemCard icon="⏱" color="red" title="15–25 min lost per ticket" description="Engineers spend more time documenting what they did than actually doing it. After a complex issue, writing notes is the last thing anyone wants to do." />
|
||||
<ProblemCard icon="📋" color="amber" title="Vague, useless notes" description={`"Fixed Outlook" tells you nothing. Documentation written under pressure tends toward generalities that help nobody the second time around.`} />
|
||||
<ProblemCard icon="🔄" color="slate" title="Knowledge walks out the door" description="When a senior engineer leaves, years of tribal knowledge disappear overnight. New hires spend months building up what was never captured." />
|
||||
<ProblemCard icon="🧠" color="violet" title="Context switching kills speed" description="Jumping between the issue, documentation tools, PSA tickets, and knowledge bases fragments focus and slows resolution." />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="landing-section-divider" />
|
||||
|
||||
{/* Brand Equation */}
|
||||
<div className="landing-equation-section landing-reveal">
|
||||
<div className="landing-section-label">The Answer</div>
|
||||
<div className="landing-brand-equation">
|
||||
<span className="landing-eq-item">Resolution</span>
|
||||
<span className="landing-eq-operator">+</span>
|
||||
<span className="landing-eq-item">Documentation</span>
|
||||
<span className="landing-eq-operator">−</span>
|
||||
<span className="landing-eq-item">Time</span>
|
||||
<span className="landing-eq-operator">=</span>
|
||||
<span className="landing-eq-result">ResolutionFlow</span>
|
||||
</div>
|
||||
<p className="landing-equation-desc">
|
||||
What if documentation was a <em>byproduct</em> of solving the issue — not a separate task? What if your engineers never had to write another ticket note?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="landing-section-divider" />
|
||||
|
||||
{/* How It Works */}
|
||||
<section id="how-it-works" className="landing-reveal">
|
||||
<div className="landing-section-inner">
|
||||
<div className="landing-section-label">How It Works</div>
|
||||
<div className="landing-section-title">Three steps. Zero note-writing.</div>
|
||||
<div className="landing-section-desc">
|
||||
Build once, run forever. Every session generates documentation automatically.
|
||||
</div>
|
||||
<div className="landing-steps-container">
|
||||
<div className="landing-step-card">
|
||||
<h3>Build a Flow</h3>
|
||||
<p>Use the visual Flow Editor to create branching decision trees for any troubleshooting scenario. Drag, connect, and enrich steps with commands, notes, and AI suggestions.</p>
|
||||
<div className="landing-step-visual">
|
||||
<div className="landing-mock-editor">
|
||||
<div className="landing-mock-node start">▶ Start</div>
|
||||
<div className="landing-mock-connector">→</div>
|
||||
<div className="landing-mock-node step">Check DNS</div>
|
||||
<div className="landing-mock-connector">→</div>
|
||||
<div className="landing-mock-node step">Yes / No?</div>
|
||||
<div className="landing-mock-connector">→</div>
|
||||
<div className="landing-mock-node start">✓ Resolved</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="landing-step-card">
|
||||
<h3>Run a Session</h3>
|
||||
<p>An engineer launches the flow on a live ticket. FlowPilot — your AI copilot — acts as a virtual senior engineer, guiding decisions and capturing every action in real time.</p>
|
||||
<div className="landing-step-visual">
|
||||
<div className="landing-mock-session">
|
||||
<div className="landing-mock-chat-line">
|
||||
<span className="label">FlowPilot:</span>
|
||||
<span className="text">Is the user on VPN?</span>
|
||||
</div>
|
||||
<div className="landing-mock-chat-line">
|
||||
<span className="label" style={{ color: 'var(--text-secondary)' }}>Engineer:</span>
|
||||
<span className="text">Yes, Cisco AnyConnect</span>
|
||||
</div>
|
||||
<div className="landing-mock-chat-line">
|
||||
<span className="label">FlowPilot:</span>
|
||||
<span className="text">Check split tunnel config →</span>
|
||||
</div>
|
||||
<div className="landing-mock-chat-line doc">
|
||||
<span className="label">Auto-doc:</span>
|
||||
<span className="text">Step captured ✓</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="landing-step-card">
|
||||
<h3>Export to Ticket</h3>
|
||||
<p>When the session ends, full documentation is generated — formatted for your PSA. Paste it directly into ConnectWise, Atera, or Syncro. Done.</p>
|
||||
<div className="landing-step-visual">
|
||||
<div className="landing-mock-ticket">
|
||||
<div className="landing-mock-ticket-header">ConnectWise Ticket #48291</div>
|
||||
<div className="landing-mock-ticket-line"><span className="time">10:04</span><span className="check">✓</span><span>Verified VPN connection active</span></div>
|
||||
<div className="landing-mock-ticket-line"><span className="time">10:06</span><span className="check">✓</span><span>Split tunnel misconfigured — fixed</span></div>
|
||||
<div className="landing-mock-ticket-line"><span className="time">10:08</span><span className="check">✓</span><span>Confirmed Outlook sync restored</span></div>
|
||||
<div className="landing-mock-ticket-line"><span className="time">10:09</span><span className="check">✓</span><span>Resolution: VPN split tunnel updated</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="landing-section-divider" />
|
||||
|
||||
{/* Features */}
|
||||
<section id="features" className="landing-reveal">
|
||||
<div className="landing-section-inner">
|
||||
<div className="landing-section-label">Features</div>
|
||||
<div className="landing-section-title">Everything your team needs to<br />resolve faster and document better.</div>
|
||||
<div className="landing-features-grid">
|
||||
<FeatureCard
|
||||
highlight
|
||||
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>}
|
||||
title="FlowPilot — Your AI Copilot"
|
||||
description="Like having a senior engineer on every call. FlowPilot suggests next steps, provides context-aware guidance, and automatically captures documentation as a byproduct of the troubleshooting session."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>}
|
||||
title="Visual Flow Editor"
|
||||
description="Build branching decision trees with a drag-and-drop canvas. Add steps, conditions, commands, and notes — no code required."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>}
|
||||
title="Auto-Documentation"
|
||||
description="Every session generates timestamped, detailed notes — formatted for your PSA. Engineers never write another ticket note."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>}
|
||||
title="Team Knowledge Sharing"
|
||||
description="Share flows across your team. When one engineer solves a new problem, the whole team benefits from that path — instantly."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>}
|
||||
title="Session History & Analytics"
|
||||
description="Track which flows are used most, identify bottlenecks, and see how your team resolves issues over time."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>}
|
||||
title="PSA Integration"
|
||||
description="Connect directly to ConnectWise, Atera, and Syncro. Export session docs straight to tickets — no copy-paste needed."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="landing-section-divider" />
|
||||
|
||||
{/* Pricing */}
|
||||
<section id="pricing" className="landing-reveal">
|
||||
<div className="landing-section-inner">
|
||||
<div className="landing-section-label">Pricing</div>
|
||||
<div className="landing-section-title">Simple pricing. No surprises.</div>
|
||||
<div className="landing-section-desc">Start free. Upgrade when your team is ready.</div>
|
||||
<div className="landing-pricing-grid">
|
||||
<PricingCard
|
||||
name="Free"
|
||||
target="For individual techs evaluating"
|
||||
amount="$0"
|
||||
note="Free forever"
|
||||
features={['3 decision trees', '20 sessions per month', 'Auto-documentation export', 'Session history (30 days)', 'Community support']}
|
||||
btnLabel="Get Started"
|
||||
btnStyle="outline"
|
||||
/>
|
||||
<PricingCard
|
||||
featured
|
||||
name="Pro"
|
||||
target="For small MSPs with 1–5 techs"
|
||||
amount="$15"
|
||||
period="/user/mo"
|
||||
note="Billed monthly or annually"
|
||||
features={['Unlimited decision trees', 'Unlimited sessions', 'FlowPilot AI copilot', 'Auto-documentation export', 'Full session history', 'Flow templates library', 'Priority support']}
|
||||
btnLabel="Start Free Trial"
|
||||
btnStyle="filled"
|
||||
/>
|
||||
<PricingCard
|
||||
name="Team"
|
||||
target="For growing MSPs with 5–25 techs"
|
||||
amount="$25"
|
||||
period="/user/mo"
|
||||
note="Billed monthly or annually"
|
||||
features={['Everything in Pro', 'PSA integration (ConnectWise, Atera, Syncro)', 'Team analytics dashboard', 'Session sharing & collaboration', 'Client context system', 'Role-based permissions', 'Dedicated support']}
|
||||
btnLabel="Start Free Trial"
|
||||
btnStyle="outline"
|
||||
/>
|
||||
</div>
|
||||
<p className="landing-pricing-enterprise">
|
||||
Need Enterprise (25+ techs, SSO, custom branding)?{' '}
|
||||
<a href="mailto:hello@resolutionflow.com">Contact us</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="landing-section-divider" />
|
||||
|
||||
{/* Testimonial */}
|
||||
<div className="landing-testimonial-section landing-reveal">
|
||||
<div className="landing-testimonial-quote">
|
||||
We used to spend more time writing ticket notes than solving the actual issue. Now it just… happens. The documentation writes itself while we work.
|
||||
</div>
|
||||
<div className="landing-testimonial-author">
|
||||
<strong>Beta Tester</strong> — MSP Engineer, Southeast US
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="landing-section-divider" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className="landing-cta-section landing-reveal">
|
||||
<h2>Ready to stop writing ticket notes?</h2>
|
||||
<p>Join the beta and see what happens when documentation becomes automatic.</p>
|
||||
<form className="landing-cta-email-form" onSubmit={handleBetaSubmit}>
|
||||
<input
|
||||
type="email"
|
||||
className="landing-cta-email-input"
|
||||
placeholder="you@yourmsp.com"
|
||||
value={betaEmail}
|
||||
onChange={e => setBetaEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<button type="submit" className="landing-btn-hero-primary" style={{ whiteSpace: 'nowrap' }} disabled={betaStatus === 'sending'}>
|
||||
{betaStatus === 'sending' ? 'Joining...' : betaStatus === 'sent' ? 'Joined!' : 'Join Beta'}
|
||||
</button>
|
||||
</form>
|
||||
{betaStatus === 'sent' && (
|
||||
<p className="landing-cta-success">Thanks! We'll be in touch with beta access details.</p>
|
||||
)}
|
||||
{betaStatus === 'error' && (
|
||||
<p className="landing-cta-error">Something went wrong. Please try again.</p>
|
||||
)}
|
||||
<p className="landing-cta-fine-print">Free to start. No credit card required.</p>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="landing-footer">
|
||||
<div className="landing-footer-inner">
|
||||
<div className="landing-footer-left">
|
||||
<div className="landing-nav-logo-icon" style={{ width: 28, height: 28, borderRadius: 8 }}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#000" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ width: 16, height: 16 }}>
|
||||
<circle cx="12" cy="5" r="2"/>
|
||||
<line x1="12" y1="7" x2="12" y2="11"/>
|
||||
<circle cx="6" cy="15" r="2"/>
|
||||
<circle cx="18" cy="15" r="2"/>
|
||||
<line x1="12" y1="11" x2="6" y2="13"/>
|
||||
<line x1="12" y1="11" x2="18" y2="13"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="landing-footer-copy">© 2026 ResolutionFlow. All rights reserved.</span>
|
||||
</div>
|
||||
<ul className="landing-footer-links">
|
||||
<li><a href="#">Privacy</a></li>
|
||||
<li><a href="#">Terms</a></li>
|
||||
<li><a href="mailto:hello@resolutionflow.com">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/* ---- Sub-components ---- */
|
||||
|
||||
function ProblemCard({ icon, color, title, description }: {
|
||||
icon: string; color: string; title: string; description: string
|
||||
}) {
|
||||
return (
|
||||
<div className="landing-problem-card">
|
||||
<div className={`landing-problem-icon ${color}`}>{icon}</div>
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FeatureCard({ icon, title, description, highlight }: {
|
||||
icon: React.ReactNode; title: string; description: string; highlight?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={`landing-feature-card ${highlight ? 'highlight' : ''}`}>
|
||||
<div className="landing-feature-icon">{icon}</div>
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PricingCard({ name, target, amount, period, note, features, btnLabel, btnStyle, featured }: {
|
||||
name: string; target: string; amount: string; period?: string; note: string
|
||||
features: string[]; btnLabel: string; btnStyle: 'outline' | 'filled'; featured?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={`landing-pricing-card ${featured ? 'featured' : ''}`}>
|
||||
<div className="landing-pricing-plan-name">{name}</div>
|
||||
<div className="landing-pricing-target">{target}</div>
|
||||
<div className="landing-pricing-price">
|
||||
<span className="amount">{amount}</span>
|
||||
{period && <span className="period">{period}</span>}
|
||||
</div>
|
||||
<div className="landing-pricing-note">{note}</div>
|
||||
<ul className="landing-pricing-features">
|
||||
{features.map(f => <li key={f}>{f}</li>)}
|
||||
</ul>
|
||||
<Link to="/register" className={`landing-pricing-btn ${btnStyle}`}>{btnLabel}</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@/pages'
|
||||
|
||||
// Public pages
|
||||
const LandingPage = lazy(() => import('@/pages/LandingPage'))
|
||||
const SharedSessionPage = lazy(() => import('@/pages/SharedSessionPage'))
|
||||
const SurveyPage = lazy(() => import('@/pages/SurveyPage'))
|
||||
const SurveyThankYouPage = lazy(() => import('@/pages/SurveyThankYouPage'))
|
||||
@@ -78,6 +79,11 @@ function page(Component: React.LazyExoticComponent<React.ComponentType>) {
|
||||
}
|
||||
|
||||
export const router = sentryCreateBrowserRouter([
|
||||
{
|
||||
path: '/landing',
|
||||
element: page(LandingPage),
|
||||
errorElement: <RouteError />,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: <LoginPage />,
|
||||
|
||||
1133
frontend/src/styles/landing.css
Normal file
1133
frontend/src/styles/landing.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user