Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 87 additions & 38 deletions app/components/common/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NavLink } from 'react-router';
import { NavLink, useLocation } from 'react-router';
import { useLayoutEffect, useRef, useState } from 'react';

export interface TabItem {
label: string;
Expand All @@ -13,50 +14,98 @@ interface TabsProps {
className?: string;
}

const SIDE_MARGIN_PX = 16;

export default function Tabs({ tabs, activeTab, onTabChange, className = "" }: TabsProps) {
const location = useLocation();
const hasPath = tabs.some((t) => t.path);
const currentActiveTab = hasPath
? tabs.find((t) => t.path && location.pathname.startsWith(t.path))?.value || tabs[0]?.value
: activeTab;

const activeIndex = Math.max(
0,
tabs.findIndex((t) => t.value === currentActiveTab)
);

const wrapRef = useRef<HTMLDivElement | null>(null);
const [wrapWidth, setWrapWidth] = useState(0);

useLayoutEffect(() => {
const el = wrapRef.current;
if (!el) return;

const measure = () => setWrapWidth(el.clientWidth);
measure();

const ro = new ResizeObserver(() => measure());
ro.observe(el);

return () => ro.disconnect();
}, []);

const pxToRem = (px: number) => {
if (typeof window === "undefined") return `${px / 16}rem`;
const root = window.getComputedStyle(document.documentElement).fontSize;
const base = Number.parseFloat(root) || 16;
return `${px / base}rem`;
};

const trackWidth = Math.max(0, wrapWidth - SIDE_MARGIN_PX * 2);
const tabWidth = trackWidth / tabs.length;
const indicatorWidth = tabWidth;
const indicatorX = SIDE_MARGIN_PX + tabWidth * activeIndex;

return (
<div className={`flex w-full bg-white border-b border-bluegray-2 shrink-0 ${className}`}>
{tabs.map((tab) => {
const isSelected = activeTab === tab.value;
<div ref={wrapRef} className={`relative w-full h-12.5 bg-white shrink-0 ${className}`}>
<div className="flex h-full items-center px-4">
{tabs.map((tab) => {
const isSelected = currentActiveTab === tab.value;

// Mode 1: Link-based Tab
if (tab.path) {
// Link-based Tab
if (tab.path) {
return (
<NavLink
key={tab.value}
to={tab.path}
className="flex-1 select-none"
>
{({ isActive }) => (
<div
className={`h-full flex items-center justify-center text-title2 transition-colors cursor-pointer ${isActive ? 'text-(--color-core-1)' : 'text-text-gray3'
}`}
>
{tab.label}
</div>
)}
</NavLink>
);
}

// Button-based Tab
return (
<NavLink
<button
key={tab.value}
to={tab.path}
className="flex-1 select-none"
type="button"
onClick={() => onTabChange?.(tab.value)}
className={`flex-1 h-full flex items-center justify-center text-title2 transition-colors ${isSelected ? 'text-(--color-core-1)' : 'text-text-gray3'
}`}
>
{({ isActive }) => (
<div
className={`py-4 text-title-2 text-center relative transition-colors cursor-pointer font-semibold ${isActive ? 'text-core-1' : 'text-text-gray3'
}`}
>
{tab.label}
{isActive && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[150px] h-[2px] bg-core-1" />
)}
</div>
)}
</NavLink>
{tab.label}
</button>
);
}

// Mode 2: Button-based Tab (State)
return (
<button
key={tab.value}
onClick={() => onTabChange?.(tab.value)}
className={`flex-1 py-4 text-title-2 text-center relative transition-colors select-none font-semibold ${isSelected ? 'text-core-1' : 'text-text-gray3'
}`}
>
{tab.label}
{isSelected && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[150px] h-[2px] bg-core-1" />
)}
</button>
);
})}
})}
</div>

<div className="absolute bottom-0 left-0 right-0 h-px bg-black/10" />

<div
className="absolute bottom-0 h-0.5 bg-(--color-success) transition-transform duration-200"
style={{
width: pxToRem(indicatorWidth),
transform: `translateX(${pxToRem(indicatorX)})`,
}}
/>
</div>
);
}
8 changes: 4 additions & 4 deletions app/routes/matching/brand/components/BrandFilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@ export default function BrandFilterBar({ category, onCategoryChange, searchKeywo
<div className="flex gap-2 shrink-0">
<button
className={cn(
"px-[12px] py-[8px] h-10 text-title2 rounded-[8px] whitespace-nowrap cursor-pointer transition-colors",
"px-[10px] py-2 rounded-[8px] text-title2 transition-all",
category === "BEAUTY"
? "bg-core-1 text-white"
: "bg-white text-text-gray3 border border-gray-100"
: "bg-white border border-text-gray5 text-text-gray3"
)}
onClick={() => onCategoryChange("BEAUTY")}
>
뷰티
</button>
<button
className={cn(
"px-[12px] py-[8px] h-10 text-title2 rounded-[8px] whitespace-nowrap cursor-pointer transition-colors",
"px-[10px] py-2 rounded-[8px] text-title2 transition-all",
category === "FASHION"
? "bg-core-1 text-white"
: "bg-white text-text-gray3 border border-gray-100"
: "bg-white border border-text-gray5 text-text-gray3"
)}
onClick={() => onCategoryChange("FASHION")}
>
Expand Down
8 changes: 4 additions & 4 deletions app/routes/matching/campaign/components/CampaignFilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@ export default function CampaignFilterBar({ category, onCategoryChange, searchKe
<div className="flex gap-2 shrink-0">
<button
className={cn(
"px-[12px] py-[8px] h-10 text-title2 rounded-[8px] whitespace-nowrap cursor-pointer transition-colors font-semibold",
"px-[10px] py-2 rounded-[8px] text-title2 transition-all",
category === "BEAUTY"
? "bg-core-1 text-white"
: "bg-white text-text-gray3 border border-gray-100"
: "bg-white border border-text-gray5 text-text-gray3"
)}
onClick={() => onCategoryChange("BEAUTY")}
>
뷰티
</button>
<button
className={cn(
"px-[12px] py-[8px] h-10 text-title2 rounded-[8px] whitespace-nowrap cursor-pointer transition-colors font-semibold",
"px-[10px] py-2 rounded-[8px] text-title2 transition-all",
category === "FASHION"
? "bg-core-1 text-white"
: "bg-white text-text-gray3 border border-gray-100"
: "bg-white border border-text-gray5 text-text-gray3"
)}
onClick={() => onCategoryChange("FASHION")}
>
Expand Down
Loading