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:
@@ -171,27 +171,27 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-white">{config.title}</h1>
|
<h1 className="text-xl font-semibold text-white">{config.title}</h1>
|
||||||
{config.description && <p className="mt-1 text-sm text-gray-400">{config.description}</p>}
|
{config.description && <p className="mt-1 text-sm text-gray-400">{config.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex w-full items-center gap-2 sm:w-auto">
|
||||||
<input
|
<input
|
||||||
className="w-52 rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"
|
className="w-full rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500 sm:w-52"
|
||||||
placeholder="جستجو…" value={query}
|
placeholder="جستجو…" value={query}
|
||||||
onChange={(e) => { setQuery(e.target.value); setPage(1); }}
|
onChange={(e) => { setQuery(e.target.value); setPage(1); }}
|
||||||
/>
|
/>
|
||||||
{config.canCreate && config.fields && (
|
{config.canCreate && config.fields && (
|
||||||
<button className={btn} onClick={openCreate}>+ مورد جدید</button>
|
<button className={`${btn} shrink-0 whitespace-nowrap`} onClick={openCreate}>+ مورد جدید</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="rounded-lg bg-red-500/10 px-3 py-2 text-sm text-red-300">{error}</p>}
|
{error && <p className="rounded-lg bg-red-500/10 px-3 py-2 text-sm text-red-300">{error}</p>}
|
||||||
|
|
||||||
<div className={`${card} overflow-hidden`}>
|
<div className={`${card} overflow-x-auto`}>
|
||||||
<table className="w-full text-sm">
|
<table className="w-full min-w-[640px] text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-[#1e2235] text-start text-xs text-gray-500">
|
<tr className="border-b border-[#1e2235] text-start text-xs text-gray-500">
|
||||||
{config.columns.map((c) => (
|
{config.columns.map((c) => (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DashboardSidebar } from "@/components/dashboard/DashboardSidebar";
|
import { DashboardSidebar } from "@/components/dashboard/DashboardSidebar";
|
||||||
|
import { DashboardSidebarDrawer } from "@/components/dashboard/DashboardSidebarDrawer";
|
||||||
|
|
||||||
interface DashboardShellProps {
|
interface DashboardShellProps {
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
@@ -16,14 +17,17 @@ export function DashboardShell({
|
|||||||
children,
|
children,
|
||||||
}: DashboardShellProps) {
|
}: DashboardShellProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-neutral-50">
|
<DashboardSidebarDrawer
|
||||||
<DashboardSidebar
|
sidebar={
|
||||||
userEmail={userEmail}
|
<DashboardSidebar
|
||||||
userName={userName}
|
userEmail={userEmail}
|
||||||
userId={userId}
|
userName={userName}
|
||||||
avatarUrl={avatarUrl}
|
userId={userId}
|
||||||
/>
|
avatarUrl={avatarUrl}
|
||||||
<div className="flex min-h-screen min-w-0 flex-1 flex-col">{children}</div>
|
/>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DashboardSidebarDrawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="flex min-h-screen bg-neutral-50">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 transition-transform max-lg:fixed max-lg:inset-y-0 max-lg:start-0 max-lg:z-50",
|
||||||
|
open ? "max-lg:translate-x-0" : "max-lg:-translate-x-full max-lg:rtl:translate-x-full",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{sidebar}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-40 bg-black/40 lg:hidden"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex min-h-screen min-w-0 flex-1 flex-col">
|
||||||
|
{/* Mobile top bar with hamburger */}
|
||||||
|
<div className="sticky top-0 z-30 flex h-14 items-center gap-3 border-b border-gray-100 bg-white/95 px-4 backdrop-blur lg:hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
aria-label="menu"
|
||||||
|
className="rounded-lg border border-gray-200 p-1.5 text-neutral-600 hover:bg-neutral-100"
|
||||||
|
>
|
||||||
|
<Menu className="h-5 w-5" aria-hidden />
|
||||||
|
</button>
|
||||||
|
<LogoMark size={28} />
|
||||||
|
<span className="font-heading text-base font-bold text-neutral-900">FlatRender</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -88,11 +88,11 @@ export function Hero({ className, config }: HeroProps) {
|
|||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={fadeUp}
|
variants={fadeUp}
|
||||||
className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row sm:gap-4"
|
className="mx-auto mt-8 flex w-full max-w-xs flex-col items-center justify-center gap-3 sm:max-w-none sm:flex-row sm:gap-4"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
className="h-12 min-w-[11rem] rounded-lg bg-gradient-to-r from-violet-600 to-rf-blue px-8 text-base font-semibold text-white shadow-md hover:from-violet-700 hover:to-rf-blue/90"
|
className="h-12 w-full rounded-lg bg-gradient-to-r from-violet-600 to-rf-blue px-8 text-base font-semibold text-white shadow-md hover:from-violet-700 hover:to-rf-blue/90 sm:w-auto sm:min-w-[11rem]"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Link href={ctaHref || "/auth?tab=sign-up"}>{ctaLabel ?? t("cta")}</Link>
|
<Link href={ctaHref || "/auth?tab=sign-up"}>{ctaLabel ?? t("cta")}</Link>
|
||||||
@@ -100,7 +100,7 @@ export function Hero({ className, config }: HeroProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="h-12 min-w-[11rem] rounded-lg border-2 border-rf-blue bg-white px-8 text-base font-semibold text-rf-blue hover:bg-rf-blue-light"
|
className="h-12 w-full rounded-lg border-2 border-rf-blue bg-white px-8 text-base font-semibold text-rf-blue hover:bg-rf-blue-light sm:w-auto sm:min-w-[11rem]"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Link href={browseHref || "#templates"}>{browseLabel ?? t("browse")}</Link>
|
<Link href={browseHref || "#templates"}>{browseLabel ?? t("browse")}</Link>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ import { PricingCheckoutButton } from "@/components/sections/PricingCheckoutButt
|
|||||||
import {
|
import {
|
||||||
PricingCompareFeatureLabel,
|
PricingCompareFeatureLabel,
|
||||||
PricingCompareValueCell,
|
PricingCompareValueCell,
|
||||||
|
PricingCompareValueInline,
|
||||||
} from "@/components/sections/PricingCompareValue";
|
} from "@/components/sections/PricingCompareValue";
|
||||||
import type { BillingPeriod, PricingTier } from "@/components/sections/pricing-data";
|
import type { BillingPeriod, PricingTier } from "@/components/sections/pricing-data";
|
||||||
import {
|
import {
|
||||||
@@ -128,7 +129,12 @@ export function PricingCompareTable({
|
|||||||
if (!lite || !pro || !business) return null;
|
if (!lite || !pro || !business) return null;
|
||||||
|
|
||||||
return (
|
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">
|
<table className="w-full min-w-[760px] border-collapse">
|
||||||
<thead className="sticky top-0 z-10 bg-white">
|
<thead className="sticky top-0 z-10 bg-white">
|
||||||
<tr className="border-b border-gray-100">
|
<tr className="border-b border-gray-100">
|
||||||
@@ -184,6 +190,100 @@ export function PricingCompareTable({
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,24 +29,22 @@ interface PricingCompareValueCellProps {
|
|||||||
highlighted?: boolean;
|
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 <Check className="h-4 w-4 text-blue-600" aria-hidden />;
|
||||||
|
if (value === false) return <Minus className="h-4 w-4 text-gray-300" aria-hidden />;
|
||||||
|
return <span className="text-sm text-gray-700">{value}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
export function PricingCompareValueCell({
|
export function PricingCompareValueCell({
|
||||||
value,
|
value,
|
||||||
highlighted = false,
|
highlighted = false,
|
||||||
}: PricingCompareValueCellProps) {
|
}: PricingCompareValueCellProps) {
|
||||||
return (
|
return (
|
||||||
<td
|
<td className={cn("px-4 py-3 text-center", highlighted && "bg-blue-50/30")}>
|
||||||
className={cn(
|
<span className="mx-auto inline-flex items-center justify-center">
|
||||||
"px-4 py-3 text-center",
|
<PricingCompareValueInline value={value} />
|
||||||
highlighted && "bg-blue-50/30"
|
</span>
|
||||||
)}
|
|
||||||
>
|
|
||||||
{value === true ? (
|
|
||||||
<Check className="mx-auto h-4 w-4 text-blue-600" aria-hidden />
|
|
||||||
) : value === false ? (
|
|
||||||
<Minus className="mx-auto h-4 w-4 text-gray-300" aria-hidden />
|
|
||||||
) : (
|
|
||||||
<span className="text-sm text-gray-700">{value}</span>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ const SIDEBAR_CATEGORIES: {
|
|||||||
{ id: "music", labelKey: "categoryMusic", icon: Music2 },
|
{ 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 {
|
interface VideoTemplatesCategorySidebarProps {
|
||||||
activeCategory: VideoSidebarCategoryId;
|
activeCategory: VideoSidebarCategoryId;
|
||||||
onCategoryChange: (id: VideoSidebarCategoryId) => void;
|
onCategoryChange: (id: VideoSidebarCategoryId) => void;
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { VideoTemplatesCarouselRow } from "@/components/templates/video/VideoTemplatesCarouselRow";
|
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 { VideoTemplatesHero } from "@/components/templates/video/VideoTemplatesHero";
|
||||||
import {
|
import {
|
||||||
VideoTemplatesToolbar,
|
VideoTemplatesToolbar,
|
||||||
@@ -40,6 +44,7 @@ export function VideoTemplatesPageContent({
|
|||||||
initialCatalog,
|
initialCatalog,
|
||||||
}: VideoTemplatesPageContentProps = {}) {
|
}: VideoTemplatesPageContentProps = {}) {
|
||||||
const t = useTranslations("auto.componentsTemplatesVideoVideoTemplatesPageContent");
|
const t = useTranslations("auto.componentsTemplatesVideoVideoTemplatesPageContent");
|
||||||
|
const tCat = useTranslations(VIDEO_CATEGORY_NS);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const categoryParam = searchParams.get("category");
|
const categoryParam = searchParams.get("category");
|
||||||
@@ -146,6 +151,27 @@ export function VideoTemplatesPageContent({
|
|||||||
onSortByChange={setSortBy}
|
onSortByChange={setSortBy}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Mobile category chips (the sidebar is desktop-only) */}
|
||||||
|
<div className="mt-4 flex gap-2 overflow-x-auto pb-1 lg:hidden [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||||
|
{VIDEO_SIDEBAR_CATEGORY_IDS.map((c) => {
|
||||||
|
const active = sidebarCategory === c.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={c.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSidebarCategory(c.id)}
|
||||||
|
className={`shrink-0 whitespace-nowrap rounded-full border px-3.5 py-1.5 text-sm font-medium transition-colors ${
|
||||||
|
active
|
||||||
|
? "border-primary-600 bg-primary-600 text-white"
|
||||||
|
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tCat(c.labelKey)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className="mt-12 rounded-xl border border-dashed border-gray-200 bg-white px-6 py-16 text-center">
|
<div className="mt-12 rounded-xl border border-dashed border-gray-200 bg-white px-6 py-16 text-center">
|
||||||
<p className="font-heading text-lg font-semibold text-gray-900">
|
<p className="font-heading text-lg font-semibold text-gray-900">
|
||||||
|
|||||||
Reference in New Issue
Block a user