Compare commits
No commits in common. "5c13dd2f297d911c136e39bbec821b3121d1cd7e" and "7e403bd53ea48079d458f0da556d8c384525074f" have entirely different histories.
5c13dd2f29
...
7e403bd53e
@ -39,17 +39,6 @@ export class MediaController {
|
|||||||
return { success: true, data };
|
return { success: true, data };
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Delete('bulk')
|
|
||||||
@ApiOperation({ summary: 'حذف دستهجمعی فایلها' })
|
|
||||||
async deleteManyMedia(@Body('ids') ids: string[]) {
|
|
||||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
|
||||||
throw new BadRequestException('لیست شناسههای رسانه الزامی است');
|
|
||||||
}
|
|
||||||
const data = await this.mediaService.deleteManyMedia(ids);
|
|
||||||
return { success: true, data };
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@ApiOperation({ summary: 'حذف فایل' })
|
@ApiOperation({ summary: 'حذف فایل' })
|
||||||
|
|||||||
@ -62,33 +62,6 @@ export class MediaService {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteManyMedia(ids: string[]) {
|
|
||||||
const mediaItems = await this.prisma.media.findMany({
|
|
||||||
where: { id: { in: ids } },
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const media of mediaItems) {
|
|
||||||
const filePath = path.join(
|
|
||||||
process.cwd(),
|
|
||||||
'uploads',
|
|
||||||
path.basename(media.url),
|
|
||||||
);
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
} catch {
|
|
||||||
// Ignore individual unlink errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.prisma.media.deleteMany({
|
|
||||||
where: { id: { in: ids } },
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, count: mediaItems.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateMedia(
|
async updateMedia(
|
||||||
id: string,
|
id: string,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Users, ShoppingCart, Tag, Settings, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image, Video, PhoneCall, Sparkles, MessageSquareQuote, FlaskConical, Building2, Globe, DollarSign, Sliders, MessageSquare, Receipt, CreditCard } from 'lucide-react';
|
import { Users, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image, Video, PhoneCall, Sparkles, MessageSquareQuote, FlaskConical, Building2, Globe, DollarSign, Sliders, MessageSquare, Receipt, CreditCard } from 'lucide-react';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
import { useAdminAuthStore } from '../store/adminAuthStore';
|
||||||
|
|
||||||
const menuGroups = [
|
const menuGroups = [
|
||||||
{
|
{
|
||||||
@ -64,7 +65,9 @@ interface SidebarProps {
|
|||||||
|
|
||||||
export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [newOrdersCount, setNewOrdersCount] = useState(0);
|
const [newOrdersCount, setNewOrdersCount] = useState(0);
|
||||||
|
const clearAuth = useAdminAuthStore((state) => state.clearAuth);
|
||||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -85,6 +88,18 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await api.post('/auth/logout');
|
||||||
|
} catch {
|
||||||
|
// Ignore logout errors
|
||||||
|
} finally {
|
||||||
|
clearAuth();
|
||||||
|
localStorage.removeItem('adminToken');
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Mobile Backdrop */}
|
{/* Mobile Backdrop */}
|
||||||
|
|||||||
@ -29,67 +29,18 @@ export default function MediaManager() {
|
|||||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
||||||
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
|
|
||||||
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const fetchMedia = useCallback(async () => {
|
const fetchMedia = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/admin/media');
|
const res = await api.get('/admin/media');
|
||||||
setMediaList(Array.isArray(res.data) ? res.data : res.data?.data || []);
|
setMediaList(Array.isArray(res.data) ? res.data : res.data?.data || []);
|
||||||
setSelectedIds(new Set());
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch media', error);
|
console.error('Failed to fetch media', error);
|
||||||
toast.error('خطا در دریافت رسانهها');
|
toast.error('خطا در دریافت رسانهها');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleSelect = (id: string, e?: React.MouseEvent) => {
|
|
||||||
if (e) e.stopPropagation();
|
|
||||||
setSelectedIds(prev => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (next.has(id)) {
|
|
||||||
next.delete(id);
|
|
||||||
} else {
|
|
||||||
next.add(id);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectAllCurrentPage = (items: Media[]) => {
|
|
||||||
setSelectedIds(prev => {
|
|
||||||
const allSelected = items.every(m => prev.has(m.id));
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (allSelected) {
|
|
||||||
items.forEach(m => next.delete(m.id));
|
|
||||||
} else {
|
|
||||||
items.forEach(m => next.add(m.id));
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBulkDelete = async () => {
|
|
||||||
if (selectedIds.size === 0) return;
|
|
||||||
try {
|
|
||||||
setIsBulkDeleting(true);
|
|
||||||
await api.delete('/admin/media/bulk', {
|
|
||||||
data: { ids: Array.from(selectedIds) }
|
|
||||||
});
|
|
||||||
toast.success(`${selectedIds.size} تصویر با موفقیت حذف شد`);
|
|
||||||
setSelectedIds(new Set());
|
|
||||||
setIsBulkDeleteModalOpen(false);
|
|
||||||
fetchMedia();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Bulk delete failed', error);
|
|
||||||
toast.error('خطا در حذف گروهی تصاویر');
|
|
||||||
} finally {
|
|
||||||
setIsBulkDeleting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadFile = useCallback(async (file: File) => {
|
const uploadFile = useCallback(async (file: File) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
@ -290,73 +241,18 @@ export default function MediaManager() {
|
|||||||
) : (() => {
|
) : (() => {
|
||||||
const totalPages = Math.ceil(filtered.length / limit) || 1;
|
const totalPages = Math.ceil(filtered.length / limit) || 1;
|
||||||
const paginatedList = filtered.slice((page - 1) * limit, page * limit);
|
const paginatedList = filtered.slice((page - 1) * limit, page * limit);
|
||||||
const isAllPageSelected = paginatedList.length > 0 && paginatedList.every(m => selectedIds.has(m.id));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-6">
|
<div className="space-y-4 p-6">
|
||||||
{/* Multi-select Toolbar */}
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 bg-purple-50/60 p-3 rounded-2xl border border-purple-100 font-vazir">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectAllCurrentPage(paginatedList)}
|
|
||||||
className="flex items-center gap-2 px-3 py-1.5 bg-white border border-purple-200 rounded-xl text-xs font-bold text-purple-700 hover:bg-purple-100/50 transition-all cursor-pointer shadow-xs"
|
|
||||||
>
|
|
||||||
<div className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${
|
|
||||||
isAllPageSelected ? 'bg-purple-600 border-purple-600 text-white' : 'border-purple-300 bg-white'
|
|
||||||
}`}>
|
|
||||||
{isAllPageSelected && <CheckCircle2 className="w-3.5 h-3.5" />}
|
|
||||||
</div>
|
|
||||||
<span>{isAllPageSelected ? 'لغو انتخاب این صفحه' : 'انتخاب همه در این صفحه'}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{selectedIds.size > 0 && (
|
|
||||||
<span className="text-xs font-black text-purple-900 bg-purple-100 px-2.5 py-1 rounded-lg">
|
|
||||||
{selectedIds.size} مورد انتخاب شده
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedIds.size > 0 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsBulkDeleteModalOpen(true)}
|
|
||||||
className="flex items-center gap-1.5 px-4 py-1.5 bg-red-600 hover:bg-red-700 text-white text-xs font-bold rounded-xl transition-all shadow-md shadow-red-200 cursor-pointer"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
|
||||||
<span>حذف موارد انتخابشده ({selectedIds.size})</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||||
{paginatedList.map(media => {
|
{paginatedList.map(media => {
|
||||||
const imgUrl = media.url.startsWith('http') ? media.url : `${BASE_DOMAIN}${media.url}`;
|
const imgUrl = media.url.startsWith('http') ? media.url : `${BASE_DOMAIN}${media.url}`;
|
||||||
const isSelected = selectedIds.has(media.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={media.id}
|
key={media.id}
|
||||||
className={`group relative bg-gray-100 rounded-xl overflow-hidden border transition-all cursor-pointer ${
|
className="group relative bg-gray-100 rounded-xl overflow-hidden border border-gray-200 hover:border-purple-400 hover:shadow-lg hover:shadow-purple-100 transition-all cursor-pointer"
|
||||||
isSelected ? 'border-purple-600 ring-2 ring-purple-400/50 shadow-md bg-purple-50/20' : 'border-gray-200 hover:border-purple-400 hover:shadow-lg hover:shadow-purple-100'
|
|
||||||
}`}
|
|
||||||
onClick={() => setPreviewUrl(imgUrl)}
|
onClick={() => setPreviewUrl(imgUrl)}
|
||||||
>
|
>
|
||||||
<div className="aspect-square bg-white p-2 flex items-center justify-center border-b border-gray-100 relative">
|
<div className="aspect-square bg-white p-2 flex items-center justify-center border-b border-gray-100">
|
||||||
{/* Checkbox badge */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => toggleSelect(media.id, e)}
|
|
||||||
className={`absolute top-2 right-2 z-20 w-6 h-6 rounded-lg flex items-center justify-center transition-all ${
|
|
||||||
isSelected
|
|
||||||
? 'bg-purple-600 text-white shadow-md'
|
|
||||||
: 'bg-white/80 border border-gray-300 text-transparent hover:border-purple-400 opacity-80 group-hover:opacity-100'
|
|
||||||
}`}
|
|
||||||
title="انتخاب"
|
|
||||||
>
|
|
||||||
<CheckCircle2 className={`w-4 h-4 ${isSelected ? 'text-white' : 'text-gray-400'}`} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={imgUrl}
|
src={imgUrl}
|
||||||
alt={media.filename}
|
alt={media.filename}
|
||||||
@ -419,16 +315,6 @@ export default function MediaManager() {
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bulk Delete Confirm Modal */}
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isBulkDeleteModalOpen}
|
|
||||||
title="حذف گروهی تصاویر"
|
|
||||||
message={`آیا از حذف دستهجمعی ${selectedIds.size} تصویر انتخاب شده مطمئن هستید؟ این عملیات غیرقابل بازگشت است.`}
|
|
||||||
onConfirm={handleBulkDelete}
|
|
||||||
onCancel={() => setIsBulkDeleteModalOpen(false)}
|
|
||||||
isLoading={isBulkDeleting}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{previewUrl && (
|
{previewUrl && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-gray-900/80 backdrop-blur-sm"
|
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-gray-900/80 backdrop-blur-sm"
|
||||||
|
|||||||
@ -659,25 +659,25 @@ export default function Products() {
|
|||||||
<div className="space-y-2 pt-2 border-t border-gray-100">
|
<div className="space-y-2 pt-2 border-t border-gray-100">
|
||||||
<label className="text-sm font-bold text-gray-700 flex items-center justify-between">
|
<label className="text-sm font-bold text-gray-700 flex items-center justify-between">
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<span>دستور مصرف بالینی و راهنمای دوز مصرفی</span>
|
<span>منطق و دوز دقیق مصرفی بالینی (Dosage Calculator Fields)</span>
|
||||||
<div className="group relative inline-block">
|
<div className="group relative inline-block">
|
||||||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||||||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-64 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-64 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||||||
دستور مصرف روان و فارسی کالا (مانند: روزانه ۱ قرص به ازای هر ۱۰ کیلوگرم وزن بدن). این متن در کاتالوگ آنلاین و صفحه مشخصات کالا به صورت برجسته نمایش داده میشود.
|
دستور مصرف دقیق کالا (مانند: ۲ قرص به ازای هر ۱۰ کیلوگرم وزن بدن در روز). این متن در کاتالوگ و کارت مشخصات کالا به صورت برجسته نمایش داده میشود.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-purple-600 font-bold">متن راهنمای بالینی</span>
|
<span className="text-xs text-purple-600 font-bold">دستور مصرف بالینی / JSON</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
value={formData.dosageLogic}
|
value={formData.dosageLogic}
|
||||||
onChange={(e) => setFormData({ ...formData, dosageLogic: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, dosageLogic: e.target.value })}
|
||||||
placeholder="مثال: روزانه ۱ قرص به ازای هر ۱۰ کیلوگرم وزن بدن، همراه با وعده غذایی مصرف شود."
|
placeholder='مثال: روزانه ۱ قرص به ازای هر ۱۰ کیلوگرم وزن بدن یا فرمت JSON: {"baseDosage": 1, "perKg": 10}'
|
||||||
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-vazir"
|
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-vazir"
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-gray-400">
|
<p className="text-[11px] text-gray-400">
|
||||||
این متن در بخش مشخصات علمی کالا، ماشینحساب دوز و کاتالوگ آنلاین برای پزشکان و خریداران نمایش داده میشود.
|
این متن در بخش مشخصات علمی کالا و کاتالوگ آنلاین برای پزشکان و خریداران نمایش داده میشود.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -402,22 +402,13 @@ export default function Settings() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1">لینک لوگوی برند (Brand Logo URL)</label>
|
<label className="block text-xs font-bold text-gray-700 mb-1">لینک لوگوی برند (Brand Logo URL)</label>
|
||||||
<div className="flex gap-2">
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
value={settings.BRAND_LOGO_URL}
|
||||||
value={settings.BRAND_LOGO_URL}
|
onChange={(e) => setSettings({ ...settings, BRAND_LOGO_URL: e.target.value })}
|
||||||
onChange={(e) => setSettings({ ...settings, BRAND_LOGO_URL: e.target.value })}
|
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-mono"
|
||||||
className="flex-1 border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-mono"
|
dir="ltr"
|
||||||
placeholder="https://... یا /logo.png"
|
/>
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
{settings.BRAND_LOGO_URL && (
|
|
||||||
<div className="w-10 h-10 border rounded-lg bg-gray-50 flex items-center justify-center p-1 overflow-hidden shrink-0">
|
|
||||||
<img src={settings.BRAND_LOGO_URL} alt="Preview" className="max-w-full max-h-full object-contain" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-[10px] text-gray-400 mt-1">این لوگو در صورت پر بودن، جایگزین تایپوگرافی هدر سایت در اپلیکیشن خواهد شد.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
|
|||||||
@ -18,10 +18,9 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function getBlogs() {
|
async function getBlogs() {
|
||||||
const rawApiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:4001/api';
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'https://apicanina.parsaaghayi.ir';
|
||||||
const apiBase = rawApiUrl.endsWith('/api') ? rawApiUrl : `${rawApiUrl}/api`;
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${apiBase}/blogs`, { next: { revalidate: 60 } });
|
const res = await fetch(`${apiUrl}/api/blogs`, { next: { revalidate: 60 } });
|
||||||
if (!res.ok) throw new Error('Failed to fetch blogs');
|
if (!res.ok) throw new Error('Failed to fetch blogs');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return (data.data || data).map((b: Record<string, unknown>) => ({
|
return (data.data || data).map((b: Record<string, unknown>) => ({
|
||||||
|
|||||||
@ -145,29 +145,19 @@ export default function Header({
|
|||||||
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Link href="/" className="flex items-center gap-2 sm:gap-3 group shrink-0">
|
<Link href="/" className="flex items-center gap-2 sm:gap-3 group shrink-0 min-w-0">
|
||||||
{(getText('BRAND_LOGO_URL', '') || getText('site_logo', '')) ? (
|
<div className="w-10 h-10 sm:w-12 sm:h-12 min-w-[40px] min-h-[40px] sm:min-w-[48px] sm:min-h-[48px] bg-canina-blue rounded-2xl flex items-center justify-center text-white font-bold text-xl sm:text-2xl group-hover:bg-medical-gray-900 transition-all shadow-md italic shrink-0">C</div>
|
||||||
<img
|
<div className="flex flex-col justify-center min-w-0">
|
||||||
src={getText('BRAND_LOGO_URL', '') || getText('site_logo', '')}
|
<span className="text-canina-blue font-black text-base sm:text-2xl tracking-tighter italic font-sans flex items-center gap-1 leading-none truncate">
|
||||||
alt={getText('brand_name_fa', "کانینا ایران")}
|
Canina
|
||||||
className="h-10 sm:h-12 w-auto max-w-[140px] sm:max-w-[180px] object-contain shrink-0"
|
<span className="text-[11px] sm:text-sm not-italic font-medium border-r border-medical-gray-300 pr-1.5 font-vazir text-medical-gray-700">
|
||||||
/>
|
{getText('brand_name_fa', "ایران")}
|
||||||
) : (
|
</span>
|
||||||
<>
|
</span>
|
||||||
<div className="w-10 h-10 sm:w-12 sm:h-12 min-w-[40px] min-h-[40px] sm:min-w-[48px] sm:min-h-[48px] bg-canina-blue rounded-2xl flex items-center justify-center text-white font-bold text-xl sm:text-2xl group-hover:bg-medical-gray-900 transition-all shadow-md italic shrink-0">C</div>
|
<span className="hidden sm:block text-[9px] text-medical-gray-400 font-bold uppercase tracking-wider leading-tight mt-0.5 truncate">
|
||||||
<div className="flex flex-col justify-center">
|
نماینده رسمی CANINA PHARMA GMBH GERMANY
|
||||||
<span className="text-canina-blue font-black text-base sm:text-2xl italic font-sans flex items-center gap-1 leading-none pl-1 whitespace-nowrap">
|
</span>
|
||||||
Canina
|
</div>
|
||||||
<span className="text-[11px] sm:text-sm not-italic font-medium border-r border-medical-gray-300 pr-1.5 mr-0.5 font-vazir text-medical-gray-700">
|
|
||||||
{getText('brand_name_fa', "ایران")}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span className="hidden sm:block text-[9px] text-medical-gray-400 font-bold uppercase tracking-wider leading-tight mt-1 whitespace-nowrap">
|
|
||||||
نماینده رسمی CANINA PHARMA GMBH GERMANY
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -2,15 +2,7 @@ import axios from 'axios';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useUserStore } from '../store/userStore';
|
import { useUserStore } from '../store/userStore';
|
||||||
|
|
||||||
const getBaseURL = () => {
|
const baseURL = process.env.NEXT_PUBLIC_API_URL || (process.env.NODE_ENV === 'development' ? 'http://localhost:4001/api' : '/api');
|
||||||
if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL;
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return 'http://127.0.0.1:4001/api';
|
|
||||||
}
|
|
||||||
return process.env.NODE_ENV === 'development' ? 'http://localhost:4001/api' : '/api';
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseURL = getBaseURL();
|
|
||||||
|
|
||||||
export interface ApiErrorPayload {
|
export interface ApiErrorPayload {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user