feat: enhance dashboard layout with user profile header and mobile navbar
feat: update campaign types to include user_id_nickname and add comments and signature item interfaces feat: improve campaign detail page with comments functionality and remove comment feature feat: refactor campaigns page to use new campaign service and update tab labels to Persian feat: enhance user profile page with image upload functionality and improved form handling fix: update router paths for campaign detail page feat: implement user authentication context and protected route handling feat: add global types for token management feat: create utility functions for image handling and uploading
This commit is contained in:
parent
ce4c33d46d
commit
f9ced9349b
|
|
@ -43,40 +43,40 @@
|
|||
}
|
||||
},
|
||||
"campaigns": {
|
||||
"title": "کمپینها",
|
||||
"subtitle": "برای تغییر جهان، کمپین ایجاد کنید و دیگران را دعوت کنید",
|
||||
"search": "جستجوی کمپین...",
|
||||
"create": "ایجاد کمپین",
|
||||
"title": "کارزارها",
|
||||
"subtitle": "برای تغییر جهان، کارزار ایجاد کنید و دیگران را دعوت کنید",
|
||||
"search": "جستجوی کارزار...",
|
||||
"create": "ایجاد کارزار",
|
||||
"tabs": {
|
||||
"all": "تمام کمپینها",
|
||||
"my": "کمپینهای من",
|
||||
"top": "کمپینهای برتر",
|
||||
"group": "کمپینهای گروه"
|
||||
"all": "تمام کارزارها",
|
||||
"my": "کارزارهای من",
|
||||
"top": "کارزارهای برتر",
|
||||
"group": "کارزارهای گروه"
|
||||
},
|
||||
"createModal": {
|
||||
"title": "ایجاد کمپین جدید",
|
||||
"titleLabel": "عنوان کمپین",
|
||||
"titlePlaceholder": "عنوان کمپین را وارد کنید",
|
||||
"title": "ایجاد کارزار جدید",
|
||||
"titleLabel": "عنوان کارزار",
|
||||
"titlePlaceholder": "عنوان کارزار را وارد کنید",
|
||||
"description": "توضیحات",
|
||||
"descriptionPlaceholder": "توضیحات کمپین را وارد کنید (حداقل 20 کاراکتر)",
|
||||
"image": "تصویر کمپین",
|
||||
"descriptionPlaceholder": "توضیحات کارزار را وارد کنید (حداقل 20 کاراکتر)",
|
||||
"image": "تصویر کارزار",
|
||||
"upload": "کلیک کنید یا تصویر را بکشید",
|
||||
"cancel": "لغو",
|
||||
"submit": "ایجاد کمپین",
|
||||
"submit": "ایجاد کارزار",
|
||||
"submitting": "در حال ایجاد...",
|
||||
"errors": {
|
||||
"titleRequired": "عنوان کمپین الزامی است",
|
||||
"titleRequired": "عنوان کارزار الزامی است",
|
||||
"descriptionRequired": "توضیحات الزامی است",
|
||||
"descriptionMinLength": "توضیحات باید حداقل 20 کاراکتر باشد",
|
||||
"imageRequired": "تصویر الزامی است"
|
||||
}
|
||||
},
|
||||
"detail": {
|
||||
"back": "بازگشت به کمپینها",
|
||||
"back": "بازگشت به کارزارها",
|
||||
"by": "توسط",
|
||||
"signatures": "امضا",
|
||||
"comments": "نظر",
|
||||
"sign": "امضای کمپین",
|
||||
"sign": "امضای کارزار",
|
||||
"signing": "در حال امضا...",
|
||||
"signed": "شما امضا کردهاید ✓",
|
||||
"signers": "امضاکنندگان",
|
||||
|
|
@ -85,17 +85,17 @@
|
|||
"addComment": "نظر خود را بنویسید...",
|
||||
"send": "ارسال",
|
||||
"sending": "ارسال...",
|
||||
"notFound": "کمپین یافت نشد"
|
||||
"notFound": "کارزار یافت نشد"
|
||||
},
|
||||
"empty": {
|
||||
"my": "هنوز کمپینی ایجاد نکردهاید",
|
||||
"noResults": "کمپینی یافت نشد",
|
||||
"createFirst": "ایجاد اولین کمپین خود"
|
||||
"my": "هنوز کارزاری ایجاد نکردهاید",
|
||||
"noResults": "کارزاری یافت نشد",
|
||||
"createFirst": "ایجاد اولین کارزار خود"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "کمپین با موفقیت ایجاد شد",
|
||||
"created": "کارزار با موفقیت ایجاد شد",
|
||||
"signed": "با موفقیت امضا کردید",
|
||||
"commentAdded": "نظر شما اضافه شد",
|
||||
"alreadySigned": "شما قبلاً این کمپین را امضا کردهاید",
|
||||
"alreadySigned": "شما قبلاً این کارزار را امضا کردهاید",
|
||||
"emptyComment": "لطفاً نظری بنویسید"
|
||||
}
|
||||
|
|
|
|||
81
src/core/components/base/dashboard-header.tsx
Normal file
81
src/core/components/base/dashboard-header.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { cn } from "@core/lib/utils";
|
||||
import { Coins, User } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export interface DashboardHeaderProps {
|
||||
profileImageUrl?: string;
|
||||
fullName: string;
|
||||
coins: number;
|
||||
className?: string;
|
||||
onProfileClick?: () => void;
|
||||
}
|
||||
|
||||
export function DashboardHeader({
|
||||
profileImageUrl,
|
||||
fullName,
|
||||
coins,
|
||||
className,
|
||||
onProfileClick,
|
||||
}: DashboardHeaderProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleProfileClick = () => {
|
||||
if (onProfileClick) {
|
||||
onProfileClick();
|
||||
} else {
|
||||
navigate("/dashboard/profile", { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"sticky top-0 z-40",
|
||||
"bg-white border-b border-gray-200 shadow-sm",
|
||||
"w-full",
|
||||
className
|
||||
)}
|
||||
dir="rtl"
|
||||
>
|
||||
<div className="flex items-center justify-between h-16 sm:px-6 lg:px-8">
|
||||
{/* RIGHT SIDE: Profile Image + Name */}
|
||||
<button
|
||||
onClick={handleProfileClick}
|
||||
className="flex items-center gap-3 hover:opacity-80 transition-opacity rounded-lg px-3 py-2 -ml-3"
|
||||
aria-label={`رفتن به پروفایل ${fullName}`}
|
||||
>
|
||||
{/* Profile Image */}
|
||||
<div className="shrink-0">
|
||||
{profileImageUrl ? (
|
||||
<img
|
||||
src={profileImageUrl}
|
||||
alt={fullName}
|
||||
className="w-10 h-10 rounded-full object-cover border-2 border-gray-200"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-linear-to-br from-blue-400 to-blue-600 flex items-center justify-center text-white">
|
||||
<User size={20} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* User Name */}
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="text-sm font-semibold text-slate-800 truncate max-w-24 sm:max-w-40">
|
||||
{fullName}
|
||||
</span>
|
||||
<span className="text-xs text-slate-500">کاربر</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* LEFT SIDE: Coins Badge */}
|
||||
<div className="flex items-center gap-2 bg-yellow-50 rounded-full px-4 py-2 border border-yellow-200 hover:bg-yellow-100 transition-colors ml-2">
|
||||
<span className="text-sm font-bold text-yellow-700">{coins}</span>
|
||||
<Coins size={18} className="text-yellow-600" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardHeader;
|
||||
|
|
@ -25,7 +25,6 @@ export function ImageUploader({
|
|||
}: ImageUploaderProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "w-24 h-24",
|
||||
md: "w-32 h-32",
|
||||
|
|
@ -85,6 +84,7 @@ export function ImageUploader({
|
|||
)}
|
||||
|
||||
{previewImage ? (
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<div className="relative inline-block group">
|
||||
<img
|
||||
src={previewImage}
|
||||
|
|
@ -103,6 +103,16 @@ export function ImageUploader({
|
|||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemove}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
|
||||
aria-label="حذف تصویر"
|
||||
>
|
||||
<X size={16} />
|
||||
<span>حذف تصویر</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<label
|
||||
onDragOver={handleDragOver}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,20 @@ export interface CustomInputProps
|
|||
variant?: "primary" | "info" | "error";
|
||||
error?: string;
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
|
||||
(
|
||||
{ className, variant = "primary", error, label, disabled, ...props },
|
||||
{
|
||||
className,
|
||||
variant = "primary",
|
||||
error,
|
||||
label,
|
||||
disabled,
|
||||
required,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const finalVariant = error ? "error" : variant;
|
||||
|
|
@ -19,7 +28,7 @@ const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
|
|||
<div className="w-full">
|
||||
{label && (
|
||||
<label className="mb-2 block text-sm font-medium text-foreground text-right">
|
||||
{label}
|
||||
{label} {required && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -5,15 +5,27 @@ interface TextAreaFieldProps extends ComponentProps<"textarea"> {
|
|||
label?: string;
|
||||
error?: string;
|
||||
minLength?: number;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const TextAreaField = forwardRef<HTMLTextAreaElement, TextAreaFieldProps>(
|
||||
({ label, error, minLength = 40, className, placeholder, ...props }, ref) => {
|
||||
(
|
||||
{
|
||||
label,
|
||||
error,
|
||||
minLength = 40,
|
||||
className,
|
||||
placeholder,
|
||||
required,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{label && (
|
||||
<label className="mb-2 block text-sm font-medium text-foreground text-right">
|
||||
{label}
|
||||
{label} {required && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { MobileNavbar, type NavItem } from "./mobile-navbar";
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
/**
|
||||
* MobileNavbar Component - Demo & Example Usage
|
||||
*
|
||||
* This file demonstrates how to use the MobileNavbar component
|
||||
* in your React application.
|
||||
*/
|
||||
|
||||
import { MobileNavbar, type NavItem } from "@/core/components/others";
|
||||
import { Home, MessageCircle, Settings, Star } from "lucide-react";
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 1: Using Default Navigation Items
|
||||
// ============================================================================
|
||||
|
||||
export function AppWithDefaultNavbar() {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<main className="flex-1 pb-20 md:pb-0">
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">داشبورد</h1>
|
||||
<p className="text-gray-600">
|
||||
سلام! روی دستگاه موبایل یک نوار ناویگیشن ثابت در پایین میبینید.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Default navbar with Profile, Group Chat, Ranking, Dashboard */}
|
||||
<MobileNavbar />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 2: Using Custom Navigation Items
|
||||
// ============================================================================
|
||||
|
||||
const customNavItems: NavItem[] = [
|
||||
{
|
||||
id: "home",
|
||||
label: "خانه",
|
||||
icon: <Home size={24} />,
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
id: "favorites",
|
||||
label: "نشانشدهها",
|
||||
icon: <Star size={24} />,
|
||||
path: "/favorites",
|
||||
},
|
||||
{
|
||||
id: "messages",
|
||||
label: "پیامها",
|
||||
icon: <MessageCircle size={24} />,
|
||||
path: "/messages",
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
label: "تنظیمات",
|
||||
icon: <Settings size={24} />,
|
||||
path: "/settings",
|
||||
},
|
||||
];
|
||||
|
||||
export function AppWithCustomNavbar() {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<main className="flex-1 pb-20 md:pb-0">
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">تطبیقشده</h1>
|
||||
<p className="text-gray-600">
|
||||
این مثال از آیتمهای ناویگیشن سفارشی استفاده میکند.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Custom navbar */}
|
||||
<MobileNavbar items={customNavItems} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 3: Using Custom Styling
|
||||
// ============================================================================
|
||||
|
||||
export function AppWithCustomStyling() {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<main className="flex-1 pb-20 md:pb-0">
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">سبک سفارشی</h1>
|
||||
<p className="text-gray-600">
|
||||
نوار ناویگیشن میتواند با کلاسهای اضافی سفارشیسازی شود.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Custom styling with dark theme */}
|
||||
<MobileNavbar className="bg-slate-900 border-slate-800" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 4: Full Layout with Content Padding
|
||||
// ============================================================================
|
||||
|
||||
export function FullAppLayout() {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen" dir="rtl">
|
||||
{/* Header (optional) */}
|
||||
<header className="bg-white border-b border-gray-200 sticky top-0 z-40">
|
||||
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||
<h1 className="text-xl font-bold text-slate-800">یاریگران</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content - Important: Add padding to prevent navbar overlap */}
|
||||
<main className="flex-1 pb-20 md:pb-0">
|
||||
<div className="max-w-7xl mx-auto px-4 py-6">
|
||||
<section className="mb-6">
|
||||
<h2 className="text-2xl font-bold mb-4">محتوای اصلی</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-blue-50 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">کارت ۱</h3>
|
||||
<p className="text-sm text-gray-600">محتوای نمونه</p>
|
||||
</div>
|
||||
<div className="p-4 bg-blue-50 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">کارت ۲</h3>
|
||||
<p className="text-sm text-gray-600">محتوای نمونه</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Mobile Navbar - Fixed at bottom */}
|
||||
<MobileNavbar />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FEATURE SHOWCASE
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Component Features:
|
||||
*
|
||||
* ✅ Mobile-First Design
|
||||
* - Fixed positioning at bottom on mobile (md: hidden)
|
||||
* - Responds to viewport size
|
||||
*
|
||||
* ✅ Automatic Route Tracking
|
||||
* - Highlights active tab based on current URL path
|
||||
* - Works with React Router DOM
|
||||
*
|
||||
* ✅ Touch-Friendly
|
||||
* - Minimum 44×44px tap area (w-16 h-16 = 64×64px)
|
||||
* - Haptic feedback on supported devices (10ms vibration)
|
||||
*
|
||||
* ✅ Visual Feedback
|
||||
* - Active tab: Blue background + blue text
|
||||
* - Icon scales to 110% when active
|
||||
* - Smooth 200ms transitions
|
||||
*
|
||||
* ✅ Accessibility
|
||||
* - ARIA labels on all buttons
|
||||
* - aria-current for active page indication
|
||||
* - RTL support for Persian text
|
||||
*
|
||||
* ✅ Safe Area Support
|
||||
* - Accounts for notched devices (iPhone, etc.)
|
||||
* - Automatically adjusts padding for safe insets
|
||||
*
|
||||
* ✅ Performance
|
||||
* - Lightweight component
|
||||
* - Minimal re-renders
|
||||
* - GPU-accelerated CSS transitions
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// INTEGRATION CHECKLIST
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 📋 To integrate MobileNavbar into your app:
|
||||
*
|
||||
* 1. ✅ Import component:
|
||||
* import { MobileNavbar } from "@/core/components/others";
|
||||
*
|
||||
* 2. ✅ Add to layout:
|
||||
* <MobileNavbar />
|
||||
*
|
||||
* 3. ✅ Add padding to main content:
|
||||
* <main className="pb-20 md:pb-0">...</main>
|
||||
*
|
||||
* Explanation:
|
||||
* - pb-20 = 5rem (80px) padding on mobile to accommodate navbar
|
||||
* - md:pb-0 = no padding on desktop (navbar hidden)
|
||||
*
|
||||
* 4. ✅ Ensure routes exist:
|
||||
* - /profile
|
||||
* - /group-chat
|
||||
* - /ranking
|
||||
* - /dashboard
|
||||
*
|
||||
* 5. ✅ Test on mobile:
|
||||
* - Browser DevTools mobile view
|
||||
* - Real mobile device
|
||||
* - Touch interactions
|
||||
*/
|
||||
26
src/core/components/others/require-Auth.tsx
Normal file
26
src/core/components/others/require-Auth.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// src/components/RequireAuth.tsx
|
||||
import { useAuth } from "@/core/context/auth-context";
|
||||
import { AUTH_ROUTE } from "@/modules/auth/routes/route.constant";
|
||||
import type { ReactNode } from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
|
||||
export function RequireAuth({ children }: { children: ReactNode }) {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="flex justify-center p-8">در حال بررسی...</div>;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<Navigate
|
||||
to={`${AUTH_ROUTE.sub}/${AUTH_ROUTE.LOGIN}`}
|
||||
state={{ from: location }}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
63
src/core/context/auth-context.tsx
Normal file
63
src/core/context/auth-context.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { AUTH_ROUTE } from "@/modules/auth/routes/route.constant";
|
||||
import { DASHBOARD_ROUTE } from "@/modules/dashboard/routes/route.constant";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
login: (token: string) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("token");
|
||||
setIsAuthenticated(!!token);
|
||||
const checkIsLogin =
|
||||
!window.location.pathname.includes(AUTH_ROUTE.LOGIN) && !token;
|
||||
|
||||
const decideRedirect = !!token && window.location.pathname === "/";
|
||||
if (checkIsLogin) {
|
||||
window.location.href = `${AUTH_ROUTE.sub}/${AUTH_ROUTE.LOGIN}`;
|
||||
}
|
||||
|
||||
if (decideRedirect) {
|
||||
window.location.href = `${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.campaigns}`;
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
const login = (token: string) => {
|
||||
localStorage.setItem("access_token", token);
|
||||
setIsAuthenticated(true);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.clear();
|
||||
setIsAuthenticated(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isAuthenticated, isLoading, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) throw new Error("useAuth must be used within AuthProvider");
|
||||
return context;
|
||||
};
|
||||
|
|
@ -6,6 +6,8 @@ export const API_ADDRESS = {
|
|||
},
|
||||
select: "/api/select",
|
||||
save: "/api/save",
|
||||
delete: "/api/delete",
|
||||
uploadImage: "/workflow/uploadImage",
|
||||
// LOGIN: "/auth/login",
|
||||
// VERIFY_OTP: "/auth/verify-otp",
|
||||
// REGISTER: "/auth/register",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { RegistrationFormData } from "@/modules/dashboard/pages/profile/profile.type";
|
||||
import type { TokenInterface } from "@core/types/global.type";
|
||||
|
||||
class UserInfoService {
|
||||
getUserInfo(): RegistrationFormData {
|
||||
|
|
@ -12,6 +13,15 @@ class UserInfoService {
|
|||
updateUserInfo(updatedInfo: RegistrationFormData): void {
|
||||
localStorage.setItem("person", JSON.stringify(updatedInfo));
|
||||
}
|
||||
|
||||
getToken(): TokenInterface | null {
|
||||
const tokenStr = localStorage.getItem("token");
|
||||
if (!tokenStr) {
|
||||
return null;
|
||||
}
|
||||
const tokenObj = JSON.parse(tokenStr);
|
||||
return tokenObj || null;
|
||||
}
|
||||
}
|
||||
|
||||
export const userInfoService = new UserInfoService();
|
||||
|
|
|
|||
6
src/core/types/global.type.ts
Normal file
6
src/core/types/global.type.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface TokenInterface {
|
||||
AccessToken: string;
|
||||
ExpAccessToken: string;
|
||||
RefreshToken: string;
|
||||
ExpRefreshToken: string;
|
||||
}
|
||||
45
src/core/utils/index.ts
Normal file
45
src/core/utils/index.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import api from "@/core/service/axios";
|
||||
import { userInfoService } from "@core/service/user-info.service";
|
||||
import to from "await-to-js";
|
||||
import { toast } from "react-toastify";
|
||||
import { API_ADDRESS } from "../service/api-address";
|
||||
|
||||
export const getContactImageUrl = (stageID: Number): string => {
|
||||
const token = userInfoService.getToken();
|
||||
if (!token) {
|
||||
throw new Error("توکن یافت نشد");
|
||||
}
|
||||
return `${
|
||||
import.meta.env.VITE_API_URL
|
||||
}/api/getimage?stageID=${stageID}&nameOrID=image&token=${token.AccessToken}`;
|
||||
};
|
||||
|
||||
export const uploadImage = async ({
|
||||
file,
|
||||
name,
|
||||
}: {
|
||||
file: File;
|
||||
name: string;
|
||||
}): Promise<{ name: string; data: any }> => {
|
||||
const formData = new FormData();
|
||||
formData.append("", file);
|
||||
const [err, res] = await to(
|
||||
api.post(API_ADDRESS.uploadImage, formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
})
|
||||
);
|
||||
|
||||
if (res?.data.resultType === 0) {
|
||||
return { name, data: res.data };
|
||||
}
|
||||
if (res?.data.resultType !== 0) {
|
||||
toast.error("خطا در بارگذاری تصویر");
|
||||
throw new Error(res?.data.message || "Upload failed");
|
||||
}
|
||||
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new Error(res?.data.message || "Upload failed");
|
||||
};
|
||||
|
|
@ -141,7 +141,7 @@
|
|||
@apply bg-background text-foreground;
|
||||
margin: 0 auto;
|
||||
max-width: 1280px;
|
||||
padding: 1rem;
|
||||
/* padding: 1rem; */
|
||||
box-sizing: border-box;
|
||||
direction: rtl;
|
||||
font-family: "Vazir", "IranSans", -apple-system, BlinkMacSystemFont,
|
||||
|
|
|
|||
19
src/main.tsx
19
src/main.tsx
|
|
@ -1,3 +1,4 @@
|
|||
import { AuthProvider, useAuth } from "@core/context/auth-context.tsx";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||
|
|
@ -9,10 +10,25 @@ import { rootRoutes } from "./router/rootRoutes.ts";
|
|||
const router = createBrowserRouter(rootRoutes);
|
||||
const client = new QueryClient();
|
||||
|
||||
function AppEntry() {
|
||||
const { isLoading } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
||||
<div className="text-xl text-slate-600">در حال بارگذاری...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
// <StrictMode>
|
||||
<QueryClientProvider client={client}>
|
||||
<RouterProvider router={router} />
|
||||
<AuthProvider>
|
||||
<AppEntry />
|
||||
<ToastContainer
|
||||
position="top-right"
|
||||
autoClose={4000}
|
||||
|
|
@ -25,6 +41,7 @@ createRoot(document.getElementById("root")!).render(
|
|||
pauseOnHover
|
||||
theme="light"
|
||||
/>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
// </StrictMode>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { getContactImageUrl } from "@/core/utils";
|
||||
import { Heart } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { Campaign } from "../pages/campaigns/campaigns.type";
|
||||
|
|
@ -11,17 +12,17 @@ export function CampaignCard({ campaign }: CampaignCardProps) {
|
|||
|
||||
return (
|
||||
<div
|
||||
onClick={() => navigate(`/dashboard/campaigns/${campaign.user_id}`)}
|
||||
onClick={() => navigate(`/dashboard/campaigns/${campaign.WorkflowID}`)}
|
||||
className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-md hover:shadow-lg transition-shadow cursor-pointer h-full"
|
||||
>
|
||||
{/* Image */}
|
||||
{/* <div className="relative h-48 bg-gray-200 overflow-hidden">
|
||||
<div className="relative h-48 bg-gray-200 overflow-hidden">
|
||||
<img
|
||||
src={campaign.image}
|
||||
alt={campaign.title}
|
||||
src={getContactImageUrl(campaign.ValueP1226S1951StageID)}
|
||||
alt={`${campaign.title}-image`}
|
||||
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
|
|
@ -29,10 +30,9 @@ export function CampaignCard({ campaign }: CampaignCardProps) {
|
|||
<h3 className="text-lg font-bold text-slate-800 mb-2 line-clamp-2 text-right">
|
||||
{campaign.title}
|
||||
</h3>
|
||||
|
||||
{/* Creator */}
|
||||
<p className="text-sm text-slate-600 mb-3 text-right">
|
||||
از طرف: {campaign.user_id}
|
||||
از طرف: {campaign.user_id_nickname}
|
||||
</p>
|
||||
|
||||
{/* Stats */}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export function CreateCampaignModal({
|
|||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!title.trim()) {
|
||||
newErrors.title = "عنوان کمپین الزامی است";
|
||||
newErrors.title = "عنوان کارزار الزامی است";
|
||||
}
|
||||
|
||||
if (!description.trim()) {
|
||||
|
|
@ -55,12 +55,12 @@ export function CreateCampaignModal({
|
|||
const createMutation = useMutation({
|
||||
mutationFn: (data: CreateCampaignData) => createCampaignService(data),
|
||||
onSuccess: () => {
|
||||
toast.success("کمپین با موفقیت ایجاد شد");
|
||||
toast.success("کارزار با موفقیت ایجاد شد");
|
||||
handleClose();
|
||||
onSuccess();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.message || "خطا در ایجاد کمپین");
|
||||
toast.error(error?.message || "خطا در ایجاد کارزار");
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -100,16 +100,17 @@ export function CreateCampaignModal({
|
|||
<div className="flex flex-col gap-4 w-full max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-semibold text-center sm:text-right">
|
||||
ایجاد کمپین جدید
|
||||
ایجاد کارزار جدید
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
{/* Title Input */}
|
||||
<CustomInput
|
||||
label="عنوان کمپین"
|
||||
label="عنوان کارزار"
|
||||
type="text"
|
||||
placeholder="عنوان کمپین را وارد کنید"
|
||||
placeholder="عنوان کارزار را وارد کنید"
|
||||
required
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
setTitle(e.target.value);
|
||||
|
|
@ -121,9 +122,10 @@ export function CreateCampaignModal({
|
|||
|
||||
{/* Description Input */}
|
||||
<TextAreaField
|
||||
label="توضیحات کمپین"
|
||||
placeholder="توضیحات کمپین را وارد کنید (حداقل 20 کاراکتر)"
|
||||
label="توضیحات کارزار"
|
||||
placeholder="توضیحات کارزار را وارد کنید (حداقل 20 کاراکتر)"
|
||||
value={description}
|
||||
required
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value);
|
||||
if (errors.description)
|
||||
|
|
@ -135,7 +137,7 @@ export function CreateCampaignModal({
|
|||
|
||||
{/* Image Upload */}
|
||||
<ImageUploader
|
||||
label="تصویر کمپین"
|
||||
label="تصویر کارزار"
|
||||
previewImage={previewImage}
|
||||
onImageChange={(file) => {
|
||||
if (file) {
|
||||
|
|
@ -166,7 +168,7 @@ export function CreateCampaignModal({
|
|||
disabled={createMutation.isPending}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{createMutation.isPending ? "در حال ایجاد..." : "ایجاد کمپین"}
|
||||
{createMutation.isPending ? "در حال ایجاد..." : "ایجاد کارزار"}
|
||||
</CustomButton>
|
||||
<CustomButton variant="info" onClick={handleClose}>
|
||||
لغو
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
// layouts/DashboardLayout.tsx
|
||||
import { MobileNavbar } from "@/core/components/others";
|
||||
import DashboardHeader from "@/core/components/base/dashboard-header";
|
||||
import { MobileNavbar } from "@/core/components/others/mobile-navbar";
|
||||
import { userInfoService } from "@/core/service/user-info.service";
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
export function DashboardLayout() {
|
||||
const user = userInfoService.getUserInfo();
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<main className="flex-1 overflow-y-auto ">
|
||||
<main className="flex-1 overflow-y-auto pb-14">
|
||||
<DashboardHeader
|
||||
profileImageUrl={""}
|
||||
fullName={`${user.name || "کاربر جدید"} ${user.family || ""}`}
|
||||
coins={100}
|
||||
/>
|
||||
<div className="p-2">
|
||||
<Outlet />
|
||||
</div>
|
||||
<MobileNavbar />
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface Campaign {
|
|||
nickname?: String;
|
||||
signature_count: Number;
|
||||
comment_count?: Number;
|
||||
user_id_nickname?: String;
|
||||
}
|
||||
|
||||
export interface Signer {
|
||||
|
|
@ -34,4 +35,21 @@ export interface CreateCampaignData {
|
|||
image: File;
|
||||
}
|
||||
|
||||
export type CampaignTab = "all" | "my" | "top" | "group";
|
||||
export type CampaignTab = "فعال" | "my" | "منتخب" | "group";
|
||||
|
||||
export interface CommentsItem {
|
||||
ValueP1228S1959StageID: Number;
|
||||
ValueP1228S1959ValueID: Number;
|
||||
WorkflowID: Number;
|
||||
comment_text: String;
|
||||
user_id_nickname: String;
|
||||
user_stage_id: String;
|
||||
}
|
||||
|
||||
export interface SignatureItem {
|
||||
ValueP1227S1955StageID: Number;
|
||||
ValueP1227S1955ValueID: Number;
|
||||
WorkflowID: Number;
|
||||
user_id_nickname: String;
|
||||
user_stage_id: String;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,69 @@
|
|||
"use client";
|
||||
|
||||
import { CustomButton } from "@/core/components/base/button";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { ArrowRight, Heart, Loader, MessageCircle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import TextAreaField from "@/core/components/base/text-area";
|
||||
import { getContactImageUrl } from "@/core/utils";
|
||||
import {
|
||||
addCommentService,
|
||||
getCampaignDetailService,
|
||||
getCommentsService,
|
||||
getSelectedCampaignsService,
|
||||
getSignsCampaignService,
|
||||
removeCommentService,
|
||||
signCampaignService,
|
||||
} from "../../service/campaigns.service";
|
||||
} from "@modules/dashboard/service/campaigns.service";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowRight,
|
||||
Blinds,
|
||||
Loader,
|
||||
MessageCircle,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import type { CommentsItem } from "./campaigns.type";
|
||||
|
||||
export function CampaignDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [commentText, setCommentText] = useState("");
|
||||
const [hasSignedCampaign, setHasSignedCampaign] = useState(false);
|
||||
const [hasSignedCampaign, setHasSignedCampaign] = useState<boolean>(false);
|
||||
const [currentComments, setCurrentComments] = useState<CommentsItem[]>([]);
|
||||
|
||||
const {
|
||||
data: campaign,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
const { data: campaign, isLoading } = useQuery({
|
||||
queryKey: ["campaign", id],
|
||||
queryFn: () => getCampaignDetailService(id!),
|
||||
queryFn: () => getSelectedCampaignsService(Number(id!)),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const { data: signs, refetch: signRefetch } = useQuery({
|
||||
queryKey: ["campaign-signs", id],
|
||||
queryFn: () => getSignsCampaignService(String(id)),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const { data: comments, refetch } = useQuery({
|
||||
queryKey: ["campaign-comments", id],
|
||||
queryFn: () => getCommentsService(String(id)),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (comments) {
|
||||
setCurrentComments(comments);
|
||||
}
|
||||
}, [comments]);
|
||||
|
||||
const signMutation = useMutation({
|
||||
mutationFn: () => signCampaignService(id!),
|
||||
onSuccess: () => {
|
||||
toast.success("با موفقیت امضا کردید");
|
||||
setHasSignedCampaign(true);
|
||||
refetch();
|
||||
signRefetch();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.message || "خطا در امضای کمپین");
|
||||
toast.error(error?.message || "خطا در امضای کارزار");
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -52,9 +79,20 @@ export function CampaignDetailPage() {
|
|||
},
|
||||
});
|
||||
|
||||
const removeCommentMutation = useMutation({
|
||||
mutationFn: (commentId: Number) => removeCommentService(commentId),
|
||||
onSuccess: () => {
|
||||
toast.success("نظر با موفقیت حذف شد");
|
||||
refetch();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.message || "خطا در حذف نظر");
|
||||
},
|
||||
});
|
||||
|
||||
const handleSignCampaign = () => {
|
||||
if (hasSignedCampaign) {
|
||||
toast.info("شما قبلاً این کمپین را امضا کردهاید");
|
||||
toast.info("شما قبلاً این کارزار را امضا کردهاید");
|
||||
return;
|
||||
}
|
||||
signMutation.mutate();
|
||||
|
|
@ -69,6 +107,17 @@ export function CampaignDetailPage() {
|
|||
commentMutation.mutate(commentText);
|
||||
};
|
||||
|
||||
const removeCommentHandler = (
|
||||
commentId: Number,
|
||||
e: React.MouseEvent
|
||||
): void => {
|
||||
e.preventDefault();
|
||||
setCurrentComments((prevComments) =>
|
||||
prevComments.filter((comment) => comment.WorkflowID !== commentId)
|
||||
);
|
||||
removeCommentMutation.mutate(commentId);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
||||
|
|
@ -80,16 +129,16 @@ export function CampaignDetailPage() {
|
|||
if (!campaign) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 gap-4">
|
||||
<p className="text-slate-600 text-lg">کمپین یافت نشد</p>
|
||||
<p className="text-slate-600 text-lg">کارزار یافت نشد</p>
|
||||
<CustomButton onClick={() => navigate("/dashboard/campaigns")}>
|
||||
بازگشت به کمپینها
|
||||
بازگشت به کارزارها
|
||||
</CustomButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4 sm:p-8" dir="rtl">
|
||||
<div className="min-h-screen bg-gray-50 p-4" dir="rtl">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Back Button */}
|
||||
<button
|
||||
|
|
@ -97,59 +146,47 @@ export function CampaignDetailPage() {
|
|||
className="flex items-center gap-2 text-blue-500 hover:text-blue-700 mb-6 transition-colors"
|
||||
>
|
||||
<ArrowRight size={20} />
|
||||
<span>بازگشت به کمپینها</span>
|
||||
<span>بازگشت به کارزارها</span>
|
||||
</button>
|
||||
|
||||
{/* Campaign Image */}
|
||||
<div className="mb-8 rounded-lg overflow-hidden h-96 bg-gray-200">
|
||||
<div className="relative h-48 bg-gray-200 overflow-hidden rounded-lg">
|
||||
<img
|
||||
src={campaign.image}
|
||||
alt={campaign.title}
|
||||
className="w-full h-full object-cover"
|
||||
src={getContactImageUrl(campaign.ValueP1226S1951StageID)}
|
||||
alt={`${campaign.title}-image`}
|
||||
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Campaign Header */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||
{/* Title */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8 mt-6">
|
||||
<h1 className="text-3xl font-bold text-slate-800 text-right mb-4">
|
||||
{campaign.title}
|
||||
</h1>
|
||||
|
||||
{/* Creator Info */}
|
||||
<p className="text-slate-600 text-right mb-6">
|
||||
توسط:{" "}
|
||||
<span className="font-semibold">{campaign.creatorNickname}</span>
|
||||
توسط: <span className="font-semibold">{campaign.nickname}</span>
|
||||
</p>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center justify-end gap-8 py-4 border-t border-b border-gray-200 mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-3xl font-bold text-red-500">
|
||||
{campaign.signatures}
|
||||
{signs?.length ?? 0}
|
||||
</span>
|
||||
<div className="flex flex-col">
|
||||
<Heart size={24} className="text-red-500" fill="currentColor" />
|
||||
<span className="text-xs text-slate-600">امضا</span>
|
||||
</div>
|
||||
<Blinds size={20} fill="currentColor" className="text-red-500" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-3xl font-bold text-blue-500">
|
||||
{campaign.comments.length}
|
||||
{comments?.length ?? 0}
|
||||
</span>
|
||||
<div className="flex flex-col">
|
||||
<MessageCircle size={24} className="text-blue-500" />
|
||||
<span className="text-xs text-slate-600">نظر</span>
|
||||
</div>
|
||||
<MessageCircle size={20} className="text-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-slate-700 text-right leading-relaxed mb-6">
|
||||
{campaign.description}
|
||||
</p>
|
||||
|
||||
{/* Sign Campaign Button */}
|
||||
<CustomButton
|
||||
variant={hasSignedCampaign ? "info" : "primary"}
|
||||
className="w-full"
|
||||
|
|
@ -160,7 +197,7 @@ export function CampaignDetailPage() {
|
|||
? "در حال امضا..."
|
||||
: hasSignedCampaign
|
||||
? "شما امضا کردهاید ✓"
|
||||
: "امضای کمپین"}
|
||||
: "امضای کارزار"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
|
|
@ -170,32 +207,28 @@ export function CampaignDetailPage() {
|
|||
امضاکنندگان
|
||||
</h2>
|
||||
|
||||
{campaign.signers.length > 0 ? (
|
||||
{Array.isArray(signs) && signs.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-4 justify-end">
|
||||
{campaign.signers.map((signer) => (
|
||||
{signs.map((signer) => (
|
||||
<div
|
||||
key={signer.id}
|
||||
key={`${signer.WorkflowID}-${signer.ValueP1227S1955StageID}`}
|
||||
className="flex flex-col items-center gap-2"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-full bg-linear-to-br from-blue-400 to-blue-600 flex items-center justify-center text-white font-bold text-xl">
|
||||
{signer.avatar ? (
|
||||
<div className="w-16 h-16 rounded-full overflow-hidden bg-gradient-to-br from-blue-400 to-blue-600">
|
||||
<img
|
||||
src={signer.avatar}
|
||||
alt={signer.nickname}
|
||||
className="w-full h-full rounded-full object-cover"
|
||||
src={getContactImageUrl(signer.ValueP1227S1955StageID)}
|
||||
alt={`${signer.user_id_nickname}-avatar`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
signer.nickname.charAt(0).toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 text-center max-w-16 truncate">
|
||||
{signer.nickname}
|
||||
{signer.user_id_nickname}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-600 text-center py-8">
|
||||
<p className="text-slate-600 text-center py-4">
|
||||
هنوز کسی امضا نکرده است. شما میتوانید اولین نفر باشید!
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -210,15 +243,16 @@ export function CampaignDetailPage() {
|
|||
{/* Add Comment Form */}
|
||||
<form
|
||||
onSubmit={handleAddComment}
|
||||
className="mb-8 pb-8 border-b border-gray-200"
|
||||
className="mb-6 pb-6 border-b border-gray-200"
|
||||
>
|
||||
<div className="flex gap-4 flex-col sm:flex-row">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="نظر خود را بنویسید..."
|
||||
<TextAreaField
|
||||
required
|
||||
placeholder="ثبت نظر شما..."
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
className="flex-1 rounded-lg border-2 border-gray-300 px-4 py-2 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
||||
className="flex-1 rounded-lg border-2 border-gray-300 px-4 py-2 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
<CustomButton
|
||||
variant="primary"
|
||||
|
|
@ -231,22 +265,36 @@ export function CampaignDetailPage() {
|
|||
</form>
|
||||
|
||||
{/* Comments List */}
|
||||
<div className="space-y-4">
|
||||
{campaign.comments.length > 0 ? (
|
||||
campaign.comments.map((comment) => (
|
||||
<div className="flex flex-col space-y-4">
|
||||
{currentComments && currentComments.length > 0 ? (
|
||||
currentComments.map((comment) => (
|
||||
<div
|
||||
key={comment.id}
|
||||
className="p-4 bg-gray-50 rounded-lg border border-gray-200"
|
||||
key={`${comment.WorkflowID}-${comment.ValueP1228S1959StageID}`}
|
||||
className="flex flex-row items-start justify-between overflow-auto gap-4 p-4 bg-gray-50 rounded-lg border border-gray-200 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-slate-500">
|
||||
{new Date(comment.createdAt).toLocaleDateString("fa-IR")}
|
||||
</span>
|
||||
<span className="font-semibold text-slate-800">
|
||||
{comment.authorNickname}
|
||||
</span>
|
||||
<div className="flex-1 w-48">
|
||||
<p className="font-semibold text-slate-800 text-right text-sm mb-2">
|
||||
{comment.user_id_nickname}
|
||||
</p>
|
||||
<div className="text-slate-700 text-right text-sm leading-relaxed">
|
||||
<p className="whitespace-normal wrap-break-word">
|
||||
{comment.comment_text}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-slate-700 text-right">{comment.text}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="text-gray-400 hover:text-red-500 transition-colors shrink-0"
|
||||
aria-label="حذف نظر"
|
||||
>
|
||||
<Trash2
|
||||
size={18}
|
||||
color="red"
|
||||
onClick={(e) =>
|
||||
removeCommentHandler(comment.WorkflowID, e)
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ import { CustomInput } from "@/core/components/base/input";
|
|||
import { userInfoService } from "@/core/service/user-info.service";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Loader, Plus, Search } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, type ChangeEvent } from "react";
|
||||
import { CampaignCard } from "../../components/campaign-card";
|
||||
import { CreateCampaignModal } from "../../components/create-campaign-modal";
|
||||
import { getCampaignsService } from "../../service/campaigns.service";
|
||||
import type { Campaign, CampaignTab } from "./campaigns.type";
|
||||
|
||||
export function CampaignsPage() {
|
||||
const [activeTab, setActiveTab] = useState<CampaignTab>("all");
|
||||
const [activeTab, setActiveTab] = useState<CampaignTab>("فعال");
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [currentCampaign, setCurrentCampaign] = useState<Array<Campaign>>([]);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
|
@ -22,8 +22,8 @@ export function CampaignsPage() {
|
|||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["campaigns", searchQuery],
|
||||
queryFn: () => getCampaignsService(activeTab, searchQuery),
|
||||
queryKey: ["campaigns"],
|
||||
queryFn: getCampaignsService,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -33,36 +33,43 @@ export function CampaignsPage() {
|
|||
}, [campaigns]);
|
||||
|
||||
const tabs: { value: CampaignTab; label: string; oreder: number }[] = [
|
||||
{ oreder: 1, value: "all", label: "تمام کمپینها" },
|
||||
{ oreder: 2, value: "my", label: "کمپینهای من" },
|
||||
{ oreder: 3, value: "top", label: "کمپینهای برتر" },
|
||||
{ oreder: 4, value: "group", label: "کمپینهای گروه" },
|
||||
{ oreder: 1, value: "فعال", label: "تمام کارزارها" },
|
||||
{ oreder: 2, value: "my", label: "کارزارهای من" },
|
||||
{ oreder: 3, value: "منتخب", label: "کارزارهای برتر" },
|
||||
{ oreder: 4, value: "group", label: "کارزارهای گروه" },
|
||||
];
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setActiveTab("فعال");
|
||||
const filteredCampaigns = campaigns.filter((campaign) =>
|
||||
campaign.title.toLowerCase().includes(e.target.value.toLowerCase())
|
||||
);
|
||||
if (e.target.value === "") {
|
||||
handleTabChange(activeTab);
|
||||
setCurrentCampaign(campaigns);
|
||||
return;
|
||||
}
|
||||
setCurrentCampaign(filteredCampaigns);
|
||||
};
|
||||
|
||||
const handleTabChange = (tab: CampaignTab) => {
|
||||
setActiveTab(tab);
|
||||
const user = userInfoService.getUserInfo();
|
||||
switch (tab) {
|
||||
case "all":
|
||||
case "فعال":
|
||||
setCurrentCampaign(campaigns);
|
||||
break;
|
||||
case "my":
|
||||
setCurrentCampaign(
|
||||
campaigns.filter(
|
||||
(campaign) =>
|
||||
Number(campaign.WorkflowID) === Number(user.WorkflowID)
|
||||
(campaign) => Number(campaign.user_id) === Number(user.WorkflowID)
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "top":
|
||||
case "منتخب":
|
||||
setCurrentCampaign(
|
||||
[...campaigns].sort(
|
||||
(a, b) => (b.signature_count || 0) - (a.signature_count || 0)
|
||||
)
|
||||
[...campaigns].filter((item, _) => item.status === "منتخب")
|
||||
);
|
||||
break;
|
||||
case "group":
|
||||
|
|
@ -80,15 +87,15 @@ export function CampaignsPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4 sm:p-8" dir="rtl">
|
||||
<div className="min-h-screen bg-gray-50 p-4 " dir="rtl">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-4xl font-bold text-slate-800 text-right mb-2">
|
||||
کمپینها
|
||||
<h1 className="text-4xl font-bold text-slate-800 text-right mb-6">
|
||||
کارزارها
|
||||
</h1>
|
||||
<p className="text-slate-600 text-right">
|
||||
برای تغییر جهان، کمپین ایجاد کنید و دیگران را دعوت کنید
|
||||
برای تغییر جهان، کارزار ایجاد کنید و دیگران را دعوت کنید
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -99,7 +106,7 @@ export function CampaignsPage() {
|
|||
<div className="relative">
|
||||
<CustomInput
|
||||
type="text"
|
||||
placeholder="جستجوی کمپین..."
|
||||
placeholder="جستجوی کارزار..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
className="pr-10"
|
||||
|
|
@ -118,7 +125,7 @@ export function CampaignsPage() {
|
|||
className="flex items-center gap-2"
|
||||
>
|
||||
<Plus size={20} />
|
||||
ایجاد کمپین
|
||||
ایجاد کارزار
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
|
|
@ -157,7 +164,9 @@ export function CampaignsPage() {
|
|||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-gray-500 mx-auto mt-20">کمپینی یافت نشد</div>
|
||||
<div className="text-gray-500 mx-auto mt-20">
|
||||
کارزاری یافت نشد
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -167,15 +176,15 @@ export function CampaignsPage() {
|
|||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<p className="text-slate-600 text-lg mb-4">
|
||||
{activeTab === "my"
|
||||
? "هنوز کمپینی ایجاد نکردهاید"
|
||||
: "کمپینی یافت نشد"}
|
||||
? "هنوز کارزاری ایجاد نکردهاید"
|
||||
: "کارزاری یافت نشد"}
|
||||
</p>
|
||||
{activeTab === "my" && (
|
||||
<CustomButton
|
||||
variant="primary"
|
||||
onClick={() => setIsCreateModalOpen(true)}
|
||||
>
|
||||
ایجاد اولین کمپین خود
|
||||
ایجاد اولین کارزار خود
|
||||
</CustomButton>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ import {
|
|||
} from "@/core/components/base/card";
|
||||
import { ImageUploader } from "@/core/components/base/image-uploader";
|
||||
import { CustomInput } from "@/core/components/base/input";
|
||||
import { getContactImageUrl } from "@/core/utils";
|
||||
import { AUTH_ROUTE } from "@/modules/auth/routes/route.constant";
|
||||
import {
|
||||
fetchUserProfile,
|
||||
updateUserProfile,
|
||||
} from "@modules/dashboard/service/user.service";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, type FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import type { RegistrationFormData } from "./profile.type";
|
||||
|
|
@ -29,8 +30,6 @@ export function RegisterPage() {
|
|||
const { data } = useQuery({
|
||||
queryKey: ["userProfile"],
|
||||
queryFn: fetchUserProfile,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState<RegistrationFormData>({
|
||||
|
|
@ -58,11 +57,13 @@ export function RegisterPage() {
|
|||
nationalcode: data.nationalcode || "",
|
||||
base: data.base || "",
|
||||
}));
|
||||
if (data.name)
|
||||
setPreviewImage(getContactImageUrl((data as any).stageID) ?? "");
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [previewImage, setPreviewImage] = useState<string>("");
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
|
@ -147,7 +148,6 @@ export function RegisterPage() {
|
|||
toast.error(data.message || "خطایی رخ داد");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("ثبت نام با موفقیت انجام شد");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["userProfile"],
|
||||
|
|
@ -188,7 +188,7 @@ export function RegisterPage() {
|
|||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
|
|
@ -199,9 +199,10 @@ export function RegisterPage() {
|
|||
registerMutation.mutate(formData);
|
||||
};
|
||||
|
||||
const logOut = () => {
|
||||
const logOut = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
localStorage.clear();
|
||||
navigate(`${AUTH_ROUTE.sub}/${AUTH_ROUTE.LOGIN}`);
|
||||
window.location.href = `${AUTH_ROUTE.sub}/${AUTH_ROUTE.LOGIN}`;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -331,13 +332,14 @@ export function RegisterPage() {
|
|||
/>
|
||||
|
||||
<ImageUploader
|
||||
label="عکس پروفایل (اختیاری)"
|
||||
label="عکس پروفایل "
|
||||
previewImage={previewImage}
|
||||
onImageChange={(file) => {
|
||||
if (!file) {
|
||||
setPreviewImage("");
|
||||
return;
|
||||
}
|
||||
setFormData((prev) => ({ ...prev, image: file }));
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setPreviewImage(reader.result as string);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const dashboardRoutes: AppRoute[] = [
|
|||
element: <CampaignsPage />,
|
||||
},
|
||||
{
|
||||
path: "campaigns/:id",
|
||||
path: `${DASHBOARD_ROUTE.campaigns}/:id`,
|
||||
element: <CampaignDetailPage />,
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
import { API_ADDRESS } from "@/core/service/api-address";
|
||||
import api from "@/core/service/axios";
|
||||
import { userInfoService } from "@/core/service/user-info.service";
|
||||
import to from "await-to-js";
|
||||
import { toast } from "react-toastify";
|
||||
import { uploadImage } from "@/core/utils";
|
||||
import type {
|
||||
Campaign,
|
||||
CommentsItem,
|
||||
CreateCampaignData,
|
||||
} from "../pages/campaigns/campaigns.type";
|
||||
SignatureItem,
|
||||
} from "@modules/dashboard/pages/campaigns/campaigns.type";
|
||||
import to from "await-to-js";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const getCampaignsService = async (
|
||||
tab: string,
|
||||
search?: string
|
||||
): Promise<Campaign[]> => {
|
||||
const params = new URLSearchParams();
|
||||
params.append("tab", tab);
|
||||
if (search) params.append("search", search);
|
||||
export const getCampaignsService = async (): Promise<Campaign[]> => {
|
||||
const userStr = userInfoService.getUserInfo();
|
||||
const query = {
|
||||
ProcessName: "campaign",
|
||||
|
|
@ -28,11 +25,11 @@ export const getCampaignsService = async (
|
|||
"status",
|
||||
"school_code",
|
||||
"signature_count",
|
||||
// "comment_count",
|
||||
"comment_count",
|
||||
],
|
||||
conditions: [
|
||||
["school_code", "=", userStr.school_code, "or"],
|
||||
["user_id", "=", "", "and"],
|
||||
// ["user_id", "=", "", "and"],
|
||||
["status", "!=", "حذف شده", "and"],
|
||||
["status", "!=", "غیر فعال"],
|
||||
],
|
||||
|
|
@ -47,35 +44,147 @@ export const getCampaignsService = async (
|
|||
}
|
||||
|
||||
if (res.data.resultType !== 0) {
|
||||
toast.error("خطا در دریافت کمپینها");
|
||||
throw new Error("خطا در دریافت کمپینها");
|
||||
toast.error("خطا در دریافت کارزارها");
|
||||
throw new Error("خطا در دریافت کارزارها");
|
||||
}
|
||||
const data = JSON.parse(res.data.data);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCampaignDetailService = async (
|
||||
export const getSignsCampaignService = async (
|
||||
campaignId: string
|
||||
): Promise<Campaign> => {
|
||||
const [err, res] = await to(api.get(`/campaigns/${campaignId}`));
|
||||
): Promise<SignatureItem[]> => {
|
||||
const query = {
|
||||
ProcessName: "signature",
|
||||
OutputFields: ["user_stage_id", "user_id.nickname"],
|
||||
conditions: [["campaign", "=", campaignId]],
|
||||
};
|
||||
const [err, res] = await to(api.post(API_ADDRESS.select, query));
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
return res?.data;
|
||||
|
||||
if (res.data.resultType !== 0) {
|
||||
toast.error("خطا در امضای کارزار");
|
||||
throw new Error("خطا در امضای کارزار");
|
||||
}
|
||||
|
||||
const data = JSON.parse(res.data.data);
|
||||
if (!data.length) {
|
||||
return [] as any;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCommentsCampaignService = async (
|
||||
campaignId: string
|
||||
): Promise<Campaign> => {
|
||||
const query = {
|
||||
ProcessName: "comment",
|
||||
OutputFields: ["author_id.nickname", "text", "createdAt"],
|
||||
conditions: [["campaign", "=", campaignId]],
|
||||
};
|
||||
const [err, res] = await to(api.post(API_ADDRESS.select, query));
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res.data.resultType !== 0) {
|
||||
toast.error("خطا در دریافت نظرات کارزار");
|
||||
throw new Error("خطا در دریافت نظرات کارزار");
|
||||
}
|
||||
|
||||
const data = JSON.parse(res.data.data);
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getCommentsService = async (
|
||||
campaignId: string
|
||||
): Promise<CommentsItem[]> => {
|
||||
const query = {
|
||||
ProcessName: "comment",
|
||||
OutputFields: ["user_stage_id", "user_id.nickname", "comment_text"],
|
||||
conditions: [
|
||||
["campaign", "=", campaignId],
|
||||
["status", "!=", "حذف شده", "and"],
|
||||
["status", "!=", "غیر فعال"],
|
||||
],
|
||||
};
|
||||
|
||||
const [err, res] = await to(api.post(API_ADDRESS.select, query));
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res.data.resultType !== 0) {
|
||||
toast.error("خطا در افزودن نظر");
|
||||
throw new Error("خطا در افزودن نظر");
|
||||
}
|
||||
|
||||
const data = JSON.parse(res.data.data);
|
||||
if (!data.length) {
|
||||
return [] as any;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getSelectedCampaignsService = async (
|
||||
campaignId: Number
|
||||
): Promise<Campaign> => {
|
||||
const query = {
|
||||
ProcessName: "campaign",
|
||||
OutputFields: [
|
||||
"title",
|
||||
"description",
|
||||
"image",
|
||||
"user_id",
|
||||
"user_id.nickname",
|
||||
"status",
|
||||
"signature_count",
|
||||
"comment_count",
|
||||
],
|
||||
conditions: [
|
||||
["WorkflowID", "=", campaignId, "and"],
|
||||
["status", "!=", "حذف شده", "and"],
|
||||
["status", "!=", "غیر فعال"],
|
||||
],
|
||||
};
|
||||
|
||||
const [err, res] = await to(api.post(API_ADDRESS.select, query));
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res.data.resultType !== 0) {
|
||||
toast.error("خطا در دریافت کارزار");
|
||||
throw new Error("خطا در دریافت کارزار");
|
||||
}
|
||||
|
||||
const data = JSON.parse(res.data.data);
|
||||
return data[0];
|
||||
};
|
||||
|
||||
export const createCampaignService = async (
|
||||
data: CreateCampaignData
|
||||
): Promise<Campaign> => {
|
||||
const user = userInfoService.getUserInfo();
|
||||
|
||||
let saveImage = null;
|
||||
if (data.image) {
|
||||
saveImage = await uploadImage({
|
||||
file: data.image as File,
|
||||
name: "profile_picture",
|
||||
});
|
||||
}
|
||||
const body = {
|
||||
ProcessName: "campaign",
|
||||
campaign: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
image: data.image,
|
||||
user_id: user.username, // ورکفلو آی دی شخص
|
||||
image: saveImage ? saveImage.data.data : undefined,
|
||||
user_id: user.username,
|
||||
status: "فعال",
|
||||
},
|
||||
};
|
||||
const [err, res] = await to(api.post(API_ADDRESS.select, body));
|
||||
|
|
@ -93,18 +202,66 @@ export const createCampaignService = async (
|
|||
|
||||
export const signCampaignService = async (
|
||||
campaignId: string
|
||||
): Promise<Campaign> => {
|
||||
const response = await api.post(`/campaigns/${campaignId}/sign`);
|
||||
return response.data;
|
||||
): Promise<void> => {
|
||||
const user = userInfoService.getUserInfo();
|
||||
const body = {
|
||||
ProcessName: "signature",
|
||||
signature: {
|
||||
campaign: campaignId,
|
||||
user_id: user.username,
|
||||
},
|
||||
};
|
||||
const [err, res] = await to(api.post(API_ADDRESS.select, body));
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res.data.resultType !== 0) {
|
||||
toast.error(res.data.message || "خطا در امضای کارزار");
|
||||
throw new Error("خطا در امضای کارزار");
|
||||
}
|
||||
};
|
||||
|
||||
export const addCommentService = async (
|
||||
campaignId: string,
|
||||
text: string
|
||||
): Promise<Campaign> => {
|
||||
const response = await api.post(`/campaigns/${campaignId}/comments`, {
|
||||
text,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
): Promise<void> => {
|
||||
const user = userInfoService.getUserInfo();
|
||||
const body = {
|
||||
comment: {
|
||||
campaign: campaignId,
|
||||
user_id: user.username,
|
||||
comment_text: text,
|
||||
status: "فعال",
|
||||
},
|
||||
};
|
||||
const [err, res] = await to(api.post(API_ADDRESS.save, body));
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res.data.resultType !== 0) {
|
||||
toast.error(res.data.message || "خطا در افزودن نظر");
|
||||
throw new Error("خطا در افزودن نظر");
|
||||
}
|
||||
};
|
||||
|
||||
export const removeCommentService = async (
|
||||
commentId: Number
|
||||
): Promise<void> => {
|
||||
const body = {
|
||||
WorkflowID: commentId,
|
||||
comment: {
|
||||
status: "حذف شده",
|
||||
},
|
||||
};
|
||||
const [err, res] = await to(api.post(API_ADDRESS.delete, body));
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res.data.resultType !== 0) {
|
||||
toast.error(res.data.message || "خطا در حذف نظر");
|
||||
throw new Error("خطا در حذف نظر");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { API_ADDRESS } from "@/core/service/api-address";
|
||||
import api from "@/core/service/axios";
|
||||
import { uploadImage } from "@/core/utils";
|
||||
import type { RegistrationFormData } from "@modules/dashboard/pages/profile/profile.type";
|
||||
import { to } from "await-to-js";
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ export const fetchUserProfile = async () => {
|
|||
"invitor",
|
||||
"nationalcode",
|
||||
],
|
||||
conditions: [["username", "=", person.ID]],
|
||||
conditions: [["username", "=", person.ID ? person.ID : person.username]],
|
||||
};
|
||||
|
||||
const res = await api.post(API_ADDRESS.select, query);
|
||||
|
|
@ -32,16 +33,17 @@ export const fetchUserProfile = async () => {
|
|||
const user = JSON.parse(res.data.data)[0];
|
||||
if (user) localStorage.setItem("person", JSON.stringify(user));
|
||||
return {
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
family: user.family,
|
||||
educationLevel: user.education_level,
|
||||
base: user.base,
|
||||
userType: user.account_type,
|
||||
nickname: user.nickname,
|
||||
schoolCode: user.school_code,
|
||||
invitor: user.invitor,
|
||||
nationalcode: user.nationalcode,
|
||||
stageID: user?.ValueP1224S1943StageID,
|
||||
username: user?.username,
|
||||
name: user?.name,
|
||||
family: user?.family,
|
||||
educationLevel: user?.education_level,
|
||||
base: user?.base,
|
||||
userType: user?.account_type,
|
||||
nickname: user?.nickname,
|
||||
schoolCode: user?.school_code,
|
||||
invitor: user?.invitor,
|
||||
nationalcode: user?.nationalcode,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -52,21 +54,29 @@ export const updateUserProfile = async (data: RegistrationFormData) => {
|
|||
}
|
||||
|
||||
const person = JSON.parse(personStr);
|
||||
const natinalCode = person.NationalCode;
|
||||
|
||||
const nationalCode = person.nationalcode;
|
||||
let saveImage = null;
|
||||
if (data.image) {
|
||||
saveImage = await uploadImage({
|
||||
file: data.image as File,
|
||||
name: "profile_picture",
|
||||
});
|
||||
}
|
||||
let payload = {
|
||||
...(nationalCode && { WorkflowID: person.WorkflowID }),
|
||||
user: {
|
||||
username: String(person.ID),
|
||||
username: person.ID ? String(person.ID) : person.username,
|
||||
name: data.name.trim(),
|
||||
family: data.family.trim(),
|
||||
nickname: data.nickname.trim() || undefined,
|
||||
education_level: data.education_level,
|
||||
base: data.base,
|
||||
image: saveImage?.data?.data,
|
||||
account_type: "عادی",
|
||||
nationalcode: data.nationalcode,
|
||||
...(saveImage?.data?.data && { image: saveImage?.data?.data }),
|
||||
...(data.school_code && { school_code: data.school_code.trim() }),
|
||||
...(data.invitor && { invitor: data.invitor.trim() }),
|
||||
...(natinalCode && { WorkflowID: person.ID }),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import React from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
element: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ProtectedRoute({ element }: ProtectedRouteProps) {
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
if (!token) return <Navigate to="/auth/login" replace />;
|
||||
|
||||
return <>{element}</>;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user