feat: Implement searchable dropdown component and refactor campaign page
This commit introduces a new `BaseDropdown` component with search functionality and refactors the campaign listing page to utilize this new component and improve its layout. The `BaseDropdown` component was significantly refactored from a native `<select>` element to a custom component built with `div` and `input` elements. This allows for: - **Search functionality**: Users can now type to filter dropdown options. - **Improved accessibility**: Custom handling of focus and keyboard navigation. - **Enhanced styling**: More control over the visual appearance. The campaign listing page (`src/pages/campaigns/index.tsx`) was updated to: - Replace the previous dropdowns with the new `BaseDropdown` component. - Adjust the layout of the header, search bar, and filter section for better responsiveness and visual appeal. - Update the `VITE_API_URL` in `.env` to ensure a newline at the end of the file. These changes enhance the user experience by providing a more interactive and user-friendly way to select options and navigate the campaign page.
This commit is contained in:
parent
024b268000
commit
d725c1b7d7
5
.env
5
.env
|
|
@ -1 +1,4 @@
|
||||||
VITE_API_URL=https://yarigaran-back.pelekan.org
|
VITE_API_URL=https://yarigaran-back.pelekan.org
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,30 @@
|
||||||
// components/ui/BaseDropdown.tsx
|
|
||||||
import { cn } from "@/core/lib/utils";
|
import { cn } from "@/core/lib/utils";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown, Search } from "lucide-react";
|
||||||
import { type SelectHTMLAttributes, forwardRef } from "react";
|
import {
|
||||||
|
forwardRef,
|
||||||
|
type InputHTMLAttributes,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
type BaseDropdownProps = SelectHTMLAttributes<HTMLSelectElement> & {
|
type Option = { value: string; label: string };
|
||||||
|
|
||||||
|
type BaseDropdownProps = Omit<
|
||||||
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
"onChange"
|
||||||
|
> & {
|
||||||
label?: string;
|
label?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
variant?: "primary" | "error";
|
variant?: "primary" | "error";
|
||||||
options: { value: string; label: string }[];
|
options: Option[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
onInputChange?: (inputValue: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BaseDropdown = forwardRef<HTMLSelectElement, BaseDropdownProps>(
|
export const BaseDropdown = forwardRef<HTMLDivElement, BaseDropdownProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
label,
|
label,
|
||||||
|
|
@ -20,51 +33,124 @@ export const BaseDropdown = forwardRef<HTMLSelectElement, BaseDropdownProps>(
|
||||||
options,
|
options,
|
||||||
placeholder = "انتخاب کنید",
|
placeholder = "انتخاب کنید",
|
||||||
className,
|
className,
|
||||||
...props
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
onInputChange,
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const selectedOption = options.find((option) => option.value === value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setTimeout(() => inputRef.current?.focus(), 0);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleSelect = (option: Option) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(option.value);
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
setSearchQuery("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredOptions = options.filter((option) =>
|
||||||
|
option.label.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
const hasError = !!error;
|
const hasError = !!error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full" ref={ref}>
|
||||||
{label && (
|
{label && (
|
||||||
<label className="mb-2 block text-sm font-medium text-foreground text-right">
|
<label className="mb-2 block text-sm font-medium text-foreground text-right">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative" ref={dropdownRef}>
|
||||||
<select
|
<button
|
||||||
ref={ref}
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-12 w-full appearance-none rounded-lg border-2 bg-background px-4 py-2 pr-4 text-sm transition-all duration-200",
|
"flex h-12 w-full items-center justify-between rounded-lg border-2 bg-background px-4 py-2 text-sm transition-all duration-200",
|
||||||
"focus-visible:outline-none focus-visible:ring-1",
|
"focus-visible:outline-none focus-visible:ring-1",
|
||||||
variant === "error" || hasError
|
variant === "error" || hasError
|
||||||
? "border-red-500 focus-visible:border-red-500 focus-visible:ring-red-500/20"
|
? "border-red-500 focus-visible:border-red-500 focus-visible:ring-red-500/20"
|
||||||
: "border-gray-300 focus-visible:border-blue-600 focus-visible:ring-blue-600/20",
|
: "border-gray-300 focus-visible:border-blue-600 focus-visible:ring-blue-600/20",
|
||||||
props.disabled && "opacity-60 cursor-not-allowed bg-gray-50",
|
disabled && "opacity-60 cursor-not-allowed bg-gray-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
<option value="">{placeholder}</option>
|
<span className={selectedOption ? "text-black" : "text-gray-400"}>
|
||||||
{options.map((option) => (
|
{selectedOption ? selectedOption.label : placeholder}
|
||||||
<option key={option.value} value={option.value}>
|
</span>
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
{/* آیکون پایین */}
|
|
||||||
<div className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2">
|
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-5 w-5 transition-transform",
|
"h-5 w-5 transition-transform",
|
||||||
|
isOpen && "rotate-180",
|
||||||
hasError ? "text-red-500" : "text-gray-500"
|
hasError ? "text-red-500" : "text-gray-500"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute z-10 mt-1 w-full rounded-lg border-2 border-gray-200 bg-white shadow-lg max-h-60 overflow-auto">
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder="جستجو..."
|
||||||
|
className="w-full rounded-md border border-gray-300 px-3 py-2 pl-8 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchQuery(e.target.value);
|
||||||
|
if (onInputChange) {
|
||||||
|
onInputChange(e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="max-h-48 overflow-auto">
|
||||||
|
{filteredOptions.map((option) => (
|
||||||
|
<li
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => handleSelect(option)}
|
||||||
|
className="cursor-pointer px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasError && (
|
{hasError && (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { cn } from "@/core/lib/utils";
|
import { cn } from "@/core/lib/utils";
|
||||||
import { DASHBOARD_ROUTE } from "@/modules/dashboard/routes/route.constant";
|
import { DASHBOARD_ROUTE } from "@/modules/dashboard/routes/route.constant";
|
||||||
import { BarChart3, Home, MessageCircle, User } from "lucide-react";
|
import { BarChart3, Bot, Home } from "lucide-react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
|
@ -18,20 +18,6 @@ interface MobileNavbarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_NAV_ITEMS: NavItem[] = [
|
const DEFAULT_NAV_ITEMS: NavItem[] = [
|
||||||
{
|
|
||||||
id: "profile",
|
|
||||||
disabled: false,
|
|
||||||
label: "پروفایل",
|
|
||||||
icon: <User size={24} />,
|
|
||||||
path: `${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.profile}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "group-chat",
|
|
||||||
label: "گروه",
|
|
||||||
disabled: true,
|
|
||||||
icon: <MessageCircle size={24} />,
|
|
||||||
path: "#",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "ranking",
|
id: "ranking",
|
||||||
label: "رتبهبندی",
|
label: "رتبهبندی",
|
||||||
|
|
@ -39,6 +25,13 @@ const DEFAULT_NAV_ITEMS: NavItem[] = [
|
||||||
icon: <BarChart3 size={24} />,
|
icon: <BarChart3 size={24} />,
|
||||||
path: "#",
|
path: "#",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "chat-group",
|
||||||
|
label: "چت گروهی",
|
||||||
|
disabled: true,
|
||||||
|
icon: <Bot size={24} />,
|
||||||
|
path: "#",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "dashboard",
|
id: "dashboard",
|
||||||
label: "کارزار",
|
label: "کارزار",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import DashboardHeader from "@/core/components/base/dashboard-header";
|
import DashboardHeader from "@/core/components/base/dashboard-header";
|
||||||
import { MobileNavbar } from "@/core/components/others/mobile-navbar";
|
import { MobileNavbar } from "@/core/components/others/mobile-navbar";
|
||||||
import { userInfoService } from "@/core/service/user-info.service";
|
import { userInfoService } from "@/core/service/user-info.service";
|
||||||
|
import { getContactImageUrl } from "@/core/utils";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
export function DashboardLayout() {
|
export function DashboardLayout() {
|
||||||
|
|
@ -10,7 +11,11 @@ export function DashboardLayout() {
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<main className="flex-1 overflow-y-auto pb-14">
|
<main className="flex-1 overflow-y-auto pb-14">
|
||||||
<DashboardHeader
|
<DashboardHeader
|
||||||
profileImageUrl={""}
|
profileImageUrl={
|
||||||
|
user.ValueP1224S1943StageID
|
||||||
|
? getContactImageUrl(user.ValueP1224S1943StageID!)
|
||||||
|
: ""
|
||||||
|
}
|
||||||
fullName={`${user.name || "کاربر جدید"} ${user.family || ""}`}
|
fullName={`${user.name || "کاربر جدید"} ${user.family || ""}`}
|
||||||
coins={100}
|
coins={100}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
export interface Campaign {
|
export interface Campaign {
|
||||||
ValueP1226S1951StageID: Number;
|
ValueP1226S1951StageID: number;
|
||||||
ValueP1226S1951ValueID: Number;
|
ValueP1226S1951ValueID: number;
|
||||||
WorkflowID: Number;
|
WorkflowID: number;
|
||||||
description: String;
|
description: string;
|
||||||
image: String;
|
image: string;
|
||||||
status: String;
|
status: string;
|
||||||
title: String;
|
title: string;
|
||||||
user_id: String;
|
user_id: string;
|
||||||
volume: String;
|
volume: string;
|
||||||
school_code: String;
|
school_code: string;
|
||||||
nickname?: String;
|
nickname?: string;
|
||||||
signature_count: Number;
|
signature_count: number;
|
||||||
comment_count?: Number;
|
comment_count?: number;
|
||||||
user_id_nickname?: String;
|
user_id_nickname?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Signer {
|
export interface Signer {
|
||||||
|
|
@ -52,4 +52,5 @@ export interface SignatureItem {
|
||||||
WorkflowID: Number;
|
WorkflowID: Number;
|
||||||
user_id_nickname: String;
|
user_id_nickname: String;
|
||||||
user_stage_id: String;
|
user_stage_id: String;
|
||||||
|
user_id_username: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { CustomButton } from "@/core/components/base/button";
|
import { CustomButton } from "@/core/components/base/button";
|
||||||
import TextAreaField from "@/core/components/base/text-area";
|
import TextAreaField from "@/core/components/base/text-area";
|
||||||
|
import { userInfoService } from "@/core/service/user-info.service";
|
||||||
import { getContactImageUrl } from "@/core/utils";
|
import { getContactImageUrl } from "@/core/utils";
|
||||||
import {
|
import {
|
||||||
addCommentService,
|
addCommentService,
|
||||||
|
|
@ -30,7 +31,6 @@ export function CampaignDetailPage() {
|
||||||
const [commentText, setCommentText] = useState("");
|
const [commentText, setCommentText] = useState("");
|
||||||
const [hasSignedCampaign, setHasSignedCampaign] = useState<boolean>(false);
|
const [hasSignedCampaign, setHasSignedCampaign] = useState<boolean>(false);
|
||||||
const [currentComments, setCurrentComments] = useState<CommentsItem[]>([]);
|
const [currentComments, setCurrentComments] = useState<CommentsItem[]>([]);
|
||||||
|
|
||||||
const { data: campaign, isLoading } = useQuery({
|
const { data: campaign, isLoading } = useQuery({
|
||||||
queryKey: ["campaign", id],
|
queryKey: ["campaign", id],
|
||||||
queryFn: () => getSelectedCampaignsService(Number(id!)),
|
queryFn: () => getSelectedCampaignsService(Number(id!)),
|
||||||
|
|
@ -55,6 +55,16 @@ export function CampaignDetailPage() {
|
||||||
}
|
}
|
||||||
}, [comments]);
|
}, [comments]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (signs && signs?.length > 0) {
|
||||||
|
const username = userInfoService.getUserInfo().username;
|
||||||
|
const findCurrentUser = signs.find(
|
||||||
|
(el) => el.user_id_username === username
|
||||||
|
);
|
||||||
|
if (findCurrentUser) setHasSignedCampaign(true);
|
||||||
|
}
|
||||||
|
}, [signs]);
|
||||||
|
|
||||||
const signMutation = useMutation({
|
const signMutation = useMutation({
|
||||||
mutationFn: () => signCampaignService(id!),
|
mutationFn: () => signCampaignService(id!),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -216,7 +226,7 @@ export function CampaignDetailPage() {
|
||||||
>
|
>
|
||||||
<div className="w-16 h-16 rounded-full overflow-hidden bg-gradient-to-br from-blue-400 to-blue-600">
|
<div className="w-16 h-16 rounded-full overflow-hidden bg-gradient-to-br from-blue-400 to-blue-600">
|
||||||
<img
|
<img
|
||||||
src={getContactImageUrl(signer.ValueP1227S1955StageID)}
|
src={getContactImageUrl(Number(signer.user_stage_id))}
|
||||||
alt={`${signer.user_id_nickname}-avatar`}
|
alt={`${signer.user_id_nickname}-avatar`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,18 @@
|
||||||
import { CustomButton } from "@/core/components/base/button";
|
import { CustomButton } from "@/core/components/base/button";
|
||||||
import { CustomInput } from "@/core/components/base/input";
|
import { CustomInput } from "@/core/components/base/input";
|
||||||
import { userInfoService } from "@/core/service/user-info.service";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import { Loader, Plus, Search } from "lucide-react";
|
import { Plus, Search, Users } from "lucide-react";
|
||||||
import { useEffect, useState, type ChangeEvent } from "react";
|
import { useEffect, useState, type ChangeEvent } from "react";
|
||||||
import { CampaignCard } from "../../components/campaign-card";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateCampaignModal } from "../../components/create-campaign-modal";
|
import { CreateCampaignModal } from "../../components/create-campaign-modal";
|
||||||
import { getCampaignsService } from "../../service/campaigns.service";
|
import { getCampaignsService } from "../../service/campaigns.service";
|
||||||
import type { Campaign, CampaignTab } from "./campaigns.type";
|
import type { Campaign, CampaignTab } from "./campaigns.type";
|
||||||
|
|
||||||
export function CampaignsPage() {
|
export function CampaignsPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [activeTab, setActiveTab] = useState<CampaignTab>("فعال");
|
const [activeTab, setActiveTab] = useState<CampaignTab>("فعال");
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
const [currentCampaign, setCurrentCampaign] = useState<Array<Campaign>>([]);
|
const [currentCampaign, setCurrentCampaign] = useState<Array<Campaign>>([]);
|
||||||
|
|
@ -28,170 +31,247 @@ export function CampaignsPage() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (campaigns) {
|
if (campaigns) {
|
||||||
setCurrentCampaign(campaigns);
|
const user = userInfoService.getUserInfo();
|
||||||
}
|
let filtered = campaigns;
|
||||||
}, [campaigns]);
|
|
||||||
|
|
||||||
const tabs: { value: CampaignTab; label: string; oreder: number }[] = [
|
switch (activeTab) {
|
||||||
{ oreder: 1, value: "فعال", label: "تمام کارزارها" },
|
case "my":
|
||||||
{ oreder: 2, value: "my", label: "کارزارهای من" },
|
filtered = campaigns.filter(
|
||||||
{ oreder: 3, value: "منتخب", label: "کارزارهای برتر" },
|
(c) => Number(c.user_id) === Number(user.WorkflowID)
|
||||||
{ oreder: 4, value: "group", label: "کارزارهای گروه" },
|
);
|
||||||
|
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 handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setSearchQuery(e.target.value);
|
const query = e.target.value;
|
||||||
|
setSearchQuery(query);
|
||||||
setActiveTab("فعال");
|
setActiveTab("فعال");
|
||||||
const filteredCampaigns = campaigns.filter((campaign) =>
|
|
||||||
campaign.title.toLowerCase().includes(e.target.value.toLowerCase())
|
if (query === "") {
|
||||||
);
|
handleTabChange(activeTab, campaigns);
|
||||||
if (e.target.value === "") {
|
} else {
|
||||||
handleTabChange(activeTab);
|
const filteredCampaigns = campaigns.filter((campaign) =>
|
||||||
setCurrentCampaign(campaigns);
|
campaign.title.toLowerCase().includes(query.toLowerCase())
|
||||||
return;
|
);
|
||||||
|
setCurrentCampaign(filteredCampaigns);
|
||||||
}
|
}
|
||||||
setCurrentCampaign(filteredCampaigns);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabChange = (tab: CampaignTab) => {
|
const handleTabChange = (tab: CampaignTab, campaignData = campaigns) => {
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
const user = userInfoService.getUserInfo();
|
const user = userInfoService.getUserInfo();
|
||||||
|
let filtered = campaignData;
|
||||||
|
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case "فعال":
|
|
||||||
setCurrentCampaign(campaigns);
|
|
||||||
break;
|
|
||||||
case "my":
|
case "my":
|
||||||
setCurrentCampaign(
|
filtered = campaignData.filter(
|
||||||
campaigns.filter(
|
(c) => Number(c.user_id) === Number(user.WorkflowID)
|
||||||
(campaign) => Number(campaign.user_id) === Number(user.WorkflowID)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "منتخب":
|
case "منتخب":
|
||||||
setCurrentCampaign(
|
filtered = campaignData.filter((c) => c.status === "منتخب");
|
||||||
[...campaigns].filter((item, _) => item.status === "منتخب")
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case "group":
|
|
||||||
setCurrentCampaign(
|
|
||||||
campaigns.filter(
|
|
||||||
(campaign) => campaign.school_code === user.school_code
|
|
||||||
)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setCurrentCampaign(campaigns);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
case "group":
|
||||||
|
filtered = campaignData.filter(
|
||||||
|
(c) => c.school_code === user.school_code
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "فعال":
|
||||||
|
default:
|
||||||
|
filtered = campaignData;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setCurrentCampaign(filtered);
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleJoin = (campaign: Campaign) => {
|
||||||
|
navigate(
|
||||||
|
`/${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.joinToCampaing}?id=${campaign.WorkflowID}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showCampaing = (cId: number) => {
|
||||||
|
navigate(`/${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.campaigns}/${cId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 p-4 " dir="rtl">
|
<div className="min-h-screen bg-gray-50 p-4" dir="rtl">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-3xl mx-auto">
|
||||||
{/* Header */}
|
<header className="mb-6">
|
||||||
<div className="mb-6">
|
<h1 className="text-3xl font-bold text-slate-800 text-right mb-2">
|
||||||
<h1 className="text-4xl font-bold text-slate-800 text-right mb-6">
|
|
||||||
کارزارها
|
کارزارها
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-slate-600 text-right">
|
<p className="text-slate-600 text-right">
|
||||||
برای تغییر جهان، کارزار ایجاد کنید و دیگران را دعوت کنید
|
برای تغییر جهان، کارزار ایجاد کنید و دیگران را دعوت کنید
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
{/* Top Bar: Search and Create Button */}
|
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="relative flex-1">
|
||||||
{/* Search Box */}
|
<CustomInput
|
||||||
<div className="flex-1 sm:max-w-md">
|
type="text"
|
||||||
<div className="relative">
|
placeholder="جستجوی کارزار..."
|
||||||
<CustomInput
|
value={searchQuery}
|
||||||
type="text"
|
onChange={handleSearchChange}
|
||||||
placeholder="جستجوی کارزار..."
|
className="pr-10 w-full"
|
||||||
value={searchQuery}
|
/>
|
||||||
onChange={handleSearchChange}
|
<Search
|
||||||
className="pr-10"
|
size={20}
|
||||||
/>
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400"
|
||||||
<Search
|
/>
|
||||||
size={20}
|
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create Campaign Button */}
|
|
||||||
<CustomButton
|
<CustomButton
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => setIsCreateModalOpen(true)}
|
onClick={() => setIsCreateModalOpen(true)}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<Plus size={20} />
|
<Plus size={20} />
|
||||||
ایجاد کارزار
|
<span>ایجاد کارزار</span>
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
<div className="mb-6">
|
||||||
<div className="mb-6 flex gap-2 overflow-x-auto pb-2 border-b border-gray-200">
|
<div className="flex gap-2 overflow-x-auto pb-2 border-b border-gray-200">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
onClick={() => handleTabChange(tab.value)}
|
onClick={() => handleTabChange(tab.value)}
|
||||||
className={`px-4 py-2 text-sm font-medium whitespace-nowrap rounded-lg transition-all ${
|
className={`px-4 py-2 text-sm font-medium whitespace-nowrap rounded-t-lg transition-colors ${
|
||||||
activeTab === tab.value
|
activeTab === tab.value
|
||||||
? "bg-blue-500 text-white border-b-2 border-blue-600"
|
? "bg-white border-gray-200 border-t border-x text-blue-600"
|
||||||
: "text-slate-600 hover:text-slate-800 hover:bg-gray-200"
|
: "text-slate-600 hover:text-slate-800 hover:bg-gray-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Loading State */}
|
<main>
|
||||||
{isLoading && (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
renderSkeleton()
|
||||||
<Loader size={40} className="text-blue-500 animate-spin" />
|
) : currentCampaign.length > 0 ? (
|
||||||
</div>
|
<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>
|
||||||
|
|
||||||
{/* Campaigns Grid */}
|
<div className="flex-1 text-right">
|
||||||
{!isLoading && campaigns.length > 0 && (
|
<h3 className="text-lg font-semibold text-slate-800">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 max-h-96 overflow-y-auto">
|
{campaign.title}
|
||||||
{currentCampaign.length > 0 ? (
|
</h3>
|
||||||
currentCampaign.map((campaign) => (
|
|
||||||
<CampaignCard
|
|
||||||
key={`${campaign.WorkflowID}-${campaign.title}`}
|
|
||||||
campaign={campaign}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-gray-500 mx-auto mt-20">
|
|
||||||
کارزاری یافت نشد
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Empty State */}
|
<div className="flex flex-row gap-0.5">
|
||||||
{!isLoading && campaigns.length === 0 && (
|
<span className="text-sm text-gray-500 ">
|
||||||
<div className="flex flex-col items-center justify-center py-12">
|
ایجاد کننده :
|
||||||
<p className="text-slate-600 text-lg mb-4">
|
</span>
|
||||||
{activeTab === "my"
|
<p className="text-sm text-gray-500 truncate">
|
||||||
? "هنوز کارزاری ایجاد نکردهاید"
|
{campaign.user_id_nickname}
|
||||||
: "کارزاری یافت نشد"}
|
</p>
|
||||||
</p>
|
</div>
|
||||||
{activeTab === "my" && (
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
<CustomButton
|
{Number(campaign.signature_count)} عضو
|
||||||
variant="primary"
|
</p>
|
||||||
onClick={() => setIsCreateModalOpen(true)}
|
</div>
|
||||||
>
|
|
||||||
ایجاد اولین کارزار خود
|
<div className="flex flex-col gap-2">
|
||||||
</CustomButton>
|
{activeTab === "منتخب" && (
|
||||||
)}
|
<CustomButton
|
||||||
</div>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Create Campaign Modal */}
|
|
||||||
<CreateCampaignModal
|
<CreateCampaignModal
|
||||||
isOpen={isCreateModalOpen}
|
isOpen={isCreateModalOpen}
|
||||||
onClose={() => setIsCreateModalOpen(false)}
|
onClose={() => setIsCreateModalOpen(false)}
|
||||||
|
|
|
||||||
150
src/modules/dashboard/pages/join-to-campaing/index.tsx
Normal file
150
src/modules/dashboard/pages/join-to-campaing/index.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import { BaseDropdown } from "@/core/components/base/base-drop-down";
|
||||||
|
|
||||||
|
import { CustomButton } from "@/core/components/base/button";
|
||||||
|
import { CustomInput } from "@/core/components/base/input";
|
||||||
|
import {
|
||||||
|
createCircleService,
|
||||||
|
searchUsersService,
|
||||||
|
} from "@modules/dashboard/service/campaigns.service";
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { User } from "./join-to-campaing";
|
||||||
|
|
||||||
|
export const JoinToCampaing = () => {
|
||||||
|
const [circleName, setCircleName] = useState("");
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [selectedMembers, setSelectedMembers] = useState<User[]>([]);
|
||||||
|
const [errors, setErrors] = useState<{
|
||||||
|
circleName?: string;
|
||||||
|
members?: string;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
const { data: users } = useQuery({
|
||||||
|
queryKey: ["users", searchTerm],
|
||||||
|
queryFn: () => searchUsersService(searchTerm),
|
||||||
|
enabled: !!searchTerm,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: createCircle, isPending: isCreating } = useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
createCircleService(
|
||||||
|
circleName,
|
||||||
|
selectedMembers.map((m) => m.user_id)
|
||||||
|
),
|
||||||
|
onSuccess: () => {
|
||||||
|
setCircleName("");
|
||||||
|
setSelectedMembers([]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddMember = (userId: string) => {
|
||||||
|
const user = users?.find((u) => u.user_id === userId);
|
||||||
|
if (user && !selectedMembers.some((m) => m.user_id === userId)) {
|
||||||
|
setSelectedMembers([...selectedMembers, user]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveMember = (userId: string) => {
|
||||||
|
setSelectedMembers(selectedMembers.filter((m) => m.user_id !== userId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkRequired = () => {
|
||||||
|
const newErrors: { circleName?: string; members?: string } = {};
|
||||||
|
if (!circleName.trim()) {
|
||||||
|
newErrors.circleName = "نام محفل الزامی است";
|
||||||
|
}
|
||||||
|
if (selectedMembers.length === 0) {
|
||||||
|
newErrors.members = "انتخاب حداقل یک عضو الزامی است";
|
||||||
|
}
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (checkRequired()) {
|
||||||
|
createCircle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-md mx-auto p-4" style={{ direction: "rtl" }}>
|
||||||
|
<h1 className="text-xl font-bold mb-4 text-right">ایجاد محفل جدید</h1>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<CustomInput
|
||||||
|
label="نام محفل"
|
||||||
|
placeholder="نام محفل را وارد کنید"
|
||||||
|
value={circleName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCircleName(e.target.value);
|
||||||
|
if (errors.circleName) {
|
||||||
|
setErrors((prev) => ({ ...prev, circleName: undefined }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
error={errors.circleName}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<BaseDropdown
|
||||||
|
label="افزودن عضو"
|
||||||
|
placeholder="جستجوی لقب عضو..."
|
||||||
|
options={
|
||||||
|
users?.map((user) => ({
|
||||||
|
value: user.user_id,
|
||||||
|
label: user.nickname,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
onInputChange={(inputValue) => setSearchTerm(inputValue)}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleAddMember(value);
|
||||||
|
if (errors.members) {
|
||||||
|
setErrors((prev) => ({ ...prev, members: undefined }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
error={errors.members}
|
||||||
|
/>
|
||||||
|
<div className="mt-2 text-xs text-gray-500 text-right">
|
||||||
|
برای جستجو، شروع به تایپ کنید.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedMembers.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold mb-2 text-right">
|
||||||
|
اعضای انتخاب شده
|
||||||
|
</h2>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{selectedMembers.map((member) => (
|
||||||
|
<li
|
||||||
|
key={member.user_id}
|
||||||
|
className="flex items-center justify-between bg-gray-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<span>{member.nickname}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveMember(member.user_id)}
|
||||||
|
className="text-red-500 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<CustomButton
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="w-full"
|
||||||
|
disabled={isCreating}
|
||||||
|
>
|
||||||
|
تایید و ایجاد محفل
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JoinToCampaing;
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type User = {
|
||||||
|
user_id: string;
|
||||||
|
nickname: string;
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export interface RegistrationFormData {
|
export interface RegistrationFormData {
|
||||||
|
ValueP1224S1943StageID?: Number;
|
||||||
username?: string;
|
username?: string;
|
||||||
WorkflowID?: string;
|
WorkflowID?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,6 @@ export const DASHBOARD_ROUTE = {
|
||||||
dashboard: "main",
|
dashboard: "main",
|
||||||
profile: "profile",
|
profile: "profile",
|
||||||
campaigns: "campaigns",
|
campaigns: "campaigns",
|
||||||
|
campaing: "campaing",
|
||||||
|
joinToCampaing: "join-to-campaing",
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { AppRoute } from "@core/types/router.type";
|
||||||
import { DashboardLayout } from "../layouts";
|
import { DashboardLayout } from "../layouts";
|
||||||
import CampaignsPage from "../pages/campaigns";
|
import CampaignsPage from "../pages/campaigns";
|
||||||
import CampaignDetailPage from "../pages/campaigns/detail";
|
import CampaignDetailPage from "../pages/campaigns/detail";
|
||||||
|
import JoinToCampaing from "../pages/join-to-campaing";
|
||||||
import DashboardPage from "../pages/main-page";
|
import DashboardPage from "../pages/main-page";
|
||||||
import ProfilePage from "../pages/profile";
|
import ProfilePage from "../pages/profile";
|
||||||
import { DASHBOARD_ROUTE } from "./route.constant";
|
import { DASHBOARD_ROUTE } from "./route.constant";
|
||||||
|
|
@ -24,9 +25,13 @@ export const dashboardRoutes: AppRoute[] = [
|
||||||
element: <CampaignsPage />,
|
element: <CampaignsPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${DASHBOARD_ROUTE.campaigns}/:id`,
|
path: `${DASHBOARD_ROUTE.campaing}/:id`,
|
||||||
element: <CampaignDetailPage />,
|
element: <CampaignDetailPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: `${DASHBOARD_ROUTE.joinToCampaing}/:id`,
|
||||||
|
element: <JoinToCampaing />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import type {
|
||||||
CreateCampaignData,
|
CreateCampaignData,
|
||||||
SignatureItem,
|
SignatureItem,
|
||||||
} from "@modules/dashboard/pages/campaigns/campaigns.type";
|
} from "@modules/dashboard/pages/campaigns/campaigns.type";
|
||||||
|
import type { User } from "@modules/dashboard/pages/join-to-campaing/join-to-campaing";
|
||||||
import to from "await-to-js";
|
import to from "await-to-js";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
|
@ -57,7 +58,7 @@ export const getSignsCampaignService = async (
|
||||||
): Promise<SignatureItem[]> => {
|
): Promise<SignatureItem[]> => {
|
||||||
const query = {
|
const query = {
|
||||||
ProcessName: "signature",
|
ProcessName: "signature",
|
||||||
OutputFields: ["user_stage_id", "user_id.nickname"],
|
OutputFields: ["user_stage_id", "user_id.nickname", "user_id.username"],
|
||||||
conditions: [["campaign", "=", campaignId]],
|
conditions: [["campaign", "=", campaignId]],
|
||||||
};
|
};
|
||||||
const [err, res] = await to(api.post(API_ADDRESS.select, query));
|
const [err, res] = await to(api.post(API_ADDRESS.select, query));
|
||||||
|
|
@ -150,7 +151,6 @@ export const getSelectedCampaignsService = async (
|
||||||
["status", "!=", "غیر فعال"],
|
["status", "!=", "غیر فعال"],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const [err, res] = await to(api.post(API_ADDRESS.select, query));
|
const [err, res] = await to(api.post(API_ADDRESS.select, query));
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
|
|
@ -178,7 +178,6 @@ export const createCampaignService = async (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const body = {
|
const body = {
|
||||||
ProcessName: "campaign",
|
|
||||||
campaign: {
|
campaign: {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
|
@ -187,7 +186,7 @@ export const createCampaignService = async (
|
||||||
status: "فعال",
|
status: "فعال",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const [err, res] = await to(api.post(API_ADDRESS.select, body));
|
const [err, res] = await to(api.post(API_ADDRESS.save, body));
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
@ -205,13 +204,13 @@ export const signCampaignService = async (
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const user = userInfoService.getUserInfo();
|
const user = userInfoService.getUserInfo();
|
||||||
const body = {
|
const body = {
|
||||||
ProcessName: "signature",
|
|
||||||
signature: {
|
signature: {
|
||||||
campaign: campaignId,
|
campaign: campaignId,
|
||||||
user_id: user.username,
|
user_id: user.WorkflowID,
|
||||||
|
status: "فعال",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const [err, res] = await to(api.post(API_ADDRESS.select, body));
|
const [err, res] = await to(api.post(API_ADDRESS.save, body));
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
@ -265,3 +264,62 @@ export const removeCommentService = async (
|
||||||
throw new Error("خطا در حذف نظر");
|
throw new Error("خطا در حذف نظر");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const searchUsersService = async (nickname: string): Promise<User[]> => {
|
||||||
|
if (!nickname) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
ProcessName: "users",
|
||||||
|
OutputFields: ["nickname", "user_id"],
|
||||||
|
conditions: [["nickname", "like", `%${nickname}%`]],
|
||||||
|
};
|
||||||
|
|
||||||
|
const [err, res] = await to(api.post(API_ADDRESS.select, query));
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
toast.error("خطا در جستجوی کاربر");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.data.resultType !== 0) {
|
||||||
|
toast.error("خطا در جستجوی کاربر");
|
||||||
|
throw new Error("خطا در جستجوی کاربر");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(res.data.data);
|
||||||
|
return data || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createCircleService = async (
|
||||||
|
circleName: string,
|
||||||
|
memberIds: string[]
|
||||||
|
): Promise<any> => {
|
||||||
|
const user = userInfoService.getUserInfo();
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
ProcessName: "circle",
|
||||||
|
circle: {
|
||||||
|
circle_name: circleName,
|
||||||
|
creator_id: user.username,
|
||||||
|
status: "فعال",
|
||||||
|
},
|
||||||
|
members: memberIds.map((id) => ({ user_id: id })),
|
||||||
|
};
|
||||||
|
|
||||||
|
const [err, res] = await to(api.post(API_ADDRESS.save, body));
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
toast.error("خطا در ایجاد محفل");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.data.resultType !== 0) {
|
||||||
|
toast.error(res.data.message || "خطا در ایجاد محفل");
|
||||||
|
throw new Error("خطا در ایجاد محفل");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("محفل با موفقیت ایجاد شد");
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user