Production upgrade: Clerk auth, Stripe billing, analytics, PWA support

Backend & Auth:
- Add @clerk/nextjs for production authentication
- Create src/proxy.ts with clerkMiddleware() for route protection
- Implement multi-tenant auth with role-based access control
- Add Clerk components (Show, UserButton, SignInButton, SignUpButton)

Billing & Payments:
- Full Stripe integration (subscriptions, add-ons, customer portal)
- Plan tiers: Starter 9/mo, Farm 49/mo, Enterprise 99/mo
- Webhook handling for subscription events
- createSubscription(), createAddonSubscription(), createCustomerPortalSession()

API & Security:
- Rate limiting with @upstash/ratelimit (100 req/min API, 20 req/min checkout)
- Zod validation schemas for all endpoints (orders, products, campaigns, etc.)
- Security headers (CSP, HSTS, X-Frame-Options)
- API routes: /api/v1/ with validated, rate-limited endpoints

Monitoring:
- Sentry error tracking with performance monitoring
- PostHog analytics for feature usage, funnels, cohorts
- User activity logging and breadcrumb tracking

Admin Features:
- Analytics dashboard with revenue charts, customer growth, conversion funnel
- Onboarding flow with 6-step interactive tour
- Referral system with share tracking and reward redemption
- Changelog feed with in-app notifications

PWA & SEO:
- Web app manifest with icons and shortcuts
- Service worker for offline support and caching
- Full SEO metadata, OpenGraph, Twitter cards
- Structured data (JSON-LD) for organization and products

