Merge branch 'main' into feat/l1-workspace
# Conflicts: # frontend/src/router.tsx
This commit is contained in:
@@ -2416,7 +2416,7 @@ export default function AssistantChatPage() {
|
||||
setShowConclude(false)
|
||||
if (activeSessionStatus === 'escalated') {
|
||||
toast.info('Session escalated. Heading back to your dashboard.')
|
||||
navigate('/')
|
||||
navigate('/home')
|
||||
}
|
||||
}}
|
||||
onConclude={handleConclude}
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function ContactPage() {
|
||||
<PageMeta title="Contact" description="Contact ResolutionFlow customer service, sales, billing, or security." />
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<Link to="/landing" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||
<Link to="/" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||
<h1 className="text-3xl font-bold font-heading mb-4">Contact ResolutionFlow</h1>
|
||||
<p className="text-muted-foreground mb-10">
|
||||
We respond to customer inquiries Monday through Friday during U.S. business hours, excluding federal holidays. Email is the fastest path to a response.
|
||||
|
||||
@@ -164,46 +164,74 @@ export default function LandingPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Problem — asymmetric: headline left, cards right */}
|
||||
<section id="problem" className="landing-section landing-section-alt landing-reveal">
|
||||
{/* Problem — editorial list, no cards */}
|
||||
<section id="problem" className="landing-section landing-section-alt landing-reveal landing-section-tight">
|
||||
<div className="landing-section-inner">
|
||||
<div className="landing-problem-layout">
|
||||
<div className="landing-problem-headline">
|
||||
<div className="landing-section-label">The Problem</div>
|
||||
<h2>Documentation is broken.<br />Everyone knows it.</h2>
|
||||
<p>Engineers don't want to write it. Managers hate chasing it. Clients never see it. The same issues get solved from scratch — every time.</p>
|
||||
</div>
|
||||
<div className="landing-problem-grid">
|
||||
<ProblemCard icon="⏱" color="red" title="15–25 min lost per ticket" description="More time documenting than resolving. After a complex issue, writing notes is the last thing anyone does." />
|
||||
<ProblemCard icon="📋" color="amber" title="Vague, useless notes" description={`"Fixed Outlook" tells no one anything. Notes under pressure are always too vague to help next time.`} />
|
||||
<ProblemCard icon="🔄" color="slate" title="Knowledge walks out the door" description="When a senior engineer leaves, years of tribal knowledge vanish overnight." />
|
||||
<ProblemCard icon="🧠" color="violet" title="Context switching kills speed" description="Jumping between the issue, docs, PSA tickets, and knowledge bases fragments focus." />
|
||||
<p>Engineers don't want to write it. Managers hate chasing it. Clients never see it. The same issues get solved from scratch, every time.</p>
|
||||
</div>
|
||||
<ol className="landing-problem-list">
|
||||
<li className="landing-problem-item">
|
||||
<span className="landing-problem-num">01</span>
|
||||
<div className="landing-problem-body">
|
||||
<h3>15–25 min lost per ticket</h3>
|
||||
<p>More time documenting than resolving. After a complex issue, writing notes is the last thing anyone does.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="landing-problem-item">
|
||||
<span className="landing-problem-num">02</span>
|
||||
<div className="landing-problem-body">
|
||||
<h3>Vague, useless notes</h3>
|
||||
<p>“Fixed Outlook” tells no one anything. Notes under pressure are always too vague to help next time.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="landing-problem-item">
|
||||
<span className="landing-problem-num">03</span>
|
||||
<div className="landing-problem-body">
|
||||
<h3>Knowledge walks out the door</h3>
|
||||
<p>When a senior engineer leaves, years of tribal knowledge vanish overnight.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="landing-problem-item">
|
||||
<span className="landing-problem-num">04</span>
|
||||
<div className="landing-problem-body">
|
||||
<h3>Context switching kills speed</h3>
|
||||
<p>Jumping between the issue, docs, PSA tickets, and knowledge bases fragments focus.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Equation */}
|
||||
{/* Equation — typographic moment */}
|
||||
<div className="landing-equation-section landing-reveal">
|
||||
<div className="landing-equation-inner">
|
||||
<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 className="landing-brand-equation" aria-label="Resolution plus documentation minus time equals ResolutionFlow">
|
||||
<div className="landing-eq-lhs">
|
||||
<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>
|
||||
</div>
|
||||
<div className="landing-eq-equals">
|
||||
<span className="landing-eq-operator-equals">=</span>
|
||||
</div>
|
||||
<div className="landing-eq-result">ResolutionFlow</div>
|
||||
</div>
|
||||
<p className="landing-equation-desc">
|
||||
What if documentation was a <em>byproduct</em> of solving the issue — not a separate task?
|
||||
What if documentation was a <em>byproduct</em> of solving the issue, not a separate task?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* How It Works — zigzag */}
|
||||
<section id="how-it-works" className="landing-section landing-reveal">
|
||||
<section id="how-it-works" className="landing-section landing-reveal landing-section-tight">
|
||||
<div className="landing-section-inner">
|
||||
<div className="landing-section-label">How It Works</div>
|
||||
<h2 className="landing-section-title">Three steps. Zero note-writing.</h2>
|
||||
@@ -268,54 +296,47 @@ export default function LandingPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features */}
|
||||
<section id="features" className="landing-section landing-section-alt landing-reveal">
|
||||
{/* Features — editorial spec list */}
|
||||
<section id="features" className="landing-section landing-section-alt landing-reveal landing-section-generous">
|
||||
<div className="landing-section-inner">
|
||||
<div className="landing-section-label">Features</div>
|
||||
<h2 className="landing-section-title">Everything you need to troubleshoot faster.</h2>
|
||||
|
||||
<div className="landing-feature-highlight">
|
||||
<div className="landing-feature-highlight-icon">
|
||||
<svg width="28" height="28" 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>
|
||||
</div>
|
||||
<div className="landing-feature-highlight-marker" aria-hidden="true">FP</div>
|
||||
<div className="landing-feature-highlight-content">
|
||||
<h3>FlowPilot — Your AI Copilot</h3>
|
||||
<p>Like having a senior engineer on every call. Describe the issue, get expert troubleshooting guidance, and documentation writes itself — as a byproduct of solving the problem.</p>
|
||||
<h3>FlowPilot, your AI copilot</h3>
|
||||
<p>Like having a senior engineer on every call. Describe the issue, get expert troubleshooting guidance, and documentation writes itself, as a byproduct of solving the problem.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="landing-features-grid">
|
||||
<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="Guided Flows"
|
||||
description="Build step-by-step troubleshooting paths your team can follow. Great for onboarding and consistency."
|
||||
/>
|
||||
<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="Zero Empty Tickets"
|
||||
description="Every session generates timestamped notes, formatted for your PSA. No more empty ticket closures."
|
||||
/>
|
||||
<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"
|
||||
description="Solutions are saved and surfaced when the next engineer hits a similar issue."
|
||||
/>
|
||||
<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 Analytics"
|
||||
description="Track resolution times, identify recurring issues, and measure team performance."
|
||||
/>
|
||||
<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 to ConnectWise, Atera, and Syncro. Push session docs straight to tickets."
|
||||
/>
|
||||
</div>
|
||||
<dl className="landing-feature-spec">
|
||||
<div className="landing-feature-row">
|
||||
<dt>Guided Flows</dt>
|
||||
<dd>Build step-by-step troubleshooting paths your team can follow. Great for onboarding and consistency.</dd>
|
||||
</div>
|
||||
<div className="landing-feature-row">
|
||||
<dt>Zero Empty Tickets</dt>
|
||||
<dd>Every session generates timestamped notes, formatted for your PSA. No more empty ticket closures.</dd>
|
||||
</div>
|
||||
<div className="landing-feature-row">
|
||||
<dt>Team Knowledge</dt>
|
||||
<dd>Solutions are saved and surfaced when the next engineer hits a similar issue.</dd>
|
||||
</div>
|
||||
<div className="landing-feature-row">
|
||||
<dt>Session Analytics</dt>
|
||||
<dd>Track resolution times, identify recurring issues, and measure team performance.</dd>
|
||||
</div>
|
||||
<div className="landing-feature-row">
|
||||
<dt>PSA Integration</dt>
|
||||
<dd>Connect to ConnectWise, Atera, and Syncro. Push session docs straight to tickets.</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing */}
|
||||
<section id="pricing" className="landing-section landing-reveal">
|
||||
<section id="pricing" className="landing-section landing-reveal landing-section-generous">
|
||||
<div className="landing-section-inner">
|
||||
<div className="landing-section-label">Pricing</div>
|
||||
<h2 className="landing-section-title">Simple pricing. No surprises.</h2>
|
||||
@@ -364,7 +385,7 @@ export default function LandingPage() {
|
||||
</section>
|
||||
|
||||
{/* FAQ */}
|
||||
<section id="faq" className="landing-section landing-section-alt landing-reveal">
|
||||
<section id="faq" className="landing-section landing-section-alt landing-reveal landing-section-tight">
|
||||
<div className="landing-section-inner">
|
||||
<div className="landing-section-label">FAQ</div>
|
||||
<h2 className="landing-section-title">Common questions</h2>
|
||||
@@ -399,15 +420,16 @@ export default function LandingPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="landing-cta-section landing-reveal">
|
||||
{/* CTA — drenched */}
|
||||
<section className="landing-cta-section landing-cta-drench landing-reveal">
|
||||
<div className="landing-cta-inner">
|
||||
<h2>Ready to stop writing ticket notes?</h2>
|
||||
<p>Get early access. Troubleshoot your next ticket with FlowPilot.</p>
|
||||
<div className="landing-cta-eyebrow">Stop writing ticket notes</div>
|
||||
<h2>Troubleshoot your next ticket with FlowPilot.</h2>
|
||||
<p>Get early access. Free to start, no credit card.</p>
|
||||
<div className="landing-cta-actions">
|
||||
<Link to="/register?from=beta" className="landing-btn-hero-primary">Get started</Link>
|
||||
<Link to="/register?from=beta" className="landing-btn-cta-invert">Get started</Link>
|
||||
<a href="#how-it-works" className="landing-btn-cta-ghost">See how it works</a>
|
||||
</div>
|
||||
<p className="landing-cta-fine-print">Free to start. No credit card required.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -421,30 +443,6 @@ export default function LandingPage() {
|
||||
|
||||
/* ---- 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 }: {
|
||||
icon: React.ReactNode; title: string; description: string
|
||||
}) {
|
||||
return (
|
||||
<div className="landing-feature-card">
|
||||
<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, plan }: {
|
||||
name: string; target: string; amount: string; period?: string; note: string
|
||||
features: string[]; btnLabel: string; btnStyle: 'outline' | 'filled'; featured?: boolean; plan: string
|
||||
|
||||
@@ -112,10 +112,10 @@ export function OAuthCallbackPage() {
|
||||
|
||||
// Invitee path lands on the dashboard with the teammate-welcome
|
||||
// marker; new self-serve owners go to the welcome wizard; returning
|
||||
// users to /.
|
||||
let dest = '/'
|
||||
// users to /home.
|
||||
let dest = '/home'
|
||||
if (decoded?.accountInviteCode) {
|
||||
dest = '/?welcome=teammate'
|
||||
dest = '/home?welcome=teammate'
|
||||
} else if (result.is_new_user) {
|
||||
dest = '/welcome'
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function PoliciesPage() {
|
||||
<PageMeta title="Customer Policies" description="ResolutionFlow customer service, billing, refunds, cancellation, legal restrictions, and promotional terms." />
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<Link to="/landing" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||
<Link to="/" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||
<h1 className="text-3xl font-bold font-heading mb-4">Customer Policies</h1>
|
||||
<p className="text-muted-foreground mb-2">Last updated: May 7, 2026</p>
|
||||
<p className="text-muted-foreground mb-2"><strong className="text-foreground">Operator:</strong> ResolutionFlow, LLC (the “Company”), operator of ResolutionFlow (“Service”).</p>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function PrivacyPage() {
|
||||
<PageMeta title="Privacy Policy" description="ResolutionFlow Privacy Policy" />
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<Link to="/landing" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||
<Link to="/" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||
<h1 className="text-3xl font-bold font-heading mb-8">Privacy Policy</h1>
|
||||
<p className="text-muted-foreground mb-6">Last updated: March 21, 2026</p>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function PromotionsPage() {
|
||||
<PageMeta title="Promotions" description="Active ResolutionFlow promotional offers and their terms." />
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<Link to="/landing" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||
<Link to="/" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||
<h1 className="text-3xl font-bold font-heading mb-4">Promotions</h1>
|
||||
<p className="text-muted-foreground mb-10">Last updated: May 7, 2026</p>
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ export default function PublicTemplatesPage() {
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-40 border-b border-border bg-background/80">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||||
<Link to="/landing" className="flex items-center gap-2.5">
|
||||
<Link to="/" className="flex items-center gap-2.5">
|
||||
<BrandLogo size="sm" />
|
||||
<span className="font-heading text-lg font-semibold">
|
||||
<span className="text-foreground">Resolution</span>
|
||||
@@ -406,7 +406,7 @@ export default function PublicTemplatesPage() {
|
||||
<footer className="border-t border-border py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
||||
<Link
|
||||
to="/landing"
|
||||
to="/"
|
||||
className="text-muted-foreground text-sm hover:text-foreground transition-colors"
|
||||
>
|
||||
Powered by <span className="font-semibold">ResolutionFlow</span>
|
||||
|
||||
@@ -423,7 +423,7 @@ export default function SessionHistoryPage() {
|
||||
description="Start a FlowPilot or chat session to begin. All your sessions will appear here."
|
||||
action={
|
||||
<Link
|
||||
to="/"
|
||||
to="/home"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-5 py-2.5 text-sm font-semibold text-white hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
>
|
||||
Start a Session
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function TermsPage() {
|
||||
<PageMeta title="Terms of Service" description="ResolutionFlow Terms of Service" />
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<Link to="/landing" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||
<Link to="/" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||
<h1 className="text-3xl font-bold font-heading mb-8">Terms of Service</h1>
|
||||
<p className="text-muted-foreground mb-6">Last updated: March 21, 2026</p>
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ const SUCCESS_REDIRECT_MS = 1200
|
||||
* "Already verified" state. No API call.
|
||||
* - Else fire `POST /auth/email/verify` exactly once (a `useRef` guard keeps
|
||||
* React 19 strict-mode double-invoke from double-firing the call). On
|
||||
* success, refresh the auth store and bounce to `/?verified=1` so the
|
||||
* dashboard surfaces a toast.
|
||||
* success, refresh the auth store and bounce to `/home`.
|
||||
* - On error, show "Invalid or expired token" + a "Resend" CTA that calls
|
||||
* `POST /auth/email/send-verification`.
|
||||
*/
|
||||
@@ -70,10 +69,9 @@ export function VerifyEmailPage() {
|
||||
if (cancelled) return
|
||||
setStatus('success')
|
||||
toast.success('Email verified')
|
||||
// Brief success state, then redirect with a query flag so the
|
||||
// dashboard can re-surface confirmation if it wants to.
|
||||
// Brief success state, then redirect to the dashboard.
|
||||
window.setTimeout(() => {
|
||||
navigate('/?verified=1', { replace: true })
|
||||
navigate('/home', { replace: true })
|
||||
}, SUCCESS_REDIRECT_MS)
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -126,7 +124,7 @@ export function VerifyEmailPage() {
|
||||
Redirecting you to the dashboard…
|
||||
</p>
|
||||
<Link
|
||||
to="/?verified=1"
|
||||
to="/home"
|
||||
replace
|
||||
className={cn(
|
||||
'mt-6 inline-flex items-center rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground',
|
||||
@@ -149,7 +147,7 @@ export function VerifyEmailPage() {
|
||||
action needed.
|
||||
</p>
|
||||
<Link
|
||||
to="/"
|
||||
to="/home"
|
||||
className={cn(
|
||||
'mt-6 inline-flex items-center rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground',
|
||||
'hover:brightness-110',
|
||||
@@ -181,7 +179,7 @@ export function VerifyEmailPage() {
|
||||
Resend verification email
|
||||
</button>
|
||||
<Link
|
||||
to="/"
|
||||
to="/home"
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-lg border border-default bg-input px-6 py-2 text-sm font-medium text-foreground',
|
||||
'hover:border-border-hover',
|
||||
@@ -204,7 +202,7 @@ export function VerifyEmailPage() {
|
||||
Try the link in your verification email again.
|
||||
</p>
|
||||
<Link
|
||||
to="/"
|
||||
to="/home"
|
||||
className={cn(
|
||||
'mt-6 inline-flex items-center rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground',
|
||||
'hover:brightness-110',
|
||||
|
||||
@@ -52,7 +52,7 @@ function renderPage(initialPath: string) {
|
||||
<MemoryRouter initialEntries={[initialPath]}>
|
||||
<Routes>
|
||||
<Route path="/verify-email" element={<VerifyEmailPage />} />
|
||||
<Route path="/" element={<div>dashboard</div>} />
|
||||
<Route path="/home" element={<div>dashboard</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</HelmetProvider>,
|
||||
@@ -130,7 +130,7 @@ describe('VerifyEmailPage', () => {
|
||||
<MemoryRouter initialEntries={['/verify-email?token=valid-token']}>
|
||||
<Routes>
|
||||
<Route path="/verify-email" element={<VerifyEmailPage />} />
|
||||
<Route path="/" element={<div>dashboard</div>} />
|
||||
<Route path="/home" element={<div>dashboard</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</HelmetProvider>,
|
||||
@@ -142,7 +142,7 @@ describe('VerifyEmailPage', () => {
|
||||
<MemoryRouter initialEntries={['/verify-email?token=valid-token']}>
|
||||
<Routes>
|
||||
<Route path="/verify-email" element={<VerifyEmailPage />} />
|
||||
<Route path="/" element={<div>dashboard</div>} />
|
||||
<Route path="/home" element={<div>dashboard</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</HelmetProvider>,
|
||||
|
||||
@@ -6,8 +6,8 @@ import { PageLoader } from '@/components/common/PageLoader'
|
||||
* `/welcome` index — redirect to the next incomplete step (or `/` if done /
|
||||
* dismissed). Decision table:
|
||||
*
|
||||
* onboarding_dismissed === true → /
|
||||
* onboarding_step_completed >= 3 → /
|
||||
* onboarding_dismissed === true → /home
|
||||
* onboarding_step_completed >= 3 → /home
|
||||
* onboarding_step_completed === null/0 → /welcome/step-1
|
||||
* onboarding_step_completed === 1 → /welcome/step-2
|
||||
* onboarding_step_completed === 2 → /welcome/step-3
|
||||
@@ -19,10 +19,10 @@ export function WelcomeRouter() {
|
||||
// the page loader rather than racing past the redirect.
|
||||
if (!user) return <PageLoader />
|
||||
|
||||
if (user.onboarding_dismissed) return <Navigate to="/" replace />
|
||||
if (user.onboarding_dismissed) return <Navigate to="/home" replace />
|
||||
|
||||
const completed = user.onboarding_step_completed ?? 0
|
||||
if (completed >= 3) return <Navigate to="/" replace />
|
||||
if (completed >= 3) return <Navigate to="/home" replace />
|
||||
if (completed === 2) return <Navigate to="/welcome/step-3" replace />
|
||||
if (completed === 1) return <Navigate to="/welcome/step-2" replace />
|
||||
return <Navigate to="/welcome/step-1" replace />
|
||||
|
||||
@@ -85,7 +85,7 @@ export function WelcomeStep1() {
|
||||
try {
|
||||
await onboardingApi.dismissRest()
|
||||
await fetchUser()
|
||||
navigate('/')
|
||||
navigate('/home')
|
||||
} catch {
|
||||
setError('Could not save. Please try again.')
|
||||
setSubmitting(null)
|
||||
|
||||
@@ -90,7 +90,7 @@ export function WelcomeStep2() {
|
||||
try {
|
||||
await onboardingApi.dismissRest()
|
||||
await fetchUser()
|
||||
navigate('/')
|
||||
navigate('/home')
|
||||
} catch {
|
||||
setError('Could not save. Please try again.')
|
||||
setSubmitting(null)
|
||||
|
||||
@@ -39,7 +39,7 @@ function makeEmptyRow(): InviteRow {
|
||||
*
|
||||
* 1. POST `/accounts/me/invites/bulk` with populated rows.
|
||||
* 2. PATCH `/users/me/onboarding-step` `{step: 3, action: "complete"}`.
|
||||
* 3. Navigate to `/?welcome=true` and fire a "You're all set" toast.
|
||||
* 3. Navigate to `/home` and fire a "You're all set" toast.
|
||||
*
|
||||
* Partial-failure UX: rows in `failed[]` keep their input and show an
|
||||
* inline error. The wizard does NOT auto-advance when there are failures —
|
||||
@@ -109,7 +109,7 @@ export function WelcomeStep3() {
|
||||
await onboardingApi.updateStep({ step: 3, action: 'complete' })
|
||||
await fetchUser()
|
||||
toast.success("You're all set!")
|
||||
navigate('/?welcome=true')
|
||||
navigate('/home')
|
||||
}
|
||||
|
||||
const handleSendInvites = async () => {
|
||||
@@ -177,7 +177,7 @@ export function WelcomeStep3() {
|
||||
await onboardingApi.updateStep({ step: 3, action: 'skip' })
|
||||
await fetchUser()
|
||||
toast.success("You're all set!")
|
||||
navigate('/?welcome=true')
|
||||
navigate('/home')
|
||||
} catch {
|
||||
setError('Could not save. Please try again.')
|
||||
setSubmitting(null)
|
||||
@@ -191,7 +191,7 @@ export function WelcomeStep3() {
|
||||
try {
|
||||
await onboardingApi.dismissRest()
|
||||
await fetchUser()
|
||||
navigate('/')
|
||||
navigate('/home')
|
||||
} catch {
|
||||
setError('Could not save. Please try again.')
|
||||
setSubmitting(null)
|
||||
|
||||
@@ -39,7 +39,7 @@ function renderRouter() {
|
||||
<Route path="/welcome/step-1" element={<div>step-1</div>} />
|
||||
<Route path="/welcome/step-2" element={<div>step-2</div>} />
|
||||
<Route path="/welcome/step-3" element={<div>step-3</div>} />
|
||||
<Route path="/" element={<div>dashboard</div>} />
|
||||
<Route path="/home" element={<div>dashboard</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
@@ -100,7 +100,7 @@ describe('WelcomeRouter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects to / when onboarding_step_completed >= 3', async () => {
|
||||
it('redirects to /home when onboarding_step_completed >= 3', async () => {
|
||||
useAuthStore.setState({
|
||||
user: makeUser({ onboarding_step_completed: 3 }),
|
||||
})
|
||||
@@ -110,7 +110,7 @@ describe('WelcomeRouter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects to / when onboarding_dismissed is true', async () => {
|
||||
it('redirects to /home when onboarding_dismissed is true', async () => {
|
||||
useAuthStore.setState({
|
||||
user: makeUser({
|
||||
onboarding_step_completed: 1,
|
||||
|
||||
@@ -65,7 +65,7 @@ function renderPage() {
|
||||
<Routes>
|
||||
<Route path="/welcome/step-1" element={<WelcomeStep1 />} />
|
||||
<Route path="/welcome/step-2" element={<div>step-2</div>} />
|
||||
<Route path="/" element={<div>dashboard</div>} />
|
||||
<Route path="/home" element={<div>dashboard</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
@@ -148,7 +148,7 @@ describe('WelcomeStep1', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Skip-the-rest dismisses and navigates to /', async () => {
|
||||
it('Skip-the-rest dismisses and navigates to /home', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ function renderPage() {
|
||||
<Route path="/welcome/step-2" element={<WelcomeStep2 />} />
|
||||
<Route path="/welcome/step-3" element={<div>step-3</div>} />
|
||||
<Route path="/account/integrations" element={<div>integrations</div>} />
|
||||
<Route path="/" element={<div>dashboard</div>} />
|
||||
<Route path="/home" element={<div>dashboard</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
@@ -158,7 +158,7 @@ describe('WelcomeStep2', () => {
|
||||
expect(screen.queryByTestId('welcome-step-2-connect-now')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('Skip-the-rest dismisses and navigates to /', async () => {
|
||||
it('Skip-the-rest dismisses and navigates to /home', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ function renderPage() {
|
||||
<MemoryRouter initialEntries={['/welcome/step-3']}>
|
||||
<Routes>
|
||||
<Route path="/welcome/step-3" element={<WelcomeStep3 />} />
|
||||
<Route path="/" element={<div>dashboard</div>} />
|
||||
<Route path="/home" element={<div>dashboard</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user