feat(responsive): mobile fixes for pricing, dashboard, admin, templates, hero
- PricingCompareTable: wide 4-col table is hidden on mobile; new tab-per-plan card view (Lite/Pro/Business) so pricing fits a phone. Extracted PricingCompareValueInline. - Dashboard: sidebar becomes an off-canvas drawer on mobile (hamburger top bar + overlay, closes on navigation) via DashboardSidebarDrawer; static column on lg+. RTL/LTR safe (max-lg: transforms avoid the lg:/rtl: specificity trap). - AdminResource: search/add row stacks on mobile (w-full sm:w-52), tables scroll horizontally (overflow-x-auto + min-w) instead of clipping. - Templates: added a mobile category chip row (lg:hidden) since the category sidebar is desktop-only; exported VIDEO_SIDEBAR_CATEGORY_IDS. - Hero: CTAs full-width on mobile, auto width on sm+. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -10,6 +10,7 @@ import { PricingCheckoutButton } from "@/components/sections/PricingCheckoutButt
|
||||
import {
|
||||
PricingCompareFeatureLabel,
|
||||
PricingCompareValueCell,
|
||||
PricingCompareValueInline,
|
||||
} from "@/components/sections/PricingCompareValue";
|
||||
import type { BillingPeriod, PricingTier } from "@/components/sections/pricing-data";
|
||||
import {
|
||||
@@ -128,7 +129,12 @@ export function PricingCompareTable({
|
||||
if (!lite || !pro || !business) return null;
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-5xl overflow-x-auto rounded-2xl border border-gray-100 bg-white shadow-sm">
|
||||
<>
|
||||
{/* Mobile: one plan at a time (tabs) — the wide table can't fit a phone. */}
|
||||
<MobileCompare billing={billing} onBillingChange={onBillingChange} />
|
||||
|
||||
{/* Desktop: full comparison table */}
|
||||
<div className="mx-auto hidden w-full max-w-5xl overflow-x-auto rounded-2xl border border-gray-100 bg-white shadow-sm sm:block">
|
||||
<table className="w-full min-w-[760px] border-collapse">
|
||||
<thead className="sticky top-0 z-10 bg-white">
|
||||
<tr className="border-b border-gray-100">
|
||||
@@ -184,6 +190,100 @@ export function PricingCompareTable({
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const TIER_IDS = ["lite", "pro", "business"] as const;
|
||||
type CompareTierId = (typeof TIER_IDS)[number];
|
||||
|
||||
function MobileCompare({
|
||||
billing,
|
||||
onBillingChange,
|
||||
}: PricingCompareTableProps) {
|
||||
const t = useTranslations("auto.componentsSectionsPricingCompareTable");
|
||||
const [active, setActive] = useState<CompareTierId>("pro");
|
||||
const tier = PRICING_TIERS.find((x) => x.id === active);
|
||||
if (!tier) return null;
|
||||
const isStripePlan = tier.id === "pro" || tier.id === "business";
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md sm:hidden">
|
||||
<div className="text-center">
|
||||
<h3 className="bg-gradient-to-r from-blue-600 to-violet-600 bg-clip-text font-heading text-lg font-bold text-transparent">
|
||||
{t("compareHeading")}
|
||||
</h3>
|
||||
<div className="mt-3 flex justify-center">
|
||||
<PricingBillingToggle
|
||||
billing={billing}
|
||||
onChange={onBillingChange}
|
||||
layoutId="pricing-compare-billing-pill-mobile"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan tabs */}
|
||||
<div className="mt-4 grid grid-cols-3 gap-1 rounded-xl border border-gray-200 bg-gray-50 p-1">
|
||||
{TIER_IDS.map((id) => {
|
||||
const x = PRICING_TIERS.find((p) => p.id === id);
|
||||
if (!x) return null;
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => setActive(id)}
|
||||
className={cn(
|
||||
"rounded-lg py-2 text-sm font-semibold transition-colors",
|
||||
active === id ? "bg-rf-blue text-white shadow-sm" : "text-neutral-600 hover:text-neutral-900",
|
||||
)}
|
||||
>
|
||||
{x.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Selected plan price + CTA */}
|
||||
<div className="mt-4 rounded-xl border border-gray-100 bg-white p-4 text-center shadow-sm">
|
||||
<PricingAnimatedPrice
|
||||
price={getDisplayPrice(tier, billing)}
|
||||
compareAt={getCompareAtPrice(tier, billing)}
|
||||
billing={billing}
|
||||
size="compact"
|
||||
/>
|
||||
{isStripePlan ? (
|
||||
<PricingCheckoutButton
|
||||
plan={tier.id as PaidPlanId}
|
||||
billing={billing}
|
||||
label={tier.cta}
|
||||
className="mt-3 h-10 w-full rounded-lg bg-rf-blue text-sm font-semibold hover:bg-rf-blue/90"
|
||||
/>
|
||||
) : (
|
||||
<Button variant="outline" className="mt-3 h-10 w-full rounded-lg border-gray-300 text-sm font-semibold" asChild>
|
||||
<Link href="/auth?tab=sign-up">{tier.cta}</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Feature sections for the selected plan */}
|
||||
<div className="mt-4 space-y-4">
|
||||
{COMPARE_SECTIONS.map((section) => (
|
||||
<div key={section.title}>
|
||||
<p className="mb-1.5 px-1 text-xs font-bold uppercase tracking-widest text-gray-500">{section.title}</p>
|
||||
<div className="divide-y divide-gray-100 rounded-xl border border-gray-100 bg-white">
|
||||
{section.rows.map((row) => (
|
||||
<div key={row.feature} className="flex items-center justify-between gap-3 px-4 py-2.5">
|
||||
<PricingCompareFeatureLabel feature={row.feature} tooltip={row.tooltip} />
|
||||
<span className="shrink-0">
|
||||
<PricingCompareValueInline value={row[active]} />
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user