stats + stat card
This commit is contained in:
@@ -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<number, string> = {
|
||||||
|
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<number, string> = {
|
||||||
|
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<number, string> = {
|
||||||
|
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<number, string> = {
|
||||||
|
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<number, string> = {
|
||||||
|
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 (
|
||||||
|
<div className={clx("grid", mobileCols, desktopColsMd, desktopColsLg, mobileGap, desktopGap, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
@@ -21,4 +21,10 @@ export const fa = {
|
|||||||
header: {
|
header: {
|
||||||
search: "جستجو",
|
search: "جستجو",
|
||||||
},
|
},
|
||||||
|
home: {
|
||||||
|
requestCount: "تعداد درخواست ها",
|
||||||
|
factureCount: "پیش فاکتور تایید نشده",
|
||||||
|
orderCount: "سفارش های در حال انجام",
|
||||||
|
orderDoneCount: "سفارشات انجام شده",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
|
import Stats from './components/Stats'
|
||||||
|
|
||||||
const Home: FC = () => {
|
const Home: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div>صفحه اصلی</div>
|
<div className='flex gap-6'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<Stats />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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> = (props) => {
|
||||||
|
const { icon, count, description } = props
|
||||||
|
return (
|
||||||
|
<div className='flex-1 bg-white rounded-3xl p-6'>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
{icon}
|
||||||
|
|
||||||
|
<div className='size-8 bg-[#FFF1D7] rounded-full flex justify-center items-center'>
|
||||||
|
<ArrowLeft size={16} color={COLORS.primary} className='rotate-45' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-2 text-2xl font-semibold'>
|
||||||
|
{count}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-2 text-sm'>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatCard
|
||||||
@@ -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 (
|
||||||
|
<GridWrapper
|
||||||
|
desktop={4}
|
||||||
|
mobile={2}
|
||||||
|
gapDesktop={24}
|
||||||
|
gapMobile={12}
|
||||||
|
className='mt-5'
|
||||||
|
>
|
||||||
|
<StatCard
|
||||||
|
count={2}
|
||||||
|
description={t('home.requestCount')}
|
||||||
|
icon={<BoxTime
|
||||||
|
size={27}
|
||||||
|
color={COLORS.primary}
|
||||||
|
/>}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
count={2}
|
||||||
|
description={t('home.factureCount')}
|
||||||
|
icon={<ReceiptText
|
||||||
|
size={27}
|
||||||
|
color={COLORS.primary}
|
||||||
|
/>}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
count={10}
|
||||||
|
description={t('home.orderCount')}
|
||||||
|
icon={<BoxTick
|
||||||
|
size={27}
|
||||||
|
color={COLORS.primary}
|
||||||
|
/>}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
count={20}
|
||||||
|
description={t('home.orderDoneCount')}
|
||||||
|
icon={<TruckTick
|
||||||
|
size={27}
|
||||||
|
color={COLORS.primary}
|
||||||
|
/>}
|
||||||
|
/>
|
||||||
|
</GridWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Stats
|
||||||
@@ -3,6 +3,7 @@ import { Route, Routes } from 'react-router-dom'
|
|||||||
import Home from '../pages/home/Home'
|
import Home from '../pages/home/Home'
|
||||||
import SideBar from '../shared/Sidebar'
|
import SideBar from '../shared/Sidebar'
|
||||||
import Header from '../shared/Header'
|
import Header from '../shared/Header'
|
||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
return (
|
return (
|
||||||
@@ -15,6 +16,7 @@ const MainRouter: FC = () => {
|
|||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path={Paths.home} element={<Home />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user