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.
293 lines
9.9 KiB
TypeScript
293 lines
9.9 KiB
TypeScript
"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;
|