Browse Source
feat: add core admin panel application with layout, UI components, and feature modules
master
feat: add core admin panel application with layout, UI components, and feature modules
master
53 changed files with 5195 additions and 2117 deletions
-
4src/app/providers.tsx
-
2src/components/layout/AppSidebar.tsx
-
12src/components/layout/ServerHealthTrigger.tsx
-
9src/components/layout/ServerStatusDialog.tsx
-
2src/components/shared/AppImage.tsx
-
5src/components/shared/DeleteConfirmDialog.tsx
-
190src/components/shared/Shimmer.tsx
-
390src/components/shared/shimmer_frontend.tsx
-
42src/components/ui/avatar.tsx
-
100src/components/ui/button.tsx
-
10src/components/ui/multi-select.tsx
-
5src/components/ui/shimmer.tsx
-
10src/components/ui/single-select.tsx
-
6src/components/ui/skeleton.tsx
-
25src/components/ui/switch.tsx
-
95src/components/ui/toast-container.tsx
-
21src/config/navigation.ts
-
8src/features/auth/LoginPage.tsx
-
146src/features/billing/BillingPage.tsx
-
823src/features/billing/components/BillingDetailModal.tsx
-
121src/features/billing/components/BillingServiceBadge.tsx
-
55src/features/billing/components/BillingStatusBadge.tsx
-
288src/features/billing/components/ReceiptLightboxDialog.tsx
-
13src/features/billing/components/RejectFactorDialog.tsx
-
16src/features/billing/services/billing-api.ts
-
2src/features/services/ServicesPage.tsx
-
18src/features/services/components/CategoriesServiceView.tsx
-
12src/features/services/components/DuasServiceView.tsx
-
558src/features/services/components/FaqServiceView.tsx
-
3src/features/services/components/LocationDetailSheet.tsx
-
7src/features/services/components/ShrinesServiceView.tsx
-
41src/features/services/components/ZayerGuideServiceView.tsx
-
576src/features/tours/OngoingToursPage.tsx
-
112src/features/tours/TourOrdersPage.tsx
-
179src/features/tours/ToursPage.tsx
-
244src/features/tours/components/CreateTourDialog.tsx
-
2482src/features/tours/components/TourDetailDialog.tsx
-
27src/features/tours/components/TourDetailSheet.tsx
-
255src/features/tours/components/TourDetailShimmer.tsx
-
44src/features/tours/components/TourStatusBadge.tsx
-
24src/features/tours/components/TourTranslationsDialog.tsx
-
127src/features/tours/services/tour-api.ts
-
2src/features/tours/types.ts
-
10src/i18n/index.ts
-
1src/icons.tsx
-
48src/index.css
-
47src/lib/utils.ts
-
2src/locales/ar.json
-
2src/locales/en.json
-
4src/locales/fa.json
-
18src/stores/billing-badge.ts
-
63src/stores/toast.ts
-
4vite.config.ts
@ -1,190 +0,0 @@ |
|||
import React from 'react' |
|||
import { cn } from '@/lib/utils' |
|||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' |
|||
import { Ic, IconName } from '@/icons' |
|||
|
|||
export interface ShimmerProps { |
|||
/** تعداد ردیفها یا آیتمهای اسکلتون (پیشفرض: ۵) */ |
|||
rows?: number |
|||
count?: number |
|||
/** ارتفاع هر ردیف (پیشفرض: h-12) */ |
|||
rowHeight?: string |
|||
/** کلاسهای سفارشی برای بدنه و والد */ |
|||
className?: string |
|||
/** کلاسهای سفارشی برای هر آیتم */ |
|||
itemClassName?: string |
|||
/** آیا درون یک Card با کادر و سایه استاندارد قرار گیرد؟ */ |
|||
withCard?: boolean |
|||
/** عنوان هدر کارت در صورت استفاده از withCard */ |
|||
cardHeaderTitle?: string |
|||
/** آیکون هدر کارت */ |
|||
cardHeaderIcon?: IconName |
|||
/** متن سمت چپ هدر کارت */ |
|||
cardHeaderSub?: React.ReactNode |
|||
/** نوع نمایش: ردیفی/جدولی (پیشفرض) یا کارتهای گرید یا KPI */ |
|||
variant?: 'list' | 'table' | 'grid' | 'kpi' |
|||
/** تعداد ستونها در حالت grid (پیشفرض: 3) */ |
|||
gridCols?: number |
|||
} |
|||
|
|||
/** |
|||
* کامپوننت یکپارچه و استاندارد Shimmer برای Loadingهای پنل مدیریت عقیله |
|||
* دارای پالت خنثی با کنتراست استاندارد و انیمیشن روان پالس (در هر دو تم دارک و لایت) |
|||
*/ |
|||
export function Shimmer({ |
|||
rows, |
|||
count = 5, |
|||
rowHeight = 'h-12', |
|||
className, |
|||
itemClassName, |
|||
withCard = false, |
|||
cardHeaderTitle, |
|||
cardHeaderIcon = 'list', |
|||
cardHeaderSub, |
|||
variant = 'table', |
|||
gridCols = 3, |
|||
}: ShimmerProps) { |
|||
const itemCount = rows !== undefined ? rows : count |
|||
|
|||
let content: React.ReactNode |
|||
|
|||
if (variant === 'grid') { |
|||
const colClasses: Record<number, string> = { |
|||
1: 'grid-cols-1', |
|||
2: 'grid-cols-1 sm:grid-cols-2', |
|||
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3', |
|||
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4', |
|||
} |
|||
|
|||
content = ( |
|||
<div className={cn('grid gap-4 p-4 sm:p-6', colClasses[gridCols] || 'grid-cols-1 sm:grid-cols-3', className)}> |
|||
{Array.from({ length: itemCount }).map((_, i) => ( |
|||
<div |
|||
key={i} |
|||
className={cn( |
|||
'h-36 rounded-2xl bg-slate-200/90 dark:bg-white/10 animate-pulse border border-border-soft', |
|||
itemClassName |
|||
)} |
|||
/> |
|||
))} |
|||
</div> |
|||
) |
|||
} else if (variant === 'kpi') { |
|||
content = ( |
|||
<div className={cn('grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3.5', className)}> |
|||
{Array.from({ length: itemCount }).map((_, i) => ( |
|||
<div |
|||
key={i} |
|||
className={cn( |
|||
'h-20 rounded-2xl bg-slate-200/90 dark:bg-white/10 animate-pulse border border-border-soft p-4 flex items-center justify-between', |
|||
itemClassName |
|||
)} |
|||
> |
|||
<div className="space-y-2"> |
|||
<div className="h-3 w-16 rounded bg-slate-300 dark:bg-white/15" /> |
|||
<div className="h-5 w-10 rounded bg-slate-300 dark:bg-white/15" /> |
|||
</div> |
|||
<div className="size-9 rounded-xl bg-slate-300/80 dark:bg-white/15" /> |
|||
</div> |
|||
))} |
|||
</div> |
|||
) |
|||
} else if (variant === 'table') { |
|||
// رندرر شبیه جدول با ستونهای شکیل و تفکیکشده
|
|||
content = ( |
|||
<div className={cn('divide-y divide-border-soft', className)}> |
|||
{/* ردیف هدر جدول */} |
|||
<div className="p-4 bg-muted/40 flex items-center justify-between gap-4"> |
|||
<div className="h-4 w-12 rounded bg-slate-300 dark:bg-white/15 animate-pulse" /> |
|||
<div className="h-4 w-32 rounded bg-slate-300 dark:bg-white/15 animate-pulse" /> |
|||
<div className="h-4 w-20 rounded bg-slate-300 dark:bg-white/15 animate-pulse" /> |
|||
<div className="h-4 w-24 rounded bg-slate-300 dark:bg-white/15 animate-pulse" /> |
|||
<div className="h-4 w-20 rounded bg-slate-300 dark:bg-white/15 animate-pulse" /> |
|||
<div className="h-4 w-16 rounded bg-slate-300 dark:bg-white/15 animate-pulse" /> |
|||
</div> |
|||
|
|||
{/* سطرهای شیمر جدول */} |
|||
{Array.from({ length: itemCount }).map((_, i) => ( |
|||
<div |
|||
key={i} |
|||
className={cn( |
|||
'p-4 flex items-center justify-between gap-4 hover:bg-muted/10 transition-colors', |
|||
itemClassName |
|||
)} |
|||
> |
|||
{/* شناسه */} |
|||
<div className="h-4 w-10 rounded bg-slate-200/90 dark:bg-white/10 animate-pulse shrink-0" /> |
|||
|
|||
{/* کاربر (آواتار + نام) */} |
|||
<div className="flex items-center gap-2.5 min-w-0 flex-1 max-w-[200px]"> |
|||
<div className="size-8 rounded-full bg-slate-200/90 dark:bg-white/10 animate-pulse shrink-0" /> |
|||
<div className="space-y-1.5 flex-1"> |
|||
<div className="h-3.5 w-24 rounded bg-slate-200/90 dark:bg-white/10 animate-pulse" /> |
|||
<div className="h-2.5 w-16 rounded bg-slate-200/90 dark:bg-white/10 animate-pulse" /> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* بج سرویس */} |
|||
<div className="h-6 w-16 rounded-lg bg-slate-200/90 dark:bg-white/10 animate-pulse shrink-0" /> |
|||
|
|||
{/* مبلغ */} |
|||
<div className="h-4 w-20 rounded bg-slate-200/90 dark:bg-white/10 animate-pulse shrink-0" /> |
|||
|
|||
{/* بج وضعیت */} |
|||
<div className="h-6 w-24 rounded-full bg-slate-200/90 dark:bg-white/10 animate-pulse shrink-0" /> |
|||
|
|||
{/* تاریخ و اکشن */} |
|||
<div className="h-3.5 w-20 rounded bg-slate-200/90 dark:bg-white/10 animate-pulse shrink-0" /> |
|||
<div className="h-8 w-16 rounded-xl bg-slate-200/90 dark:bg-white/10 animate-pulse shrink-0" /> |
|||
</div> |
|||
))} |
|||
</div> |
|||
) |
|||
} else { |
|||
// ردیفی ساده
|
|||
content = ( |
|||
<div className={cn('p-6 space-y-3', className)}> |
|||
{Array.from({ length: itemCount }).map((_, i) => ( |
|||
<div |
|||
key={i} |
|||
className={cn( |
|||
rowHeight, |
|||
'bg-slate-200/90 dark:bg-white/10 animate-pulse rounded-xl transition-all duration-300 border border-border-soft/40', |
|||
itemClassName |
|||
)} |
|||
/> |
|||
))} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
if (withCard) { |
|||
return ( |
|||
<Card className="border-border-soft bg-card/40 shadow-xs overflow-hidden"> |
|||
{(cardHeaderTitle || cardHeaderSub) && ( |
|||
<CardHeader className="py-3 px-4 border-b border-border-soft bg-card/60 flex flex-row items-center justify-between"> |
|||
{cardHeaderTitle && ( |
|||
<CardTitle className="text-xs font-bold flex items-center gap-2 text-foreground"> |
|||
<Ic name={cardHeaderIcon} className="size-4 text-primary" /> |
|||
<span>{cardHeaderTitle}</span> |
|||
</CardTitle> |
|||
)} |
|||
{cardHeaderSub && ( |
|||
<span className="text-xs text-muted-foreground font-mono"> |
|||
{cardHeaderSub} |
|||
</span> |
|||
)} |
|||
</CardHeader> |
|||
)} |
|||
<CardContent className="p-0"> |
|||
{content} |
|||
</CardContent> |
|||
</Card> |
|||
) |
|||
} |
|||
|
|||
return <>{content}</> |
|||
} |
|||
|
|||
export const shimmer = Shimmer |
|||
export default Shimmer |
|||
@ -0,0 +1,390 @@ |
|||
import React from 'react' |
|||
import { cn } from '@/lib/utils' |
|||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' |
|||
import { Ic, IconName } from '@/icons' |
|||
|
|||
/** |
|||
* shimmer_frontend (Shimmer UI Component & Atoms for Aqila Web Panel) |
|||
* |
|||
* الگوبرداریشده از معماری استاندارد AppShimmer در فلاتر (Neutral Slate Palette): |
|||
* - حالت لایت (Light Mode): رنگ پایه #E2E8F0 و هایلایت #F8FAFC |
|||
* - حالت دارک (Dark Mode): رنگ پایه #22252C و هایلایت #333742 |
|||
* - دوره تناوب (Period): ۱۵۰۰ میلیثانیه برای حرکت نرم و یکدست موج نوری |
|||
* |
|||
* شامل اتمهای لگویی: |
|||
* - ShimmerBox: برای کارتها، تصاویر، فیلدها و دکمهها |
|||
* - ShimmerCircle: برای آواتارها، آیکونها و بجهای دایرهای |
|||
* - ShimmerLine: برای خطوط متنی و عناوین تایپوگرافی |
|||
*/ |
|||
|
|||
/* ───────────────────────────────────────────────────────── |
|||
۱. اتمهای پایه لودینگ شیمر (Shimmer Atoms) |
|||
───────────────────────────────────────────────────────── */ |
|||
|
|||
export interface ShimmerBoxProps extends React.HTMLAttributes<HTMLDivElement> { |
|||
width?: string | number |
|||
height?: string | number |
|||
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' |
|||
className?: string |
|||
style?: React.CSSProperties |
|||
children?: React.ReactNode |
|||
} |
|||
|
|||
/** |
|||
* اتم مستطیلی اسکلتون (مشابه ShimmerBox در فلاتر) |
|||
*/ |
|||
export function ShimmerBox({ |
|||
width, |
|||
height, |
|||
rounded = 'xl', |
|||
className, |
|||
style, |
|||
children, |
|||
...props |
|||
}: ShimmerBoxProps) { |
|||
const roundedClass = { |
|||
none: 'rounded-none', |
|||
sm: 'rounded-sm', |
|||
md: 'rounded-md', |
|||
lg: 'rounded-lg', |
|||
xl: 'rounded-xl', |
|||
'2xl': 'rounded-2xl', |
|||
'3xl': 'rounded-3xl', |
|||
full: 'rounded-full', |
|||
}[rounded] |
|||
|
|||
const customStyle: React.CSSProperties = { |
|||
...(width !== undefined ? { width: typeof width === 'number' ? `${width}px` : width } : {}), |
|||
...(height !== undefined ? { height: typeof height === 'number' ? `${height}px` : height } : {}), |
|||
...style, |
|||
} |
|||
|
|||
return ( |
|||
<div |
|||
className={cn( |
|||
'shimmer-wave border border-border-soft shrink-0', |
|||
roundedClass, |
|||
className |
|||
)} |
|||
style={customStyle} |
|||
{...props} |
|||
> |
|||
{children} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export interface ShimmerCircleProps extends React.HTMLAttributes<HTMLDivElement> { |
|||
size?: number | string |
|||
radius?: number |
|||
className?: string |
|||
style?: React.CSSProperties |
|||
} |
|||
|
|||
/** |
|||
* اتم دایرهای اسکلتون (مشابه ShimmerCircle در فلاتر) |
|||
*/ |
|||
export function ShimmerCircle({ |
|||
size = 40, |
|||
radius, |
|||
className, |
|||
style, |
|||
...props |
|||
}: ShimmerCircleProps) { |
|||
const finalSize = radius !== undefined ? radius * 2 : size |
|||
const sizeValue = typeof finalSize === 'number' ? `${finalSize}px` : finalSize |
|||
|
|||
return ( |
|||
<div |
|||
className={cn( |
|||
'shimmer-wave rounded-full border border-border-soft shrink-0', |
|||
className |
|||
)} |
|||
style={{ |
|||
width: sizeValue, |
|||
height: sizeValue, |
|||
...style, |
|||
}} |
|||
{...props} |
|||
/> |
|||
) |
|||
} |
|||
|
|||
export interface ShimmerLineProps extends React.HTMLAttributes<HTMLDivElement> { |
|||
width?: string | number |
|||
height?: string | number |
|||
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full' |
|||
className?: string |
|||
style?: React.CSSProperties |
|||
} |
|||
|
|||
/** |
|||
* اتم خطی متنی اسکلتون (مشابه ShimmerLine در فلاتر) |
|||
*/ |
|||
export function ShimmerLine({ |
|||
width = '100%', |
|||
height = '14px', |
|||
rounded = 'sm', |
|||
className, |
|||
style, |
|||
...props |
|||
}: ShimmerLineProps) { |
|||
const roundedClass = { |
|||
none: 'rounded-none', |
|||
sm: 'rounded-sm', |
|||
md: 'rounded-md', |
|||
lg: 'rounded-lg', |
|||
full: 'rounded-full', |
|||
}[rounded] |
|||
|
|||
return ( |
|||
<div |
|||
className={cn( |
|||
'shimmer-wave border border-border-soft shrink-0', |
|||
roundedClass, |
|||
className |
|||
)} |
|||
style={{ |
|||
width: typeof width === 'number' ? `${width}px` : width, |
|||
height: typeof height === 'number' ? `${height}px` : height, |
|||
...style, |
|||
}} |
|||
{...props} |
|||
/> |
|||
) |
|||
} |
|||
|
|||
/* ───────────────────────────────────────────────────────── |
|||
۲. کامپوننت جامع قالبها (Composite Shimmer Component) |
|||
───────────────────────────────────────────────────────── */ |
|||
|
|||
export interface ShimmerProps { |
|||
/** تعداد ردیفها یا آیتمهای اسکلتون (پیشفرض: ۵) */ |
|||
rows?: number |
|||
count?: number |
|||
/** ارتفاع هر ردیف (در صورت استفاده سفارشی) */ |
|||
rowHeight?: string |
|||
/** کلاسهای سفارشی برای بدنه و والد */ |
|||
className?: string |
|||
/** کلاسهای سفارشی برای هر آیتم */ |
|||
itemClassName?: string |
|||
/** آیا درون یک Card با کادر و سایه استاندارد قرار گیرد؟ */ |
|||
withCard?: boolean |
|||
/** عنوان هدر کارت در صورت استفاده از withCard */ |
|||
cardHeaderTitle?: string |
|||
/** آیکون هدر کارت */ |
|||
cardHeaderIcon?: IconName |
|||
/** متن سمت چپ هدر کارت */ |
|||
cardHeaderSub?: React.ReactNode |
|||
/** نوع نمایش: جدولی (پیشفرض)، ردیفی، کارتی، آمار KPI یا جزئیات */ |
|||
variant?: 'table' | 'grid' | 'kpi' | 'list' | 'detail' | 'custom' |
|||
/** تعداد ستونها در حالت grid (پیشفرض: 3) */ |
|||
gridCols?: number |
|||
/** فرزندان سفارشی در صورت استفاده از کامپوننت به عنوان Wrapper */ |
|||
children?: React.ReactNode |
|||
/** آیا انیمیشن فعال باشد؟ (پیشفرض: true) */ |
|||
isEnabled?: boolean |
|||
} |
|||
|
|||
/** |
|||
* کامپوننت مرجع و یکپارچه Shimmer برای پنل مدیریت عقیله |
|||
*/ |
|||
export function Shimmer({ |
|||
rows, |
|||
count = 5, |
|||
rowHeight, |
|||
className, |
|||
itemClassName, |
|||
withCard = false, |
|||
cardHeaderTitle, |
|||
cardHeaderIcon = 'list', |
|||
cardHeaderSub, |
|||
variant = 'table', |
|||
gridCols = 3, |
|||
children, |
|||
isEnabled = true, |
|||
}: ShimmerProps) { |
|||
if (!isEnabled) { |
|||
return <>{children}</> |
|||
} |
|||
|
|||
const itemCount = rows !== undefined ? rows : count |
|||
|
|||
let content: React.ReactNode |
|||
|
|||
if (children && variant === 'custom') { |
|||
content = <div className={className}>{children}</div> |
|||
} else if (variant === 'grid') { |
|||
const colClasses: Record<number, string> = { |
|||
1: 'grid-cols-1', |
|||
2: 'grid-cols-1 sm:grid-cols-2', |
|||
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3', |
|||
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4', |
|||
} |
|||
|
|||
content = ( |
|||
<div className={cn('grid gap-4 p-4 sm:p-6', colClasses[gridCols] || 'grid-cols-1 sm:grid-cols-3', className)}> |
|||
{Array.from({ length: itemCount }).map((_, i) => ( |
|||
<div |
|||
key={i} |
|||
className={cn( |
|||
'flex flex-col gap-3 rounded-2xl border border-border-soft bg-card/40 p-4', |
|||
itemClassName |
|||
)} |
|||
> |
|||
<ShimmerBox height={160} rounded="xl" className="w-full" /> |
|||
<div className="space-y-2 pt-1"> |
|||
<ShimmerLine width="75%" height={16} rounded="sm" /> |
|||
<ShimmerLine width="45%" height={12} rounded="sm" /> |
|||
</div> |
|||
<div className="flex items-center justify-between pt-2 border-t border-border-soft"> |
|||
<ShimmerLine width="30%" height={12} rounded="sm" /> |
|||
<ShimmerBox width={60} height={24} rounded="lg" /> |
|||
</div> |
|||
</div> |
|||
))} |
|||
</div> |
|||
) |
|||
} else if (variant === 'kpi') { |
|||
content = ( |
|||
<div className={cn('grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3.5', className)}> |
|||
{Array.from({ length: itemCount }).map((_, i) => ( |
|||
<div |
|||
key={i} |
|||
className={cn( |
|||
'h-20 rounded-2xl border border-border-soft bg-card/40 p-4 flex items-center justify-between', |
|||
itemClassName |
|||
)} |
|||
> |
|||
<div className="space-y-2"> |
|||
<ShimmerLine width={60} height={12} rounded="sm" /> |
|||
<ShimmerLine width={40} height={20} rounded="sm" /> |
|||
</div> |
|||
<ShimmerBox width={36} height={36} rounded="xl" /> |
|||
</div> |
|||
))} |
|||
</div> |
|||
) |
|||
} else if (variant === 'list') { |
|||
content = ( |
|||
<div className={cn('space-y-3 p-4', className)}> |
|||
{Array.from({ length: itemCount }).map((_, i) => ( |
|||
<div |
|||
key={i} |
|||
className={cn( |
|||
'flex items-center justify-between rounded-xl border border-border-soft bg-card/40 p-3.5', |
|||
itemClassName |
|||
)} |
|||
> |
|||
<div className="flex items-center gap-3"> |
|||
<ShimmerCircle size={36} /> |
|||
<div className="space-y-1.5"> |
|||
<ShimmerLine width={140} height={14} rounded="sm" /> |
|||
<ShimmerLine width={80} height={10} rounded="sm" /> |
|||
</div> |
|||
</div> |
|||
<ShimmerBox width={70} height={28} rounded="lg" /> |
|||
</div> |
|||
))} |
|||
</div> |
|||
) |
|||
} else if (variant === 'detail') { |
|||
content = ( |
|||
<div className={cn('space-y-5 p-4 sm:p-6', className)}> |
|||
<div className="flex items-center justify-between pb-4 border-b border-border-soft"> |
|||
<div className="flex items-center gap-3"> |
|||
<ShimmerCircle size={48} /> |
|||
<div className="space-y-2"> |
|||
<ShimmerLine width={180} height={18} rounded="sm" /> |
|||
<ShimmerLine width={110} height={12} rounded="sm" /> |
|||
</div> |
|||
</div> |
|||
<div className="flex items-center gap-2"> |
|||
<ShimmerBox width={80} height={32} rounded="xl" /> |
|||
<ShimmerBox width={32} height={32} rounded="xl" /> |
|||
</div> |
|||
</div> |
|||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> |
|||
<ShimmerBox height={110} rounded="2xl" /> |
|||
<ShimmerBox height={110} rounded="2xl" /> |
|||
</div> |
|||
<div className="space-y-2.5 pt-2"> |
|||
<ShimmerLine width="100%" height={14} rounded="sm" /> |
|||
<ShimmerLine width="90%" height={14} rounded="sm" /> |
|||
<ShimmerLine width="60%" height={14} rounded="sm" /> |
|||
</div> |
|||
</div> |
|||
) |
|||
} else { |
|||
// پیشفرض: Table (جدول دادهها با تفکیک ستونها)
|
|||
content = ( |
|||
<div className={cn('divide-y divide-border-soft', className)}> |
|||
{/* ردیف هدر جدول */} |
|||
<div className="p-4 bg-muted/40 flex items-center justify-between gap-4"> |
|||
<ShimmerLine width={48} height={14} rounded="sm" /> |
|||
<ShimmerLine width={120} height={14} rounded="sm" /> |
|||
<ShimmerLine width={80} height={14} rounded="sm" /> |
|||
<ShimmerLine width={64} height={14} rounded="sm" /> |
|||
<ShimmerLine width={32} height={14} rounded="sm" /> |
|||
</div> |
|||
|
|||
{/* ردیفهای محتوای جدول */} |
|||
{Array.from({ length: itemCount }).map((_, i) => ( |
|||
<div key={i} className={cn('p-4 flex items-center justify-between gap-4', itemClassName)}> |
|||
<div className="flex items-center gap-3"> |
|||
<ShimmerBox width={18} height={18} rounded="sm" /> |
|||
<ShimmerLine width={36} height={14} rounded="sm" /> |
|||
</div> |
|||
<div className="space-y-1.5 flex-1 max-w-xs"> |
|||
<ShimmerLine width="80%" height={14} rounded="sm" /> |
|||
<ShimmerLine width="45%" height={10} rounded="sm" /> |
|||
</div> |
|||
<ShimmerLine width={70} height={14} rounded="sm" /> |
|||
<ShimmerBox width={80} height={24} rounded="full" /> |
|||
<ShimmerBox width={28} height={28} rounded="lg" /> |
|||
</div> |
|||
))} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
if (withCard) { |
|||
return ( |
|||
<Card className="border-border-soft bg-card/40 shadow-xs overflow-hidden"> |
|||
{(cardHeaderTitle || cardHeaderSub) && ( |
|||
<CardHeader className="py-3 px-4 border-b border-border-soft bg-card/60 flex flex-row items-center justify-between"> |
|||
{cardHeaderTitle && ( |
|||
<CardTitle className="text-xs font-bold flex items-center gap-2 text-foreground"> |
|||
<Ic name={cardHeaderIcon} className="size-4 text-primary" /> |
|||
<span>{cardHeaderTitle}</span> |
|||
</CardTitle> |
|||
)} |
|||
{cardHeaderSub && ( |
|||
<span className="text-xs text-muted-foreground font-mono"> |
|||
{cardHeaderSub} |
|||
</span> |
|||
)} |
|||
</CardHeader> |
|||
)} |
|||
<CardContent className="p-0"> |
|||
{content} |
|||
</CardContent> |
|||
</Card> |
|||
) |
|||
} |
|||
|
|||
return <>{content}</> |
|||
} |
|||
|
|||
// ضمیمه کردن متدهای کمکی استاتیک (Static Helper Properties)
|
|||
Shimmer.Box = ShimmerBox |
|||
Shimmer.Circle = ShimmerCircle |
|||
Shimmer.Line = ShimmerLine |
|||
|
|||
// نامهای مستعار و سازگاری
|
|||
export const shimmer = Shimmer |
|||
export const ShimmerFrontend = Shimmer |
|||
export const AppShimmer = Shimmer |
|||
export const shimmer_frontend = Shimmer |
|||
|
|||
export default Shimmer |
|||
@ -1,35 +1,49 @@ |
|||
import * as React from 'react' |
|||
import { cn } from '@/lib/utils' |
|||
import { Ic, type IconName } from '@/icons' |
|||
|
|||
type AvatarProps = React.ComponentProps<'div'> & { |
|||
tone?: string |
|||
icon?: IconName |
|||
iconClassName?: string |
|||
/** پارامترهای سازگاری با فراخوانیهای قبلی */ |
|||
src?: string | null |
|||
letter?: string |
|||
tone?: string |
|||
ring?: boolean |
|||
/** URL تصویر واقعی؛ اگر باشد بهجای گرادیان+حرف نمایش داده میشود. */ |
|||
src?: string | null |
|||
} |
|||
|
|||
/* Hosseinieh letter-avatar: gradient circle + Persian initial |
|||
اگر src داده شود، تصویر واقعی نمایش داده میشود و گرادیان حذف میشود. */ |
|||
function Avatar({ className, tone, letter = 'م', ring = false, src, children, ...props }: AvatarProps) { |
|||
/** |
|||
* کامپوننت آواتار استاندارد پنل مدیریت عقیله |
|||
* به صورت پیشفرض آیکون ادمک (User Icon) با کنتراست بالا، پالت خنثی و ظاهری شارپ رندر میکند. |
|||
*/ |
|||
function Avatar({ |
|||
className, |
|||
icon = 'user', |
|||
iconClassName, |
|||
children, |
|||
src, |
|||
letter, |
|||
tone, |
|||
ring, |
|||
...props |
|||
}: AvatarProps) { |
|||
return ( |
|||
<div |
|||
className={cn( |
|||
'relative flex shrink-0 items-center justify-center rounded-full text-white select-none', |
|||
!src && tone, |
|||
src && 'overflow-hidden', |
|||
ring && 'ring-2 ring-red-mid/70 ring-offset-2 ring-offset-background', |
|||
'relative flex shrink-0 items-center justify-center rounded-full bg-slate-100 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700/60 text-slate-600 dark:text-slate-300 select-none shadow-2xs', |
|||
className, |
|||
)} |
|||
{...props} |
|||
> |
|||
{src ? ( |
|||
<img src={src} alt="" loading="lazy" className="size-full object-cover" /> |
|||
) : ( |
|||
children ?? letter |
|||
{children ?? ( |
|||
<Ic |
|||
name={icon} |
|||
className={cn('size-4 shrink-0', iconClassName)} |
|||
/> |
|||
)} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export { Avatar } |
|||
|
|||
@ -1,2 +1,3 @@ |
|||
export * from '@/components/shared/Shimmer' |
|||
export { default } from '@/components/shared/Shimmer' |
|||
export * from '@/components/shared/shimmer_frontend' |
|||
export { default } from '@/components/shared/shimmer_frontend' |
|||
|
|||
@ -1,13 +1,15 @@ |
|||
import * as React from 'react' |
|||
import { cn } from '@/lib/utils' |
|||
|
|||
function Skeleton({ className, ...props }) { |
|||
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { |
|||
return ( |
|||
<div |
|||
data-slot="skeleton" |
|||
className={cn('animate-pulse rounded-md bg-white/8', className)} |
|||
className={cn('shimmer-wave rounded-md border border-border-soft', className)} |
|||
{...props} |
|||
/> |
|||
) |
|||
} |
|||
|
|||
export { Skeleton } |
|||
|
|||
@ -0,0 +1,95 @@ |
|||
import React from 'react' |
|||
import { useToastStore, type ToastItem } from '@/stores/toast' |
|||
import { Ic, type IconName } from '@/icons' |
|||
import { cn } from '@/lib/utils' |
|||
|
|||
export function ToastContainer() { |
|||
const { toasts, removeToast } = useToastStore() |
|||
|
|||
if (toasts.length === 0) return null |
|||
|
|||
return ( |
|||
<div |
|||
dir="rtl" |
|||
aria-live="polite" |
|||
className="fixed top-5 left-5 z-[9999] flex max-w-sm w-full flex-col gap-2.5 pointer-events-none select-none transition-all" |
|||
> |
|||
{toasts.map((toast) => ( |
|||
<ToastCard key={toast.id} toast={toast} onClose={() => removeToast(toast.id)} /> |
|||
))} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
function ToastCard({ toast, onClose }: { toast: ToastItem; onClose: () => void }) { |
|||
const configs: Record< |
|||
ToastItem['type'], |
|||
{ |
|||
containerClass: string |
|||
icon: IconName |
|||
iconClass: string |
|||
borderClass: string |
|||
} |
|||
> = { |
|||
success: { |
|||
containerClass: |
|||
'bg-card/95 text-foreground dark:bg-card/95 border-emerald-500/30 shadow-lg shadow-emerald-500/5', |
|||
icon: 'checkCircle', |
|||
iconClass: 'text-emerald-500 dark:text-emerald-400', |
|||
borderClass: 'border-l-4 border-l-emerald-500', |
|||
}, |
|||
error: { |
|||
containerClass: |
|||
'bg-card/95 text-foreground dark:bg-card/95 border-rose-500/30 shadow-lg shadow-rose-500/5', |
|||
icon: 'xCircle', |
|||
iconClass: 'text-rose-500 dark:text-rose-400', |
|||
borderClass: 'border-l-4 border-l-rose-500', |
|||
}, |
|||
warning: { |
|||
containerClass: |
|||
'bg-card/95 text-foreground dark:bg-card/95 border-amber-500/30 shadow-lg shadow-amber-500/5', |
|||
icon: 'alert', |
|||
iconClass: 'text-amber-500 dark:text-amber-400', |
|||
borderClass: 'border-l-4 border-l-amber-500', |
|||
}, |
|||
info: { |
|||
containerClass: |
|||
'bg-card/95 text-foreground dark:bg-card/95 border-primary/30 shadow-lg shadow-primary/5', |
|||
icon: 'helpCircle', |
|||
iconClass: 'text-primary dark:text-primary', |
|||
borderClass: 'border-l-4 border-l-primary', |
|||
}, |
|||
} |
|||
|
|||
const current = configs[toast.type] || configs.info |
|||
|
|||
return ( |
|||
<div |
|||
role="alert" |
|||
className={cn( |
|||
'pointer-events-auto flex items-start gap-3 rounded-2xl border p-3.5 backdrop-blur-md transition-all duration-300 animate-in fade-in slide-in-from-top-3', |
|||
current.containerClass, |
|||
current.borderClass, |
|||
)} |
|||
> |
|||
<div className="mt-0.5 shrink-0"> |
|||
<Ic name={current.icon} className={cn('size-4.5', current.iconClass)} /> |
|||
</div> |
|||
|
|||
<div className="flex-1 min-w-0"> |
|||
{toast.title && ( |
|||
<h5 className="text-xs font-bold leading-tight text-foreground mb-0.5">{toast.title}</h5> |
|||
)} |
|||
<p className="text-xs font-medium leading-relaxed text-muted-foreground">{toast.message}</p> |
|||
</div> |
|||
|
|||
<button |
|||
onClick={onClose} |
|||
aria-label="بستن اعلان" |
|||
className="shrink-0 p-1 -m-1 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors" |
|||
> |
|||
<Ic name="x" className="size-3.5" /> |
|||
</button> |
|||
</div> |
|||
) |
|||
} |
|||
823
src/features/billing/components/BillingDetailModal.tsx
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2482
src/features/tours/components/TourDetailDialog.tsx
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,255 @@ |
|||
import React from 'react' |
|||
import { ShimmerBox, ShimmerCircle, ShimmerLine } from '@/components/shared/shimmer_frontend' |
|||
|
|||
/** |
|||
* Shimmer اسکلتون استاندارد برای تب «مشخصات و متون» در پنجره جزئیات تور |
|||
* منطبق با توکنهای AppShimmer و معماری Slate خنثی |
|||
*/ |
|||
export function TourDetailOverviewShimmer() { |
|||
return ( |
|||
<div className="space-y-6"> |
|||
{/* بخش ۱: ترجمه پویا برای عنوان و توضیحات تور */} |
|||
<div className="rounded-2xl border border-border-soft bg-card/40 p-4 space-y-3"> |
|||
<div className="flex items-center justify-between pb-1 border-b border-border-soft/60"> |
|||
<div className="flex items-center gap-2"> |
|||
<ShimmerCircle size={18} /> |
|||
<ShimmerLine width={220} height={14} rounded="sm" /> |
|||
</div> |
|||
<ShimmerLine width={80} height={12} rounded="sm" /> |
|||
</div> |
|||
|
|||
<div className="space-y-3 pt-1"> |
|||
<div className="space-y-1.5"> |
|||
<ShimmerLine width={90} height={12} rounded="sm" /> |
|||
<ShimmerBox height={38} rounded="xl" className="w-full" /> |
|||
</div> |
|||
|
|||
<div className="space-y-1.5"> |
|||
<ShimmerLine width={120} height={12} rounded="sm" /> |
|||
<ShimmerBox height={96} rounded="xl" className="w-full" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* بخش ۲: مشخصات عمومی و زمانبندی تور */} |
|||
<div className="rounded-2xl border border-border-soft bg-card/40 p-4 space-y-4"> |
|||
<div className="flex items-center gap-2 pb-2 border-b border-border-soft"> |
|||
<ShimmerCircle size={18} /> |
|||
<ShimmerLine width={180} height={14} rounded="sm" /> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> |
|||
{Array.from({ length: 6 }).map((_, i) => ( |
|||
<div key={i} className="space-y-1.5"> |
|||
<ShimmerLine width={70} height={12} rounded="sm" /> |
|||
<ShimmerBox height={38} rounded="xl" className="w-full" /> |
|||
</div> |
|||
))} |
|||
</div> |
|||
</div> |
|||
|
|||
{/* بخش ۳: جدول ۳ ردیف نرخهای مصوب و تخفیف */} |
|||
<div className="rounded-2xl border border-border-soft bg-card/40 p-4 space-y-4"> |
|||
<div className="flex items-center justify-between pb-2 border-b border-border-soft"> |
|||
<div className="flex items-center gap-2"> |
|||
<ShimmerCircle size={18} /> |
|||
<ShimmerLine width={200} height={14} rounded="sm" /> |
|||
</div> |
|||
<ShimmerBox width={80} height={22} rounded="md" /> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3"> |
|||
{Array.from({ length: 4 }).map((_, i) => ( |
|||
<div key={i} className="rounded-xl border border-border-soft bg-surface-base/50 p-3 space-y-2"> |
|||
<ShimmerLine width={110} height={12} rounded="sm" /> |
|||
<ShimmerBox height={34} rounded="xl" className="w-full" /> |
|||
<ShimmerLine width={130} height={10} rounded="sm" /> |
|||
</div> |
|||
))} |
|||
</div> |
|||
</div> |
|||
|
|||
{/* بخش ۴: وضعیت تکمیل ظرفیت */} |
|||
<div className="rounded-xl border border-border-soft bg-card/20 p-4 space-y-2.5"> |
|||
<div className="flex justify-between"> |
|||
<ShimmerLine width={120} height={14} rounded="sm" /> |
|||
<ShimmerLine width={40} height={14} rounded="sm" /> |
|||
</div> |
|||
<ShimmerBox height={8} rounded="full" className="w-full" /> |
|||
<div className="flex justify-between"> |
|||
<ShimmerLine width={140} height={12} rounded="sm" /> |
|||
<ShimmerLine width={160} height={12} rounded="sm" /> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* بخش ۵: گالری تصاویر تور */} |
|||
<div className="space-y-2"> |
|||
<ShimmerLine width={100} height={12} rounded="sm" /> |
|||
<div className="flex gap-2.5 overflow-x-auto pb-2 no-scrollbar"> |
|||
{Array.from({ length: 4 }).map((_, i) => ( |
|||
<ShimmerBox key={i} width={96} height={96} rounded="xl" /> |
|||
))} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* Shimmer اسکلتون استاندارد برای تب «برنامه سفر» |
|||
*/ |
|||
export function TourItineraryShimmer() { |
|||
return ( |
|||
<div className="space-y-4"> |
|||
{/* نوار بالایی هدر زبان */} |
|||
<div className="flex items-center justify-between bg-surface-base p-3 rounded-xl border border-border-soft"> |
|||
<div className="flex items-center gap-2"> |
|||
<ShimmerCircle size={18} /> |
|||
<ShimmerLine width={200} height={14} rounded="sm" /> |
|||
</div> |
|||
<div className="flex items-center gap-3"> |
|||
<ShimmerLine width={80} height={12} rounded="sm" /> |
|||
<ShimmerBox width={120} height={28} rounded="xl" /> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* ۳ ردیف مراحل روزانه سفر */} |
|||
<div className="space-y-4"> |
|||
{Array.from({ length: 3 }).map((_, i) => ( |
|||
<div key={i} className="rounded-2xl border border-border-soft bg-card/40 p-4 space-y-3"> |
|||
<div className="flex items-center justify-between"> |
|||
<div className="flex items-center gap-2"> |
|||
<ShimmerCircle size={24} /> |
|||
<ShimmerLine width={130} height={14} rounded="sm" /> |
|||
</div> |
|||
<ShimmerBox width={75} height={28} rounded="xl" /> |
|||
</div> |
|||
<div className="space-y-2"> |
|||
<ShimmerBox height={36} rounded="xl" className="w-full" /> |
|||
<ShimmerBox height={56} rounded="xl" className="w-full" /> |
|||
</div> |
|||
</div> |
|||
))} |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* Shimmer اسکلتون استاندارد برای تب «امکانات و نکات» |
|||
*/ |
|||
export function TourFeaturesShimmer() { |
|||
return ( |
|||
<div className="space-y-6"> |
|||
<div className="flex items-center justify-between bg-surface-base p-3 rounded-xl border border-border-soft"> |
|||
<div className="flex items-center gap-2"> |
|||
<ShimmerCircle size={18} /> |
|||
<ShimmerLine width={200} height={14} rounded="sm" /> |
|||
</div> |
|||
<ShimmerBox width={120} height={28} rounded="xl" /> |
|||
</div> |
|||
|
|||
{/* امکانات */} |
|||
<div className="rounded-2xl border border-border-soft bg-card/40 p-4 space-y-3"> |
|||
<ShimmerLine width={150} height={14} rounded="sm" /> |
|||
<div className="flex flex-wrap gap-2"> |
|||
{Array.from({ length: 6 }).map((_, i) => ( |
|||
<ShimmerBox key={i} width={85 + (i % 3) * 15} height={28} rounded="xl" /> |
|||
))} |
|||
</div> |
|||
<div className="flex items-center gap-2 pt-2"> |
|||
<ShimmerBox height={36} rounded="xl" className="flex-1" /> |
|||
<ShimmerBox width={75} height={36} rounded="xl" /> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* نکات راهنما */} |
|||
<div className="rounded-2xl border border-border-soft bg-card/40 p-4 space-y-3"> |
|||
<ShimmerLine width={150} height={14} rounded="sm" /> |
|||
<div className="space-y-2"> |
|||
{Array.from({ length: 2 }).map((_, i) => ( |
|||
<div key={i} className="rounded-xl border border-border-soft bg-surface-base/50 p-3 space-y-1.5"> |
|||
<ShimmerLine width={130} height={14} rounded="sm" /> |
|||
<ShimmerLine width="90%" height={12} rounded="sm" /> |
|||
</div> |
|||
))} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* Shimmer اسکلتون استاندارد برای تب «مسافران» |
|||
*/ |
|||
export function TourPassengersShimmer() { |
|||
return ( |
|||
<div className="space-y-3"> |
|||
{Array.from({ length: 2 }).map((_, i) => ( |
|||
<div key={i} className="rounded-2xl border border-border-soft bg-card/40 p-4 space-y-3"> |
|||
<div className="flex items-center justify-between border-b border-border-soft pb-2"> |
|||
<ShimmerLine width={140} height={14} rounded="sm" /> |
|||
<ShimmerLine width={60} height={12} rounded="sm" /> |
|||
</div> |
|||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5"> |
|||
{Array.from({ length: 2 }).map((_, j) => ( |
|||
<div key={j} className="rounded-xl border border-border-soft bg-surface-base/50 p-3 space-y-2"> |
|||
<ShimmerLine width={120} height={14} rounded="sm" /> |
|||
<div className="flex justify-between"> |
|||
<ShimmerLine width={90} height={10} rounded="sm" /> |
|||
<ShimmerLine width={70} height={10} rounded="sm" /> |
|||
</div> |
|||
</div> |
|||
))} |
|||
</div> |
|||
</div> |
|||
))} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* Shimmer اسکلتون استاندارد برای تب «نظرات» |
|||
*/ |
|||
export function TourCommentsShimmer() { |
|||
return ( |
|||
<div className="space-y-3"> |
|||
{Array.from({ length: 3 }).map((_, i) => ( |
|||
<div key={i} className="rounded-2xl border border-border-soft bg-card/40 p-4 space-y-2"> |
|||
<div className="flex items-center justify-between"> |
|||
<div className="flex items-center gap-2"> |
|||
<ShimmerCircle size={24} /> |
|||
<ShimmerLine width={100} height={14} rounded="sm" /> |
|||
</div> |
|||
<ShimmerLine width={40} height={12} rounded="sm" /> |
|||
</div> |
|||
<ShimmerLine width="95%" height={12} rounded="sm" /> |
|||
<ShimmerLine width="60%" height={12} rounded="sm" /> |
|||
</div> |
|||
))} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
interface TourDetailModalShimmerProps { |
|||
activeTab?: 'overview' | 'itinerary' | 'features' | 'passengers' | 'comments' |
|||
} |
|||
|
|||
/** |
|||
* رندرر هوشمند و جامع Shimmer متناسب با تب فعال در پنجره جزئیات تور |
|||
*/ |
|||
export function TourDetailModalShimmer({ activeTab = 'overview' }: TourDetailModalShimmerProps) { |
|||
switch (activeTab) { |
|||
case 'itinerary': |
|||
return <TourItineraryShimmer /> |
|||
case 'features': |
|||
return <TourFeaturesShimmer /> |
|||
case 'passengers': |
|||
return <TourPassengersShimmer /> |
|||
case 'comments': |
|||
return <TourCommentsShimmer /> |
|||
case 'overview': |
|||
default: |
|||
return <TourDetailOverviewShimmer /> |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
import { create } from 'zustand' |
|||
|
|||
export type ToastType = 'success' | 'error' | 'info' | 'warning' |
|||
|
|||
export interface ToastItem { |
|||
id: string |
|||
type: ToastType |
|||
title?: string |
|||
message: string |
|||
duration?: number |
|||
} |
|||
|
|||
interface ToastStore { |
|||
toasts: ToastItem[] |
|||
addToast: (toast: Omit<ToastItem, 'id'>) => string |
|||
removeToast: (id: string) => void |
|||
clearToasts: () => void |
|||
} |
|||
|
|||
export const useToastStore = create<ToastStore>((set) => ({ |
|||
toasts: [], |
|||
addToast: (toast) => { |
|||
const id = Math.random().toString(36).substring(2, 9) |
|||
const newToast: ToastItem = { ...toast, id } |
|||
|
|||
set((state) => ({ |
|||
toasts: [newToast, ...state.toasts.slice(0, 4)], // Keep at most 5 toasts
|
|||
})) |
|||
|
|||
const duration = toast.duration ?? 4000 |
|||
if (duration > 0) { |
|||
setTimeout(() => { |
|||
set((state) => ({ |
|||
toasts: state.toasts.filter((t) => t.id !== id), |
|||
})) |
|||
}, duration) |
|||
} |
|||
|
|||
return id |
|||
}, |
|||
removeToast: (id) => { |
|||
set((state) => ({ |
|||
toasts: state.toasts.filter((t) => t.id !== id), |
|||
})) |
|||
}, |
|||
clearToasts: () => set({ toasts: [] }), |
|||
})) |
|||
|
|||
/** |
|||
* Convenient standalone toast helper callable anywhere (components, hooks, async handlers). |
|||
*/ |
|||
export const toast = { |
|||
success: (message: string, title?: string, duration?: number) => |
|||
useToastStore.getState().addToast({ type: 'success', message, title, duration }), |
|||
error: (message: string, title?: string, duration?: number) => |
|||
useToastStore.getState().addToast({ type: 'error', message, title, duration }), |
|||
info: (message: string, title?: string, duration?: number) => |
|||
useToastStore.getState().addToast({ type: 'info', message, title, duration }), |
|||
warning: (message: string, title?: string, duration?: number) => |
|||
useToastStore.getState().addToast({ type: 'warning', message, title, duration }), |
|||
remove: (id: string) => useToastStore.getState().removeToast(id), |
|||
clear: () => useToastStore.getState().clearToasts(), |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue