diff --git a/src/components/common/PageHeader.tsx b/src/components/common/PageHeader.tsx index bd990f1d..6acac19b 100644 --- a/src/components/common/PageHeader.tsx +++ b/src/components/common/PageHeader.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from "react"; +import { memo } from "react"; import { twMerge } from "tailwind-merge"; interface IPageHeaderProps { @@ -8,7 +9,7 @@ interface IPageHeaderProps { className?: string; } -export default function PageHeader({ +const PageHeader = memo(function PageHeader({ title, description, actions, @@ -25,4 +26,6 @@ export default function PageHeader({ {actions &&
{actions}
} ); -} +}); + +export default PageHeader; diff --git a/src/components/common/card/Card.tsx b/src/components/common/card/Card.tsx index 9385a354..d9c54ad9 100644 --- a/src/components/common/card/Card.tsx +++ b/src/components/common/card/Card.tsx @@ -1,4 +1,5 @@ import type { HTMLAttributes, ReactNode } from "react"; +import { memo } from "react"; import { twMerge } from "tailwind-merge"; export interface ICardProps extends HTMLAttributes { @@ -7,7 +8,7 @@ export interface ICardProps extends HTMLAttributes { RightElement?: ReactNode; } -export default function Card({ +const Card = memo(function Card({ title, description, RightElement, @@ -20,7 +21,7 @@ export default function Card({ return (
); -} +}); + +export default Card; diff --git a/src/components/common/card/StatCard.tsx b/src/components/common/card/StatCard.tsx index 5245293b..2d9e4a7e 100644 --- a/src/components/common/card/StatCard.tsx +++ b/src/components/common/card/StatCard.tsx @@ -1,4 +1,5 @@ import type { HTMLAttributes } from "react"; +import { memo } from "react"; import { twMerge } from "tailwind-merge"; import TrendDownIcon from "@/assets/icon/chevron/trend-down.svg?react"; @@ -20,7 +21,10 @@ const trendClasses: Record = { down: "bg-status-blue/[0.08] text-status-blue font-bold", }; -export function TrendBadge({ direction, value }: ITrend) { +export const TrendBadge = memo(function TrendBadge({ + direction, + value, +}: ITrend) { return ( ); -} +}); -export default function StatCard({ +const StatCard = memo(function StatCard({ title, value, trend, @@ -49,7 +53,7 @@ export default function StatCard({ return (
}
); -} +}); + +export default StatCard; diff --git a/src/components/common/chart/ChartLegend.tsx b/src/components/common/chart/ChartLegend.tsx index ad74a8e8..0096aa80 100644 --- a/src/components/common/chart/ChartLegend.tsx +++ b/src/components/common/chart/ChartLegend.tsx @@ -1,3 +1,4 @@ +import { memo } from "react"; import { twMerge } from "tailwind-merge"; export interface IChartLegendItem { @@ -10,7 +11,10 @@ export interface IChartLegendProps { className?: string; } -export default function ChartLegend({ items, className }: IChartLegendProps) { +const ChartLegend = memo(function ChartLegend({ + items, + className, +}: IChartLegendProps) { return (
{items.map((item) => ( @@ -25,4 +29,6 @@ export default function ChartLegend({ items, className }: IChartLegendProps) { ))}
); -} +}); + +export default ChartLegend; diff --git a/src/components/common/drawer/Drawer.tsx b/src/components/common/drawer/Drawer.tsx index 90d8aed1..33196254 100644 --- a/src/components/common/drawer/Drawer.tsx +++ b/src/components/common/drawer/Drawer.tsx @@ -1,5 +1,5 @@ -import type { ReactNode } from "react"; -import { useEffect, useState } from "react"; +import type { ReactNode, TouchEvent } from "react"; +import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { twMerge } from "tailwind-merge"; @@ -19,6 +19,9 @@ export interface IDrawerProps { disableScroll?: boolean; } +const TABLET_MQ = "(max-width: 1024px)"; +const SWIPE_CLOSE_THRESHOLD = 80; + export default function Drawer({ isOpen, onClose, @@ -30,6 +33,9 @@ export default function Drawer({ disableScroll = false, }: IDrawerProps) { const [isMounted, setIsMounted] = useState(false); + const panelRef = useRef(null); + const touchStartY = useRef(0); + const isDragging = useRef(false); useEffect(() => { setIsMounted(true); @@ -46,13 +52,50 @@ export default function Drawer({ }; }, [isOpen]); + useEffect(() => { + const panel = panelRef.current; + if (!panel) return; + panel.style.transform = ""; + panel.style.transition = ""; + }, [isOpen]); + + const handleTouchStart = (e: TouchEvent) => { + if (!window.matchMedia(TABLET_MQ).matches) return; + touchStartY.current = e.touches[0].clientY; + isDragging.current = true; + if (panelRef.current) { + panelRef.current.style.transition = "none"; + } + }; + + const handleTouchMove = (e: TouchEvent) => { + if (!isDragging.current || !panelRef.current) return; + const delta = e.touches[0].clientY - touchStartY.current; + if (delta < 0) return; + panelRef.current.style.transform = `translateY(${delta}px)`; + }; + + const handleTouchEnd = (e: TouchEvent) => { + if (!isDragging.current || !panelRef.current) return; + isDragging.current = false; + const delta = e.changedTouches[0].clientY - touchStartY.current; + panelRef.current.style.transition = ""; + + if (delta > SWIPE_CLOSE_THRESHOLD) { + panelRef.current.style.transform = ""; + onClose(); + } else { + panelRef.current.style.transform = ""; + } + }; + if (!isMounted) return null; return createPortal(