yari-garan/src/modules/dashboard/pages/campaigns/index.tsx
MehrdadAdabi 6634ecfda7 feat(dropdown): Add async option fetching and improve search
This commit introduces the ability to fetch dropdown options asynchronously, enhancing the component's flexibility for large datasets or dynamic content.

Key changes include:
- **`fetchOptions` prop:** A new prop `fetchOptions` is added to allow external functions to provide options based on the current search query.
- **Internal state for options:** `internalOptions` state is introduced to manage options, which can be populated either from the initial `options` prop or by `fetchOptions`.
- **Loading state:** `isLoading` state is added to indicate when options are being fetched.
- **Improved search handling:** The `handleSearchInputChange` function now triggers `fetchOptions` when available, allowing real-time filtering from an external source.
- **Option type update:** The `Option` type now uses `name` instead of `label` for consistency.
- **Selected option display:** The displayed selected option now uses `value` instead of `label` for consistency.

These changes make the `BaseDropdown` component more robust and adaptable to various data sources, especially for scenarios requiring dynamic or remote option loading.
2025-11-25 16:59:12 +03:30

293 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { CustomButton } from "@/core/components/base/button";
import { CustomInput } from "@/core/components/base/input";
import { userInfoService } from "@/core/service/user-info.service";
import { getContactImageUrl } from "@/core/utils";
import { DASHBOARD_ROUTE } from "@modules/dashboard/routes/route.constant";
import { useQuery } from "@tanstack/react-query";
import { Plus, Search, Users } from "lucide-react";
import { useEffect, useState, type ChangeEvent } from "react";
import { useNavigate } from "react-router-dom";
import { CreateCampaignModal } from "../../components/create-campaign-modal";
import { getCampaignsService } from "../../service/campaigns.service";
import type { Campaign, CampaignTab } from "./campaigns.type";
export function CampaignsPage() {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<CampaignTab>("فعال");
const [searchQuery, setSearchQuery] = useState<string>("");
const [currentCampaign, setCurrentCampaign] = useState<Array<Campaign>>([]);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const {
data: campaigns = [],
isLoading,
refetch,
} = useQuery({
queryKey: ["campaigns"],
queryFn: getCampaignsService,
});
useEffect(() => {
if (campaigns) {
const user = userInfoService.getUserInfo();
let filtered = campaigns;
switch (activeTab) {
case "my":
filtered = campaigns.filter(
(c) => Number(c.user_id) === Number(user.WorkflowID)
);
break;
case "منتخب":
filtered = campaigns.filter((c) => c.status === "منتخب");
break;
case "group":
filtered = campaigns.filter(
(c) => c.school_code === user.school_code
);
break;
case "فعال":
default:
filtered = campaigns;
break;
}
setCurrentCampaign(filtered);
}
}, [campaigns, activeTab]); // Added activeTab to dependencies
const tabs: { value: CampaignTab; label: string }[] = [
{ value: "فعال", label: "تمام کارزار‌ها" },
{ value: "my", label: "کارزار‌های من" },
{ value: "منتخب", label: "کارزار‌های برتر" },
{ value: "group", label: "کارزار‌های گروه" },
];
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
const query = e.target.value;
setSearchQuery(query);
setActiveTab("فعال");
if (query === "") {
handleTabChange(activeTab, campaigns);
} else {
const filteredCampaigns = campaigns.filter((campaign) =>
campaign.title.toLowerCase().includes(query.toLowerCase())
);
setCurrentCampaign(filteredCampaigns);
}
};
const handleTabChange = (tab: CampaignTab, campaignData = campaigns) => {
setActiveTab(tab);
const user = userInfoService.getUserInfo();
let filtered = campaignData;
switch (tab) {
case "my":
filtered = campaignData.filter(
(c) => Number(c.user_id) === Number(user.WorkflowID)
);
break;
case "منتخب":
filtered = campaignData.filter((c) => c.status === "منتخب");
break;
case "group":
filtered = campaignData.filter(
(c) => c.school_code === user.school_code
);
break;
case "فعال":
default:
filtered = campaignData;
break;
}
setCurrentCampaign(filtered);
setSearchQuery("");
};
const handleJoin = (campaign: Campaign) => {
navigate(
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.joinToCampaing}?id=${campaign.WorkflowID}`,
{
replace: true,
}
);
};
const showCampaing = (cId: number) => {
navigate(`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.campaing}/${cId}`, {
replace: true,
});
};
const renderSkeleton = () => (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div
key={i}
className="bg-white rounded-xl shadow-md p-4 animate-pulse"
>
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-lg bg-gray-200" />
<div className="flex-1 space-y-2">
<div className="w-3/4 h-4 bg-gray-200 rounded" />
<div className="w-1/2 h-3 bg-gray-200 rounded" />
</div>
<div className="w-24 h-10 bg-gray-200 rounded-lg" />
</div>
</div>
))}
</div>
);
return (
<div className="min-h-screen bg-gray-50 p-4" dir="rtl">
<div className="max-w-3xl mx-auto">
<header className="mb-6">
<h1 className="text-3xl font-bold text-slate-800 text-right mb-2">
کارزارها
</h1>
<p className="text-slate-600 text-right">
برای تغییر جهان، کارزار ایجاد کنید و دیگران را دعوت کنید
</p>
</header>
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="relative flex-1">
<CustomInput
type="text"
placeholder="جستجوی کارزار..."
value={searchQuery}
onChange={handleSearchChange}
className="pr-10 w-full"
/>
<Search
size={20}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400"
/>
</div>
<CustomButton
variant="primary"
onClick={() => setIsCreateModalOpen(true)}
className="flex items-center justify-center gap-2"
>
<Plus size={20} />
<span>ایجاد کارزار</span>
</CustomButton>
</div>
<div className="mb-6">
<div className="flex gap-2 overflow-x-auto pb-2 border-b border-gray-200">
{tabs.map((tab) => (
<button
key={tab.value}
onClick={() => handleTabChange(tab.value)}
className={`px-4 py-2 text-sm font-medium whitespace-nowrap rounded-t-lg transition-colors ${
activeTab === tab.value
? "bg-white border-gray-200 border-t border-x text-blue-600"
: "text-slate-600 hover:text-slate-800 hover:bg-gray-100"
}`}
>
{tab.label}
</button>
))}
</div>
</div>
<main>
{isLoading ? (
renderSkeleton()
) : currentCampaign.length > 0 ? (
<ul className="space-y-4">
{currentCampaign.map((campaign) => (
<li
key={`${campaign.user_id}-${campaign.WorkflowID}`}
className="bg-white rounded-xl shadow-sm overflow-hidden transition-shadow hover:shadow-lg"
>
<div className="p-4 flex items-center gap-4">
<div className="w-16 h-16 rounded-lg overflow-hidden bg-gray-100 shrink-0">
{campaign.image ? (
<img
src={getContactImageUrl(
campaign.ValueP1226S1951StageID
)}
alt={campaign.title}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-blue-50 to-blue-100 flex items-center justify-center text-blue-500">
<Users size={24} />
</div>
)}
</div>
<div className="flex-1 text-right">
<h3 className="text-lg font-semibold text-slate-800">
{campaign.title}
</h3>
<div className="flex flex-row gap-0.5">
<span className="text-sm text-gray-500 ">
ایجاد کننده :
</span>
<p className="text-sm text-gray-500 truncate">
{campaign.user_id_nickname}
</p>
</div>
<p className="text-sm text-gray-500 mt-1">
{Number(campaign.signature_count)} عضو
</p>
<p className="text-sm text-gray-500 mt-1">
{Number(campaign.comment_count)} کامنت
</p>
</div>
<div className="flex flex-col gap-2">
{activeTab === "منتخب" && (
<CustomButton
onClick={() => handleJoin(campaign)}
variant="info"
className="shrink-0"
>
انتخاب{" "}
</CustomButton>
)}
<CustomButton
onClick={() => showCampaing(campaign.WorkflowID)}
variant="info"
className="shrink-0"
>
جزییات
</CustomButton>
</div>
</div>
</li>
))}
</ul>
) : (
<div className="text-center py-12">
<p className="text-slate-600 text-lg">
{searchQuery
? "کارزاری با این مشخصات یافت نشد."
: "کارزاری در این دسته وجود ندارد."}
</p>
</div>
)}
</main>
</div>
<CreateCampaignModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSuccess={() => refetch()}
/>
</div>
);
}
export default CampaignsPage;