From bdb043e6c561ef4a93cc8d8c4db431205ac001f7 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 15 Oct 2025 15:41:58 +0330 Subject: [PATCH] stats + stat card --- src/components/GridWrapper.tsx | 125 +++++++++++++++++++++++++ src/constants/colors.ts | 14 +++ src/locale/fa.ts | 6 ++ src/pages/home/Home.tsx | 7 +- src/pages/home/components/StatCard.tsx | 34 +++++++ src/pages/home/components/Stats.tsx | 53 +++++++++++ src/router/MainRouter.tsx | 2 + 7 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/components/GridWrapper.tsx create mode 100644 src/constants/colors.ts create mode 100644 src/pages/home/components/StatCard.tsx create mode 100644 src/pages/home/components/Stats.tsx diff --git a/src/components/GridWrapper.tsx b/src/components/GridWrapper.tsx new file mode 100644 index 0000000..63f687c --- /dev/null +++ b/src/components/GridWrapper.tsx @@ -0,0 +1,125 @@ +import React from "react"; +import { clx } from "@/helpers/utils"; + +type GridWrapperProps = { + children: React.ReactNode; + desktop: number; // تعداد ستون‌ها در دسکتاپ + mobile: number; // تعداد ستون‌ها در موبایل + gapDesktop?: number; // فاصله بین آیتم‌ها در دسکتاپ. اگر مقدار > 12 باشد به عنوان px تفسیر می‌شود (مثلاً 24 => 24px) + gapMobile?: number; // فاصله بین آیتم‌ها در موبایل. اگر مقدار > 12 باشد به عنوان px تفسیر می‌شود + className?: string; +}; + +const GRID_COLS_CLASS: Record = { + 1: "grid-cols-1", + 2: "grid-cols-2", + 3: "grid-cols-3", + 4: "grid-cols-4", + 5: "grid-cols-5", + 6: "grid-cols-6", + 7: "grid-cols-7", + 8: "grid-cols-8", + 9: "grid-cols-9", + 10: "grid-cols-10", + 11: "grid-cols-11", + 12: "grid-cols-12", +}; + +const MD_GRID_COLS_CLASS: Record = { + 1: "md:grid-cols-1", + 2: "md:grid-cols-2", + 3: "md:grid-cols-3", + 4: "md:grid-cols-4", + 5: "md:grid-cols-5", + 6: "md:grid-cols-6", + 7: "md:grid-cols-7", + 8: "md:grid-cols-8", + 9: "md:grid-cols-9", + 10: "md:grid-cols-10", + 11: "md:grid-cols-11", + 12: "md:grid-cols-12", +}; + +// نسخه‌ی دسکتاپ با پیشوند lg: به صورت literal تا با purge مشکلی نداشته باشد +const LG_GRID_COLS_CLASS: Record = { + 1: "lg:grid-cols-1", + 2: "lg:grid-cols-2", + 3: "lg:grid-cols-3", + 4: "lg:grid-cols-4", + 5: "lg:grid-cols-5", + 6: "lg:grid-cols-6", + 7: "lg:grid-cols-7", + 8: "lg:grid-cols-8", + 9: "lg:grid-cols-9", + 10: "lg:grid-cols-10", + 11: "lg:grid-cols-11", + 12: "lg:grid-cols-12", +}; + +// نگاشت Gap بر اساس scale رایج Tailwind: gap-N که هر N معادل 4px است (N * 4px) +const GAP_CLASS: Record = { + 0: "gap-0", 1: "gap-1", 2: "gap-2", 3: "gap-3", 4: "gap-4", 5: "gap-5", + 6: "gap-6", 7: "gap-7", 8: "gap-8", 9: "gap-9", 10: "gap-10", 11: "gap-11", + 12: "gap-12", 13: "gap-13", 14: "gap-14", 15: "gap-15", 16: "gap-16", 17: "gap-17", + 18: "gap-18", 19: "gap-19", 20: "gap-20", 21: "gap-21", 22: "gap-22", 23: "gap-23", 24: "gap-24", +}; + +const LG_GAP_CLASS: Record = { + 0: "lg:gap-0", 1: "lg:gap-1", 2: "lg:gap-2", 3: "lg:gap-3", 4: "lg:gap-4", 5: "lg:gap-5", + 6: "lg:gap-6", 7: "lg:gap-7", 8: "lg:gap-8", 9: "lg:gap-9", 10: "lg:gap-10", 11: "lg:gap-11", + 12: "lg:gap-12", 13: "lg:gap-13", 14: "lg:gap-14", 15: "lg:gap-15", 16: "lg:gap-16", 17: "lg:gap-17", + 18: "lg:gap-18", 19: "lg:gap-19", 20: "lg:gap-20", 21: "lg:gap-21", 22: "lg:gap-22", 23: "lg:gap-23", 24: "lg:gap-24", +}; + +function clampToRange(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, value)); +} + +function getGridColsClass(count: number): string { + const safe = clampToRange(count, 1, 12); + return GRID_COLS_CLASS[safe]; +} + +function getLgGridColsClass(count: number): string { + const safe = clampToRange(count, 1, 12); + return LG_GRID_COLS_CLASS[safe]; +} + +function getMdGridColsClass(count: number): string { + const safe = clampToRange(count, 1, 12); + return MD_GRID_COLS_CLASS[safe]; +} + +function getGapClass(value?: number): string | undefined { + if (value === undefined) return undefined; + // اگر کاربر مقدار را به صورت px فرستاده باشد (بزرگ‌تر از 12)، آن را به scale تبدیل می‌کنیم: N = round(px/4) + const interpretedAsScale = value > 12 ? Math.round(value / 4) : value; + const safe = clampToRange(interpretedAsScale, 0, 24); + return GAP_CLASS[safe]; +} + +function getLgGapClass(value?: number): string | undefined { + if (value === undefined) return undefined; + const interpretedAsScale = value > 12 ? Math.round(value / 4) : value; + const safe = clampToRange(interpretedAsScale, 0, 24); + return LG_GAP_CLASS[safe]; +} + +export default function GridWrapper(props: GridWrapperProps) { + const { children, desktop, mobile, className, gapDesktop = 4, gapMobile = 4 } = props; + + const mobileCols = getGridColsClass(mobile); + const desktopColsMd = getMdGridColsClass(desktop); + const desktopColsLg = getLgGridColsClass(desktop); + + const mobileGap = getGapClass(gapMobile); + const desktopGap = getLgGapClass(gapDesktop); + + return ( +
+ {children} +
+ ); +} + + diff --git a/src/constants/colors.ts b/src/constants/colors.ts new file mode 100644 index 0000000..284b581 --- /dev/null +++ b/src/constants/colors.ts @@ -0,0 +1,14 @@ +export const COLORS = { + primary: "#ffa800", + border: "#f5f7fc", + description: "#c3c7dd", + gray: "#4F5260", + grayLight: "#8C90A3", + success: "#00BA4B", + error: "red", + info: "blue", + black: "black", + white: "#fff", +} as const; + +export type ColorKey = keyof typeof COLORS; diff --git a/src/locale/fa.ts b/src/locale/fa.ts index b2bef8b..9e5a5d1 100644 --- a/src/locale/fa.ts +++ b/src/locale/fa.ts @@ -21,4 +21,10 @@ export const fa = { header: { search: "جستجو", }, + home: { + requestCount: "تعداد درخواست ها", + factureCount: "پیش فاکتور تایید نشده", + orderCount: "سفارش های در حال انجام", + orderDoneCount: "سفارشات انجام شده", + }, }; diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx index a456241..9ed81c9 100644 --- a/src/pages/home/Home.tsx +++ b/src/pages/home/Home.tsx @@ -1,8 +1,13 @@ import { type FC } from 'react' +import Stats from './components/Stats' const Home: FC = () => { return ( -
صفحه اصلی
+
+
+ +
+
) } diff --git a/src/pages/home/components/StatCard.tsx b/src/pages/home/components/StatCard.tsx new file mode 100644 index 0000000..c36b92c --- /dev/null +++ b/src/pages/home/components/StatCard.tsx @@ -0,0 +1,34 @@ +import { COLORS } from '@/constants/colors' +import { ArrowLeft } from 'iconsax-react' +import { type FC, type ReactNode } from 'react' + +type Props = { + icon: ReactNode, + count: number, + description: string +} + +const StatCard: FC = (props) => { + const { icon, count, description } = props + return ( +
+
+ {icon} + +
+ +
+
+ +
+ {count} +
+ +
+ {description} +
+
+ ) +} + +export default StatCard \ No newline at end of file diff --git a/src/pages/home/components/Stats.tsx b/src/pages/home/components/Stats.tsx new file mode 100644 index 0000000..2e0753a --- /dev/null +++ b/src/pages/home/components/Stats.tsx @@ -0,0 +1,53 @@ +import GridWrapper from '@/components/GridWrapper' +import { COLORS } from '@/constants/colors' +import { t } from '@/locale' +import { BoxTick, BoxTime, ReceiptText, TruckTick } from 'iconsax-react' +import { type FC } from 'react' +import StatCard from './StatCard' + +const Stats: FC = () => { + return ( + + } + /> + } + /> + } + /> + } + /> + + ) +} + +export default Stats \ No newline at end of file diff --git a/src/router/MainRouter.tsx b/src/router/MainRouter.tsx index 1447a8c..99b13c8 100644 --- a/src/router/MainRouter.tsx +++ b/src/router/MainRouter.tsx @@ -3,6 +3,7 @@ import { Route, Routes } from 'react-router-dom' import Home from '../pages/home/Home' import SideBar from '../shared/Sidebar' import Header from '../shared/Header' +import { Paths } from '@/config/Paths' const MainRouter: FC = () => { return ( @@ -15,6 +16,7 @@ const MainRouter: FC = () => { } /> + } />