15 changed files with 1382 additions and 2445 deletions
-
7src/features/custom-trip/CustomTripDestinationDetailPage.tsx
-
27src/features/custom-trip/components/CustomTripDestinationsTab.tsx
-
65src/features/custom-trip/components/CustomTripOrderReviewDialog.tsx
-
14src/features/custom-trip/components/CustomTripOrdersTab.tsx
-
2777src/features/custom-trip/services/custom-trip-api.ts
-
11src/features/custom-trip/types.ts
-
32src/features/settings/SettingsPage.tsx
-
714src/features/settings/components/SocialMediaSettingsTab.tsx
-
25src/features/settings/services/settings-api.ts
-
34src/features/settings/types.ts
-
7src/icons.tsx
-
35src/locales/ar.json
-
35src/locales/en.json
-
35src/locales/fa.json
-
9src/services/http.ts
2777
src/features/custom-trip/services/custom-trip-api.ts
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,714 @@ |
|||||
|
import * as React from 'react' |
||||
|
import { useTranslation } from '@/i18n' |
||||
|
import { Card, CardContent } from '@/components/ui/card' |
||||
|
import { Badge } from '@/components/ui/badge' |
||||
|
import { Button } from '@/components/ui/button' |
||||
|
import { Switch } from '@/components/ui/switch' |
||||
|
import { |
||||
|
Dialog, |
||||
|
DialogContent, |
||||
|
DialogHeader, |
||||
|
DialogTitle, |
||||
|
DialogFooter, |
||||
|
} from '@/components/ui/dialog' |
||||
|
import { Ic, type IconName } from '@/icons' |
||||
|
import { toast } from '@/stores/toast' |
||||
|
import { |
||||
|
fetchSocialMediaSettings, |
||||
|
saveSocialMediaSettings, |
||||
|
} from '../services/settings-api' |
||||
|
import type { |
||||
|
SocialMediaItem, |
||||
|
SocialMediaContactUs, |
||||
|
} from '../types' |
||||
|
|
||||
|
interface PlatformMeta { |
||||
|
key: string |
||||
|
name: string |
||||
|
nameEn: string |
||||
|
icon: IconName |
||||
|
brandColor: string |
||||
|
defaultUrl: string |
||||
|
placeholder: string |
||||
|
} |
||||
|
|
||||
|
const PRESET_PLATFORMS: Record<string, PlatformMeta> = { |
||||
|
instagram: { |
||||
|
key: 'instagram', |
||||
|
name: 'اینستاگرام', |
||||
|
nameEn: 'Instagram', |
||||
|
icon: 'instagram', |
||||
|
brandColor: '#E1306C', |
||||
|
defaultUrl: 'https://instagram.com/', |
||||
|
placeholder: 'https://instagram.com/aqila_group', |
||||
|
}, |
||||
|
telegram: { |
||||
|
key: 'telegram', |
||||
|
name: 'تلگرام', |
||||
|
nameEn: 'Telegram', |
||||
|
icon: 'telegram', |
||||
|
brandColor: '#229ED9', |
||||
|
defaultUrl: 'https://t.me/', |
||||
|
placeholder: 'https://t.me/aqila_official', |
||||
|
}, |
||||
|
whatsapp: { |
||||
|
key: 'whatsapp', |
||||
|
name: 'واتساپ', |
||||
|
nameEn: 'WhatsApp', |
||||
|
icon: 'whatsapp', |
||||
|
brandColor: '#25D366', |
||||
|
defaultUrl: 'https://wa.me/', |
||||
|
placeholder: 'https://wa.me/989123456789', |
||||
|
}, |
||||
|
youtube: { |
||||
|
key: 'youtube', |
||||
|
name: 'یوتیوب', |
||||
|
nameEn: 'YouTube', |
||||
|
icon: 'youtube', |
||||
|
brandColor: '#FF0000', |
||||
|
defaultUrl: 'https://youtube.com/@', |
||||
|
placeholder: 'https://youtube.com/@aqila_tours', |
||||
|
}, |
||||
|
x: { |
||||
|
key: 'x', |
||||
|
name: 'ایکس / توییتر', |
||||
|
nameEn: 'X (Twitter)', |
||||
|
icon: 'twitter', |
||||
|
brandColor: '#E2E8F0', |
||||
|
defaultUrl: 'https://x.com/', |
||||
|
placeholder: 'https://x.com/aqila', |
||||
|
}, |
||||
|
linkedin: { |
||||
|
key: 'linkedin', |
||||
|
name: 'لینکدین', |
||||
|
nameEn: 'LinkedIn', |
||||
|
icon: 'linkedin', |
||||
|
brandColor: '#0A66C2', |
||||
|
defaultUrl: 'https://linkedin.com/company/', |
||||
|
placeholder: 'https://linkedin.com/company/aqila', |
||||
|
}, |
||||
|
facebook: { |
||||
|
key: 'facebook', |
||||
|
name: 'فیسبوک', |
||||
|
nameEn: 'Facebook', |
||||
|
icon: 'facebook', |
||||
|
brandColor: '#1877F2', |
||||
|
defaultUrl: 'https://facebook.com/', |
||||
|
placeholder: 'https://facebook.com/aqila', |
||||
|
}, |
||||
|
tiktok: { |
||||
|
key: 'tiktok', |
||||
|
name: 'تیکتاک', |
||||
|
nameEn: 'TikTok', |
||||
|
icon: 'tiktok', |
||||
|
brandColor: '#F43F5E', |
||||
|
defaultUrl: 'https://tiktok.com/@', |
||||
|
placeholder: 'https://tiktok.com/@aqila', |
||||
|
}, |
||||
|
aparat: { |
||||
|
key: 'aparat', |
||||
|
name: 'آپارات', |
||||
|
nameEn: 'Aparat', |
||||
|
icon: 'video', |
||||
|
brandColor: '#EA1D5D', |
||||
|
defaultUrl: 'https://aparat.com/', |
||||
|
placeholder: 'https://aparat.com/aqila', |
||||
|
}, |
||||
|
bale: { |
||||
|
key: 'bale', |
||||
|
name: 'پیامرسان بله', |
||||
|
nameEn: 'Bale', |
||||
|
icon: 'message', |
||||
|
brandColor: '#10B981', |
||||
|
defaultUrl: 'https://ble.ir/', |
||||
|
placeholder: 'https://ble.ir/aqila_group', |
||||
|
}, |
||||
|
eitaa: { |
||||
|
key: 'eitaa', |
||||
|
name: 'پیامرسان ایتا', |
||||
|
nameEn: 'Eitaa', |
||||
|
icon: 'message', |
||||
|
brandColor: '#F97316', |
||||
|
defaultUrl: 'https://eitaa.com/', |
||||
|
placeholder: 'https://eitaa.com/aqila_official', |
||||
|
}, |
||||
|
threads: { |
||||
|
key: 'threads', |
||||
|
name: 'تردز', |
||||
|
nameEn: 'Threads', |
||||
|
icon: 'share2', |
||||
|
brandColor: '#F1F5F9', |
||||
|
defaultUrl: 'https://threads.net/@', |
||||
|
placeholder: 'https://threads.net/@aqila', |
||||
|
}, |
||||
|
website: { |
||||
|
key: 'website', |
||||
|
name: 'وبسایت رسمی', |
||||
|
nameEn: 'Official Website', |
||||
|
icon: 'globe', |
||||
|
brandColor: '#38BDF8', |
||||
|
defaultUrl: 'https://', |
||||
|
placeholder: 'https://aqila.ir', |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
export function SocialMediaSettingsTab() { |
||||
|
const { t } = useTranslation() |
||||
|
|
||||
|
const [loading, setLoading] = React.useState(true) |
||||
|
const [saving, setSaving] = React.useState(false) |
||||
|
const [items, setItems] = React.useState<SocialMediaItem[]>([]) |
||||
|
const [contactus, setContactus] = React.useState<SocialMediaContactUs>({ |
||||
|
phone_number: '', |
||||
|
whatsapp_number: '', |
||||
|
address: '', |
||||
|
}) |
||||
|
|
||||
|
// Modal State for adding new platform
|
||||
|
const [addModalOpen, setAddModalOpen] = React.useState(false) |
||||
|
const [selectedPlatformKey, setSelectedPlatformKey] = React.useState('telegram') |
||||
|
const [newChannelName, setNewChannelName] = React.useState('') |
||||
|
const [newChannelLink, setNewChannelLink] = React.useState('') |
||||
|
|
||||
|
const loadData = React.useCallback(async () => { |
||||
|
setLoading(true) |
||||
|
try { |
||||
|
const res = await fetchSocialMediaSettings() |
||||
|
setItems(res.items || []) |
||||
|
setContactus(res.contactus || {}) |
||||
|
} catch (err: any) { |
||||
|
console.error('Error fetching social media settings:', err) |
||||
|
toast.error(err?.message || t('settings.socialFetchError')) |
||||
|
} finally { |
||||
|
setLoading(false) |
||||
|
} |
||||
|
}, [t]) |
||||
|
|
||||
|
React.useEffect(() => { |
||||
|
loadData() |
||||
|
}, [loadData]) |
||||
|
|
||||
|
const handleOpenAddModal = (platformKey?: string) => { |
||||
|
const pKey = platformKey || 'telegram' |
||||
|
const preset = PRESET_PLATFORMS[pKey] || PRESET_PLATFORMS.telegram |
||||
|
setSelectedPlatformKey(pKey) |
||||
|
setNewChannelName(preset.name) |
||||
|
setNewChannelLink(preset.defaultUrl) |
||||
|
setAddModalOpen(true) |
||||
|
} |
||||
|
|
||||
|
const handleSelectPreset = (key: string) => { |
||||
|
const preset = PRESET_PLATFORMS[key] |
||||
|
setSelectedPlatformKey(key) |
||||
|
if (preset) { |
||||
|
setNewChannelName(preset.name) |
||||
|
setNewChannelLink(preset.defaultUrl) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const handleAddChannel = () => { |
||||
|
if (!newChannelLink.trim()) { |
||||
|
toast.error('لطفاً آدرس پیوند (URL) را وارد نمایید.') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const newItem: SocialMediaItem = { |
||||
|
id: `social-${selectedPlatformKey}-${Date.now()}`, |
||||
|
platform: selectedPlatformKey, |
||||
|
name: newChannelName.trim() || selectedPlatformKey, |
||||
|
link: newChannelLink.trim(), |
||||
|
is_active: true, |
||||
|
} |
||||
|
|
||||
|
setItems((prev) => [...prev, newItem]) |
||||
|
setAddModalOpen(false) |
||||
|
toast.success(t('settings.channelAdded')) |
||||
|
} |
||||
|
|
||||
|
const handleItemChange = (index: number, field: keyof SocialMediaItem, value: any) => { |
||||
|
setItems((prev) => { |
||||
|
const next = [...prev] |
||||
|
next[index] = { ...next[index], [field]: value } |
||||
|
return next |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const handleDeleteItem = (index: number) => { |
||||
|
setItems((prev) => prev.filter((_, idx) => idx !== index)) |
||||
|
} |
||||
|
|
||||
|
const handleCopyLink = (link: string) => { |
||||
|
if (!link) return |
||||
|
navigator.clipboard.writeText(link) |
||||
|
toast.success(t('settings.linkCopied')) |
||||
|
} |
||||
|
|
||||
|
const handleSaveToDatabase = async () => { |
||||
|
setSaving(true) |
||||
|
try { |
||||
|
const res = await saveSocialMediaSettings({ |
||||
|
items, |
||||
|
contactus, |
||||
|
}) |
||||
|
if (res.items) { |
||||
|
setItems(res.items) |
||||
|
} |
||||
|
if (res.contactus) { |
||||
|
setContactus(res.contactus) |
||||
|
} |
||||
|
toast.success(res.message || t('settings.socialSaveSuccess')) |
||||
|
} catch (err: any) { |
||||
|
console.error('Error saving social media settings:', err) |
||||
|
toast.error(err?.message || t('settings.socialSaveError')) |
||||
|
} finally { |
||||
|
setSaving(false) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const activeCount = items.filter((i) => i.is_active).length |
||||
|
|
||||
|
return ( |
||||
|
<div className="space-y-6"> |
||||
|
{/* ۱. کارت سربرگ و راهنمای تنظیمات شبکههای اجتماعی (کالیبره حالت دارک) */} |
||||
|
<Card className="border border-white/[0.08] bg-[#1e2129] shadow-md"> |
||||
|
<CardContent className="p-6"> |
||||
|
<div className="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between"> |
||||
|
<div className="flex items-start gap-4"> |
||||
|
<div className="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-primary/15 text-primary border border-primary/25 shadow-inner"> |
||||
|
<Ic name="share" className="size-6" /> |
||||
|
</div> |
||||
|
<div className="space-y-1.5"> |
||||
|
<div className="flex items-center gap-2.5 flex-wrap"> |
||||
|
<h2 className="text-base sm:text-lg font-black text-white"> |
||||
|
{t('settings.socialTabTitle')} |
||||
|
</h2> |
||||
|
<Badge variant="outline" className="border-emerald-500/30 bg-emerald-500/10 text-emerald-400 font-bold text-xs"> |
||||
|
{activeCount} {t('settings.activeStatus')} |
||||
|
</Badge> |
||||
|
<Badge variant="outline" className="border-white/10 bg-white/[0.04] text-slate-300 font-mono text-[11px]"> |
||||
|
PostgreSQL Live Sync |
||||
|
</Badge> |
||||
|
</div> |
||||
|
<p className="text-xs text-slate-400 leading-relaxed max-w-2xl"> |
||||
|
{t('settings.socialTabSubtitle')} |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div className="flex items-center gap-2.5 shrink-0 flex-wrap sm:flex-nowrap"> |
||||
|
<Button |
||||
|
type="button" |
||||
|
variant="outline" |
||||
|
size="sm" |
||||
|
onClick={() => handleOpenAddModal()} |
||||
|
className="gap-2 rounded-xl text-xs font-bold border-white/15 bg-white/[0.04] hover:bg-white/[0.08] text-slate-100 hover:text-white" |
||||
|
> |
||||
|
<Ic name="plus" className="size-4 text-primary" /> |
||||
|
<span>{t('settings.addSocialBtn')}</span> |
||||
|
</Button> |
||||
|
|
||||
|
<Button |
||||
|
type="button" |
||||
|
size="sm" |
||||
|
disabled={saving || loading} |
||||
|
onClick={handleSaveToDatabase} |
||||
|
className="gap-2 rounded-xl text-xs font-black bg-gradient-to-r from-[#d09460] to-[#b37745] hover:from-[#dd9e69] hover:to-[#be804c] text-white shadow-md shadow-[#d09460]/20 transition-all active:scale-[0.98]" |
||||
|
> |
||||
|
<Ic name="save" className={`size-4 ${saving ? 'animate-spin' : ''}`} /> |
||||
|
<span>{saving ? t('settings.saving') : t('settings.saveSocialBtn')}</span> |
||||
|
</Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{/* نوار انتخاب سریع پلتفرمهای مطرح (کالیبره و منسجم در تم تیره) */} |
||||
|
<div className="mt-5 pt-4 border-t border-white/[0.07]"> |
||||
|
<div className="text-[11px] font-bold text-slate-400 mb-2.5 flex items-center gap-1.5"> |
||||
|
<Ic name="sparkles" className="size-3.5 text-primary" /> |
||||
|
<span>پلتفرمهای پیشنهادی جهت افزودن فوری:</span> |
||||
|
</div> |
||||
|
<div className="flex items-center gap-2 overflow-x-auto pb-1.5 scrollbar-none"> |
||||
|
{Object.values(PRESET_PLATFORMS).map((preset) => { |
||||
|
const isAlreadyAdded = items.some((it) => it.platform.toLowerCase() === preset.key.toLowerCase()) |
||||
|
return ( |
||||
|
<button |
||||
|
key={preset.key} |
||||
|
type="button" |
||||
|
onClick={() => handleOpenAddModal(preset.key)} |
||||
|
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-xl border text-xs font-medium shrink-0 transition-all ${ |
||||
|
isAlreadyAdded |
||||
|
? 'border-emerald-500/35 bg-emerald-500/10 text-emerald-300' |
||||
|
: 'border-white/[0.08] bg-[#16181f] hover:bg-[#252833] hover:border-white/[0.2] text-slate-200' |
||||
|
}`}
|
||||
|
> |
||||
|
<span |
||||
|
className="size-2 rounded-full shrink-0" |
||||
|
style={{ backgroundColor: preset.brandColor }} |
||||
|
/> |
||||
|
<Ic |
||||
|
name={preset.icon} |
||||
|
className="size-3.5 shrink-0" |
||||
|
style={{ color: preset.brandColor }} |
||||
|
/> |
||||
|
<span>{preset.name}</span> |
||||
|
{isAlreadyAdded ? ( |
||||
|
<span className="text-[10px] size-4 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center font-bold"> |
||||
|
✓ |
||||
|
</span> |
||||
|
) : ( |
||||
|
<span className="text-[10px] text-slate-500 group-hover:text-white">+</span> |
||||
|
)} |
||||
|
</button> |
||||
|
) |
||||
|
})} |
||||
|
</div> |
||||
|
</div> |
||||
|
</CardContent> |
||||
|
</Card> |
||||
|
|
||||
|
{/* ۲. فهرست شبکههای اجتماعی ثبتشده */} |
||||
|
{loading ? ( |
||||
|
<Card className="border border-white/[0.08] bg-[#1e2129] p-12 text-center shadow-xs"> |
||||
|
<div className="flex flex-col items-center justify-center gap-3"> |
||||
|
<span className="size-8 rounded-full border-3 border-primary/20 border-t-primary animate-spin" /> |
||||
|
<p className="text-xs text-slate-300 font-bold">در حال دریافت شبکههای اجتماعی از دیتابیس...</p> |
||||
|
</div> |
||||
|
</Card> |
||||
|
) : items.length === 0 ? ( |
||||
|
<Card className="border-dashed border-2 border-white/[0.1] bg-[#1e2129]/60 p-12 text-center shadow-xs"> |
||||
|
<div className="max-w-md mx-auto space-y-4"> |
||||
|
<div className="size-16 rounded-3xl bg-primary/10 text-primary border border-primary/20 flex items-center justify-center mx-auto shadow-inner"> |
||||
|
<Ic name="share" className="size-8" /> |
||||
|
</div> |
||||
|
<div className="space-y-1"> |
||||
|
<h3 className="text-base font-black text-white"> |
||||
|
{t('settings.noSocialChannels')} |
||||
|
</h3> |
||||
|
<p className="text-xs text-slate-400"> |
||||
|
{t('settings.noSocialChannelsDesc')} |
||||
|
</p> |
||||
|
</div> |
||||
|
<Button |
||||
|
type="button" |
||||
|
onClick={() => handleOpenAddModal('instagram')} |
||||
|
className="gap-2 rounded-xl text-xs font-bold bg-primary text-white hover:bg-primary/90" |
||||
|
> |
||||
|
<Ic name="plus" className="size-4" /> |
||||
|
<span>{t('settings.addSocialBtn')}</span> |
||||
|
</Button> |
||||
|
</div> |
||||
|
</Card> |
||||
|
) : ( |
||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
||||
|
{items.map((item, index) => { |
||||
|
const preset = PRESET_PLATFORMS[item.platform.toLowerCase()] || { |
||||
|
key: item.platform, |
||||
|
name: item.name || item.platform, |
||||
|
nameEn: item.platform, |
||||
|
icon: 'link' as IconName, |
||||
|
brandColor: '#38BDF8', |
||||
|
defaultUrl: '', |
||||
|
placeholder: 'https://...', |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Card |
||||
|
key={item.id || index} |
||||
|
className={`border transition-all duration-200 ${ |
||||
|
item.is_active |
||||
|
? 'border-white/[0.1] bg-[#1e2129] shadow-sm hover:border-white/[0.18]' |
||||
|
: 'border-white/[0.05] bg-[#171920] opacity-65' |
||||
|
}`}
|
||||
|
> |
||||
|
<CardContent className="p-4 sm:p-5 space-y-4"> |
||||
|
{/* ردیف بالا: آیکون برند، عنوان پلتفرم، بج وضعیت، سوییچ و دکمه حذف */} |
||||
|
<div className="flex items-center justify-between gap-3"> |
||||
|
<div className="flex items-center gap-3"> |
||||
|
<div |
||||
|
className="size-11 rounded-2xl flex items-center justify-center shrink-0 border border-white/[0.08] bg-[#15171e] shadow-inner" |
||||
|
style={{ color: preset.brandColor }} |
||||
|
> |
||||
|
<Ic name={preset.icon} className="size-5.5" /> |
||||
|
</div> |
||||
|
<div className="space-y-1"> |
||||
|
<div className="flex items-center gap-2"> |
||||
|
<span className="text-sm font-black text-white"> |
||||
|
{preset.name} |
||||
|
</span> |
||||
|
<span className="text-[10px] text-slate-400 uppercase font-mono px-1.5 py-0.5 rounded bg-white/[0.05] border border-white/[0.05]"> |
||||
|
{preset.nameEn || item.platform} |
||||
|
</span> |
||||
|
</div> |
||||
|
<div> |
||||
|
{item.is_active ? ( |
||||
|
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-[10.5px] font-bold bg-emerald-500/15 text-emerald-400 border border-emerald-500/25"> |
||||
|
<span className="size-1.5 rounded-full bg-emerald-400 animate-pulse" /> |
||||
|
<span>{t('settings.activeStatus')}</span> |
||||
|
</span> |
||||
|
) : ( |
||||
|
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-[10.5px] font-medium bg-slate-800 text-slate-400 border border-white/[0.06]"> |
||||
|
<span>{t('settings.inactiveStatus')}</span> |
||||
|
</span> |
||||
|
)} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div className="flex items-center gap-2.5 shrink-0"> |
||||
|
<Switch |
||||
|
checked={item.is_active} |
||||
|
onCheckedChange={(checked) => handleItemChange(index, 'is_active', checked)} |
||||
|
/> |
||||
|
<button |
||||
|
type="button" |
||||
|
onClick={() => handleDeleteItem(index)} |
||||
|
title={t('settings.deleteChannel')} |
||||
|
className="size-8.5 rounded-xl text-slate-400 hover:text-rose-400 hover:bg-rose-500/10 flex items-center justify-center transition-colors" |
||||
|
> |
||||
|
<Ic name="trash" className="size-4" /> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{/* فیلدهای ورودی عنوان نمایشی و آدرس اینترنتی (با اصلاح کامل مشکل همپوشانی) */} |
||||
|
<div className="space-y-3 pt-1"> |
||||
|
<div className="space-y-1"> |
||||
|
<label className="text-xs font-semibold text-slate-300"> |
||||
|
{t('settings.channelName')} |
||||
|
</label> |
||||
|
<input |
||||
|
type="text" |
||||
|
value={item.name} |
||||
|
onChange={(e) => handleItemChange(index, 'name', e.target.value)} |
||||
|
placeholder="مثال: کانال رسمی اطلاعرسانی عقیله" |
||||
|
className="w-full h-9.5 rounded-xl bg-[#14161c] border border-white/[0.1] px-3.5 text-xs font-medium text-slate-100 placeholder:text-slate-500 outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary/40" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-1"> |
||||
|
<label className="text-xs font-semibold text-slate-300"> |
||||
|
{t('settings.channelLink')} |
||||
|
</label> |
||||
|
{/* کانتینر LTR اختصاصی جهت جلوگیری از هرگونه تداخل آیکونها با متن */} |
||||
|
<div className="relative flex items-center" dir="ltr"> |
||||
|
<input |
||||
|
type="text" |
||||
|
dir="ltr" |
||||
|
value={item.link} |
||||
|
onChange={(e) => handleItemChange(index, 'link', e.target.value)} |
||||
|
placeholder={preset.placeholder} |
||||
|
className="w-full h-9.5 rounded-xl bg-[#14161c] border border-white/[0.1] pl-3.5 pr-20 text-xs font-mono text-slate-100 placeholder:text-slate-500 outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary/40 text-left" |
||||
|
/> |
||||
|
{/* دکمههای عملیات ثابت در سمت راست فیلد LTR */} |
||||
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1"> |
||||
|
<button |
||||
|
type="button" |
||||
|
onClick={() => handleCopyLink(item.link)} |
||||
|
title={t('settings.copyLinkTooltip')} |
||||
|
className="size-7 rounded-lg bg-white/[0.04] hover:bg-white/[0.1] border border-white/[0.06] text-slate-300 hover:text-white flex items-center justify-center transition-colors" |
||||
|
> |
||||
|
<Ic name="copy" className="size-3.5" /> |
||||
|
</button> |
||||
|
{item.link ? ( |
||||
|
<a |
||||
|
href={item.link} |
||||
|
target="_blank" |
||||
|
rel="noopener noreferrer" |
||||
|
title={t('settings.testLinkTooltip')} |
||||
|
className="size-7 rounded-lg bg-primary/10 hover:bg-primary/20 border border-primary/20 text-primary flex items-center justify-center transition-colors" |
||||
|
> |
||||
|
<Ic name="external" className="size-3.5" /> |
||||
|
</a> |
||||
|
) : null} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</CardContent> |
||||
|
</Card> |
||||
|
) |
||||
|
})} |
||||
|
</div> |
||||
|
)} |
||||
|
|
||||
|
{/* ۳. کارت اطلاعات تماس تکمیلی (Contact Us - کالیبره در تم تیره) */} |
||||
|
<Card className="border border-white/[0.08] bg-[#1e2129] shadow-md"> |
||||
|
<CardContent className="p-6 space-y-5"> |
||||
|
<div className="flex items-center gap-3.5"> |
||||
|
<div className="flex size-11 items-center justify-center rounded-2xl bg-amber-500/15 text-amber-400 border border-amber-500/25 shadow-inner"> |
||||
|
<Ic name="phone" className="size-5.5" /> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h3 className="text-sm sm:text-base font-black text-white"> |
||||
|
{t('settings.contactInfoCardTitle')} |
||||
|
</h3> |
||||
|
<p className="text-xs text-slate-400 mt-0.5"> |
||||
|
{t('settings.contactInfoCardDesc')} |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-1"> |
||||
|
{/* شمارههای پشتیبانی */} |
||||
|
<div className="space-y-1.5"> |
||||
|
<label className="text-xs font-semibold text-slate-300 flex items-center gap-1.5"> |
||||
|
<Ic name="phone" className="size-3.5 text-primary" /> |
||||
|
<span>{t('settings.phoneNumberLabel')}</span> |
||||
|
</label> |
||||
|
<div className="relative flex items-center" dir="ltr"> |
||||
|
<input |
||||
|
type="text" |
||||
|
dir="ltr" |
||||
|
value={contactus.phone_number || ''} |
||||
|
onChange={(e) => setContactus((prev) => ({ ...prev, phone_number: e.target.value }))} |
||||
|
placeholder="+982188888888 | +9647700000000" |
||||
|
className="w-full h-10 rounded-xl bg-[#14161c] border border-white/[0.1] px-3.5 text-xs font-mono text-slate-100 placeholder:text-slate-500 outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary/40 text-left" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{/* شماره اختصاصی واتساپ */} |
||||
|
<div className="space-y-1.5"> |
||||
|
<label className="text-xs font-semibold text-slate-300 flex items-center gap-1.5"> |
||||
|
<Ic name="whatsapp" className="size-3.5 text-emerald-400" /> |
||||
|
<span>{t('settings.whatsappNumberLabel')}</span> |
||||
|
</label> |
||||
|
<div className="relative flex items-center" dir="ltr"> |
||||
|
<input |
||||
|
type="text" |
||||
|
dir="ltr" |
||||
|
value={contactus.whatsapp_number || ''} |
||||
|
onChange={(e) => setContactus((prev) => ({ ...prev, whatsapp_number: e.target.value }))} |
||||
|
placeholder="+989120000000" |
||||
|
className="w-full h-10 rounded-xl bg-[#14161c] border border-white/[0.1] px-3.5 text-xs font-mono text-slate-100 placeholder:text-slate-500 outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary/40 text-left" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{/* آدرس دفتر مرکزی */} |
||||
|
<div className="space-y-1.5 md:col-span-2"> |
||||
|
<label className="text-xs font-semibold text-slate-300 flex items-center gap-1.5"> |
||||
|
<Ic name="mapPin" className="size-3.5 text-rose-400" /> |
||||
|
<span>{t('settings.addressLabel')}</span> |
||||
|
</label> |
||||
|
<textarea |
||||
|
rows={2} |
||||
|
value={contactus.address || ''} |
||||
|
onChange={(e) => setContactus((prev) => ({ ...prev, address: e.target.value }))} |
||||
|
placeholder="آدرس دقیق دفتر مرکزی عقیله جهت نمایش در بخش ارتباط با ما..." |
||||
|
className="w-full rounded-xl bg-[#14161c] border border-white/[0.1] p-3 text-xs font-medium text-slate-100 placeholder:text-slate-500 outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary/40 resize-none" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div className="flex justify-end pt-2 border-t border-white/[0.07]"> |
||||
|
<Button |
||||
|
type="button" |
||||
|
disabled={saving || loading} |
||||
|
onClick={handleSaveToDatabase} |
||||
|
className="gap-2 rounded-xl text-xs font-black bg-gradient-to-r from-[#d09460] to-[#b37745] hover:from-[#dd9e69] hover:to-[#be804c] text-white shadow-md shadow-[#d09460]/20 transition-all active:scale-[0.98]" |
||||
|
> |
||||
|
<Ic name="save" className={`size-4 ${saving ? 'animate-spin' : ''}`} /> |
||||
|
<span>{saving ? t('settings.saving') : t('settings.saveSocialBtn')}</span> |
||||
|
</Button> |
||||
|
</div> |
||||
|
</CardContent> |
||||
|
</Card> |
||||
|
|
||||
|
{/* مدال افزودن پلتفرم جدید (کالیبره تم تیره) */} |
||||
|
<Dialog open={addModalOpen} onOpenChange={setAddModalOpen}> |
||||
|
<DialogContent className="max-w-md bg-[#1e2129] border border-white/[0.12] text-white"> |
||||
|
<DialogHeader className="p-5 pb-0"> |
||||
|
<DialogTitle className="text-sm font-black text-white flex items-center gap-2"> |
||||
|
<div className="flex size-7 items-center justify-center rounded-lg bg-primary/15 text-primary"> |
||||
|
<Ic name="share" className="size-4" /> |
||||
|
</div> |
||||
|
<span>{t('settings.addSocialBtn')}</span> |
||||
|
</DialogTitle> |
||||
|
</DialogHeader> |
||||
|
|
||||
|
<div className="p-5 space-y-4"> |
||||
|
<div className="space-y-2"> |
||||
|
<label className="text-xs font-semibold text-slate-300"> |
||||
|
{t('settings.selectPlatform')} |
||||
|
</label> |
||||
|
<div className="grid grid-cols-3 sm:grid-cols-4 gap-2 max-h-52 overflow-y-auto p-1.5 border rounded-xl border-white/[0.08] bg-[#14161c] scrollbar-thin"> |
||||
|
{Object.values(PRESET_PLATFORMS).map((preset) => { |
||||
|
const isSelected = selectedPlatformKey === preset.key |
||||
|
return ( |
||||
|
<button |
||||
|
key={preset.key} |
||||
|
type="button" |
||||
|
onClick={() => handleSelectPreset(preset.key)} |
||||
|
className={`flex flex-col items-center justify-center p-2 rounded-xl border text-center transition-all ${ |
||||
|
isSelected |
||||
|
? 'border-primary bg-primary/20 text-primary shadow-sm' |
||||
|
: 'border-white/[0.06] bg-[#191b22] hover:bg-[#22252f] text-slate-300' |
||||
|
}`}
|
||||
|
> |
||||
|
<div |
||||
|
className="size-8 rounded-lg flex items-center justify-center mb-1.5 bg-white/[0.05] border border-white/[0.05]" |
||||
|
style={{ color: preset.brandColor }} |
||||
|
> |
||||
|
<Ic name={preset.icon} className="size-4.5" /> |
||||
|
</div> |
||||
|
<span className="text-[11px] font-bold truncate max-w-full"> |
||||
|
{preset.name} |
||||
|
</span> |
||||
|
</button> |
||||
|
) |
||||
|
})} |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-1.5"> |
||||
|
<label className="text-xs font-semibold text-slate-300"> |
||||
|
{t('settings.channelName')} |
||||
|
</label> |
||||
|
<input |
||||
|
type="text" |
||||
|
value={newChannelName} |
||||
|
onChange={(e) => setNewChannelName(e.target.value)} |
||||
|
placeholder="عنوان نمایشی کانال یا حساب کاربری" |
||||
|
className="w-full h-10 rounded-xl bg-[#14161c] border border-white/[0.1] px-3.5 text-xs text-slate-100 placeholder:text-slate-500 outline-none focus:border-primary" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-1.5"> |
||||
|
<label className="text-xs font-semibold text-slate-300"> |
||||
|
{t('settings.channelLink')} |
||||
|
</label> |
||||
|
<input |
||||
|
type="text" |
||||
|
dir="ltr" |
||||
|
value={newChannelLink} |
||||
|
onChange={(e) => setNewChannelLink(e.target.value)} |
||||
|
placeholder={PRESET_PLATFORMS[selectedPlatformKey]?.placeholder || 'https://...'} |
||||
|
className="w-full h-10 rounded-xl bg-[#14161c] border border-white/[0.1] px-3.5 text-xs font-mono text-slate-100 placeholder:text-slate-500 outline-none focus:border-primary text-left" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<DialogFooter className="px-5 py-4 border-t border-white/[0.08] flex items-center justify-end gap-2 bg-[#171920]"> |
||||
|
<Button |
||||
|
type="button" |
||||
|
variant="outline" |
||||
|
size="sm" |
||||
|
onClick={() => setAddModalOpen(false)} |
||||
|
className="rounded-xl text-xs border-white/10 hover:bg-white/[0.06] text-slate-300" |
||||
|
> |
||||
|
انصراف |
||||
|
</Button> |
||||
|
<Button |
||||
|
type="button" |
||||
|
size="sm" |
||||
|
onClick={handleAddChannel} |
||||
|
className="rounded-xl text-xs font-black bg-gradient-to-r from-[#d09460] to-[#b37745] hover:from-[#dd9e69] hover:to-[#be804c] text-white shadow-sm" |
||||
|
> |
||||
|
افزودن به لیست |
||||
|
</Button> |
||||
|
</DialogFooter> |
||||
|
</DialogContent> |
||||
|
</Dialog> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue