feat: complete url deeplinking across all admin modal/tab pages
All checks were successful
Deploy Canina / deploy (push) Successful in 30s
All checks were successful
Deploy Canina / deploy (push) Successful in 30s
This commit is contained in:
parent
5029edaaeb
commit
fc324aacaf
@ -11,6 +11,8 @@ import Modal from '../components/ui/Modal';
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
export interface BlogPost {
|
export interface BlogPost {
|
||||||
author?: {
|
author?: {
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
@ -29,10 +31,11 @@ export interface BlogPost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Blogs() {
|
export default function Blogs() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [blogs, setBlogs] = useState<BlogPost[]>([]);
|
const [blogs, setBlogs] = useState<BlogPost[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState(() => searchParams.get('search') || '');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(() => Number(searchParams.get('page')) || 1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
|
|
||||||
@ -41,6 +44,16 @@ export default function Blogs() {
|
|||||||
const [editingBlog, setEditingBlog] = useState<BlogPost | null>(null);
|
const [editingBlog, setEditingBlog] = useState<BlogPost | null>(null);
|
||||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const updateUrlParams = (paramsObj: Record<string, string | number | undefined | null>) => {
|
||||||
|
const current = Object.fromEntries(searchParams.entries());
|
||||||
|
const merged = { ...current, ...paramsObj };
|
||||||
|
const cleaned: Record<string, string> = {};
|
||||||
|
Object.entries(merged).forEach(([k, v]) => {
|
||||||
|
if (v !== undefined && v !== null && String(v).trim() !== '') cleaned[k] = String(v);
|
||||||
|
});
|
||||||
|
setSearchParams(cleaned, { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
title: '',
|
title: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
@ -57,15 +70,25 @@ export default function Blogs() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const res = await api.get('/admin/blogs', { params: { page, limit, search } });
|
const res = await api.get('/admin/blogs', { params: { page, limit, search } });
|
||||||
if (res.data?.data) {
|
if (res.data?.data) {
|
||||||
setBlogs(res.data.data);
|
const blogList: BlogPost[] = res.data.data;
|
||||||
|
setBlogs(blogList);
|
||||||
setTotalPages(res.data.meta?.lastPage || 1);
|
setTotalPages(res.data.meta?.lastPage || 1);
|
||||||
|
|
||||||
|
const modalParam = searchParams.get('modal');
|
||||||
|
const blogId = searchParams.get('blogId');
|
||||||
|
if (modalParam === 'create') {
|
||||||
|
openModal(null);
|
||||||
|
} else if (modalParam === 'edit' && blogId) {
|
||||||
|
const found = blogList.find((b) => b.id === blogId);
|
||||||
|
if (found) openModal(found);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, search]);
|
}, [page, search, searchParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -84,7 +107,7 @@ export default function Blogs() {
|
|||||||
await api.post('/admin/blogs', formData);
|
await api.post('/admin/blogs', formData);
|
||||||
toast.success('مقاله جدید با موفقیت ذخیره شد');
|
toast.success('مقاله جدید با موفقیت ذخیره شد');
|
||||||
}
|
}
|
||||||
setIsModalOpen(false);
|
closeModal();
|
||||||
fetchBlogs();
|
fetchBlogs();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Save failed', error);
|
console.error('Save failed', error);
|
||||||
@ -109,6 +132,7 @@ export default function Blogs() {
|
|||||||
const openModal = (blog: BlogPost | null = null) => {
|
const openModal = (blog: BlogPost | null = null) => {
|
||||||
if (blog) {
|
if (blog) {
|
||||||
setEditingBlog(blog);
|
setEditingBlog(blog);
|
||||||
|
updateUrlParams({ modal: 'edit', blogId: blog.id });
|
||||||
setFormData({
|
setFormData({
|
||||||
title: blog.title,
|
title: blog.title,
|
||||||
slug: blog.slug,
|
slug: blog.slug,
|
||||||
@ -121,6 +145,7 @@ export default function Blogs() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setEditingBlog(null);
|
setEditingBlog(null);
|
||||||
|
updateUrlParams({ modal: 'create', blogId: undefined });
|
||||||
setFormData({
|
setFormData({
|
||||||
title: '', slug: '', content: '', isPublished: true, metaTitle: '', metaDescription: '', keywords: '', imageUrl: ''
|
title: '', slug: '', content: '', isPublished: true, metaTitle: '', metaDescription: '', keywords: '', imageUrl: ''
|
||||||
});
|
});
|
||||||
@ -128,6 +153,12 @@ export default function Blogs() {
|
|||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
updateUrlParams({ modal: undefined, blogId: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col sm:flex-row justify-between gap-4">
|
<div className="flex flex-col sm:flex-row justify-between gap-4">
|
||||||
@ -234,7 +265,7 @@ export default function Blogs() {
|
|||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={closeModal}
|
||||||
title={editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'}
|
title={editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'}
|
||||||
icon={FileText}
|
icon={FileText}
|
||||||
maxWidth="4xl"
|
maxWidth="4xl"
|
||||||
@ -244,10 +275,11 @@ export default function Blogs() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsModalOpen(false)}
|
onClick={closeModal}
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import Modal from '../components/ui/Modal';
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -24,10 +26,11 @@ export interface Category {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Categories() {
|
export default function Categories() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState(() => searchParams.get('search') || '');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(() => Number(searchParams.get('page')) || 1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
|
|
||||||
@ -36,6 +39,16 @@ export default function Categories() {
|
|||||||
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
|
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
|
||||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const updateUrlParams = (paramsObj: Record<string, string | number | undefined | null>) => {
|
||||||
|
const current = Object.fromEntries(searchParams.entries());
|
||||||
|
const merged = { ...current, ...paramsObj };
|
||||||
|
const cleaned: Record<string, string> = {};
|
||||||
|
Object.entries(merged).forEach(([k, v]) => {
|
||||||
|
if (v !== undefined && v !== null && String(v).trim() !== '') cleaned[k] = String(v);
|
||||||
|
});
|
||||||
|
setSearchParams(cleaned, { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
@ -51,15 +64,25 @@ export default function Categories() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const res = await api.get('/admin/categories', { params: { page, limit, search } });
|
const res = await api.get('/admin/categories', { params: { page, limit, search } });
|
||||||
if (res.data?.data) {
|
if (res.data?.data) {
|
||||||
setCategories(res.data.data);
|
const catList: Category[] = res.data.data;
|
||||||
|
setCategories(catList);
|
||||||
setTotalPages(res.data.meta?.lastPage || 1);
|
setTotalPages(res.data.meta?.lastPage || 1);
|
||||||
|
|
||||||
|
const modalParam = searchParams.get('modal');
|
||||||
|
const catId = searchParams.get('categoryId');
|
||||||
|
if (modalParam === 'create') {
|
||||||
|
openModal(null);
|
||||||
|
} else if (modalParam === 'edit' && catId) {
|
||||||
|
const found = catList.find((c) => c.id === catId);
|
||||||
|
if (found) openModal(found);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, search]);
|
}, [page, search, searchParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -78,7 +101,7 @@ export default function Categories() {
|
|||||||
await api.post('/admin/categories', formData);
|
await api.post('/admin/categories', formData);
|
||||||
toast.success('دستهبندی جدید با موفقیت ایجاد شد');
|
toast.success('دستهبندی جدید با موفقیت ایجاد شد');
|
||||||
}
|
}
|
||||||
setIsModalOpen(false);
|
closeModal();
|
||||||
fetchCategories();
|
fetchCategories();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Save failed', error);
|
console.error('Save failed', error);
|
||||||
@ -103,6 +126,7 @@ export default function Categories() {
|
|||||||
const openModal = (category: Category | null = null) => {
|
const openModal = (category: Category | null = null) => {
|
||||||
if (category) {
|
if (category) {
|
||||||
setEditingCategory(category);
|
setEditingCategory(category);
|
||||||
|
updateUrlParams({ modal: 'edit', categoryId: category.id });
|
||||||
setFormData({
|
setFormData({
|
||||||
name: category.name,
|
name: category.name,
|
||||||
slug: category.slug,
|
slug: category.slug,
|
||||||
@ -114,6 +138,7 @@ export default function Categories() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setEditingCategory(null);
|
setEditingCategory(null);
|
||||||
|
updateUrlParams({ modal: 'create', categoryId: undefined });
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '', slug: '', description: '', metaTitle: '', metaDescription: '', keywords: '', imageUrl: ''
|
name: '', slug: '', description: '', metaTitle: '', metaDescription: '', keywords: '', imageUrl: ''
|
||||||
});
|
});
|
||||||
@ -121,6 +146,12 @@ export default function Categories() {
|
|||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
updateUrlParams({ modal: undefined, categoryId: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col sm:flex-row justify-between gap-4">
|
<div className="flex flex-col sm:flex-row justify-between gap-4">
|
||||||
@ -237,7 +268,7 @@ export default function Categories() {
|
|||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={closeModal}
|
||||||
title={editingCategory ? 'ویرایش دستهبندی' : 'افزودن دستهبندی جدید'}
|
title={editingCategory ? 'ویرایش دستهبندی' : 'افزودن دستهبندی جدید'}
|
||||||
icon={FolderTree}
|
icon={FolderTree}
|
||||||
maxWidth="2xl"
|
maxWidth="2xl"
|
||||||
@ -247,10 +278,11 @@ export default function Categories() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsModalOpen(false)}
|
onClick={closeModal}
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -37,14 +37,17 @@ export interface UserRecord {
|
|||||||
pets?: PetSummary[];
|
pets?: PetSummary[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
export default function Users() {
|
export default function Users() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [users, setUsers] = useState<UserRecord[]>([]);
|
const [users, setUsers] = useState<UserRecord[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(() => Number(searchParams.get('page')) || 1);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState(() => searchParams.get('search') || '');
|
||||||
const [role, setRole] = useState('');
|
const [role, setRole] = useState(() => searchParams.get('role') || '');
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
|
||||||
@ -55,6 +58,16 @@ export default function Users() {
|
|||||||
const [userToDelete, setUserToDelete] = useState<UserRecord | null>(null);
|
const [userToDelete, setUserToDelete] = useState<UserRecord | null>(null);
|
||||||
const [walletUser, setWalletUser] = useState<UserRecord | null>(null);
|
const [walletUser, setWalletUser] = useState<UserRecord | null>(null);
|
||||||
|
|
||||||
|
const updateUrlParams = (paramsObj: Record<string, string | number | undefined | null>) => {
|
||||||
|
const current = Object.fromEntries(searchParams.entries());
|
||||||
|
const merged = { ...current, ...paramsObj };
|
||||||
|
const cleaned: Record<string, string> = {};
|
||||||
|
Object.entries(merged).forEach(([k, v]) => {
|
||||||
|
if (v !== undefined && v !== null && String(v).trim() !== '') cleaned[k] = String(v);
|
||||||
|
});
|
||||||
|
setSearchParams(cleaned, { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
// Form States
|
// Form States
|
||||||
const [createFormData, setCreateFormData] = useState({
|
const [createFormData, setCreateFormData] = useState({
|
||||||
firstName: '',
|
firstName: '',
|
||||||
@ -101,16 +114,30 @@ export default function Users() {
|
|||||||
|
|
||||||
const response = await api.get(`/admin/users?${params.toString()}`);
|
const response = await api.get(`/admin/users?${params.toString()}`);
|
||||||
if (response.data?.success) {
|
if (response.data?.success) {
|
||||||
setUsers(response.data.data || []);
|
const userList = response.data.data || [];
|
||||||
|
setUsers(userList);
|
||||||
setTotalPages(response.data.meta?.lastPage || 1);
|
setTotalPages(response.data.meta?.lastPage || 1);
|
||||||
setTotalCount(response.data.meta?.total || 0);
|
setTotalCount(response.data.meta?.total || 0);
|
||||||
|
|
||||||
|
const modalParam = searchParams.get('modal');
|
||||||
|
const userId = searchParams.get('userId');
|
||||||
|
if (modalParam === 'create') {
|
||||||
|
handleOpenCreate();
|
||||||
|
} else if (userId && userList.length > 0) {
|
||||||
|
const found = userList.find((u: UserRecord) => u.id === userId);
|
||||||
|
if (found) {
|
||||||
|
if (modalParam === 'view') handleOpenView(found);
|
||||||
|
else if (modalParam === 'edit') handleOpenEdit(found);
|
||||||
|
else if (modalParam === 'wallet') handleOpenWallet(found);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch users', err);
|
console.error('Failed to fetch users', err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, search, role]);
|
}, [page, search, role, searchParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const delayDebounceFn = setTimeout(() => {
|
const delayDebounceFn = setTimeout(() => {
|
||||||
@ -120,9 +147,32 @@ export default function Users() {
|
|||||||
return () => clearTimeout(delayDebounceFn);
|
return () => clearTimeout(delayDebounceFn);
|
||||||
}, [fetchUsers]);
|
}, [fetchUsers]);
|
||||||
|
|
||||||
|
// Open View Modal
|
||||||
|
const handleOpenView = (user: UserRecord) => {
|
||||||
|
setViewUser(user);
|
||||||
|
updateUrlParams({ modal: 'view', userId: user.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseView = () => {
|
||||||
|
setViewUser(null);
|
||||||
|
updateUrlParams({ modal: undefined, userId: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open Create Modal
|
||||||
|
const handleOpenCreate = () => {
|
||||||
|
setShowCreateModal(true);
|
||||||
|
updateUrlParams({ modal: 'create', userId: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseCreate = () => {
|
||||||
|
setShowCreateModal(false);
|
||||||
|
updateUrlParams({ modal: undefined, userId: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
// Open Edit Modal
|
// Open Edit Modal
|
||||||
const handleOpenEdit = (user: UserRecord) => {
|
const handleOpenEdit = (user: UserRecord) => {
|
||||||
setEditUser(user);
|
setEditUser(user);
|
||||||
|
updateUrlParams({ modal: 'edit', userId: user.id });
|
||||||
setEditFormData({
|
setEditFormData({
|
||||||
firstName: user.firstName || '',
|
firstName: user.firstName || '',
|
||||||
lastName: user.lastName || '',
|
lastName: user.lastName || '',
|
||||||
@ -134,9 +184,15 @@ export default function Users() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseEdit = () => {
|
||||||
|
setEditUser(null);
|
||||||
|
updateUrlParams({ modal: undefined, userId: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
// Open Wallet Modal
|
// Open Wallet Modal
|
||||||
const handleOpenWallet = (user: UserRecord) => {
|
const handleOpenWallet = (user: UserRecord) => {
|
||||||
setWalletUser(user);
|
setWalletUser(user);
|
||||||
|
updateUrlParams({ modal: 'wallet', userId: user.id });
|
||||||
setWalletFormData({
|
setWalletFormData({
|
||||||
amount: 50000,
|
amount: 50000,
|
||||||
type: 'deposit',
|
type: 'deposit',
|
||||||
@ -144,6 +200,17 @@ export default function Users() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseWallet = () => {
|
||||||
|
setWalletUser(null);
|
||||||
|
updateUrlParams({ modal: undefined, userId: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDelete = () => {
|
||||||
|
setUserToDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Create User
|
// Create User
|
||||||
const handleCreateUser = async (e: React.FormEvent) => {
|
const handleCreateUser = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -441,7 +508,7 @@ export default function Users() {
|
|||||||
{showCreateModal && (
|
{showCreateModal && (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showCreateModal}
|
isOpen={showCreateModal}
|
||||||
onClose={() => setShowCreateModal(false)}
|
onClose={handleCloseCreate}
|
||||||
title="افزودن کاربر جدید"
|
title="افزودن کاربر جدید"
|
||||||
icon={Plus}
|
icon={Plus}
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
@ -450,7 +517,7 @@ export default function Users() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowCreateModal(false)}
|
onClick={handleCloseCreate}
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</Button>
|
</Button>
|
||||||
@ -562,6 +629,7 @@ export default function Users() {
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="1000"
|
step="1000"
|
||||||
|
placeholder="0"
|
||||||
value={createFormData.walletBalance}
|
value={createFormData.walletBalance}
|
||||||
onChange={(e) => setCreateFormData({ ...createFormData, walletBalance: Number(e.target.value) })}
|
onChange={(e) => setCreateFormData({ ...createFormData, walletBalance: Number(e.target.value) })}
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono"
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono"
|
||||||
@ -575,7 +643,7 @@ export default function Users() {
|
|||||||
{editUser && (
|
{editUser && (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={!!editUser}
|
isOpen={!!editUser}
|
||||||
onClose={() => setEditUser(null)}
|
onClose={handleCloseEdit}
|
||||||
title="ویرایش اطلاعات کاربر"
|
title="ویرایش اطلاعات کاربر"
|
||||||
icon={Edit3}
|
icon={Edit3}
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
@ -584,7 +652,7 @@ export default function Users() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setEditUser(null)}
|
onClick={handleCloseEdit}
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</Button>
|
</Button>
|
||||||
@ -691,7 +759,7 @@ export default function Users() {
|
|||||||
{walletUser && (
|
{walletUser && (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={!!walletUser}
|
isOpen={!!walletUser}
|
||||||
onClose={() => setWalletUser(null)}
|
onClose={handleCloseWallet}
|
||||||
title="مدیریت کیف پول کاربر"
|
title="مدیریت کیف پول کاربر"
|
||||||
icon={Wallet}
|
icon={Wallet}
|
||||||
maxWidth="md"
|
maxWidth="md"
|
||||||
@ -700,7 +768,7 @@ export default function Users() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setWalletUser(null)}
|
onClick={handleCloseWallet}
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</Button>
|
</Button>
|
||||||
@ -774,7 +842,6 @@ export default function Users() {
|
|||||||
type="number"
|
type="number"
|
||||||
min="1000"
|
min="1000"
|
||||||
step="1000"
|
step="1000"
|
||||||
required
|
|
||||||
value={walletFormData.amount}
|
value={walletFormData.amount}
|
||||||
onChange={(e) => setWalletFormData({ ...walletFormData, amount: Number(e.target.value) })}
|
onChange={(e) => setWalletFormData({ ...walletFormData, amount: Number(e.target.value) })}
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono font-bold text-sm"
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono font-bold text-sm"
|
||||||
@ -800,7 +867,7 @@ export default function Users() {
|
|||||||
{userToDelete && (
|
{userToDelete && (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={!!userToDelete}
|
isOpen={!!userToDelete}
|
||||||
onClose={() => setUserToDelete(null)}
|
onClose={handleCloseDelete}
|
||||||
title="تایید حذف کاربر"
|
title="تایید حذف کاربر"
|
||||||
icon={AlertTriangle}
|
icon={AlertTriangle}
|
||||||
maxWidth="md"
|
maxWidth="md"
|
||||||
@ -809,7 +876,7 @@ export default function Users() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setUserToDelete(null)}
|
onClick={handleCloseDelete}
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</Button>
|
</Button>
|
||||||
@ -840,7 +907,7 @@ export default function Users() {
|
|||||||
{viewUser && (
|
{viewUser && (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={!!viewUser}
|
isOpen={!!viewUser}
|
||||||
onClose={() => setViewUser(null)}
|
onClose={handleCloseView}
|
||||||
title="مشاهده جزئیات پروفایل کاربر"
|
title="مشاهده جزئیات پروفایل کاربر"
|
||||||
icon={UsersIcon}
|
icon={UsersIcon}
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import Pagination from '../components/ui/Pagination';
|
|||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import Button from '../components/ui/Button';
|
import Button from '../components/ui/Button';
|
||||||
import Modal from '../components/ui/Modal';
|
import Modal from '../components/ui/Modal';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
export interface WikiTerm {
|
export interface WikiTerm {
|
||||||
@ -27,10 +28,11 @@ export interface ProductItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Wiki() {
|
export default function Wiki() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [terms, setTerms] = useState<WikiTerm[]>([]);
|
const [terms, setTerms] = useState<WikiTerm[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState(() => searchParams.get('search') || '');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(() => Number(searchParams.get('page')) || 1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
|
|
||||||
@ -38,6 +40,17 @@ export default function Wiki() {
|
|||||||
const [editingTerm, setEditingTerm] = useState<WikiTerm | null>(null);
|
const [editingTerm, setEditingTerm] = useState<WikiTerm | null>(null);
|
||||||
const [deleteTargetKey, setDeleteTargetKey] = useState<string | null>(null);
|
const [deleteTargetKey, setDeleteTargetKey] = useState<string | null>(null);
|
||||||
const [productsList, setProductsList] = useState<ProductItem[]>([]);
|
const [productsList, setProductsList] = useState<ProductItem[]>([]);
|
||||||
|
|
||||||
|
const updateUrlParams = (paramsObj: Record<string, string | number | undefined | null>) => {
|
||||||
|
const current = Object.fromEntries(searchParams.entries());
|
||||||
|
const merged = { ...current, ...paramsObj };
|
||||||
|
const cleaned: Record<string, string> = {};
|
||||||
|
Object.entries(merged).forEach(([k, v]) => {
|
||||||
|
if (v !== undefined && v !== null && String(v).trim() !== '') cleaned[k] = String(v);
|
||||||
|
});
|
||||||
|
setSearchParams(cleaned, { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
key: '',
|
key: '',
|
||||||
term: '',
|
term: '',
|
||||||
@ -54,15 +67,25 @@ export default function Wiki() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const res = await api.get('/admin/wiki', { params: { page, limit, search } });
|
const res = await api.get('/admin/wiki', { params: { page, limit, search } });
|
||||||
if (res.data?.data) {
|
if (res.data?.data) {
|
||||||
setTerms(res.data.data);
|
const termList: WikiTerm[] = res.data.data;
|
||||||
|
setTerms(termList);
|
||||||
setTotalPages(res.data.meta?.lastPage || 1);
|
setTotalPages(res.data.meta?.lastPage || 1);
|
||||||
|
|
||||||
|
const modalParam = searchParams.get('modal');
|
||||||
|
const termKey = searchParams.get('termKey');
|
||||||
|
if (modalParam === 'create') {
|
||||||
|
openModal(null);
|
||||||
|
} else if (modalParam === 'edit' && termKey) {
|
||||||
|
const found = termList.find((t) => t.key === termKey);
|
||||||
|
if (found) openModal(found);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, search]);
|
}, [page, search, searchParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -81,7 +104,7 @@ export default function Wiki() {
|
|||||||
await api.post('/admin/wiki', formData);
|
await api.post('/admin/wiki', formData);
|
||||||
toast.success('اصطلاح علمی جدید با موفقیت ایجاد شد');
|
toast.success('اصطلاح علمی جدید با موفقیت ایجاد شد');
|
||||||
}
|
}
|
||||||
setIsModalOpen(false);
|
closeModal();
|
||||||
fetchTerms();
|
fetchTerms();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Save failed', error);
|
console.error('Save failed', error);
|
||||||
@ -112,6 +135,7 @@ export default function Wiki() {
|
|||||||
const openModal = (term: WikiTerm | null = null) => {
|
const openModal = (term: WikiTerm | null = null) => {
|
||||||
if (term) {
|
if (term) {
|
||||||
setEditingTerm(term);
|
setEditingTerm(term);
|
||||||
|
updateUrlParams({ modal: 'edit', termKey: term.key });
|
||||||
setFormData({
|
setFormData({
|
||||||
key: term.key,
|
key: term.key,
|
||||||
term: term.term,
|
term: term.term,
|
||||||
@ -124,6 +148,7 @@ export default function Wiki() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setEditingTerm(null);
|
setEditingTerm(null);
|
||||||
|
updateUrlParams({ modal: 'create', termKey: undefined });
|
||||||
setFormData({
|
setFormData({
|
||||||
key: '', term: '', definition: '', relatedProducts: [], wikiId: 'general', metaTitle: '', metaDescription: '', keywords: ''
|
key: '', term: '', definition: '', relatedProducts: [], wikiId: 'general', metaTitle: '', metaDescription: '', keywords: ''
|
||||||
});
|
});
|
||||||
@ -131,6 +156,11 @@ export default function Wiki() {
|
|||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
updateUrlParams({ modal: undefined, termKey: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col sm:flex-row justify-between gap-4">
|
<div className="flex flex-col sm:flex-row justify-between gap-4">
|
||||||
@ -221,7 +251,7 @@ export default function Wiki() {
|
|||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={closeModal}
|
||||||
title={editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'}
|
title={editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'}
|
||||||
icon={BookOpen}
|
icon={BookOpen}
|
||||||
maxWidth="2xl"
|
maxWidth="2xl"
|
||||||
@ -231,10 +261,11 @@ export default function Wiki() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsModalOpen(false)}
|
onClick={closeModal}
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user