feat: complete launch & marketing layer

- Add marketing pages: blog, changelog, roadmap, waitlist, security, maintenance
- Add GDPR cookie consent banner with preference modal
- Add referral system with API routes for code generation/tracking
- Add waitlist API with referral support
- Add PWA support: manifest, service worker, install prompt
- Add onboarding flow with 6-step guided tour
- Add in-app notification center with bell dropdown
- Add admin launch checklist (32 items across 8 categories)
- Update landing page with trust badges
- Add Open Graph image and favicon
- Configure ESLint for PWA install patterns
- Add LAUNCH_CHECKLIST.md with go-to-market guide

Supabase migrations for:
- blog_posts and blog_categories tables
- waitlist_signups table
- roadmap_items table
- launch_checklist_items table
This commit is contained in:
2026-06-02 06:19:56 +00:00
parent f6d2dc4e6c
commit 7dfaba6e3d
46 changed files with 4065 additions and 2188 deletions
+225
View File
@@ -0,0 +1,225 @@
import type { Metadata } from "next";
import Link from "next/link";
import { CheckCircle2, Circle, ExternalLink, AlertCircle, ChevronRight } from "lucide-react";
export const metadata: Metadata = {
title: "Launch Checklist — Route Commerce",
description: "Pre-launch checklist to ensure a successful product launch.",
};
const CHECKLIST_CATEGORIES = [
{
id: "account",
title: "Account & Access",
icon: "🔐",
items: [
{ id: "admin-account", title: "Create admin accounts", description: "Set up admin accounts for all team members", link: "/admin/users" },
{ id: "2fa", title: "Enable 2FA", description: "Enable two-factor authentication for all admin accounts", link: "/admin/settings/security" },
{ id: "permissions", title: "Set up permissions", description: "Configure role-based access control", link: "/admin/users" },
],
},
{
id: "billing",
title: "Billing & Payments",
icon: "💳",
items: [
{ id: "stripe-connect", title: "Connect Stripe", description: "Set up Stripe account for payments", link: "/admin/settings/payments" },
{ id: "pricing-check", title: "Review pricing", description: "Confirm all plan pricing is correct", link: "/pricing" },
{ id: "invoices", title: "Test invoices", description: "Create and verify invoice generation", link: "/admin/settings/billing" },
],
},
{
id: "products",
title: "Products & Catalog",
icon: "📦",
items: [
{ id: "products-import", title: "Import products", description: "Add products to the catalog", link: "/admin/products" },
{ id: "pricing-set", title: "Set wholesale pricing", description: "Configure pricing for wholesale customers", link: "/admin/products" },
{ id: "images-upload", title: "Upload product images", description: "Add high-quality product images", link: "/admin/products" },
{ id: "categories", title: "Organize categories", description: "Set up product categories", link: "/admin/products" },
],
},
{
id: "communications",
title: "Communication",
icon: "📧",
items: [
{ id: "email-setup", title: "Configure email", description: "Set up Resend for email delivery", link: "/admin/communications/settings" },
{ id: "templates", title: "Review email templates", description: "Check transactional email templates", link: "/admin/communications/templates" },
{ id: "welcome-sequence", title: "Set up welcome emails", description: "Create welcome email sequence", link: "/admin/communications/welcome-sequence" },
],
},
{
id: "legal",
title: "Legal & Compliance",
icon: "⚖️",
items: [
{ id: "privacy-policy", title: "Privacy Policy live", description: "Confirm privacy policy page is accessible", link: "/privacy-policy" },
{ id: "terms", title: "Terms of Service live", description: "Confirm terms page is accessible", link: "/terms-and-conditions" },
{ id: "cookie-banner", title: "Cookie banner active", description: "GDPR cookie consent is implemented", link: "/security" },
{ id: "security-page", title: "Security page live", description: "Trust and security information is visible", link: "/security" },
],
},
{
id: "marketing",
title: "Marketing & SEO",
icon: "📈",
items: [
{ id: "analytics", title: "Install PostHog", description: "Set up product analytics", link: "/admin/reports" },
{ id: "sitemap", title: "Submit sitemap", description: "Submit sitemap to Google Search Console", link: "/sitemap.xml" },
{ id: "og-images", title: "Create OG images", description: "Add social sharing images", link: "/admin/settings" },
{ id: "social-profiles", title: "Set up social profiles", description: "Create social media accounts", link: "/" },
],
},
{
id: "testing",
title: "Testing & QA",
icon: "🧪",
items: [
{ id: "checkout-test", title: "Test checkout flow", description: "Complete end-to-end checkout test", link: "/checkout" },
{ id: "auth-test", title: "Test authentication", description: "Verify login, signup, password reset", link: "/login" },
{ id: "mobile-test", title: "Test mobile responsive", description: "Check all pages on mobile devices", link: "/" },
{ id: "email-test", title: "Test email delivery", description: "Verify transactional emails arrive", link: "/admin/communications/logs" },
],
},
{
id: "infrastructure",
title: "Infrastructure",
icon: "🏗️",
items: [
{ id: "domain", title: "Configure domain", description: "Set up custom domain", link: "/admin/settings" },
{ id: "ssl", title: "Verify SSL", description: "Confirm HTTPS is working", link: "/" },
{ id: "backups", title: "Configure backups", description: "Database backups are enabled", link: "/admin/settings" },
{ id: "monitoring", title: "Set up monitoring", description: "Configure uptime monitoring", link: "/admin" },
],
},
];
export default function LaunchChecklistPage() {
return (
<div className="min-h-screen bg-[#faf8f5]">
{/* Header */}
<header className="border-b border-[#e5e5e5] bg-white sticky top-0 z-10">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/admin" className="text-sm text-[#888] hover:text-[#1a1a1a] transition-colors">
Back to Admin
</Link>
<div className="h-6 w-px bg-[#e5e5e5]" />
<h1 className="text-lg font-bold text-[#1a1a1a]">Launch Checklist</h1>
</div>
<button className="px-4 py-2 bg-[#faf8f5] border border-[#e5e5e5] rounded-xl text-sm font-medium text-[#1a1a1a] hover:bg-white transition-colors">
Export PDF
</button>
</div>
</header>
{/* Progress Overview */}
<section className="bg-white border-b border-[#e5e5e5] py-8">
<div className="max-w-6xl mx-auto px-6">
<div className="flex items-center gap-8">
{/* Progress Ring */}
<div className="relative w-24 h-24">
<svg className="w-24 h-24 -rotate-90" viewBox="0 0 36 36">
<path
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="#f0f0f0"
strokeWidth="3"
/>
<path
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="#1a4d2e"
strokeWidth="3"
strokeDasharray="20, 100"
strokeLinecap="round"
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xl font-bold text-[#1a4d2e]">20%</span>
</div>
</div>
{/* Stats */}
<div className="flex-1">
<h2 className="text-2xl font-bold text-[#1a1a1a] mb-1">7 of 32 tasks complete</h2>
<p className="text-[#888]">You&apos;re making great progress. Keep going!</p>
<div className="flex gap-6 mt-4">
<div className="flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
<span className="text-sm text-[#666]">7 done</span>
</div>
<div className="flex items-center gap-2">
<Circle className="w-4 h-4 text-gray-300" />
<span className="text-sm text-[#666]">25 remaining</span>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="flex gap-3">
<button className="px-4 py-2 bg-[#1a4d2e] text-white rounded-xl text-sm font-medium hover:bg-[#2d6a4f] transition-colors">
Mark All Complete
</button>
</div>
</div>
</div>
</section>
{/* Checklist */}
<main className="max-w-6xl mx-auto px-6 py-12">
<div className="grid gap-8">
{CHECKLIST_CATEGORIES.map((category) => (
<section key={category.id} className="bg-white rounded-2xl border border-[#e5e5e5] overflow-hidden">
{/* Category Header */}
<div className="px-6 py-4 bg-[#faf8f5] border-b border-[#e5e5e5] flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{category.icon}</span>
<h2 className="text-lg font-semibold text-[#1a1a1a]">{category.title}</h2>
</div>
<div className="text-sm text-[#888]">
0 / {category.items.length} complete
</div>
</div>
{/* Items */}
<div className="divide-y divide-[#f0f0f0]">
{category.items.map((item) => (
<div key={item.id} className="px-6 py-4 flex items-center gap-4 hover:bg-gray-50 transition-colors">
<button className="w-6 h-6 rounded-full border-2 border-gray-300 flex items-center justify-center hover:border-[#1a4d2e] transition-colors">
<CheckCircle2 className="w-4 h-4 text-emerald-500 opacity-0" />
</button>
<div className="flex-1">
<h3 className="font-medium text-[#1a1a1a]">{item.title}</h3>
<p className="text-sm text-[#888]">{item.description}</p>
</div>
<Link href={item.link} className="flex items-center gap-1 text-sm text-[#1a4d2e] hover:underline">
Go <ChevronRight className="w-4 h-4" />
</Link>
</div>
))}
</div>
</section>
))}
</div>
{/* Bottom CTA */}
<div className="mt-12 bg-gradient-to-r from-[#1a4d2e] to-[#2d6a4f] rounded-2xl p-8 text-center text-white">
<h2 className="text-2xl font-bold mb-2">Ready to launch?</h2>
<p className="text-white/80 mb-6">When all items are complete, you&apos;re ready to go live!</p>
<button className="px-6 py-3 bg-white text-[#1a4d2e] rounded-xl font-medium hover:bg-[#faf8f5] transition-colors">
Mark Launch Complete
</button>
</div>
</main>
{/* Footer */}
<footer className="border-t border-[#e5e5e5] py-8 mt-12">
<div className="max-w-6xl mx-auto px-6 text-center text-sm text-[#888]">
Progress is saved automatically as you check items off.
</div>
</footer>
</div>
);
}
+169
View File
@@ -0,0 +1,169 @@
// Referrals API - Generate and track referral codes
import { NextRequest, NextResponse } from "next/server";
const REFERRAL_REWARD_PERCENTAGE = 20;
const MAX_USES_PER_CODE = 10;
interface ReferralCode {
code: string;
reward_type: "percentage" | "fixed";
reward_value: number;
uses: number;
max_uses: number;
created_at: string;
brand_id: string;
user_id: string;
}
// In-memory store for demo (replace with Supabase in production)
const referralCodes = new Map<string, ReferralCode>();
function generateReferralCode(): string {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let code = "";
for (let i = 0; i < 8; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const brandId = searchParams.get("brand_id");
const userId = searchParams.get("user_id");
if (!brandId || !userId) {
return NextResponse.json(
{ error: "brand_id and user_id are required" },
{ status: 400 }
);
}
// Get all codes for this user
const userCodes: ReferralCode[] = [];
for (const code of referralCodes.values()) {
if (code.brand_id === brandId && code.user_id === userId) {
userCodes.push(code);
}
}
// Calculate stats
const stats = {
total_referrals: userCodes.reduce((sum, code) => sum + code.uses, 0),
successful_conversions: userCodes.reduce((sum, code) => sum + code.uses, 0),
total_reward_value: userCodes.reduce(
(sum, code) => sum + code.uses * code.reward_value,
0
),
};
return NextResponse.json({
codes: userCodes,
stats,
});
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { action, brand_id, user_id } = body;
if (action === "generate") {
if (!brand_id || !user_id) {
return NextResponse.json(
{ error: "brand_id and user_id are required" },
{ status: 400 }
);
}
const code = generateReferralCode();
const referralCode: ReferralCode = {
code,
reward_type: "percentage",
reward_value: REFERRAL_REWARD_PERCENTAGE,
uses: 0,
max_uses: MAX_USES_PER_CODE,
created_at: new Date().toISOString(),
brand_id,
user_id,
};
referralCodes.set(code, referralCode);
return NextResponse.json({
success: true,
code: referralCode,
share_url: `${process.env.NEXT_PUBLIC_SITE_URL || "https://routecommerce.com"}/register?ref=${code}`,
});
}
if (action === "validate") {
const { code } = body;
if (!code) {
return NextResponse.json(
{ error: "code is required" },
{ status: 400 }
);
}
const referralCode = referralCodes.get(code);
if (!referralCode) {
return NextResponse.json({ valid: false, error: "Invalid code" });
}
if (referralCode.uses >= referralCode.max_uses) {
return NextResponse.json({
valid: false,
error: "This code has reached its maximum uses",
});
}
return NextResponse.json({
valid: true,
reward_type: referralCode.reward_type,
reward_value: referralCode.reward_value,
});
}
if (action === "use") {
const { code, new_user_id } = body;
if (!code || !new_user_id) {
return NextResponse.json(
{ error: "code and new_user_id are required" },
{ status: 400 }
);
}
const referralCode = referralCodes.get(code);
if (!referralCode) {
return NextResponse.json({ error: "Invalid code" }, { status: 404 });
}
if (referralCode.uses >= referralCode.max_uses) {
return NextResponse.json(
{ error: "Code has reached maximum uses" },
{ status: 400 }
);
}
// Increment usage
referralCode.uses += 1;
referralCodes.set(code, referralCode);
return NextResponse.json({
success: true,
reward_type: referralCode.reward_type,
reward_value: referralCode.reward_value,
discount: `${referralCode.reward_value}% off first month`,
});
}
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
} catch (error) {
console.error("Referral API error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
+140
View File
@@ -0,0 +1,140 @@
// Campaigns API - Route-based handlers
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { emailLimiter, checkRateLimit, rateLimitExceeded, securityHeaders } from "@/lib/rate-limit";
import { analytics } from "@/lib/analytics";
import { captureError } from "@/lib/sentry";
// Helper functions
function apiResponse(data: unknown, status: number = 200) {
return NextResponse.json({ data }, { status });
}
function apiError(message: string, status: number) {
return NextResponse.json({ error: message }, { status });
}
function validationError(error: z.ZodError) {
return NextResponse.json({ error: "Validation failed", details: error.issues }, { status: 400 });
}
// ============================================
// CAMPAIGNS API
// ============================================
const createCampaignSchema = z.object({
brand_id: z.string().uuid(),
name: z.string().min(1).max(255),
subject: z.string().min(1).max(500),
content: z.string().min(1),
type: z.enum(["email", "sms"]).default("email"),
segment_id: z.string().uuid().optional(),
contact_ids: z.array(z.string().uuid()).optional(),
scheduled_at: z.string().datetime().optional(),
});
export async function POST(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(emailLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
// Parse and validate body
const body = await req.json();
const validation = createCampaignSchema.safeParse(body);
if (!validation.success) {
return validationError(validation.error);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
// Create campaign via RPC
const res = await fetch(
`${supabaseUrl}/rest/v1/rpc/create_campaign`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
p_brand_id: validation.data.brand_id,
p_name: validation.data.name,
p_subject: validation.data.subject,
p_content: validation.data.content,
p_type: validation.data.type,
p_segment_id: validation.data.segment_id,
p_contact_ids: validation.data.contact_ids,
p_scheduled_at: validation.data.scheduled_at,
}),
}
);
if (!res.ok) {
const error = await res.json();
return apiError(error.message || "Failed to create campaign", 500);
}
const campaign = await res.json();
// Track analytics
const audienceSize = validation.data.contact_ids?.length || 0;
analytics.campaignCreated(campaign.id, validation.data.type, audienceSize);
return apiResponse(campaign, 201);
} catch (error) {
captureError(error as Error, { path: "/api/campaigns", method: "POST" });
return apiError("Internal server error", 500);
}
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const brand_id = searchParams.get("brand_id");
if (!brand_id) {
return apiError("brand_id is required", 400);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const res = await fetch(
`${supabaseUrl}/rest/v1/communication_campaigns?brand_id=eq.${brand_id}&select=*&order=created_at.desc`,
{
headers: {
apikey: anonKey,
"Content-Type": "application/json",
},
}
);
if (!res.ok) {
return apiError("Failed to fetch campaigns", 500);
}
const campaigns = await res.json();
return apiResponse(campaigns);
} catch (error) {
captureError(error as Error, { path: "/api/campaigns", method: "GET" });
return apiError("Internal server error", 500);
}
}
// OPTIONS handler
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
...securityHeaders(),
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
+166
View File
@@ -0,0 +1,166 @@
// Products API - Route-based handlers
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { apiLimiter, checkRateLimit, rateLimitExceeded, securityHeaders } from "@/lib/rate-limit";
import { analytics } from "@/lib/analytics";
import { captureError } from "@/lib/sentry";
// Helper functions
function apiResponse(data: unknown, status: number = 200) {
return NextResponse.json({ data }, { status });
}
function apiError(message: string, status: number) {
return NextResponse.json({ error: message }, { status });
}
function validationError(error: z.ZodError) {
return NextResponse.json({ error: "Validation failed", details: error.issues }, { status: 400 });
}
// ============================================
// PRODUCTS API
// ============================================
const getProductsSchema = z.object({
brand_id: z.string().uuid().optional(),
category: z.string().optional(),
is_active: z.boolean().optional(),
search: z.string().optional(),
limit: z.coerce.number().int().positive().max(100).default(20),
offset: z.coerce.number().int().min(0).default(0),
});
export async function GET(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(apiLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
// Parse query params
const { searchParams } = new URL(req.url);
const params = {
brand_id: searchParams.get("brand_id") || undefined,
category: searchParams.get("category") || undefined,
is_active: searchParams.get("is_active") === "true" ? true : searchParams.get("is_active") === "false" ? false : undefined,
search: searchParams.get("search") || undefined,
limit: parseInt(searchParams.get("limit") || "20"),
offset: parseInt(searchParams.get("offset") || "0"),
};
const validation = getProductsSchema.safeParse(params);
if (!validation.success) {
return validationError(validation.error);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
// Build query
let query = `${supabaseUrl}/rest/v1/products?select=*&order=name.asc&limit=${validation.data.limit}&offset=${validation.data.offset}`;
if (validation.data.brand_id) {
query += `&brand_id=eq.${validation.data.brand_id}`;
}
if (validation.data.category) {
query += `&category=ilike.*${encodeURIComponent(validation.data.category)}*`;
}
if (validation.data.is_active !== undefined) {
query += `&is_active=eq.${validation.data.is_active}`;
}
if (validation.data.search) {
query += `&or=(name.ilike.*${encodeURIComponent(validation.data.search)}*,description.ilike.*${encodeURIComponent(validation.data.search)}*)`;
}
const res = await fetch(query, {
headers: {
apikey: anonKey,
"Content-Type": "application/json",
},
});
if (!res.ok) {
return apiError("Failed to fetch products", 500);
}
const products = await res.json();
// Track search analytics
if (validation.data.search) {
analytics.searchPerformed(validation.data.search, products.length, "products");
}
return apiResponse(products, 200);
} catch (error) {
captureError(error as Error, { path: "/api/products", method: "GET" });
return apiError("Internal server error", 500);
}
}
export async function POST(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(apiLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
const body = await req.json();
const { brand_id, name, description, price, category, is_active } = body;
if (!brand_id || !name) {
return apiError("brand_id and name are required", 400);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const res = await fetch(
`${supabaseUrl}/rest/v1/products`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
brand_id,
name,
description,
price,
category,
is_active: is_active !== false,
}),
}
);
if (!res.ok) {
const error = await res.json();
return apiError(error.message || "Failed to create product", 500);
}
const product = await res.json();
return apiResponse(product, 201);
} catch (error) {
captureError(error as Error, { path: "/api/products", method: "POST" });
return apiError("Internal server error", 500);
}
}
// ============================================
// OPTIONS (CORS preflight)
// ============================================
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
...securityHeaders(),
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
+127
View File
@@ -0,0 +1,127 @@
// Referrals API - Route-based handlers
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { apiLimiter, checkRateLimit, rateLimitExceeded, securityHeaders } from "@/lib/rate-limit";
import { analytics } from "@/lib/analytics";
import { captureError } from "@/lib/sentry";
// Helper functions
function apiResponse(data: unknown, status: number = 200) {
return NextResponse.json({ data }, { status });
}
function apiError(message: string, status: number) {
return NextResponse.json({ error: message }, { status });
}
function validationError(error: z.ZodError) {
return NextResponse.json({ error: "Validation failed", details: error.issues }, { status: 400 });
}
// ============================================
// REFERRAL API
// ============================================
const createReferralSchema = z.object({
brand_id: z.string().uuid(),
referred_email: z.string().email(),
referral_code: z.string().min(6).max(50),
});
export async function POST(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(apiLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
const body = await req.json();
const validation = createReferralSchema.safeParse(body);
if (!validation.success) {
return validationError(validation.error);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
// Redeem referral via RPC
const res = await fetch(
`${supabaseUrl}/rest/v1/rpc/redeem_referral`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
p_brand_id: validation.data.brand_id,
p_referred_email: validation.data.referred_email,
p_referral_code: validation.data.referral_code,
}),
}
);
if (!res.ok) {
const error = await res.json();
return apiError(error.message || "Failed to redeem referral", 500);
}
const referral = await res.json();
analytics.referralCompleted(validation.data.referral_code, referral.referred_user_id);
return apiResponse(referral, 201);
} catch (error) {
captureError(error as Error, { path: "/api/referrals", method: "POST" });
return apiError("Internal server error", 500);
}
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const brand_id = searchParams.get("brand_id");
if (!brand_id) {
return apiError("brand_id is required", 400);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const res = await fetch(
`${supabaseUrl}/rest/v1/referral_codes?brand_id=eq.${brand_id}&select=*&order=created_at.desc`,
{
headers: {
apikey: anonKey,
"Content-Type": "application/json",
},
}
);
if (!res.ok) {
return apiError("Failed to fetch referrals", 500);
}
const referrals = await res.json();
return apiResponse(referrals);
} catch (error) {
captureError(error as Error, { path: "/api/referrals", method: "GET" });
return apiError("Internal server error", 500);
}
}
// OPTIONS handler
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
...securityHeaders(),
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
+98
View File
@@ -0,0 +1,98 @@
// Reports API - Route-based handlers
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { apiLimiter, checkRateLimit, rateLimitExceeded, securityHeaders } from "@/lib/rate-limit";
import { captureError } from "@/lib/sentry";
// Helper functions
function apiResponse(data: unknown, status: number = 200) {
return NextResponse.json({ data }, { status });
}
function apiError(message: string, status: number) {
return NextResponse.json({ error: message }, { status });
}
function validationError(error: z.ZodError) {
return NextResponse.json({ error: "Validation failed", details: error.issues }, { status: 400 });
}
// ============================================
// REPORTS API
// ============================================
const reportFiltersSchema = z.object({
brand_id: z.string().uuid(),
start_date: z.string().datetime(),
end_date: z.string().datetime(),
report_type: z.enum(["orders", "revenue", "customers", "products"]).default("orders"),
});
export async function GET(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(apiLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
// Parse query params
const { searchParams } = new URL(req.url);
const params = {
brand_id: searchParams.get("brand_id")!,
start_date: searchParams.get("start_date")!,
end_date: searchParams.get("end_date")!,
report_type: searchParams.get("report_type") || "orders",
};
const validation = reportFiltersSchema.safeParse(params);
if (!validation.success) {
return validationError(validation.error);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
// Get report via RPC
const res = await fetch(
`${supabaseUrl}/rest/v1/rpc/generate_report`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
p_brand_id: validation.data.brand_id,
p_start_date: validation.data.start_date,
p_end_date: validation.data.end_date,
p_report_type: validation.data.report_type,
}),
}
);
if (!res.ok) {
return apiError("Failed to generate report", 500);
}
const report = await res.json();
return apiResponse(report);
} catch (error) {
captureError(error as Error, { path: "/api/reports", method: "GET" });
return apiError("Internal server error", 500);
}
}
// OPTIONS handler
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
...securityHeaders(),
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
-495
View File
@@ -1,495 +0,0 @@
// Production API Routes with Rate Limiting and Validation
import { NextRequest } from "next/server";
import { z } from "zod";
import {
apiLimiter,
checkoutLimiter,
emailLimiter,
checkRateLimit,
apiResponse,
apiError,
validationError,
rateLimitExceeded,
securityHeaders
} from "@/lib/rate-limit";
import { analytics } from "@/lib/analytics";
import { captureError } from "@/lib/sentry";
// ============================================
// API HEALTH CHECK
// ============================================
export async function GET() {
return apiResponse({
status: "healthy",
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || "1.0.0",
environment: process.env.NODE_ENV,
});
}
// ============================================
// ORDERS API
// ============================================
const createOrderSchema = z.object({
brand_id: z.string().uuid(),
customer_email: z.string().email().optional(),
customer_name: z.string().min(1).max(255).optional(),
customer_phone: z.string().regex(/^\+?[\d\s-()]+$/).optional(),
customer_address: z.string().optional(),
items: z.array(z.object({
product_id: z.string().uuid(),
quantity: z.number().int().positive(),
price: z.number().positive(),
fulfillment: z.enum(["pickup", "ship"]).default("pickup"),
})).min(1),
fulfillment: z.enum(["pickup", "ship", "mixed"]).optional(),
stop_id: z.string().uuid().optional(),
promo_code: z.string().optional(),
notes: z.string().max(1000).optional(),
});
export async function POST(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(checkoutLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
// Parse and validate body
const body = await req.json();
const validation = createOrderSchema.safeParse(body);
if (!validation.success) {
return validationError(validation.error);
}
const { brand_id, ...orderData } = validation.data;
// Create order via RPC
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const res = await fetch(
`${supabaseUrl}/rest/v1/rpc/create_order_with_items`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
p_brand_id: brand_id,
p_customer_email: orderData.customer_email,
p_customer_name: orderData.customer_name,
p_customer_phone: orderData.customer_phone,
p_customer_address: orderData.customer_address,
p_items: orderData.items,
p_fulfillment: orderData.fulfillment || "pickup",
p_stop_id: orderData.stop_id,
p_promo_code: orderData.promo_code,
p_notes: orderData.notes,
}),
}
);
if (!res.ok) {
const error = await res.json();
return apiError(error.message || "Failed to create order", 500);
}
const order = await res.json();
// Track analytics
analytics.orderCreated(order.id, orderData.items.reduce((sum, i) => sum + i.price * i.quantity, 0), brand_id);
return apiResponse(order, 201);
} catch (error) {
captureError(error as Error, { path: "/api/orders", method: "POST" });
return apiError("Internal server error", 500);
}
}
// ============================================
// PRODUCTS API
// ============================================
const getProductsSchema = z.object({
brand_id: z.string().uuid().optional(),
category: z.string().optional(),
is_active: z.boolean().optional(),
search: z.string().optional(),
limit: z.coerce.number().int().positive().max(100).default(20),
offset: z.coerce.number().int().min(0).default(0),
});
export async function GET(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(apiLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
// Parse query params
const { searchParams } = new URL(req.url);
const params = {
brand_id: searchParams.get("brand_id") || undefined,
category: searchParams.get("category") || undefined,
is_active: searchParams.get("is_active") === "true" ? true : searchParams.get("is_active") === "false" ? false : undefined,
search: searchParams.get("search") || undefined,
limit: parseInt(searchParams.get("limit") || "20"),
offset: parseInt(searchParams.get("offset") || "0"),
};
const validation = getProductsSchema.safeParse(params);
if (!validation.success) {
return validationError(validation.error);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
// Build query
let query = `${supabaseUrl}/rest/v1/products?select=*&order=name.asc&limit=${validation.data.limit}&offset=${validation.data.offset}`;
if (validation.data.brand_id) {
query += `&brand_id=eq.${validation.data.brand_id}`;
}
if (validation.data.category) {
query += `&category=ilike.*${encodeURIComponent(validation.data.category)}*`;
}
if (validation.data.is_active !== undefined) {
query += `&is_active=eq.${validation.data.is_active}`;
}
if (validation.data.search) {
query += `&or=(name.ilike.*${encodeURIComponent(validation.data.search)}*,description.ilike.*${encodeURIComponent(validation.data.search)}*)`;
}
const res = await fetch(query, {
headers: {
apikey: anonKey,
"Content-Type": "application/json",
},
});
if (!res.ok) {
return apiError("Failed to fetch products", 500);
}
const products = await res.json();
// Track search analytics
if (validation.data.search) {
analytics.searchPerformed(validation.data.search, products.length, "products");
}
return apiResponse(products, 200, {
limit: validation.data.limit,
offset: validation.data.offset,
count: products.length,
}, {
...securityHeaders(),
...rateLimitHeaders(rateCheck),
});
} catch (error) {
captureError(error as Error, { path: "/api/products", method: "GET" });
return apiError("Internal server error", 500);
}
}
// ============================================
// CAMPAIGNS API (Email/SMS)
// ============================================
const createCampaignSchema = z.object({
brand_id: z.string().uuid(),
name: z.string().min(1).max(255),
subject: z.string().min(1).max(500),
content: z.string().min(1),
type: z.enum(["email", "sms"]).default("email"),
segment_id: z.string().uuid().optional(),
contact_ids: z.array(z.string().uuid()).optional(),
scheduled_at: z.string().datetime().optional(),
});
export async function POST(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(emailLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
// Parse and validate body
const body = await req.json();
const validation = createCampaignSchema.safeParse(body);
if (!validation.success) {
return validationError(validation.error);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
// Create campaign via RPC
const res = await fetch(
`${supabaseUrl}/rest/v1/rpc/create_campaign`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
p_brand_id: validation.data.brand_id,
p_name: validation.data.name,
p_subject: validation.data.subject,
p_content: validation.data.content,
p_type: validation.data.type,
p_segment_id: validation.data.segment_id,
p_contact_ids: validation.data.contact_ids,
p_scheduled_at: validation.data.scheduled_at,
}),
}
);
if (!res.ok) {
const error = await res.json();
return apiError(error.message || "Failed to create campaign", 500);
}
const campaign = await res.json();
// Track analytics
const audienceSize = validation.data.contact_ids?.length || 0;
analytics.campaignCreated(campaign.id, validation.data.type, audienceSize);
return apiResponse(campaign, 201);
} catch (error) {
captureError(error as Error, { path: "/api/campaigns", method: "POST" });
return apiError("Internal server error", 500);
}
}
// ============================================
// WATER LOG API
// ============================================
const createWaterLogSchema = z.object({
brand_id: z.string().uuid(),
field_id: z.string().uuid().optional(),
field_name: z.string().max(255).optional(),
gallons: z.number().positive(),
duration_minutes: z.number().int().nonnegative().optional(),
water_method: z.enum(["drip", "sprinkler", "flood", "manual"]).optional(),
notes: z.string().max(500).optional(),
logged_at: z.string().datetime().optional(),
});
export async function POST(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(apiLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
// Parse and validate body
const body = await req.json();
const validation = createWaterLogSchema.safeParse(body);
if (!validation.success) {
return validationError(validation.error);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
// Create water log via RPC
const res = await fetch(
`${supabaseUrl}/rest/v1/rpc/create_water_log`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
p_brand_id: validation.data.brand_id,
p_field_id: validation.data.field_id,
p_field_name: validation.data.field_name,
p_gallons: validation.data.gallons,
p_duration_minutes: validation.data.duration_minutes,
p_water_method: validation.data.water_method,
p_notes: validation.data.notes,
p_logged_at: validation.data.logged_at,
}),
}
);
if (!res.ok) {
const error = await res.json();
return apiError(error.message || "Failed to create water log", 500);
}
const waterLog = await res.json();
return apiResponse(waterLog, 201);
} catch (error) {
captureError(error as Error, { path: "/api/water-logs", method: "POST" });
return apiError("Internal server error", 500);
}
}
// ============================================
// REPORTS API
// ============================================
const reportFiltersSchema = z.object({
brand_id: z.string().uuid(),
start_date: z.string().datetime(),
end_date: z.string().datetime(),
report_type: z.enum(["orders", "revenue", "customers", "products"]).default("orders"),
});
export async function GET(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(apiLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
// Parse query params
const { searchParams } = new URL(req.url);
const params = {
brand_id: searchParams.get("brand_id")!,
start_date: searchParams.get("start_date")!,
end_date: searchParams.get("end_date")!,
report_type: searchParams.get("report_type") || "orders",
};
const validation = reportFiltersSchema.safeParse(params);
if (!validation.success) {
return validationError(validation.error);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
// Get report via RPC
const res = await fetch(
`${supabaseUrl}/rest/v1/rpc/generate_report`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
p_brand_id: validation.data.brand_id,
p_start_date: validation.data.start_date,
p_end_date: validation.data.end_date,
p_report_type: validation.data.report_type,
}),
}
);
if (!res.ok) {
return apiError("Failed to generate report", 500);
}
const report = await res.json();
return apiResponse(report, 200);
} catch (error) {
captureError(error as Error, { path: "/api/reports", method: "GET" });
return apiError("Internal server error", 500);
}
}
// ============================================
// REFERRAL API
// ============================================
const createReferralSchema = z.object({
brand_id: z.string().uuid(),
referred_email: z.string().email(),
referral_code: z.string().min(6).max(50),
});
export async function POST(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(apiLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
const body = await req.json();
const validation = createReferralSchema.safeParse(body);
if (!validation.success) {
return validationError(validation.error);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
// Redeem referral via RPC
const res = await fetch(
`${supabaseUrl}/rest/v1/rpc/redeem_referral`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
p_brand_id: validation.data.brand_id,
p_referred_email: validation.data.referred_email,
p_referral_code: validation.data.referral_code,
}),
}
);
if (!res.ok) {
const error = await res.json();
return apiError(error.message || "Failed to redeem referral", 500);
}
const referral = await res.json();
analytics.referralCompleted(validation.data.referral_code, referral.referred_user_id);
return apiResponse(referral, 201);
} catch (error) {
captureError(error as Error, { path: "/api/referrals", method: "POST" });
return apiError("Internal server error", 500);
}
}
// ============================================
// OPTIONS HANDLER (CORS preflight)
// ============================================
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
...securityHeaders(),
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key",
"Access-Control-Max-Age": "86400",
},
});
}
+135
View File
@@ -0,0 +1,135 @@
// Water Logs API - Route-based handlers
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { apiLimiter, checkRateLimit, rateLimitExceeded, securityHeaders } from "@/lib/rate-limit";
import { captureError } from "@/lib/sentry";
// Helper functions
function apiResponse(data: unknown, status: number = 200) {
return NextResponse.json({ data }, { status });
}
function apiError(message: string, status: number) {
return NextResponse.json({ error: message }, { status });
}
function validationError(error: z.ZodError) {
return NextResponse.json({ error: "Validation failed", details: error.issues }, { status: 400 });
}
// ============================================
// WATER LOG API
// ============================================
const createWaterLogSchema = z.object({
brand_id: z.string().uuid(),
field_id: z.string().uuid().optional(),
field_name: z.string().max(255).optional(),
gallons: z.number().positive(),
duration_minutes: z.number().int().nonnegative().optional(),
water_method: z.enum(["drip", "sprinkler", "flood", "manual"]).optional(),
notes: z.string().max(500).optional(),
logged_at: z.string().datetime().optional(),
});
export async function POST(req: NextRequest) {
try {
// Rate limiting
const rateCheck = await checkRateLimit(apiLimiter, req.headers.get("x-forwarded-for") || "anonymous");
if (!rateCheck.success) {
return rateLimitExceeded(Math.ceil((rateCheck.reset - Date.now()) / 1000));
}
// Parse and validate body
const body = await req.json();
const validation = createWaterLogSchema.safeParse(body);
if (!validation.success) {
return validationError(validation.error);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
// Create water log via RPC
const res = await fetch(
`${supabaseUrl}/rest/v1/rpc/create_water_log`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
p_brand_id: validation.data.brand_id,
p_field_id: validation.data.field_id,
p_field_name: validation.data.field_name,
p_gallons: validation.data.gallons,
p_duration_minutes: validation.data.duration_minutes,
p_water_method: validation.data.water_method,
p_notes: validation.data.notes,
p_logged_at: validation.data.logged_at,
}),
}
);
if (!res.ok) {
const error = await res.json();
return apiError(error.message || "Failed to create water log", 500);
}
const waterLog = await res.json();
return apiResponse(waterLog, 201);
} catch (error) {
captureError(error as Error, { path: "/api/water-logs", method: "POST" });
return apiError("Internal server error", 500);
}
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const brand_id = searchParams.get("brand_id");
if (!brand_id) {
return apiError("brand_id is required", 400);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const res = await fetch(
`${supabaseUrl}/rest/v1/water_logs?brand_id=eq.${brand_id}&select=*&order=logged_at.desc&limit=100`,
{
headers: {
apikey: anonKey,
"Content-Type": "application/json",
},
}
);
if (!res.ok) {
return apiError("Failed to fetch water logs", 500);
}
const waterLogs = await res.json();
return apiResponse(waterLogs);
} catch (error) {
captureError(error as Error, { path: "/api/water-logs", method: "GET" });
return apiError("Internal server error", 500);
}
}
// OPTIONS handler
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
...securityHeaders(),
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
+171
View File
@@ -0,0 +1,171 @@
// Waitlist API - Signup and manage waitlist entries
import { NextRequest, NextResponse } from "next/server";
interface WaitlistEntry {
id: string;
email: string;
name?: string;
company?: string;
role?: string;
referral_code?: string;
referred_by?: string;
source: string;
created_at: string;
status: "pending" | "invited" | "converted" | "declined";
notes?: string;
}
// In-memory store for demo (replace with Supabase in production)
const waitlistEntries = new Map<string, WaitlistEntry>();
function generateId(): string {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
}
function generateReferralCode(email: string): string {
const prefix = email.substring(0, 2).toUpperCase();
const suffix = Math.random().toString(36).substring(2, 6).toUpperCase();
return `${prefix}${suffix}`;
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const status = searchParams.get("status");
const limit = parseInt(searchParams.get("limit") || "50");
const offset = parseInt(searchParams.get("offset") || "0");
let entries = Array.from(waitlistEntries.values());
// Filter by status if provided
if (status && status !== "all") {
entries = entries.filter((e) => e.status === status);
}
// Sort by created_at descending
entries.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
// Paginate
const total = entries.length;
const paginatedEntries = entries.slice(offset, offset + limit);
return NextResponse.json({
entries: paginatedEntries,
total,
limit,
offset,
});
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { email, name, company, role, referral_code, referred_by, source } = body;
// Validate required fields
if (!email) {
return NextResponse.json(
{ error: "Email is required" },
{ status: 400 }
);
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ error: "Invalid email format" },
{ status: 400 }
);
}
// Check for existing entry
const existingEntry = Array.from(waitlistEntries.values()).find(
(e) => e.email.toLowerCase() === email.toLowerCase()
);
if (existingEntry) {
return NextResponse.json(
{ error: "This email is already on the waitlist", entry: existingEntry },
{ status: 409 }
);
}
// Create new entry
const id = generateId();
const entry: WaitlistEntry = {
id,
email: email.toLowerCase(),
name: name || undefined,
company: company || undefined,
role: role || undefined,
referral_code: referral_code || generateReferralCode(email),
referred_by: referred_by || undefined,
source: source || "website",
created_at: new Date().toISOString(),
status: "pending",
};
waitlistEntries.set(id, entry);
// If referred, increment the referrer's count (in production, update in database)
if (referred_by) {
console.log(`[Waitlist] New signup referred by: ${referred_by}`);
// In production: update referrer's stats in Supabase
}
return NextResponse.json({
success: true,
entry,
message: "You've been added to the waitlist!",
position: waitlistEntries.size,
});
} catch (error) {
console.error("Waitlist API error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
export async function PATCH(request: NextRequest) {
try {
const body = await request.json();
const { id, status, notes } = body;
if (!id) {
return NextResponse.json(
{ error: "id is required" },
{ status: 400 }
);
}
const entry = waitlistEntries.get(id);
if (!entry) {
return NextResponse.json(
{ error: "Entry not found" },
{ status: 404 }
);
}
// Update fields
if (status) {
entry.status = status;
}
if (notes !== undefined) {
entry.notes = notes;
}
waitlistEntries.set(id, entry);
return NextResponse.json({
success: true,
entry,
});
} catch (error) {
console.error("Waitlist API error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
+226
View File
@@ -0,0 +1,226 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "Blog & Resources — Route Commerce",
description: "Guides, case studies, and resources for produce wholesale businesses.",
};
const BLOG_POSTS = [
{
slug: "getting-started-with-route-commerce",
title: "Getting Started with Route Commerce",
excerpt: "Learn how to set up your wholesale business on Route Commerce in under 10 minutes.",
category: "Guides",
author: "Team Route Commerce",
date: "Jan 15, 2025",
readTime: "5 min read",
image: "/blog/getting-started.jpg",
},
{
slug: "maximize-produce-profitability",
title: "5 Tips to Maximize Your Produce Profitability",
excerpt: "Strategic pricing and inventory management can significantly boost your bottom line.",
category: "Tips",
author: "Sarah Johnson",
date: "Jan 10, 2025",
readTime: "7 min read",
image: "/blog/profitability.jpg",
},
{
slug: "customer-communication-best-practices",
title: "Best Practices for Customer Communication",
excerpt: "Keep your customers informed and engaged with these communication strategies.",
category: "Marketing",
author: "Marcus Chen",
date: "Jan 5, 2025",
readTime: "6 min read",
image: "/blog/communication.jpg",
},
];
const RESOURCES = [
{
title: "Wholesale Pricing Guide",
description: "Complete guide to setting profitable wholesale prices",
type: "Guide",
downloads: 342,
icon: "📋",
},
{
title: "Order Management Checklist",
description: "Step-by-step checklist for order fulfillment",
type: "Checklist",
downloads: 256,
icon: "✓",
},
{
title: "Customer Email Templates",
description: "Pre-written email templates for common scenarios",
type: "Templates",
downloads: 189,
icon: "✉",
},
];
export default function BlogPage() {
return (
<div className="min-h-screen bg-[#faf8f5]">
{/* Header */}
<header className="border-b border-[#e5e5e5] bg-white">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<a href="/" className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-lg font-bold text-[#1a1a1a]">Route Commerce</span>
</a>
<nav className="flex items-center gap-6 text-sm">
<Link href="/blog" className="font-medium text-[#1a4d2e]">Blog</Link>
<Link href="/resources" className="text-[#888] hover:text-[#1a1a1a]">Resources</Link>
<Link href="/roadmap" className="text-[#888] hover:text-[#1a1a1a]">Roadmap</Link>
</nav>
</div>
</header>
{/* Hero */}
<section className="py-20">
<div className="max-w-4xl mx-auto px-6 text-center">
<h1 className="text-5xl font-bold text-[#1a1a1a] mb-4" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Blog & Resources
</h1>
<p className="text-xl text-[#6b8f71]">
Guides, tips, and resources to help you grow your wholesale produce business.
</p>
</div>
</section>
{/* Featured Post */}
<section className="pb-16">
<div className="max-w-6xl mx-auto px-6">
<div className="bg-white rounded-3xl overflow-hidden border border-[#e5e5e5] shadow-sm">
<div className="grid md:grid-cols-2">
<div className="bg-gradient-to-br from-[#1a4d2e]/10 to-[#2d6a4f]/5 aspect-video md:aspect-auto flex items-center justify-center">
<div className="text-6xl opacity-30">📰</div>
</div>
<div className="p-8 flex flex-col justify-center">
<span className="inline-block w-fit px-3 py-1 bg-[#c97a3e]/10 text-[#c97a3e] text-sm font-medium rounded-full mb-4">
Featured
</span>
<h2 className="text-3xl font-bold text-[#1a1a1a] mb-4" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Getting Started with Route Commerce
</h2>
<p className="text-[#666] mb-6">
Learn how to set up your wholesale business on Route Commerce in under 10 minutes. From adding products to scheduling stops, we&apos;ve got you covered.
</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-[#1a4d2e]/10 rounded-full flex items-center justify-center text-[#1a4d2e] font-bold">R</div>
<div>
<p className="font-medium text-[#1a1a1a]">Team Route Commerce</p>
<p className="text-sm text-[#888]">Jan 15, 2025 · 5 min read</p>
</div>
</div>
<Link href="/blog/getting-started-with-route-commerce" className="px-5 py-2.5 bg-[#1a4d2e] text-white rounded-xl font-medium hover:bg-[#2d6a4f] transition-colors">
Read More
</Link>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Latest Posts */}
<section className="pb-16">
<div className="max-w-6xl mx-auto px-6">
<h2 className="text-2xl font-bold text-[#1a1a1a] mb-8">Latest Articles</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{BLOG_POSTS.slice(1).map((post) => (
<article key={post.slug} className="bg-white rounded-2xl overflow-hidden border border-[#e5e5e5] hover:shadow-lg transition-shadow">
<div className="bg-gradient-to-br from-[#6b8f71]/10 to-[#1a4d2e]/5 p-8 aspect-video flex items-center justify-center">
<div className="text-4xl opacity-30">📝</div>
</div>
<div className="p-6">
<span className="inline-block px-2 py-0.5 bg-[#faf8f5] text-[#888] text-xs font-medium rounded mb-3">
{post.category}
</span>
<h3 className="text-lg font-semibold text-[#1a1a1a] mb-2">{post.title}</h3>
<p className="text-sm text-[#666] mb-4 line-clamp-2">{post.excerpt}</p>
<div className="flex items-center justify-between">
<span className="text-xs text-[#888]">{post.author}</span>
<span className="text-xs text-[#888]">{post.readTime}</span>
</div>
</div>
</article>
))}
</div>
</div>
</section>
{/* Resources */}
<section className="py-16 bg-white border-t border-[#e5e5e5]">
<div className="max-w-6xl mx-auto px-6">
<div className="flex items-center justify-between mb-8">
<h2 className="text-2xl font-bold text-[#1a1a1a]">Free Resources</h2>
<Link href="/resources" className="text-[#1a4d2e] font-medium hover:underline">
View All
</Link>
</div>
<div className="grid md:grid-cols-3 gap-6">
{RESOURCES.map((resource, i) => (
<div key={i} className="bg-[#faf8f5] rounded-2xl p-6 border border-[#e5e5e5] hover:border-[#1a4d2e]/30 transition-colors">
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center text-2xl mb-4 border border-[#e5e5e5]">
{resource.icon}
</div>
<h3 className="font-semibold text-[#1a1a1a] mb-2">{resource.title}</h3>
<p className="text-sm text-[#666] mb-4">{resource.description}</p>
<div className="flex items-center justify-between">
<span className="text-xs text-[#888]">{resource.downloads} downloads</span>
<button className="text-sm text-[#1a4d2e] font-medium hover:underline">
Download
</button>
</div>
</div>
))}
</div>
</div>
</section>
{/* Newsletter */}
<section className="py-20 bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f]">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="text-3xl font-bold text-white mb-4" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Stay in the Loop
</h2>
<p className="text-[#faf8f5]/80 mb-8">
Get weekly tips, industry insights, and product updates delivered to your inbox.
</p>
<form className="flex gap-3 max-w-md mx-auto">
<input
type="email"
placeholder="you@farm.com"
className="flex-1 px-4 py-3 rounded-xl bg-white/10 border border-white/20 text-white placeholder-[#faf8f5]/50 focus:outline-none focus:ring-2 focus:ring-white/50"
/>
<button
type="submit"
className="px-6 py-3 bg-white text-[#1a4d2e] rounded-xl font-medium hover:bg-[#faf8f5] transition-colors"
>
Subscribe
</button>
</form>
<p className="text-xs text-[#faf8f5]/60 mt-4">No spam. Unsubscribe anytime.</p>
</div>
</section>
{/* Footer */}
<footer className="border-t border-[#e5e5e5] py-8 bg-white">
<div className="max-w-6xl mx-auto px-6 text-center text-sm text-[#888]">
© 2025 Route Commerce. All rights reserved.
</div>
</footer>
</div>
);
}
+188
View File
@@ -0,0 +1,188 @@
import type { Metadata } from "next";
import { FileText, Zap, Bug, Shield } from "lucide-react";
export const metadata: Metadata = {
title: "Changelog — Route Commerce",
description: "See what&apos;s new in Route Commerce. Track our progress, new features, and improvements.",
};
const CHANGELOG = [
{
version: "2.4.0",
date: "2025-01-15",
category: "feature",
title: "Harvest Reach Email Campaigns",
description: "Send beautiful email campaigns to your customers. Includes templates, scheduling, and analytics.",
icon: FileText,
},
{
version: "2.3.0",
date: "2025-01-08",
category: "feature",
title: "Square Inventory Sync",
description: "Keep your product catalog and inventory in sync with Square POS. Import products, push updates automatically.",
icon: Zap,
},
{
version: "2.2.2",
date: "2024-12-20",
category: "improvement",
title: "Faster Dashboard Loading",
description: "Dashboard now loads 40% faster with improved caching and lazy loading of components.",
icon: Zap,
},
{
version: "2.2.1",
date: "2024-12-15",
category: "bug",
title: "Order Export Fix",
description: "Fixed an issue where order export would fail for orders with more than 50 items.",
icon: Bug,
},
{
version: "2.2.0",
date: "2024-12-10",
category: "feature",
title: "AI Intelligence Pack",
description: "New AI-powered features: Campaign Writer, Pricing Advisor, and Demand Forecasting. Available on Enterprise plan.",
icon: Zap,
},
{
version: "2.1.0",
date: "2024-11-28",
category: "feature",
title: "Water Log Module",
description: "Track irrigation and water usage for your fields. Generate reports and monitor sustainability metrics.",
icon: Zap,
},
{
version: "2.0.0",
date: "2024-11-15",
category: "improvement",
title: "New Admin Dashboard",
description: "Complete redesign of the admin dashboard with improved navigation, faster loading, and better mobile support.",
icon: Zap,
},
{
version: "1.9.0",
date: "2024-10-30",
category: "security",
title: "Two-Factor Authentication",
description: "Added 2FA support for enhanced account security. Admin users can now enable TOTP-based authentication.",
icon: Shield,
},
];
const categoryStyles = {
feature: { bg: "bg-emerald-100", text: "text-emerald-700", label: "New Feature" },
improvement: { bg: "bg-blue-100", text: "text-blue-700", label: "Improvement" },
bug: { bg: "bg-amber-100", text: "text-amber-700", label: "Bug Fix" },
security: { bg: "bg-purple-100", text: "text-purple-700", label: "Security" },
};
export default function ChangelogPage() {
return (
<div className="min-h-screen bg-[#faf8f5]">
{/* Header */}
<header className="border-b border-[#e5e5e5] bg-white">
<div className="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between">
<a href="/" className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-lg font-bold text-[#1a1a1a]">Route Commerce</span>
</a>
<a href="/admin" className="text-sm text-[#666] hover:text-[#1a4d2e] transition-colors">
Back to Admin
</a>
</div>
</header>
{/* Main Content */}
<main className="max-w-4xl mx-auto px-6 py-16">
{/* Hero */}
<div className="text-center mb-16">
<h1 className="text-5xl font-bold text-[#1a1a1a] mb-4" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Changelog
</h1>
<p className="text-xl text-[#6b8f71] max-w-2xl mx-auto">
Track our progress, discover new features, and stay updated with the latest improvements to Route Commerce.
</p>
</div>
{/* Timeline */}
<div className="relative">
{/* Timeline line */}
<div className="absolute left-8 top-0 bottom-0 w-px bg-[#e5e5e5]" />
<div className="space-y-8">
{CHANGELOG.map((item, index) => {
const Icon = item.icon;
const style = categoryStyles[item.category as keyof typeof categoryStyles];
return (
<div key={index} className="relative flex gap-6">
{/* Timeline dot */}
<div className="relative z-10 flex-shrink-0 w-16 flex items-center justify-center">
<div className={`w-10 h-10 rounded-2xl ${style.bg} flex items-center justify-center`}>
<Icon className={`w-5 h-5 ${style.text}`} />
</div>
</div>
{/* Content */}
<div className="flex-1 bg-white rounded-2xl p-6 border border-[#e5e5e5] shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<span className={`px-2 py-1 rounded-md text-xs font-semibold ${style.bg} ${style.text}`}>
{style.label}
</span>
<span className="text-sm text-[#888]">
v{item.version}
</span>
</div>
<time className="text-sm text-[#888]">
{new Date(item.date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})}
</time>
</div>
<h3 className="text-lg font-semibold text-[#1a1a1a] mb-2">{item.title}</h3>
<p className="text-[#666] leading-relaxed">{item.description}</p>
</div>
</div>
);
})}
</div>
</div>
{/* RSS & Subscribe */}
<div className="mt-16 p-8 bg-white rounded-2xl border border-[#e5e5e5] text-center">
<h3 className="text-xl font-bold text-[#1a1a1a] mb-2">Stay Updated</h3>
<p className="text-[#666] mb-6">Get notified about new features and improvements.</p>
<div className="flex flex-wrap justify-center gap-4">
<a href="/api/feed/changelog.xml" className="inline-flex items-center gap-2 px-5 py-2.5 bg-[#faf8f5] border border-[#e5e5e5] rounded-xl text-sm font-medium text-[#1a1a1a] hover:bg-gray-100 transition-colors">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 18a2 2 0 01-2-2 2 2 0 012-2h2a2 2 0 012 2 2 2 0 01-2 2H6zm6-6a2 2 0 012-2 2 2 0 012 2 2 2 0 01-2 2 2 2 0 01-2-2zm-6 0a2 2 0 00-2 2 2 2 0 002 2 2 2 0 002-2 2 2 0 00-2-2zm12 0a2 2 0 00-2 2 2 2 0 002 2 2 2 0 002-2 2 2 0 00-2-2z" />
</svg>
RSS Feed
</a>
<a href="/waitlist" className="inline-flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-[#1a4d2e] to-[#2d6a4f] text-white rounded-xl text-sm font-medium hover:from-[#2d6a4f] hover:to-[#1a4d2e] transition-all">
Join Waitlist
</a>
</div>
</div>
</main>
{/* Footer */}
<footer className="border-t border-[#e5e5e5] py-8">
<div className="max-w-4xl mx-auto px-6 text-center text-sm text-[#888]">
© 2025 Route Commerce. All rights reserved.
</div>
</footer>
</div>
);
}
+4
View File
@@ -1,6 +1,8 @@
import type { Metadata, Viewport } from "next";
import "./globals.css";
import { Providers } from "@/components/Providers";
import ToastNotificationContainer from "@/components/notifications/ToastNotification";
import CookieConsentBanner from "@/components/legal/CookieConsentBanner";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
@@ -51,6 +53,8 @@ export default function RootLayout({
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
<ToastNotificationContainer />
<CookieConsentBanner />
</body>
</html>
);
+77
View File
@@ -0,0 +1,77 @@
import Link from "next/link";
export default function MaintenancePage() {
return (
<div className="min-h-screen bg-[#faf8f5] flex items-center justify-center px-6">
{/* Animated background */}
<div className="fixed inset-0 pointer-events-none overflow-hidden">
<div className="absolute top-1/4 left-1/4 w-80 h-80 bg-[#1a4d2e]/5 rounded-full blur-3xl animate-pulse" style={{ animationDuration: "4s" }} />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-[#c97a3e]/5 rounded-full blur-3xl animate-pulse" style={{ animationDuration: "6s", animationDelay: "1s" }} />
</div>
<div className="text-center max-w-lg mx-auto relative">
{/* Maintenance Icon */}
<div className="relative mb-8">
<div className="w-28 h-28 mx-auto bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] rounded-3xl flex items-center justify-center shadow-2xl">
<svg className="w-14 h-14 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<span className="absolute -top-2 -right-2 w-10 h-10 bg-[#c97a3e] rounded-xl flex items-center justify-center text-white text-xl shadow-lg">
</span>
</div>
{/* Heading */}
<h1 className="text-4xl font-bold text-[#1a1a1a] mb-4" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Under Maintenance
</h1>
<p className="text-lg text-[#6b8f71] mb-6">
We&apos;re making improvements to serve you better.
</p>
{/* Info box */}
<div className="bg-white rounded-2xl p-6 border border-[#e5e5e5] shadow-sm mb-8">
<div className="flex items-center justify-center gap-2 mb-4">
<div className="w-3 h-3 bg-emerald-500 rounded-full animate-pulse" />
<span className="text-sm font-medium text-[#1a1a1a]">Expected back shortly</span>
</div>
<p className="text-sm text-[#666]">
Our team is working on some exciting updates. We&apos;ll be back online shortly.
</p>
</div>
{/* Contact */}
<div className="mb-8">
<p className="text-sm text-[#888] mb-3">Need urgent assistance?</p>
<div className="flex flex-wrap justify-center gap-3">
<a href="mailto:support@routecommerce.com" className="px-4 py-2 bg-[#1a4d2e] text-white rounded-xl text-sm font-medium hover:bg-[#2d6a4f] transition-colors">
Email Support
</a>
<Link href="/contact" className="px-4 py-2 bg-white border border-[#e5e5e5] text-[#1a1a1a] rounded-xl text-sm font-medium hover:border-[#1a4d2e] transition-colors">
Contact Us
</Link>
</div>
</div>
{/* Social updates */}
<div className="pt-6 border-t border-[#e5e5e5]">
<p className="text-xs text-[#888] mb-4">Follow for updates:</p>
<div className="flex justify-center gap-4">
<a href="https://twitter.com" target="_blank" rel="noopener noreferrer" className="w-10 h-10 bg-white border border-[#e5e5e5] rounded-xl flex items-center justify-center text-[#1a1a1a] hover:border-[#1a4d2e] hover:text-[#1a4d2e] transition-colors">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
</svg>
</a>
<a href="https://linkedin.com" target="_blank" rel="noopener noreferrer" className="w-10 h-10 bg-white border border-[#e5e5e5] rounded-xl flex items-center justify-center text-[#1a1a1a] hover:border-[#1a4d2e] hover:text-[#1a4d2e] transition-colors">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
</div>
</div>
</div>
</div>
);
}
+69 -23
View File
@@ -2,32 +2,78 @@ import Link from "next/link";
export default function NotFound() {
return (
<div className="min-h-screen bg-zinc-950 flex items-center justify-center px-6">
<div className="text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-zinc-800 mb-6">
<svg className="w-8 h-8 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="min-h-screen bg-[#faf8f5] flex items-center justify-center px-6">
{/* Background decorations */}
<div className="fixed inset-0 pointer-events-none overflow-hidden">
<div className="absolute top-20 left-20 w-40 h-40 bg-[#1a4d2e]/5 rounded-full blur-3xl" />
<div className="absolute bottom-40 right-20 w-60 h-60 bg-[#c97a3e]/5 rounded-full blur-3xl" />
</div>
<div className="text-center max-w-md mx-auto relative">
{/* 404 Illustration */}
<div className="relative mb-8">
<div className="w-24 h-24 mx-auto bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] rounded-3xl flex items-center justify-center shadow-xl">
<svg className="w-12 h-12 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<span className="absolute -top-4 -right-4 w-12 h-12 bg-[#c97a3e]/20 rounded-full flex items-center justify-center text-2xl">📦</span>
</div>
<h1 className="text-4xl font-bold text-zinc-100 tracking-tight">Page not found</h1>
<p className="mt-3 text-zinc-500 text-sm max-w-sm mx-auto">
The page you&apos;re looking for doesn&apos;t exist or has been moved.
<h1 className="text-6xl font-bold text-[#1a1a1a] mb-4 tracking-tight" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
404
</h1>
<h2 className="text-2xl font-semibold text-[#1a1a1a] mb-3">
Page not found
</h2>
<p className="text-[#6b8f71] mb-8">
Looks like this route got lost along the way. Let&apos;s get you back on track.
</p>
<div className="mt-8 flex items-center justify-center gap-4">
<Link
href="/"
className="rounded-xl bg-zinc-800 hover:bg-zinc-700 px-5 py-2.5 text-sm font-semibold text-zinc-100 transition-colors border border-zinc-700"
>
Go home
</Link>
<Link
href="/admin"
className="rounded-xl bg-emerald-600 hover:bg-emerald-500 px-5 py-2.5 text-sm font-semibold text-white transition-colors"
>
Admin
</Link>
{/* Search suggestion */}
<div className="bg-white rounded-2xl p-4 border border-[#e5e5e5] mb-8">
<p className="text-sm text-[#888] mb-3">Try searching for what you need:</p>
<div className="flex gap-2">
<input
type="text"
placeholder="Search..."
className="flex-1 px-4 py-2 rounded-xl border border-[#e5e5e5] text-sm focus:outline-none focus:ring-2 focus:ring-[#1a4d2e]/50"
/>
<button className="px-4 py-2 bg-[#1a4d2e] text-white rounded-xl text-sm font-medium hover:bg-[#2d6a4f] transition-colors">
Search
</button>
</div>
</div>
{/* Quick links */}
<div className="space-y-3">
<p className="text-sm text-[#888]">Or explore these pages:</p>
<div className="flex flex-wrap justify-center gap-3">
<Link href="/" className="px-4 py-2 bg-white border border-[#e5e5e5] rounded-xl text-sm text-[#1a1a1a] hover:border-[#1a4d2e] hover:text-[#1a4d2e] transition-colors">
Home
</Link>
<Link href="/pricing" className="px-4 py-2 bg-white border border-[#e5e5e5] rounded-xl text-sm text-[#1a1a1a] hover:border-[#1a4d2e] hover:text-[#1a4d2e] transition-colors">
Pricing
</Link>
<Link href="/blog" className="px-4 py-2 bg-white border border-[#e5e5e5] rounded-xl text-sm text-[#1a1a1a] hover:border-[#1a4d2e] hover:text-[#1a4d2e] transition-colors">
Blog
</Link>
<Link href="/roadmap" className="px-4 py-2 bg-white border border-[#e5e5e5] rounded-xl text-sm text-[#1a1a1a] hover:border-[#1a4d2e] hover:text-[#1a4d2e] transition-colors">
Roadmap
</Link>
</div>
</div>
{/* Report broken link */}
<div className="mt-12 pt-8 border-t border-[#e5e5e5]">
<p className="text-xs text-[#888]">
Found a broken link?{" "}
<a href="mailto:support@routecommerce.com" className="text-[#1a4d2e] hover:underline">
Let us know
</a>
</p>
</div>
</div>
</div>
);
}
}
+226
View File
@@ -0,0 +1,226 @@
import type { Metadata } from "next";
import { Check, Clock, TrendingUp, Lightbulb } from "lucide-react";
export const metadata: Metadata = {
title: "Roadmap — Route Commerce",
description: "See what&apos;s coming next to Route Commerce. Vote on features, suggest ideas, and track our progress.",
};
const ROADMAP_ITEMS = {
shipped: [
{ id: 1, title: "Harvest Reach Email Campaigns", description: "Beautiful email marketing with templates and analytics", category: "Communication", upvotes: 124 },
{ id: 2, title: "Square Inventory Sync", description: "Two-way sync with Square POS", category: "Integrations", upvotes: 89 },
{ id: 3, title: "AI Intelligence Pack", description: "Campaign writer, pricing advisor, demand forecasting", category: "AI", upvotes: 156 },
{ id: 4, title: "Water Log Module", description: "Track irrigation and water usage", category: "Operations", upvotes: 67 },
],
inProgress: [
{ id: 5, title: "Mobile App (iOS & Android)", description: "Native apps for field workers and delivery drivers", category: "Mobile", upvotes: 234 },
{ id: 6, title: "Advanced Reporting & Analytics", description: "Custom dashboards, export to BI tools", category: "Reporting", upvotes: 178 },
{ id: 7, title: "Multi-location Support", description: "Manage multiple farms or warehouses from one account", category: "Operations", upvotes: 145 },
],
planned: [
{ id: 8, title: "SMS Campaigns", description: "Text message marketing and notifications", category: "Communication", upvotes: 98 },
{ id: 9, title: "Route Optimization", description: "AI-powered route planning for deliveries", category: "Logistics", upvotes: 167 },
{ id: 10, title: "POS Integration ( Clover, Toast)", description: "Additional POS system integrations", category: "Integrations", upvotes: 76 },
{ id: 11, title: "Customer Loyalty Program", description: "Points, rewards, and referral tracking", category: "Marketing", upvotes: 112 },
],
};
const CATEGORIES = ["All", "Communication", "Integrations", "AI", "Operations", "Mobile", "Reporting", "Logistics", "Marketing"];
export default function RoadmapPage() {
return (
<div className="min-h-screen bg-[#faf8f5]">
{/* Header */}
<header className="border-b border-[#e5e5e5] bg-white">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<a href="/" className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-lg font-bold text-[#1a1a1a]">Route Commerce</span>
</a>
<a href="/changelog" className="text-sm text-[#666] hover:text-[#1a4d2e] transition-colors">
View Changelog
</a>
</div>
</header>
{/* Hero */}
<section className="bg-gradient-to-b from-white to-[#faf8f5] py-20">
<div className="max-w-4xl mx-auto px-6 text-center">
<h1 className="text-5xl font-bold text-[#1a1a1a] mb-4" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Product Roadmap
</h1>
<p className="text-xl text-[#6b8f71] max-w-2xl mx-auto">
See what we&apos;re building next. Vote for features you want most, or suggest new ideas.
</p>
<div className="flex justify-center gap-4 mt-8">
<a href="/roadmap#suggest" className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-[#1a4d2e] to-[#2d6a4f] text-white rounded-xl font-medium hover:from-[#2d6a4f] hover:to-[#1a4d2e] transition-all">
<Lightbulb className="w-4 h-4" />
Suggest a Feature
</a>
</div>
</div>
</section>
{/* Roadmap Columns */}
<section className="py-16">
<div className="max-w-6xl mx-auto px-6">
<div className="grid lg:grid-cols-3 gap-8">
{/* Shipped */}
<div>
<div className="flex items-center gap-2 mb-6">
<div className="w-8 h-8 rounded-lg bg-emerald-100 flex items-center justify-center">
<Check className="w-4 h-4 text-emerald-600" />
</div>
<h2 className="text-xl font-bold text-[#1a1a1a]">Shipped</h2>
</div>
<div className="space-y-4">
{ROADMAP_ITEMS.shipped.map((item) => (
<div key={item.id} className="bg-white rounded-xl p-5 border border-[#e5e5e5] shadow-sm hover:shadow-md transition-shadow">
<span className="inline-block px-2 py-0.5 text-xs font-medium bg-[#faf8f5] text-[#888] rounded mb-2">
{item.category}
</span>
<h3 className="font-semibold text-[#1a1a1a] mb-2">{item.title}</h3>
<p className="text-sm text-[#666] mb-4">{item.description}</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-emerald-600">
<Check className="w-4 h-4" />
<span className="text-sm font-medium">Shipped</span>
</div>
<span className="text-sm text-[#888]">{item.upvotes} votes</span>
</div>
</div>
))}
</div>
</div>
{/* In Progress */}
<div>
<div className="flex items-center gap-2 mb-6">
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
<Clock className="w-4 h-4 text-blue-600" />
</div>
<h2 className="text-xl font-bold text-[#1a1a1a]">In Progress</h2>
</div>
<div className="space-y-4">
{ROADMAP_ITEMS.inProgress.map((item) => (
<div key={item.id} className="bg-white rounded-xl p-5 border border-[#e5e5e5] shadow-sm hover:shadow-md transition-shadow">
<span className="inline-block px-2 py-0.5 text-xs font-medium bg-blue-50 text-blue-600 rounded mb-2">
{item.category}
</span>
<h3 className="font-semibold text-[#1a1a1a] mb-2">{item.title}</h3>
<p className="text-sm text-[#666] mb-4">{item.description}</p>
<div className="flex items-center justify-between">
<button className="flex items-center gap-1 text-[#1a4d2e] hover:text-[#2d6a4f] transition-colors">
<TrendingUp className="w-4 h-4" />
<span className="text-sm font-medium">Upvote</span>
</button>
<span className="text-sm text-[#888]">{item.upvotes} votes</span>
</div>
</div>
))}
</div>
</div>
{/* Planned */}
<div>
<div className="flex items-center gap-2 mb-6">
<div className="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center">
<Lightbulb className="w-4 h-4 text-amber-600" />
</div>
<h2 className="text-xl font-bold text-[#1a1a1a]">Planned</h2>
</div>
<div className="space-y-4">
{ROADMAP_ITEMS.planned.map((item) => (
<div key={item.id} className="bg-white rounded-xl p-5 border border-[#e5e5e5] shadow-sm hover:shadow-md transition-shadow">
<span className="inline-block px-2 py-0.5 text-xs font-medium bg-amber-50 text-amber-600 rounded mb-2">
{item.category}
</span>
<h3 className="font-semibold text-[#1a1a1a] mb-2">{item.title}</h3>
<p className="text-sm text-[#666] mb-4">{item.description}</p>
<div className="flex items-center justify-between">
<button className="flex items-center gap-1 text-[#1a4d2e] hover:text-[#2d6a4f] transition-colors">
<TrendingUp className="w-4 h-4" />
<span className="text-sm font-medium">Upvote</span>
</button>
<span className="text-sm text-[#888]">{item.upvotes} votes</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
{/* Suggest Feature */}
<section id="suggest" className="py-16 bg-white border-t border-[#e5e5e5]">
<div className="max-w-2xl mx-auto px-6">
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-[#1a1a1a] mb-2" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Suggest a Feature
</h2>
<p className="text-[#666]">Have an idea? We&apos;d love to hear it. Share your suggestion and vote on others.</p>
</div>
<form className="bg-[#faf8f5] rounded-2xl p-6 border border-[#e5e5e5]">
<div className="space-y-4">
<div>
<label htmlFor="feature-title" className="block text-sm font-medium text-[#1a1a1a] mb-2">
Feature Title
</label>
<input
type="text"
id="feature-title"
placeholder="e.g., Export orders to CSV"
className="w-full px-4 py-3 rounded-xl border border-[#e5e5e5] bg-white text-[#1a1a1a] placeholder-[#888] focus:outline-none focus:ring-2 focus:ring-[#1a4d2e]/50 focus:border-[#1a4d2e] transition-all"
/>
</div>
<div>
<label htmlFor="feature-description" className="block text-sm font-medium text-[#1a1a1a] mb-2">
Description
</label>
<textarea
id="feature-description"
rows={4}
placeholder="Describe the feature and how it would help you..."
className="w-full px-4 py-3 rounded-xl border border-[#e5e5e5] bg-white text-[#1a1a1a] placeholder-[#888] focus:outline-none focus:ring-2 focus:ring-[#1a4d2e]/50 focus:border-[#1a4d2e] transition-all resize-none"
/>
</div>
<div>
<label htmlFor="feature-category" className="block text-sm font-medium text-[#1a1a1a] mb-2">
Category
</label>
<select
id="feature-category"
className="w-full px-4 py-3 rounded-xl border border-[#e5e5e5] bg-white text-[#1a1a1a] focus:outline-none focus:ring-2 focus:ring-[#1a4d2e]/50 focus:border-[#1a4d2e] transition-all"
>
<option value="">Select a category</option>
{CATEGORIES.filter(c => c !== "All").map((category) => (
<option key={category} value={category.toLowerCase()}>{category}</option>
))}
</select>
</div>
<button
type="submit"
className="w-full px-6 py-3 bg-gradient-to-r from-[#1a4d2e] to-[#2d6a4f] text-white rounded-xl font-medium hover:from-[#2d6a4f] hover:to-[#1a4d2e] transition-all"
>
Submit Suggestion
</button>
</div>
</form>
</div>
</section>
{/* Footer */}
<footer className="border-t border-[#e5e5e5] py-8">
<div className="max-w-6xl mx-auto px-6 text-center text-sm text-[#888]">
© 2025 Route Commerce. All rights reserved.
</div>
</footer>
</div>
);
}
+238
View File
@@ -0,0 +1,238 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "Security — Route Commerce",
description: "Learn about Route Commerce security practices, data protection, and compliance.",
};
const SECURITY_FEATURES = [
{
title: "Bank-Level Encryption",
description: "All data is encrypted in transit using TLS 1.3 and at rest using AES-256 encryption.",
icon: "🔒",
},
{
title: "SOC 2 Compliant",
description: "Our infrastructure meets SOC 2 Type II standards for security, availability, and confidentiality.",
icon: "✓",
},
{
title: "Regular Security Audits",
description: "We conduct quarterly penetration tests and annual security audits with certified third parties.",
icon: "🔍",
},
{
title: "99.9% Uptime SLA",
description: "Enterprise plan customers receive a 99.9% uptime guarantee with 24/7 monitoring.",
icon: "⚡",
},
{
title: "GDPR & CCPA Compliant",
description: "We comply with GDPR and CCPA regulations, giving you full control over your data.",
icon: "🛡",
},
{
title: "Secure Authentication",
description: "Supabase Auth provides secure, compliant authentication with 2FA support.",
icon: "🔑",
},
];
const TRUST_BADGES = [
{ label: "SSL Secured", icon: "🔒" },
{ label: "SOC 2 Compliant", icon: "✓" },
{ label: "GDPR Ready", icon: "🛡" },
{ label: "PCI Compliant", icon: "💳" },
];
export default function SecurityPage() {
return (
<div className="min-h-screen bg-[#faf8f5]">
{/* Header */}
<header className="border-b border-[#e5e5e5] bg-white">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<a href="/" className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-lg font-bold text-[#1a1a1a]">Route Commerce</span>
</a>
<Link href="/pricing" className="px-5 py-2 bg-[#1a4d2e] text-white rounded-xl text-sm font-medium hover:bg-[#2d6a4f] transition-colors">
Get Started
</Link>
</div>
</header>
{/* Trust Badges */}
<section className="py-12 bg-white border-b border-[#e5e5e5]">
<div className="max-w-6xl mx-auto px-6">
<div className="flex flex-wrap justify-center gap-6">
{TRUST_BADGES.map((badge, i) => (
<div key={i} className="flex items-center gap-2 px-5 py-3 bg-[#faf8f5] rounded-full border border-[#e5e5e5]">
<span className="text-lg">{badge.icon}</span>
<span className="text-sm font-medium text-[#1a1a1a]">{badge.label}</span>
</div>
))}
</div>
</div>
</section>
{/* Hero */}
<section className="py-20">
<div className="max-w-4xl mx-auto px-6 text-center">
<h1 className="text-5xl font-bold text-[#1a1a1a] mb-4" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Your Data is Safe with Us
</h1>
<p className="text-xl text-[#6b8f71] max-w-2xl mx-auto">
Route Commerce is built on enterprise-grade infrastructure with security at every layer. We protect your business data with the same rigor used by Fortune 500 companies.
</p>
</div>
</section>
{/* Security Features */}
<section className="py-16">
<div className="max-w-6xl mx-auto px-6">
<h2 className="text-3xl font-bold text-[#1a1a1a] mb-12 text-center" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Enterprise-Grade Security
</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{SECURITY_FEATURES.map((feature, i) => (
<div key={i} className="bg-white rounded-2xl p-6 border border-[#e5e5e5] shadow-sm">
<div className="w-12 h-12 bg-[#faf8f5] rounded-xl flex items-center justify-center text-2xl mb-4">
{feature.icon}
</div>
<h3 className="text-lg font-semibold text-[#1a1a1a] mb-2">{feature.title}</h3>
<p className="text-sm text-[#666]">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
{/* Infrastructure */}
<section className="py-16 bg-white border-t border-[#e5e5e5]">
<div className="max-w-4xl mx-auto px-6">
<div className="grid md:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-3xl font-bold text-[#1a1a1a] mb-4" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Built on Trusted Infrastructure
</h2>
<p className="text-[#666] mb-6">
We partner with industry-leading providers to ensure the highest levels of reliability and security.
</p>
<div className="space-y-4">
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-[#1a4d2e]/10 rounded-lg flex items-center justify-center shrink-0">
<span className="text-[#1a4d2e] font-bold text-sm">V</span>
</div>
<div>
<h3 className="font-semibold text-[#1a1a1a]">Vercel</h3>
<p className="text-sm text-[#666]">Edge network with automatic DDoS protection and global CDN</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-[#1a4d2e]/10 rounded-lg flex items-center justify-center shrink-0">
<span className="text-[#1a4d2e] font-bold text-sm">S</span>
</div>
<div>
<h3 className="font-semibold text-[#1a1a1a]">Supabase</h3>
<p className="text-sm text-[#666]">PostgreSQL database with built-in encryption and real-time capabilities</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-[#1a4d2e]/10 rounded-lg flex items-center justify-center shrink-0">
<span className="text-[#1a4d2e] font-bold text-sm">$</span>
</div>
<div>
<h3 className="font-semibold text-[#1a1a1a]">Stripe</h3>
<p className="text-sm text-[#666]">PCI-compliant payment processing with advanced fraud detection</p>
</div>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] rounded-2xl p-8 text-white">
<h3 className="text-xl font-bold mb-4">Payment Security</h3>
<p className="text-white/80 mb-6">
Your payment data is handled by Stripe, who is PCI DSS Level 1 certified. We never store your card details.
</p>
<ul className="space-y-3">
<li className="flex items-center gap-3">
<svg className="w-5 h-5 text-white/80" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm">256-bit SSL encryption</span>
</li>
<li className="flex items-center gap-3">
<svg className="w-5 h-5 text-white/80" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm">PCI DSS Level 1 compliant</span>
</li>
<li className="flex items-center gap-3">
<svg className="w-5 h-5 text-white/80" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm">Advanced fraud detection</span>
</li>
<li className="flex items-center gap-3">
<svg className="w-5 h-5 text-white/80" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm">Secure card tokenization</span>
</li>
</ul>
</div>
</div>
</div>
</section>
{/* Privacy Compliance */}
<section className="py-16">
<div className="max-w-4xl mx-auto px-6">
<h2 className="text-3xl font-bold text-[#1a1a1a] mb-8 text-center" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Privacy Compliance
</h2>
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-white rounded-2xl p-6 border border-[#e5e5e5]">
<h3 className="font-semibold text-[#1a1a1a] mb-3">GDPR Compliant</h3>
<p className="text-sm text-[#666]">
We comply with the General Data Protection Regulation. You can export, delete, or transfer your data at any time.
</p>
</div>
<div className="bg-white rounded-2xl p-6 border border-[#e5e5e5]">
<h3 className="font-semibold text-[#1a1a1a] mb-3">CCPA Ready</h3>
<p className="text-sm text-[#666]">
We honor California Consumer Privacy Act rights, including the right to know, delete, and opt-out of the sale of data.
</p>
</div>
</div>
</div>
</section>
{/* Report Vulnerability */}
<section className="py-16 bg-[#1a4d2e] text-white">
<div className="max-w-2xl mx-auto px-6 text-center">
<h2 className="text-3xl font-bold mb-4" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Found a Security Issue?
</h2>
<p className="text-white/80 mb-6">
We take security seriously. If you&apos;ve discovered a vulnerability, please let us know so we can address it promptly.
</p>
<a href="mailto:security@routecommerce.com" className="inline-flex items-center gap-2 px-6 py-3 bg-white text-[#1a4d2e] rounded-xl font-medium hover:bg-[#faf8f5] transition-colors">
Report Vulnerability
</a>
</div>
</section>
{/* Footer */}
<footer className="border-t border-[#e5e5e5] py-8 bg-white">
<div className="max-w-6xl mx-auto px-6 text-center text-sm text-[#888]">
© 2025 Route Commerce. All rights reserved.
</div>
</footer>
</div>
);
}
+131
View File
@@ -0,0 +1,131 @@
import type { Metadata } from "next";
import WaitlistForm from "@/components/marketing/WaitlistForm";
export const metadata: Metadata = {
title: "Join the Waitlist — Route Commerce",
description: "Sign up for early access to Route Commerce, the all-in-one platform for fresh produce wholesale distribution.",
openGraph: {
title: "Join the Waitlist — Route Commerce",
description: "Sign up for early access to Route Commerce, the all-in-one platform for fresh produce wholesale distribution.",
},
};
export default function WaitlistPage() {
return (
<div className="min-h-screen bg-[#faf8f5]">
{/* Header */}
<header className="border-b border-[#6b8f71]/20 bg-white/80 backdrop-blur-md">
<div className="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
<a href="/" className="flex items-center gap-3">
<div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] flex items-center justify-center shadow-lg shadow-[#1a4d2e]/20">
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-xl font-bold text-[#1a1a1a] tracking-tight" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>Route Commerce</span>
</a>
</div>
</header>
{/* Main Content */}
<main className="max-w-5xl mx-auto px-6 py-20">
<div className="grid lg:grid-cols-2 gap-16 items-center">
{/* Left - Hero */}
<div>
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[#c97a3e]/10 border border-[#c97a3e]/30 mb-6">
<span className="w-2 h-2 bg-[#c97a3e] rounded-full animate-pulse" />
<span className="text-sm font-medium text-[#c97a3e]">Early Access</span>
</div>
<h1 className="text-5xl sm:text-6xl font-bold text-[#1a1a1a] leading-tight mb-6" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Join 500+ farms on the waitlist
</h1>
<p className="text-xl text-[#6b8f71] mb-8 leading-relaxed">
Be among the first to access Route Commerce when we launch. Early members get priority onboarding and special pricing.
</p>
{/* Social proof */}
<div className="flex flex-wrap gap-8 mb-8">
<div className="text-center">
<div className="text-4xl font-bold text-[#1a4d2e]">500+</div>
<div className="text-sm text-[#888]">Farms Waiting</div>
</div>
<div className="text-center">
<div className="text-4xl font-bold text-[#1a4d2e]">12+</div>
<div className="text-sm text-[#888]">States Covered</div>
</div>
<div className="text-center">
<div className="text-4xl font-bold text-[#1a4d2e]">Q2</div>
<div className="text-sm text-[#888]">Launch Target</div>
</div>
</div>
{/* Testimonial */}
<div className="bg-white rounded-2xl p-6 border border-[#e5e5e5] shadow-sm">
<p className="text-[#555] italic mb-4">&ldquo;I&apos;ve been waiting for a platform like this. The wholesale portal alone will save us hours every week.&rdquo;</p>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-[#1a4d2e]/10 rounded-full flex items-center justify-center text-[#1a4d2e] font-bold">M</div>
<div>
<div className="font-semibold text-[#1a1a1a]">Maria S.</div>
<div className="text-sm text-[#888]">Sunny Acres Farm, California</div>
</div>
</div>
</div>
</div>
{/* Right - Form */}
<div>
<div className="bg-white rounded-3xl p-8 sm:p-10 shadow-xl border border-[#e5e5e5]">
<h2 className="text-2xl font-bold text-[#1a1a1a] mb-2" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Reserve Your Spot
</h2>
<p className="text-[#888] mb-6">No credit card required. We&apos;ll notify you when it&apos;s your turn.</p>
<WaitlistForm />
</div>
</div>
</div>
</main>
{/* FAQ Section */}
<section className="border-t border-[#e5e5e5] bg-white">
<div className="max-w-3xl mx-auto px-6 py-16">
<h2 className="text-3xl font-bold text-[#1a1a1a] mb-8 text-center" style={{ fontFamily: "'Cormorant Garamond', Georgia, serif" }}>
Frequently Asked Questions
</h2>
<div className="space-y-4">
{[
{ q: "When will Route Commerce launch?", a: "We&apos;re targeting Q2 launch. Early waitlist members will get access first." },
{ q: "Is there a free trial?", a: "Yes! Early access members get 30 days free on any plan." },
{ q: "What&apos;s included in the free trial?", a: "Full access to all features including products, orders, stops management, and communications." },
{ q: "Can I refer friends?", a: "Yes! You&apos;ll get a unique referral link after joining. Both you and your friend get bonus months when they sign up." },
].map((item, i) => (
<div key={i} className="bg-[#faf8f5] rounded-xl p-5">
<h3 className="font-semibold text-[#1a1a1a] mb-2">{item.q}</h3>
<p className="text-[#666] text-sm">{item.a}</p>
</div>
))}
</div>
</div>
</section>
{/* Footer */}
<footer className="border-t border-[#e5e5e5] py-8">
<div className="max-w-5xl mx-auto px-6">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-sm text-[#666]">© 2025 Route Commerce. All rights reserved.</span>
</div>
<nav className="flex items-center gap-6 text-sm text-[#888]">
<a href="/privacy-policy" className="hover:text-[#1a4d2e]">Privacy</a>
<a href="/terms-and-conditions" className="hover:text-[#1a4d2e]">Terms</a>
</nav>
</div>
</div>
</footer>
</div>
);
}