feat(admin): rewrite orders page with tabs, search, multi-select filters
- AdminOrdersPanel: clean rewrite matching earth-tone theme - Add status tabs (All/Pending/Picked Up) with pill-style buttons - Add search bar for name, phone, order ID - Add stop multi-select dropdown with checkbox filter - Standard 7-column table with pagination - UpgradePlanModal: Apple HIG glass-style modal component - DashboardHeader: reusable header with upgrade modal integration - DashboardUpgradeButton: standalone upgrade button component - Update stripe-checkout to support annual billing period - Fix readability in tools/addons section (darker badges)
This commit is contained in:
+13
-26
@@ -2,6 +2,7 @@ import Link from "next/link";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { isFeatureEnabled } from "@/lib/feature-flags";
|
||||
import { getBrandPlanInfo } from "@/actions/billing/stripe-portal";
|
||||
import DashboardHeader from "@/components/admin/DashboardHeader";
|
||||
|
||||
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
||||
|
||||
@@ -247,25 +248,11 @@ export default async function AdminPage() {
|
||||
<main className="min-h-screen px-6 py-10">
|
||||
<div className="mx-auto max-w-7xl space-y-10">
|
||||
{/* Header */}
|
||||
<div className="flex items-end justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-widest text-emerald-600 mb-2">Control Center</p>
|
||||
<h1 className="text-4xl font-semibold tracking-tight text-stone-950">{brandDisplayName}</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin/settings/billing" className="text-sm font-medium text-stone-500 hover:text-stone-700 transition-colors">
|
||||
Billing →
|
||||
</Link>
|
||||
{planTier === "starter" && (
|
||||
<Link
|
||||
href="/admin/settings/billing"
|
||||
className="rounded-full bg-emerald-600 hover:bg-emerald-500 px-5 py-2.5 text-sm font-semibold text-white transition-all shadow-sm"
|
||||
>
|
||||
Upgrade Plan
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DashboardHeader
|
||||
brandId={adminUser?.brand_id ?? null}
|
||||
brandName={brandDisplayName}
|
||||
planTier={planTier}
|
||||
/>
|
||||
|
||||
{/* Usage bar */}
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 shadow-sm">
|
||||
@@ -309,7 +296,7 @@ export default async function AdminPage() {
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className="flex items-center gap-4 mb-5">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-stone-500 flex-shrink-0">{label}</p>
|
||||
<p className="text-[11px] font-bold uppercase tracking-widest text-stone-600 flex-shrink-0">{label}</p>
|
||||
<div className="h-px flex-1 bg-gradient-to-r from-stone-300 to-transparent" />
|
||||
</div>
|
||||
<div className={`grid ${cols} gap-4`}>
|
||||
@@ -340,24 +327,24 @@ export default async function AdminPage() {
|
||||
? "bg-stone-100 text-stone-400"
|
||||
: isProminent
|
||||
? "bg-emerald-100 text-emerald-600"
|
||||
: "bg-blue-100 text-blue-600"
|
||||
: "bg-violet-100 text-violet-600"
|
||||
}`}>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
{isAddon && !isEnabled && (
|
||||
<span className="text-[10px] font-semibold text-amber-600 bg-amber-50 border border-amber-200 rounded-full px-2.5 py-0.5">
|
||||
<span className="text-[10px] font-semibold text-amber-800 bg-amber-100 border border-amber-200 rounded-full px-2.5 py-0.5">
|
||||
Add-on
|
||||
</span>
|
||||
)}
|
||||
{isAddon && isEnabled && (
|
||||
<span className="text-[10px] font-semibold text-emerald-600 bg-emerald-50 border border-emerald-200 rounded-full px-2.5 py-0.5">
|
||||
<span className="text-[10px] font-semibold text-emerald-800 bg-emerald-100 border border-emerald-200 rounded-full px-2.5 py-0.5">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
{isProminent && (
|
||||
<span className="text-[10px] font-semibold text-emerald-600 bg-emerald-50 border border-emerald-200 rounded-full px-2.5 py-0.5">
|
||||
<span className="text-[10px] font-semibold text-emerald-800 bg-emerald-100 border border-emerald-200 rounded-full px-2.5 py-0.5">
|
||||
Core
|
||||
</span>
|
||||
)}
|
||||
@@ -368,13 +355,13 @@ export default async function AdminPage() {
|
||||
</h3>
|
||||
|
||||
<p className={`mt-2 text-sm leading-relaxed ${
|
||||
isAddon && !isEnabled ? "text-stone-400" : "text-stone-500"
|
||||
isAddon && !isEnabled ? "text-stone-500" : "text-stone-600"
|
||||
}`}>
|
||||
{section.description}
|
||||
</p>
|
||||
|
||||
{isAddon && !isEnabled && section.upgradeText && (
|
||||
<p className="mt-3 text-xs text-amber-600 font-medium">{section.upgradeText}</p>
|
||||
<p className="mt-3 text-xs text-amber-700 font-medium">{section.upgradeText}</p>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://yourdomain.com";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Our Story | A Passion for All Things Fruit",
|
||||
description:
|
||||
"Since 1985, Indian River Direct has been bringing the finest peaches and citrus from our Florida groves directly to your neighborhood. Family-owned and operated.",
|
||||
openGraph: {
|
||||
title: "Our Story | Indian River Direct",
|
||||
description:
|
||||
"Since 1985, bringing the finest peaches and citrus from our Florida groves directly to your neighborhood. A passion for all things fruit.",
|
||||
url: `${BASE_URL}/indian-river-direct/about`,
|
||||
images: [
|
||||
{
|
||||
url: `${BASE_URL}/og-indian-river-about.jpg`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Indian River Direct - Our Family Grove Story",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Our Story | Indian River Direct",
|
||||
description:
|
||||
"Since 1985, bringing the finest peaches and citrus from our Florida groves directly to your neighborhood.",
|
||||
images: [`${BASE_URL}/og-indian-river-about.jpg`],
|
||||
},
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/indian-river-direct/about`,
|
||||
},
|
||||
};
|
||||
|
||||
export default function IndianRiverAboutLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { MetadataRoute } from "next";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://yourdomain.com";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
disallow: [
|
||||
"/admin/*",
|
||||
"/api/*",
|
||||
"/wholesale/portal", // Wholesale is behind auth anyway
|
||||
"/cart",
|
||||
"/checkout",
|
||||
"/water/admin/*",
|
||||
],
|
||||
},
|
||||
],
|
||||
sitemap: `${BASE_URL}/sitemap.xml`,
|
||||
host: BASE_URL,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { MetadataRoute } from "next";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://yourdomain.com";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const now = new Date();
|
||||
|
||||
return [
|
||||
// Main site
|
||||
{
|
||||
url: BASE_URL,
|
||||
lastModified: now,
|
||||
changeFrequency: "weekly",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/contact`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/pricing`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/privacy-policy`,
|
||||
lastModified: now,
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/terms-and-conditions`,
|
||||
lastModified: now,
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.3,
|
||||
},
|
||||
|
||||
// Tuxedo Corn storefront
|
||||
{
|
||||
url: `${BASE_URL}/tuxedo`,
|
||||
lastModified: now,
|
||||
changeFrequency: "weekly",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/tuxedo/about`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/tuxedo/faq`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/tuxedo/contact`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.6,
|
||||
},
|
||||
|
||||
// Indian River Direct storefront
|
||||
{
|
||||
url: `${BASE_URL}/indian-river-direct`,
|
||||
lastModified: now,
|
||||
changeFrequency: "weekly",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/indian-river-direct/about`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/indian-river-direct/faq`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/indian-river-direct/contact`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.6,
|
||||
},
|
||||
|
||||
// Wholesale portal
|
||||
{
|
||||
url: `${BASE_URL}/wholesale/portal`,
|
||||
lastModified: now,
|
||||
changeFrequency: "weekly",
|
||||
priority: 0.8,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://yourdomain.com";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Our Story | Three Generations of Sweet Corn Excellence",
|
||||
description:
|
||||
"Learn about Tuxedo Corn's heritage - three generations of growing and shipping Olathe Sweet Sweet Corn from our family farm in Olathe, Colorado since 1982.",
|
||||
openGraph: {
|
||||
title: "Our Story | Tuxedo Corn",
|
||||
description:
|
||||
"Three generations of sweet corn excellence. Learn about our family farm and the Olathe Sweet difference.",
|
||||
url: `${BASE_URL}/tuxedo/about`,
|
||||
images: [
|
||||
{
|
||||
url: `${BASE_URL}/og-tuxedo-about.jpg`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Tuxedo Corn Family Farm - Three Generations of Excellence",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Our Story | Tuxedo Corn",
|
||||
description:
|
||||
"Three generations of sweet corn excellence. Learn about our family farm and the Olathe Sweet difference.",
|
||||
images: [`${BASE_URL}/og-tuxedo-about.jpg`],
|
||||
},
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/tuxedo/about`,
|
||||
},
|
||||
};
|
||||
|
||||
export default function TuxedoAboutLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
Reference in New Issue
Block a user