diff --git a/src/components/admin/AdminResource.tsx b/src/components/admin/AdminResource.tsx index a1b31b4..e9acac4 100644 --- a/src/components/admin/AdminResource.tsx +++ b/src/components/admin/AdminResource.tsx @@ -171,27 +171,27 @@ export function AdminResource({ config }: { config: ResourceConfig }) { return (
-
+

{config.title}

{config.description &&

{config.description}

}
-
+
{ setQuery(e.target.value); setPage(1); }} /> {config.canCreate && config.fields && ( - + )}
{error &&

{error}

} -
- +
+
{config.columns.map((c) => ( diff --git a/src/components/dashboard/DashboardShell.tsx b/src/components/dashboard/DashboardShell.tsx index 6b6592f..898639f 100644 --- a/src/components/dashboard/DashboardShell.tsx +++ b/src/components/dashboard/DashboardShell.tsx @@ -1,4 +1,5 @@ import { DashboardSidebar } from "@/components/dashboard/DashboardSidebar"; +import { DashboardSidebarDrawer } from "@/components/dashboard/DashboardSidebarDrawer"; interface DashboardShellProps { userEmail: string; @@ -16,14 +17,17 @@ export function DashboardShell({ children, }: DashboardShellProps) { return ( -
- -
{children}
-
+ + } + > + {children} + ); } diff --git a/src/components/dashboard/DashboardSidebarDrawer.tsx b/src/components/dashboard/DashboardSidebarDrawer.tsx new file mode 100644 index 0000000..696c4b3 --- /dev/null +++ b/src/components/dashboard/DashboardSidebarDrawer.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useEffect, useState, type ReactNode } from "react"; +import { usePathname } from "next/navigation"; +import { Menu } from "lucide-react"; + +import { LogoMark } from "@/components/ui/LogoMark"; +import { cn } from "@/lib/utils"; + +/** + * Dashboard layout shell with a responsive sidebar: a normal static column on + * desktop (lg+), and an off-canvas drawer with a hamburger top bar on mobile. + * + * The closed-state transform is applied only via `max-lg:` so there is no `lg:` + * transform to conflict with — this sidesteps the RTL/`lg:` specificity trap + * (see AdminShell) and works for both fa (RTL) and en (LTR). + */ +export function DashboardSidebarDrawer({ + sidebar, + children, +}: { + sidebar: ReactNode; + children: ReactNode; +}) { + const [open, setOpen] = useState(false); + const pathname = usePathname(); + + // Close the drawer after navigating (the dashboard layout persists across routes). + useEffect(() => setOpen(false), [pathname]); + + return ( +
+
+ {sidebar} +
+ + {open && ( +
setOpen(false)} + aria-hidden + /> + )} + +
+ {/* Mobile top bar with hamburger */} +
+ + + FlatRender +
+ + {children} +
+
+ ); +} diff --git a/src/components/sections/Hero.tsx b/src/components/sections/Hero.tsx index 3070516..09c61fb 100644 --- a/src/components/sections/Hero.tsx +++ b/src/components/sections/Hero.tsx @@ -88,11 +88,11 @@ export function Hero({ className, config }: HeroProps) {
@@ -184,6 +190,100 @@ export function PricingCompareTable({ ))}
+
+ + ); +} + +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("pro"); + const tier = PRICING_TIERS.find((x) => x.id === active); + if (!tier) return null; + const isStripePlan = tier.id === "pro" || tier.id === "business"; + + return ( +
+
+

+ {t("compareHeading")} +

+
+ +
+
+ + {/* Plan tabs */} +
+ {TIER_IDS.map((id) => { + const x = PRICING_TIERS.find((p) => p.id === id); + if (!x) return null; + return ( + + ); + })} +
+ + {/* Selected plan price + CTA */} +
+ + {isStripePlan ? ( + + ) : ( + + )} +
+ + {/* Feature sections for the selected plan */} +
+ {COMPARE_SECTIONS.map((section) => ( +
+

{section.title}

+
+ {section.rows.map((row) => ( +
+ + + + +
+ ))} +
+
+ ))} +
); } diff --git a/src/components/sections/PricingCompareValue.tsx b/src/components/sections/PricingCompareValue.tsx index 1eeda8e..484aa53 100644 --- a/src/components/sections/PricingCompareValue.tsx +++ b/src/components/sections/PricingCompareValue.tsx @@ -29,24 +29,22 @@ interface PricingCompareValueCellProps { highlighted?: boolean; } +/** Renders a compare value (✓ / – / text) without a table cell, for the mobile card view. */ +export function PricingCompareValueInline({ value }: { value: CompareValue }) { + if (value === true) return ; + if (value === false) return ; + return {value}; +} + export function PricingCompareValueCell({ value, highlighted = false, }: PricingCompareValueCellProps) { return ( - - {value === true ? ( - - ) : value === false ? ( - - ) : ( - {value} - )} + + + + ); } diff --git a/src/components/templates/video/VideoTemplatesCategorySidebar.tsx b/src/components/templates/video/VideoTemplatesCategorySidebar.tsx index eea5148..5ba6890 100644 --- a/src/components/templates/video/VideoTemplatesCategorySidebar.tsx +++ b/src/components/templates/video/VideoTemplatesCategorySidebar.tsx @@ -44,6 +44,12 @@ const SIDEBAR_CATEGORIES: { { id: "music", labelKey: "categoryMusic", icon: Music2 }, ]; +/** id + labelKey only — shared with the mobile category chip row. */ +export const VIDEO_SIDEBAR_CATEGORY_IDS: { id: VideoSidebarCategoryId; labelKey: string }[] = + SIDEBAR_CATEGORIES.map(({ id, labelKey }) => ({ id, labelKey })); + +export const VIDEO_CATEGORY_NS = "auto.componentsTemplatesVideoVideoTemplatesCategorySidebar"; + interface VideoTemplatesCategorySidebarProps { activeCategory: VideoSidebarCategoryId; onCategoryChange: (id: VideoSidebarCategoryId) => void; diff --git a/src/components/templates/video/VideoTemplatesPageContent.tsx b/src/components/templates/video/VideoTemplatesPageContent.tsx index adc5544..5b0b703 100644 --- a/src/components/templates/video/VideoTemplatesPageContent.tsx +++ b/src/components/templates/video/VideoTemplatesPageContent.tsx @@ -5,7 +5,11 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslations } from "next-intl"; import { VideoTemplatesCarouselRow } from "@/components/templates/video/VideoTemplatesCarouselRow"; -import { VideoTemplatesCategorySidebar } from "@/components/templates/video/VideoTemplatesCategorySidebar"; +import { + VideoTemplatesCategorySidebar, + VIDEO_SIDEBAR_CATEGORY_IDS, + VIDEO_CATEGORY_NS, +} from "@/components/templates/video/VideoTemplatesCategorySidebar"; import { VideoTemplatesHero } from "@/components/templates/video/VideoTemplatesHero"; import { VideoTemplatesToolbar, @@ -40,6 +44,7 @@ export function VideoTemplatesPageContent({ initialCatalog, }: VideoTemplatesPageContentProps = {}) { const t = useTranslations("auto.componentsTemplatesVideoVideoTemplatesPageContent"); + const tCat = useTranslations(VIDEO_CATEGORY_NS); const router = useRouter(); const searchParams = useSearchParams(); const categoryParam = searchParams.get("category"); @@ -146,6 +151,27 @@ export function VideoTemplatesPageContent({ onSortByChange={setSortBy} /> + {/* Mobile category chips (the sidebar is desktop-only) */} +
+ {VIDEO_SIDEBAR_CATEGORY_IDS.map((c) => { + const active = sidebarCategory === c.id; + return ( + + ); + })} +
+ {filtered.length === 0 ? (