Browse Source
feat: add comprehensive billing page and supporting UI components, services, and localizations
master
feat: add comprehensive billing page and supporting UI components, services, and localizations
master
35 changed files with 1703 additions and 553 deletions
-
8scripts/generate-locales.js
-
8src/app/providers.tsx
-
4src/components/layout/PanelSlot.tsx
-
163src/components/shared/ServiceTranslation/ServiceTranslation.tsx
-
2src/components/shared/ServiceTranslation/index.ts
-
74src/components/shared/ServiceTranslation/types.ts
-
2src/components/ui/card.tsx
-
11src/components/ui/dialog.tsx
-
162src/components/ui/sidebar.tsx
-
2src/components/ui/table.tsx
-
3src/features/billing/BillingPage.tsx
-
155src/features/billing/components/BillingDetailModal.tsx
-
9src/features/billing/components/BillingStatusBadge.tsx
-
11src/features/billing/services/billing-api.ts
-
1src/features/billing/types.ts
-
2src/features/services/ServicesPage.tsx
-
157src/features/services/components/CategoriesServiceView.tsx
-
50src/features/services/components/CreateLocationDialog.tsx
-
10src/features/services/components/DuasServiceView.tsx
-
91src/features/services/components/FaqServiceView.tsx
-
2src/features/services/components/ShrinesServiceView.tsx
-
66src/features/services/components/SimcardFormDialog.tsx
-
808src/features/services/components/StoreServiceView.tsx
-
81src/features/services/services/store-api.ts
-
21src/features/services/types/store.ts
-
2src/features/tours/TourOrdersPage.tsx
-
53src/features/tours/components/CreateTourDialog.tsx
-
88src/features/tours/components/TourDetailDialog.tsx
-
50src/features/tours/components/TourTranslationsDialog.tsx
-
7src/features/users/components/UserFactorsTab.tsx
-
2src/features/users/types.ts
-
11src/index.css
-
12src/locales/ar.json
-
12src/locales/en.json
-
12src/locales/fa.json
@ -0,0 +1,163 @@ |
|||||
|
import React from 'react' |
||||
|
import { Ic } from '@/icons' |
||||
|
import { |
||||
|
SERVICE_LANGUAGES, |
||||
|
type ServiceLanguageCode, |
||||
|
type ServiceLanguageMeta, |
||||
|
type ServiceTranslationProps, |
||||
|
} from './types' |
||||
|
|
||||
|
export function ServiceTranslation({ |
||||
|
activeLang, |
||||
|
onLangChange, |
||||
|
completedLangs, |
||||
|
dirtyLangs, |
||||
|
primaryLang, |
||||
|
title = 'زبان ترجمه و محتوا', |
||||
|
showDirectionBadge = true, |
||||
|
showGlobeIcon = true, |
||||
|
required = false, |
||||
|
className = '', |
||||
|
variant = 'grid', |
||||
|
children, |
||||
|
}: ServiceTranslationProps) { |
||||
|
const currentLang = |
||||
|
SERVICE_LANGUAGES.find((l) => l.code === activeLang) || SERVICE_LANGUAGES[0] |
||||
|
|
||||
|
const isCompleted = (code: ServiceLanguageCode): boolean => { |
||||
|
if (!completedLangs) return false |
||||
|
if (Array.isArray(completedLangs)) { |
||||
|
return completedLangs.includes(code) |
||||
|
} |
||||
|
return Boolean(completedLangs[code]) |
||||
|
} |
||||
|
|
||||
|
const isDirty = (code: ServiceLanguageCode): boolean => { |
||||
|
if (!dirtyLangs) return false |
||||
|
if (Array.isArray(dirtyLangs)) { |
||||
|
return dirtyLangs.includes(code) |
||||
|
} |
||||
|
return Boolean(dirtyLangs[code]) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className={`space-y-3 ${className}`}> |
||||
|
{/* هدر انتخاب زبان و نمایش دایرکشن */} |
||||
|
<div className="flex items-center justify-between flex-wrap gap-2"> |
||||
|
<label className="text-xs font-bold text-foreground flex items-center gap-1.5"> |
||||
|
{showGlobeIcon && <Ic name="globe" className="size-3.5 text-primary" />} |
||||
|
<span>{title}</span> |
||||
|
{required && <span className="text-rose-500">*</span>} |
||||
|
</label> |
||||
|
|
||||
|
{showDirectionBadge && ( |
||||
|
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground font-mono"> |
||||
|
<span>جهت متن:</span> |
||||
|
<span |
||||
|
className={`px-1.5 py-0.5 rounded-md font-bold text-[10px] uppercase border ${ |
||||
|
currentLang.dir === 'rtl' |
||||
|
? 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20' |
||||
|
: 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20' |
||||
|
}`}
|
||||
|
> |
||||
|
{currentLang.dir.toUpperCase()} |
||||
|
</span> |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
|
||||
|
{/* تبهای انتخاب زبان */} |
||||
|
{variant === 'pills' ? ( |
||||
|
<div className="flex items-center gap-1.5 overflow-x-auto no-scrollbar p-1 rounded-xl bg-surface-base border border-border-soft"> |
||||
|
{SERVICE_LANGUAGES.map((lang) => { |
||||
|
const isSelected = activeLang === lang.code |
||||
|
const hasData = isCompleted(lang.code) |
||||
|
const dirty = isDirty(lang.code) |
||||
|
const isBase = primaryLang ? primaryLang === lang.code : lang.code === 'ar' |
||||
|
|
||||
|
return ( |
||||
|
<button |
||||
|
key={lang.code} |
||||
|
type="button" |
||||
|
onClick={() => onLangChange(lang.code)} |
||||
|
className={`relative px-3 py-1.5 text-xs rounded-lg transition-colors font-medium flex items-center gap-1.5 shrink-0 cursor-pointer ${ |
||||
|
isSelected |
||||
|
? 'bg-primary text-white shadow-xs font-bold' |
||||
|
: hasData |
||||
|
? 'bg-card text-foreground hover:bg-card/80 border border-border-soft' |
||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-card/50' |
||||
|
}`}
|
||||
|
> |
||||
|
<span className="text-sm leading-none">{lang.flag}</span> |
||||
|
<span>{lang.name}</span> |
||||
|
{isBase && !isSelected && ( |
||||
|
<span className="text-[9px] px-1 rounded bg-muted text-muted-foreground font-normal"> |
||||
|
پیشفرض |
||||
|
</span> |
||||
|
)} |
||||
|
{hasData && ( |
||||
|
<span |
||||
|
className={`size-1.5 rounded-full shrink-0 ${ |
||||
|
isSelected ? 'bg-white' : 'bg-emerald-500' |
||||
|
}`}
|
||||
|
/> |
||||
|
)} |
||||
|
{dirty && ( |
||||
|
<span className="size-1.5 rounded-full bg-amber-400 animate-pulse shrink-0" /> |
||||
|
)} |
||||
|
</button> |
||||
|
) |
||||
|
})} |
||||
|
</div> |
||||
|
) : ( |
||||
|
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2 p-1.5 rounded-2xl bg-surface-base/80 border border-border-soft"> |
||||
|
{SERVICE_LANGUAGES.map((lang) => { |
||||
|
const isSelected = activeLang === lang.code |
||||
|
const hasData = isCompleted(lang.code) |
||||
|
const dirty = isDirty(lang.code) |
||||
|
const isBase = primaryLang ? primaryLang === lang.code : lang.code === 'ar' |
||||
|
|
||||
|
return ( |
||||
|
<button |
||||
|
key={lang.code} |
||||
|
type="button" |
||||
|
onClick={() => onLangChange(lang.code)} |
||||
|
className={`h-11 px-3 rounded-xl text-xs font-bold transition flex items-center justify-center gap-2 relative cursor-pointer ${ |
||||
|
isSelected |
||||
|
? 'bg-primary text-primary-foreground shadow-xs' |
||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-card border border-transparent hover:border-border-soft' |
||||
|
}`}
|
||||
|
> |
||||
|
<span className="text-base">{lang.flag}</span> |
||||
|
<span className="truncate">{lang.name}</span> |
||||
|
{isBase && ( |
||||
|
<span |
||||
|
className={`text-[9px] px-1 py-0.2 rounded font-normal shrink-0 ${ |
||||
|
isSelected ? 'bg-white/20 text-white' : 'bg-muted text-muted-foreground' |
||||
|
}`}
|
||||
|
> |
||||
|
پایه |
||||
|
</span> |
||||
|
)} |
||||
|
{hasData && ( |
||||
|
<span |
||||
|
className={`size-2 rounded-full shrink-0 ${ |
||||
|
isSelected ? 'bg-white' : 'bg-emerald-500' |
||||
|
}`}
|
||||
|
title="دارای محتوای ترجمهشده" |
||||
|
/> |
||||
|
)} |
||||
|
{dirty && ( |
||||
|
<span className="size-2 rounded-full bg-amber-400 animate-pulse shrink-0" /> |
||||
|
)} |
||||
|
</button> |
||||
|
) |
||||
|
})} |
||||
|
</div> |
||||
|
)} |
||||
|
|
||||
|
{/* اگر کامپوننت حاوی فرزند بود، اینجا قرار میگیرد */} |
||||
|
{children} |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
export * from './types' |
||||
|
export * from './ServiceTranslation' |
||||
@ -0,0 +1,74 @@ |
|||||
|
export type ServiceLanguageCode = 'ar' | 'fa' | 'en' | 'ru' | 'id' |
||||
|
|
||||
|
export interface ServiceLanguageMeta { |
||||
|
code: ServiceLanguageCode |
||||
|
name: string |
||||
|
nativeName: string |
||||
|
flag: string |
||||
|
badge: string |
||||
|
dir: 'rtl' | 'ltr' |
||||
|
label: string |
||||
|
} |
||||
|
|
||||
|
export const SERVICE_LANGUAGES: readonly ServiceLanguageMeta[] = [ |
||||
|
{ |
||||
|
code: 'ar', |
||||
|
name: 'العربية', |
||||
|
nativeName: 'العربية', |
||||
|
flag: '🇸🇦', |
||||
|
badge: 'AR', |
||||
|
dir: 'rtl', |
||||
|
label: 'العربية (پیشفرض)', |
||||
|
}, |
||||
|
{ |
||||
|
code: 'fa', |
||||
|
name: 'فارسی', |
||||
|
nativeName: 'فارسی', |
||||
|
flag: '🇮🇷', |
||||
|
badge: 'FA', |
||||
|
dir: 'rtl', |
||||
|
label: 'فارسی', |
||||
|
}, |
||||
|
{ |
||||
|
code: 'en', |
||||
|
name: 'English', |
||||
|
nativeName: 'English', |
||||
|
flag: '🇬🇧', |
||||
|
badge: 'EN', |
||||
|
dir: 'ltr', |
||||
|
label: 'انگلیسی', |
||||
|
}, |
||||
|
{ |
||||
|
code: 'ru', |
||||
|
name: 'Русский', |
||||
|
nativeName: 'Русский', |
||||
|
flag: '🇷🇺', |
||||
|
badge: 'RU', |
||||
|
dir: 'ltr', |
||||
|
label: 'روسی', |
||||
|
}, |
||||
|
{ |
||||
|
code: 'id', |
||||
|
name: 'Bahasa Indonesia', |
||||
|
nativeName: 'Indonesia', |
||||
|
flag: '🇮🇩', |
||||
|
badge: 'ID', |
||||
|
dir: 'ltr', |
||||
|
label: 'اندونزیایی', |
||||
|
}, |
||||
|
] as const |
||||
|
|
||||
|
export interface ServiceTranslationProps { |
||||
|
activeLang: ServiceLanguageCode |
||||
|
onLangChange: (lang: ServiceLanguageCode) => void |
||||
|
completedLangs?: Partial<Record<ServiceLanguageCode, boolean>> | ServiceLanguageCode[] |
||||
|
dirtyLangs?: Partial<Record<ServiceLanguageCode, boolean>> | ServiceLanguageCode[] |
||||
|
primaryLang?: ServiceLanguageCode |
||||
|
title?: string |
||||
|
showDirectionBadge?: boolean |
||||
|
showGlobeIcon?: boolean |
||||
|
required?: boolean |
||||
|
className?: string |
||||
|
variant?: 'grid' | 'pills' |
||||
|
children?: React.ReactNode |
||||
|
} |
||||
808
src/features/services/components/StoreServiceView.tsx
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue