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