Database:
- Add referral_codes, changelogs, onboarding_progress tables
- Add user_activity_logs, api_keys, notification_preferences
- Comprehensive RLS policies for all new tables
- Seed data for demo brands and products
This commit is contained in:
2026-06-02 05:33:42 +00:00
parent b845d69aba
commit 6ab52a2499
32 changed files with 5816 additions and 501 deletions
@@ -0,0 +1,309 @@
// Onboarding Flow - Welcome tour and interactive demo
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { analytics } from "@/lib/analytics";
interface OnboardingStep {
id: string;
title: string;
description: string;
icon: React.ReactNode;
action?: string;
}
const STEPS: OnboardingStep[] = [
{
id: "welcome",
title: "Welcome to Route Commerce",
description: "The all-in-one platform for fresh produce wholesale distribution. Let's take a quick tour to get you started.",
icon: (
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
},
{
id: "products",
title: "Manage Your Products",
description: "Add your fresh produce with pricing, categories, and images. Perfect for showcasing your farm's best offerings.",
icon: (
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
),
action: "Add First Product",
},
{
id: "stops",
title: "Schedule Pickup Stops",
description: "Create scheduled stops for customers to pick up their orders. Manage locations, times, and capacity.",
icon: (
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
action: "Create First Stop",
},
{
id: "orders",
title: "Track Orders",
description: "Monitor customer orders in real-time. Handle pickups and shipments with ease.",
icon: (
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
),
},
{
id: "communications",
title: "Harvest Reach",
description: "Send beautiful email campaigns to your customers. Keep them informed about seasonal offerings and promotions.",
icon: (
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
),
},
{
id: "complete",
title: "You're All Set!",
description: "Your dashboard is ready. Start adding products and scheduling stops to grow your wholesale business.",
icon: (
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
},
];
interface OnboardingProps {
brandId: string;
userId: string;
onComplete?: () => void;
}
export function OnboardingFlow({ brandId, userId, onComplete }: OnboardingProps) {
const [currentStep, setCurrentStep] = useState(0);
const [isCompleted, setIsCompleted] = useState(false);
const router = useRouter();
useEffect(() => {
// Track onboarding start
analytics.onboardingStep("start", false);
}, []);
const handleNext = () => {
if (currentStep < STEPS.length - 1) {
const step = STEPS[currentStep];
analytics.onboardingStep(step.id, false);
setCurrentStep(currentStep + 1);
}
};
const handlePrevious = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
const handleSkip = () => {
analytics.onboardingStep("skipped", true);
setIsCompleted(true);
onComplete?.();
};
const handleComplete = () => {
analytics.onboardingStep("complete", true);
setIsCompleted(true);
onComplete?.();
};
const handleAction = () => {
const step = STEPS[currentStep];
analytics.buttonClicked(step.action || "next", "onboarding");
if (step.id === "products") {
router.push("/admin/products/new");
} else if (step.id === "stops") {
router.push("/admin/stops/new");
} else {
handleNext();
}
};
if (isCompleted) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="bg-white rounded-2xl p-8 max-w-md text-center"
>
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Setup Complete!</h2>
<p className="text-gray-600 mb-6">
You're ready to start growing your wholesale business.
</p>
<button
onClick={() => router.push("/admin")}
className="w-full px-4 py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary/90"
>
Go to Dashboard
</button>
</motion.div>
</div>
);
}
const step = STEPS[currentStep];
const progress = ((currentStep + 1) / STEPS.length) * 100;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="bg-white rounded-2xl shadow-2xl max-w-lg w-full overflow-hidden"
>
{/* Progress bar */}
<div className="h-1 bg-gray-200">
<motion.div
className="h-full bg-primary"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.3 }}
/>
</div>
<div className="p-8">
{/* Step indicator */}
<div className="flex items-center justify-center gap-2 mb-8">
{STEPS.map((_, i) => (
<div
key={i}
className={`w-2 h-2 rounded-full transition-colors ${
i <= currentStep ? "bg-primary" : "bg-gray-300"
}`}
/>
))}
</div>
{/* Icon */}
<div className="w-20 h-20 bg-primary/10 rounded-2xl flex items-center justify-center mx-auto mb-6 text-primary">
{step.icon}
</div>
{/* Content */}
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-2">{step.title}</h2>
<p className="text-gray-600">{step.description}</p>
</div>
{/* Actions */}
<div className="flex gap-3">
{currentStep > 0 ? (
<button
onClick={handlePrevious}
className="flex-1 px-4 py-3 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50"
>
Back
</button>
) : (
<button
onClick={handleSkip}
className="flex-1 px-4 py-3 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50"
>
Skip Tour
</button>
)}
{step.action ? (
<button
onClick={handleAction}
className="flex-1 px-4 py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary/90"
>
{step.action}
</button>
) : currentStep === STEPS.length - 1 ? (
<button
onClick={handleComplete}
className="flex-1 px-4 py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary/90"
>
Get Started
</button>
) : (
<button
onClick={handleNext}
className="flex-1 px-4 py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary/90"
>
Next
</button>
)}
</div>
</div>
{/* Step counter */}
<div className="bg-gray-50 px-8 py-4 text-center text-sm text-gray-500">
Step {currentStep + 1} of {STEPS.length}
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
}
// Interactive demo component
export function InteractiveDemo({ onClose }: { onClose: () => void }) {
const [demoStep, setDemoStep] = useState(0);
const demoSteps = [
{ title: "Add Products", description: "Create your product catalog" },
{ title: "Schedule Stops", description: "Set up pickup locations" },
{ title: "Send Campaigns", description: "Email your customers" },
];
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl max-w-md w-full p-6">
<h3 className="text-lg font-bold mb-4">Interactive Demo</h3>
<div className="space-y-4">
{demoSteps.map((step, i) => (
<div
key={i}
className={`p-4 rounded-lg border-2 cursor-pointer transition-colors ${
i === demoStep ? "border-primary bg-primary/5" : "border-gray-200"
}`}
onClick={() => setDemoStep(i)}
>
<div className="font-medium">{step.title}</div>
<div className="text-sm text-gray-500">{step.description}</div>
</div>
))}
</div>
<div className="mt-6 flex gap-3">
<button onClick={onClose} className="flex-1 px-4 py-2 border rounded-lg">
Close
</button>
<button className="flex-1 px-4 py-2 bg-primary text-white rounded-lg">
Start Demo
</button>
</div>
</div>
</div>
);
}