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": {
|
"campaigns": {
|
||||||
"title": "کمپینها",
|
"title": "کارزارها",
|
||||||
"subtitle": "برای تغییر جهان، کمپین ایجاد کنید و دیگران را دعوت کنید",
|
"subtitle": "برای تغییر جهان، کارزار ایجاد کنید و دیگران را دعوت کنید",
|
||||||
"search": "جستجوی کمپین...",
|
"search": "جستجوی کارزار...",
|
||||||
"create": "ایجاد کمپین",
|
"create": "ایجاد کارزار",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "تمام کمپینها",
|
"all": "تمام کارزارها",
|
||||||
"my": "کمپینهای من",
|
"my": "کارزارهای من",
|
||||||
"top": "کمپینهای برتر",
|
"top": "کارزارهای برتر",
|
||||||
"group": "کمپینهای گروه"
|
"group": "کارزارهای گروه"
|
||||||
},
|
},
|
||||||
"createModal": {
|
"createModal": {
|
||||||
"title": "ایجاد کمپین جدید",
|
"title": "ایجاد کارزار جدید",
|
||||||
"titleLabel": "عنوان کمپین",
|
"titleLabel": "عنوان کارزار",
|
||||||
"titlePlaceholder": "عنوان کمپین را وارد کنید",
|
"titlePlaceholder": "عنوان کارزار را وارد کنید",
|
||||||
"description": "توضیحات",
|
"description": "توضیحات",
|
||||||
"descriptionPlaceholder": "توضیحات کمپین را وارد کنید (حداقل 20 کاراکتر)",
|
"descriptionPlaceholder": "توضیحات کارزار را وارد کنید (حداقل 20 کاراکتر)",
|
||||||
"image": "تصویر کمپین",
|
"image": "تصویر کارزار",
|
||||||
"upload": "کلیک کنید یا تصویر را بکشید",
|
"upload": "کلیک کنید یا تصویر را بکشید",
|
||||||
"cancel": "لغو",
|
"cancel": "لغو",
|
||||||
"submit": "ایجاد کمپین",
|
"submit": "ایجاد کارزار",
|
||||||
"submitting": "در حال ایجاد...",
|
"submitting": "در حال ایجاد...",
|
||||||
"errors": {
|
"errors": {
|
||||||
"titleRequired": "عنوان کمپین الزامی است",
|
"titleRequired": "عنوان کارزار الزامی است",
|
||||||
"descriptionRequired": "توضیحات الزامی است",
|
"descriptionRequired": "توضیحات الزامی است",
|
||||||
"descriptionMinLength": "توضیحات باید حداقل 20 کاراکتر باشد",
|
"descriptionMinLength": "توضیحات باید حداقل 20 کاراکتر باشد",
|
||||||
"imageRequired": "تصویر الزامی است"
|
"imageRequired": "تصویر الزامی است"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"detail": {
|
"detail": {
|
||||||
"back": "بازگشت به کمپینها",
|
"back": "بازگشت به کارزارها",
|
||||||
"by": "توسط",
|
"by": "توسط",
|
||||||
"signatures": "امضا",
|
"signatures": "امضا",
|
||||||
"comments": "نظر",
|
"comments": "نظر",
|
||||||
"sign": "امضای کمپین",
|
"sign": "امضای کارزار",
|
||||||
"signing": "در حال امضا...",
|
"signing": "در حال امضا...",
|
||||||
"signed": "شما امضا کردهاید ✓",
|
"signed": "شما امضا کردهاید ✓",
|
||||||
"signers": "امضاکنندگان",
|
"signers": "امضاکنندگان",
|
||||||
|
|
@ -85,17 +85,17 @@
|
||||||
"addComment": "نظر خود را بنویسید...",
|
"addComment": "نظر خود را بنویسید...",
|
||||||
"send": "ارسال",
|
"send": "ارسال",
|
||||||
"sending": "ارسال...",
|
"sending": "ارسال...",
|
||||||
"notFound": "کمپین یافت نشد"
|
"notFound": "کارزار یافت نشد"
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"my": "هنوز کمپینی ایجاد نکردهاید",
|
"my": "هنوز کارزاری ایجاد نکردهاید",
|
||||||
"noResults": "کمپینی یافت نشد",
|
"noResults": "کارزاری یافت نشد",
|
||||||
"createFirst": "ایجاد اولین کمپین خود"
|
"createFirst": "ایجاد اولین کارزار خود"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"created": "کمپین با موفقیت ایجاد شد",
|
"created": "کارزار با موفقیت ایجاد شد",
|
||||||
"signed": "با موفقیت امضا کردید",
|
"signed": "با موفقیت امضا کردید",
|
||||||
"commentAdded": "نظر شما اضافه شد",
|
"commentAdded": "نظر شما اضافه شد",
|
||||||
"alreadySigned": "شما قبلاً این کمپین را امضا کردهاید",
|
"alreadySigned": "شما قبلاً این کارزار را امضا کردهاید",
|
||||||
"emptyComment": "لطفاً نظری بنویسید"
|
"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) {
|
}: ImageUploaderProps) {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: "w-24 h-24",
|
sm: "w-24 h-24",
|
||||||
md: "w-32 h-32",
|
md: "w-32 h-32",
|
||||||
|
|
@ -85,22 +84,33 @@ export function ImageUploader({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{previewImage ? (
|
{previewImage ? (
|
||||||
<div className="relative inline-block group">
|
<div className="flex flex-col items-start gap-3">
|
||||||
<img
|
<div className="relative inline-block group">
|
||||||
src={previewImage}
|
<img
|
||||||
alt="پیشنمایش تصویر"
|
src={previewImage}
|
||||||
className={cn(
|
alt="پیشنمایش تصویر"
|
||||||
"rounded-lg object-cover border-2 border-gray-300 shadow-sm",
|
className={cn(
|
||||||
sizeClasses[imageSize]
|
"rounded-lg object-cover border-2 border-gray-300 shadow-sm",
|
||||||
)}
|
sizeClasses[imageSize]
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRemove}
|
||||||
|
className="absolute -top-3 -right-3 bg-red-500 text-white rounded-full p-2 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-red-600 shadow-lg"
|
||||||
|
aria-label="حذف تصویر"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
className="absolute -top-3 -right-3 bg-red-500 text-white rounded-full p-2 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-red-600 shadow-lg"
|
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="حذف تصویر"
|
aria-label="حذف تصویر"
|
||||||
>
|
>
|
||||||
<X size={18} />
|
<X size={16} />
|
||||||
|
<span>حذف تصویر</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,20 @@ export interface CustomInputProps
|
||||||
variant?: "primary" | "info" | "error";
|
variant?: "primary" | "info" | "error";
|
||||||
error?: string;
|
error?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
required?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
|
const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
|
||||||
(
|
(
|
||||||
{ className, variant = "primary", error, label, disabled, ...props },
|
{
|
||||||
|
className,
|
||||||
|
variant = "primary",
|
||||||
|
error,
|
||||||
|
label,
|
||||||
|
disabled,
|
||||||
|
required,
|
||||||
|
...props
|
||||||
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const finalVariant = error ? "error" : variant;
|
const finalVariant = error ? "error" : variant;
|
||||||
|
|
@ -19,7 +28,7 @@ const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{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} {required && <span className="text-red-500">*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,27 @@ interface TextAreaFieldProps extends ComponentProps<"textarea"> {
|
||||||
label?: string;
|
label?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
minLength?: number;
|
minLength?: number;
|
||||||
|
required?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextAreaField = forwardRef<HTMLTextAreaElement, TextAreaFieldProps>(
|
const TextAreaField = forwardRef<HTMLTextAreaElement, TextAreaFieldProps>(
|
||||||
({ label, error, minLength = 40, className, placeholder, ...props }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
minLength = 40,
|
||||||
|
className,
|
||||||
|
placeholder,
|
||||||
|
required,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{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} {required && <span className="text-red-500">*</span>}
|
||||||
</label>
|
</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",
|
select: "/api/select",
|
||||||
save: "/api/save",
|
save: "/api/save",
|
||||||
|
delete: "/api/delete",
|
||||||
|
uploadImage: "/workflow/uploadImage",
|
||||||
// LOGIN: "/auth/login",
|
// LOGIN: "/auth/login",
|
||||||
// VERIFY_OTP: "/auth/verify-otp",
|
// VERIFY_OTP: "/auth/verify-otp",
|
||||||
// REGISTER: "/auth/register",
|
// REGISTER: "/auth/register",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { RegistrationFormData } from "@/modules/dashboard/pages/profile/profile.type";
|
import type { RegistrationFormData } from "@/modules/dashboard/pages/profile/profile.type";
|
||||||
|
import type { TokenInterface } from "@core/types/global.type";
|
||||||
|
|
||||||
class UserInfoService {
|
class UserInfoService {
|
||||||
getUserInfo(): RegistrationFormData {
|
getUserInfo(): RegistrationFormData {
|
||||||
|
|
@ -12,6 +13,15 @@ class UserInfoService {
|
||||||
updateUserInfo(updatedInfo: RegistrationFormData): void {
|
updateUserInfo(updatedInfo: RegistrationFormData): void {
|
||||||
localStorage.setItem("person", JSON.stringify(updatedInfo));
|
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();
|
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;
|
@apply bg-background text-foreground;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
padding: 1rem;
|
/* padding: 1rem; */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
font-family: "Vazir", "IranSans", -apple-system, BlinkMacSystemFont,
|
font-family: "Vazir", "IranSans", -apple-system, BlinkMacSystemFont,
|
||||||
|
|
|
||||||
43
src/main.tsx
43
src/main.tsx
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { AuthProvider, useAuth } from "@core/context/auth-context.tsx";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||||
|
|
@ -9,22 +10,38 @@ import { rootRoutes } from "./router/rootRoutes.ts";
|
||||||
const router = createBrowserRouter(rootRoutes);
|
const router = createBrowserRouter(rootRoutes);
|
||||||
const client = new QueryClient();
|
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(
|
createRoot(document.getElementById("root")!).render(
|
||||||
// <StrictMode>
|
// <StrictMode>
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
<RouterProvider router={router} />
|
<AuthProvider>
|
||||||
<ToastContainer
|
<AppEntry />
|
||||||
position="top-right"
|
<ToastContainer
|
||||||
autoClose={4000}
|
position="top-right"
|
||||||
hideProgressBar={false}
|
autoClose={4000}
|
||||||
newestOnTop={false}
|
hideProgressBar={false}
|
||||||
closeOnClick
|
newestOnTop={false}
|
||||||
rtl={true}
|
closeOnClick
|
||||||
pauseOnFocusLoss
|
rtl={true}
|
||||||
draggable
|
pauseOnFocusLoss
|
||||||
pauseOnHover
|
draggable
|
||||||
theme="light"
|
pauseOnHover
|
||||||
/>
|
theme="light"
|
||||||
|
/>
|
||||||
|
</AuthProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
// </StrictMode>
|
// </StrictMode>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getContactImageUrl } from "@/core/utils";
|
||||||
import { Heart } from "lucide-react";
|
import { Heart } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import type { Campaign } from "../pages/campaigns/campaigns.type";
|
import type { Campaign } from "../pages/campaigns/campaigns.type";
|
||||||
|
|
@ -11,17 +12,17 @@ export function CampaignCard({ campaign }: CampaignCardProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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"
|
className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-md hover:shadow-lg transition-shadow cursor-pointer h-full"
|
||||||
>
|
>
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
{/* <div className="relative h-48 bg-gray-200 overflow-hidden">
|
<div className="relative h-48 bg-gray-200 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src={campaign.image}
|
src={getContactImageUrl(campaign.ValueP1226S1951StageID)}
|
||||||
alt={campaign.title}
|
alt={`${campaign.title}-image`}
|
||||||
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
|
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
|
||||||
/>
|
/>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-4">
|
<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">
|
<h3 className="text-lg font-bold text-slate-800 mb-2 line-clamp-2 text-right">
|
||||||
{campaign.title}
|
{campaign.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Creator */}
|
{/* Creator */}
|
||||||
<p className="text-sm text-slate-600 mb-3 text-right">
|
<p className="text-sm text-slate-600 mb-3 text-right">
|
||||||
از طرف: {campaign.user_id}
|
از طرف: {campaign.user_id_nickname}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export function CreateCampaignModal({
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
if (!title.trim()) {
|
if (!title.trim()) {
|
||||||
newErrors.title = "عنوان کمپین الزامی است";
|
newErrors.title = "عنوان کارزار الزامی است";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!description.trim()) {
|
if (!description.trim()) {
|
||||||
|
|
@ -55,12 +55,12 @@ export function CreateCampaignModal({
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: CreateCampaignData) => createCampaignService(data),
|
mutationFn: (data: CreateCampaignData) => createCampaignService(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("کمپین با موفقیت ایجاد شد");
|
toast.success("کارزار با موفقیت ایجاد شد");
|
||||||
handleClose();
|
handleClose();
|
||||||
onSuccess();
|
onSuccess();
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
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">
|
<div className="flex flex-col gap-4 w-full max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-lg font-semibold text-center sm:text-right">
|
<DialogTitle className="text-lg font-semibold text-center sm:text-right">
|
||||||
ایجاد کمپین جدید
|
ایجاد کارزار جدید
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||||
{/* Title Input */}
|
{/* Title Input */}
|
||||||
<CustomInput
|
<CustomInput
|
||||||
label="عنوان کمپین"
|
label="عنوان کارزار"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="عنوان کمپین را وارد کنید"
|
placeholder="عنوان کارزار را وارد کنید"
|
||||||
|
required
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setTitle(e.target.value);
|
setTitle(e.target.value);
|
||||||
|
|
@ -121,9 +122,10 @@ export function CreateCampaignModal({
|
||||||
|
|
||||||
{/* Description Input */}
|
{/* Description Input */}
|
||||||
<TextAreaField
|
<TextAreaField
|
||||||
label="توضیحات کمپین"
|
label="توضیحات کارزار"
|
||||||
placeholder="توضیحات کمپین را وارد کنید (حداقل 20 کاراکتر)"
|
placeholder="توضیحات کارزار را وارد کنید (حداقل 20 کاراکتر)"
|
||||||
value={description}
|
value={description}
|
||||||
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setDescription(e.target.value);
|
setDescription(e.target.value);
|
||||||
if (errors.description)
|
if (errors.description)
|
||||||
|
|
@ -135,7 +137,7 @@ export function CreateCampaignModal({
|
||||||
|
|
||||||
{/* Image Upload */}
|
{/* Image Upload */}
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
label="تصویر کمپین"
|
label="تصویر کارزار"
|
||||||
previewImage={previewImage}
|
previewImage={previewImage}
|
||||||
onImageChange={(file) => {
|
onImageChange={(file) => {
|
||||||
if (file) {
|
if (file) {
|
||||||
|
|
@ -166,7 +168,7 @@ export function CreateCampaignModal({
|
||||||
disabled={createMutation.isPending}
|
disabled={createMutation.isPending}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
>
|
>
|
||||||
{createMutation.isPending ? "در حال ایجاد..." : "ایجاد کمپین"}
|
{createMutation.isPending ? "در حال ایجاد..." : "ایجاد کارزار"}
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
<CustomButton variant="info" onClick={handleClose}>
|
<CustomButton variant="info" onClick={handleClose}>
|
||||||
لغو
|
لغو
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
// layouts/DashboardLayout.tsx
|
// 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";
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
export function DashboardLayout() {
|
export function DashboardLayout() {
|
||||||
|
const user = userInfoService.getUserInfo();
|
||||||
return (
|
return (
|
||||||
<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 ">
|
<main className="flex-1 overflow-y-auto pb-14">
|
||||||
<Outlet />
|
<DashboardHeader
|
||||||
|
profileImageUrl={""}
|
||||||
|
fullName={`${user.name || "کاربر جدید"} ${user.family || ""}`}
|
||||||
|
coins={100}
|
||||||
|
/>
|
||||||
|
<div className="p-2">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
<MobileNavbar />
|
<MobileNavbar />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface Campaign {
|
||||||
nickname?: String;
|
nickname?: String;
|
||||||
signature_count: Number;
|
signature_count: Number;
|
||||||
comment_count?: Number;
|
comment_count?: Number;
|
||||||
|
user_id_nickname?: String;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Signer {
|
export interface Signer {
|
||||||
|
|
@ -34,4 +35,21 @@ export interface CreateCampaignData {
|
||||||
image: File;
|
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";
|
"use client";
|
||||||
|
|
||||||
import { CustomButton } from "@/core/components/base/button";
|
import { CustomButton } from "@/core/components/base/button";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import TextAreaField from "@/core/components/base/text-area";
|
||||||
import { ArrowRight, Heart, Loader, MessageCircle } from "lucide-react";
|
import { getContactImageUrl } from "@/core/utils";
|
||||||
import { useState } from "react";
|
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import {
|
import {
|
||||||
addCommentService,
|
addCommentService,
|
||||||
getCampaignDetailService,
|
getCommentsService,
|
||||||
|
getSelectedCampaignsService,
|
||||||
|
getSignsCampaignService,
|
||||||
|
removeCommentService,
|
||||||
signCampaignService,
|
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() {
|
export function CampaignDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [commentText, setCommentText] = useState("");
|
const [commentText, setCommentText] = useState("");
|
||||||
const [hasSignedCampaign, setHasSignedCampaign] = useState(false);
|
const [hasSignedCampaign, setHasSignedCampaign] = useState<boolean>(false);
|
||||||
|
const [currentComments, setCurrentComments] = useState<CommentsItem[]>([]);
|
||||||
|
|
||||||
const {
|
const { data: campaign, isLoading } = useQuery({
|
||||||
data: campaign,
|
|
||||||
isLoading,
|
|
||||||
refetch,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["campaign", id],
|
queryKey: ["campaign", id],
|
||||||
queryFn: () => getCampaignDetailService(id!),
|
queryFn: () => getSelectedCampaignsService(Number(id!)),
|
||||||
enabled: !!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({
|
const signMutation = useMutation({
|
||||||
mutationFn: () => signCampaignService(id!),
|
mutationFn: () => signCampaignService(id!),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("با موفقیت امضا کردید");
|
toast.success("با موفقیت امضا کردید");
|
||||||
setHasSignedCampaign(true);
|
setHasSignedCampaign(true);
|
||||||
refetch();
|
signRefetch();
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
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 = () => {
|
const handleSignCampaign = () => {
|
||||||
if (hasSignedCampaign) {
|
if (hasSignedCampaign) {
|
||||||
toast.info("شما قبلاً این کمپین را امضا کردهاید");
|
toast.info("شما قبلاً این کارزار را امضا کردهاید");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
signMutation.mutate();
|
signMutation.mutate();
|
||||||
|
|
@ -69,6 +107,17 @@ export function CampaignDetailPage() {
|
||||||
commentMutation.mutate(commentText);
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
||||||
|
|
@ -80,16 +129,16 @@ export function CampaignDetailPage() {
|
||||||
if (!campaign) {
|
if (!campaign) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 gap-4">
|
<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 onClick={() => navigate("/dashboard/campaigns")}>
|
||||||
بازگشت به کمپینها
|
بازگشت به کارزارها
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="max-w-4xl mx-auto">
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
<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"
|
className="flex items-center gap-2 text-blue-500 hover:text-blue-700 mb-6 transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowRight size={20} />
|
<ArrowRight size={20} />
|
||||||
<span>بازگشت به کمپینها</span>
|
<span>بازگشت به کارزارها</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Campaign Image */}
|
{/* 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
|
<img
|
||||||
src={campaign.image}
|
src={getContactImageUrl(campaign.ValueP1226S1951StageID)}
|
||||||
alt={campaign.title}
|
alt={`${campaign.title}-image`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Campaign Header */}
|
{/* Campaign Header */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
<div className="bg-white rounded-lg shadow-md p-6 mb-8 mt-6">
|
||||||
{/* Title */}
|
|
||||||
<h1 className="text-3xl font-bold text-slate-800 text-right mb-4">
|
<h1 className="text-3xl font-bold text-slate-800 text-right mb-4">
|
||||||
{campaign.title}
|
{campaign.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Creator Info */}
|
|
||||||
<p className="text-slate-600 text-right mb-6">
|
<p className="text-slate-600 text-right mb-6">
|
||||||
توسط:{" "}
|
توسط: <span className="font-semibold">{campaign.nickname}</span>
|
||||||
<span className="font-semibold">{campaign.creatorNickname}</span>
|
|
||||||
</p>
|
</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 justify-end gap-8 py-4 border-t border-b border-gray-200 mb-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-3xl font-bold text-red-500">
|
<span className="text-3xl font-bold text-red-500">
|
||||||
{campaign.signatures}
|
{signs?.length ?? 0}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col">
|
<Blinds size={20} fill="currentColor" className="text-red-500" />
|
||||||
<Heart size={24} className="text-red-500" fill="currentColor" />
|
|
||||||
<span className="text-xs text-slate-600">امضا</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-3xl font-bold text-blue-500">
|
<span className="text-3xl font-bold text-blue-500">
|
||||||
{campaign.comments.length}
|
{comments?.length ?? 0}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col">
|
<MessageCircle size={20} className="text-blue-500" />
|
||||||
<MessageCircle size={24} className="text-blue-500" />
|
|
||||||
<span className="text-xs text-slate-600">نظر</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="text-slate-700 text-right leading-relaxed mb-6">
|
<p className="text-slate-700 text-right leading-relaxed mb-6">
|
||||||
{campaign.description}
|
{campaign.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Sign Campaign Button */}
|
|
||||||
<CustomButton
|
<CustomButton
|
||||||
variant={hasSignedCampaign ? "info" : "primary"}
|
variant={hasSignedCampaign ? "info" : "primary"}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|
@ -160,7 +197,7 @@ export function CampaignDetailPage() {
|
||||||
? "در حال امضا..."
|
? "در حال امضا..."
|
||||||
: hasSignedCampaign
|
: hasSignedCampaign
|
||||||
? "شما امضا کردهاید ✓"
|
? "شما امضا کردهاید ✓"
|
||||||
: "امضای کمپین"}
|
: "امضای کارزار"}
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -170,32 +207,28 @@ export function CampaignDetailPage() {
|
||||||
امضاکنندگان
|
امضاکنندگان
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{campaign.signers.length > 0 ? (
|
{Array.isArray(signs) && signs.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-4 justify-end">
|
<div className="flex flex-wrap gap-4 justify-end">
|
||||||
{campaign.signers.map((signer) => (
|
{signs.map((signer) => (
|
||||||
<div
|
<div
|
||||||
key={signer.id}
|
key={`${signer.WorkflowID}-${signer.ValueP1227S1955StageID}`}
|
||||||
className="flex flex-col items-center gap-2"
|
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">
|
<div className="w-16 h-16 rounded-full overflow-hidden bg-gradient-to-br from-blue-400 to-blue-600">
|
||||||
{signer.avatar ? (
|
<img
|
||||||
<img
|
src={getContactImageUrl(signer.ValueP1227S1955StageID)}
|
||||||
src={signer.avatar}
|
alt={`${signer.user_id_nickname}-avatar`}
|
||||||
alt={signer.nickname}
|
className="w-full h-full object-cover"
|
||||||
className="w-full h-full rounded-full object-cover"
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
signer.nickname.charAt(0).toUpperCase()
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-600 text-center max-w-16 truncate">
|
<p className="text-sm text-slate-600 text-center max-w-16 truncate">
|
||||||
{signer.nickname}
|
{signer.user_id_nickname}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-slate-600 text-center py-8">
|
<p className="text-slate-600 text-center py-4">
|
||||||
هنوز کسی امضا نکرده است. شما میتوانید اولین نفر باشید!
|
هنوز کسی امضا نکرده است. شما میتوانید اولین نفر باشید!
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -210,15 +243,16 @@ export function CampaignDetailPage() {
|
||||||
{/* Add Comment Form */}
|
{/* Add Comment Form */}
|
||||||
<form
|
<form
|
||||||
onSubmit={handleAddComment}
|
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">
|
<div className="flex gap-4 flex-col sm:flex-row">
|
||||||
<input
|
<TextAreaField
|
||||||
type="text"
|
required
|
||||||
placeholder="نظر خود را بنویسید..."
|
placeholder="ثبت نظر شما..."
|
||||||
value={commentText}
|
value={commentText}
|
||||||
onChange={(e) => setCommentText(e.target.value)}
|
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
|
<CustomButton
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|
@ -231,22 +265,36 @@ export function CampaignDetailPage() {
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Comments List */}
|
{/* Comments List */}
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
{campaign.comments.length > 0 ? (
|
{currentComments && currentComments.length > 0 ? (
|
||||||
campaign.comments.map((comment) => (
|
currentComments.map((comment) => (
|
||||||
<div
|
<div
|
||||||
key={comment.id}
|
key={`${comment.WorkflowID}-${comment.ValueP1228S1959StageID}`}
|
||||||
className="p-4 bg-gray-50 rounded-lg border border-gray-200"
|
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">
|
<div className="flex-1 w-48">
|
||||||
<span className="text-sm text-slate-500">
|
<p className="font-semibold text-slate-800 text-right text-sm mb-2">
|
||||||
{new Date(comment.createdAt).toLocaleDateString("fa-IR")}
|
{comment.user_id_nickname}
|
||||||
</span>
|
</p>
|
||||||
<span className="font-semibold text-slate-800">
|
<div className="text-slate-700 text-right text-sm leading-relaxed">
|
||||||
{comment.authorNickname}
|
<p className="whitespace-normal wrap-break-word">
|
||||||
</span>
|
{comment.comment_text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-slate-700 text-right">{comment.text}</p>
|
|
||||||
|
<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>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ import { CustomInput } from "@/core/components/base/input";
|
||||||
import { userInfoService } from "@/core/service/user-info.service";
|
import { userInfoService } from "@/core/service/user-info.service";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Loader, Plus, Search } from "lucide-react";
|
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 { CampaignCard } from "../../components/campaign-card";
|
||||||
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 [activeTab, setActiveTab] = useState<CampaignTab>("all");
|
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>>([]);
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
|
@ -22,8 +22,8 @@ export function CampaignsPage() {
|
||||||
isLoading,
|
isLoading,
|
||||||
refetch,
|
refetch,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["campaigns", searchQuery],
|
queryKey: ["campaigns"],
|
||||||
queryFn: () => getCampaignsService(activeTab, searchQuery),
|
queryFn: getCampaignsService,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -33,36 +33,43 @@ export function CampaignsPage() {
|
||||||
}, [campaigns]);
|
}, [campaigns]);
|
||||||
|
|
||||||
const tabs: { value: CampaignTab; label: string; oreder: number }[] = [
|
const tabs: { value: CampaignTab; label: string; oreder: number }[] = [
|
||||||
{ oreder: 1, value: "all", label: "تمام کمپینها" },
|
{ oreder: 1, value: "فعال", label: "تمام کارزارها" },
|
||||||
{ oreder: 2, value: "my", label: "کمپینهای من" },
|
{ oreder: 2, value: "my", label: "کارزارهای من" },
|
||||||
{ oreder: 3, value: "top", label: "کمپینهای برتر" },
|
{ oreder: 3, value: "منتخب", label: "کارزارهای برتر" },
|
||||||
{ oreder: 4, value: "group", label: "کمپینهای گروه" },
|
{ oreder: 4, value: "group", label: "کارزارهای گروه" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setSearchQuery(e.target.value);
|
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) => {
|
const handleTabChange = (tab: CampaignTab) => {
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
const user = userInfoService.getUserInfo();
|
const user = userInfoService.getUserInfo();
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case "all":
|
case "فعال":
|
||||||
setCurrentCampaign(campaigns);
|
setCurrentCampaign(campaigns);
|
||||||
break;
|
break;
|
||||||
case "my":
|
case "my":
|
||||||
setCurrentCampaign(
|
setCurrentCampaign(
|
||||||
campaigns.filter(
|
campaigns.filter(
|
||||||
(campaign) =>
|
(campaign) => Number(campaign.user_id) === Number(user.WorkflowID)
|
||||||
Number(campaign.WorkflowID) === Number(user.WorkflowID)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "top":
|
case "منتخب":
|
||||||
setCurrentCampaign(
|
setCurrentCampaign(
|
||||||
[...campaigns].sort(
|
[...campaigns].filter((item, _) => item.status === "منتخب")
|
||||||
(a, b) => (b.signature_count || 0) - (a.signature_count || 0)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "group":
|
case "group":
|
||||||
|
|
@ -80,15 +87,15 @@ export function CampaignsPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6">
|
<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>
|
</h1>
|
||||||
<p className="text-slate-600 text-right">
|
<p className="text-slate-600 text-right">
|
||||||
برای تغییر جهان، کمپین ایجاد کنید و دیگران را دعوت کنید
|
برای تغییر جهان، کارزار ایجاد کنید و دیگران را دعوت کنید
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -99,7 +106,7 @@ export function CampaignsPage() {
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CustomInput
|
<CustomInput
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="جستجوی کمپین..."
|
placeholder="جستجوی کارزار..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
className="pr-10"
|
className="pr-10"
|
||||||
|
|
@ -118,7 +125,7 @@ export function CampaignsPage() {
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Plus size={20} />
|
<Plus size={20} />
|
||||||
ایجاد کمپین
|
ایجاد کارزار
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -167,15 +176,15 @@ export function CampaignsPage() {
|
||||||
<div className="flex flex-col items-center justify-center py-12">
|
<div className="flex flex-col items-center justify-center py-12">
|
||||||
<p className="text-slate-600 text-lg mb-4">
|
<p className="text-slate-600 text-lg mb-4">
|
||||||
{activeTab === "my"
|
{activeTab === "my"
|
||||||
? "هنوز کمپینی ایجاد نکردهاید"
|
? "هنوز کارزاری ایجاد نکردهاید"
|
||||||
: "کمپینی یافت نشد"}
|
: "کارزاری یافت نشد"}
|
||||||
</p>
|
</p>
|
||||||
{activeTab === "my" && (
|
{activeTab === "my" && (
|
||||||
<CustomButton
|
<CustomButton
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => setIsCreateModalOpen(true)}
|
onClick={() => setIsCreateModalOpen(true)}
|
||||||
>
|
>
|
||||||
ایجاد اولین کمپین خود
|
ایجاد اولین کارزار خود
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@ import {
|
||||||
} from "@/core/components/base/card";
|
} from "@/core/components/base/card";
|
||||||
import { ImageUploader } from "@/core/components/base/image-uploader";
|
import { ImageUploader } from "@/core/components/base/image-uploader";
|
||||||
import { CustomInput } from "@/core/components/base/input";
|
import { CustomInput } from "@/core/components/base/input";
|
||||||
|
import { getContactImageUrl } from "@/core/utils";
|
||||||
import { AUTH_ROUTE } from "@/modules/auth/routes/route.constant";
|
import { AUTH_ROUTE } from "@/modules/auth/routes/route.constant";
|
||||||
import {
|
import {
|
||||||
fetchUserProfile,
|
fetchUserProfile,
|
||||||
updateUserProfile,
|
updateUserProfile,
|
||||||
} from "@modules/dashboard/service/user.service";
|
} from "@modules/dashboard/service/user.service";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
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 { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import type { RegistrationFormData } from "./profile.type";
|
import type { RegistrationFormData } from "./profile.type";
|
||||||
|
|
@ -29,8 +30,6 @@ export function RegisterPage() {
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ["userProfile"],
|
queryKey: ["userProfile"],
|
||||||
queryFn: fetchUserProfile,
|
queryFn: fetchUserProfile,
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [formData, setFormData] = useState<RegistrationFormData>({
|
const [formData, setFormData] = useState<RegistrationFormData>({
|
||||||
|
|
@ -58,11 +57,13 @@ export function RegisterPage() {
|
||||||
nationalcode: data.nationalcode || "",
|
nationalcode: data.nationalcode || "",
|
||||||
base: data.base || "",
|
base: data.base || "",
|
||||||
}));
|
}));
|
||||||
|
if (data.name)
|
||||||
|
setPreviewImage(getContactImageUrl((data as any).stageID) ?? "");
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
const [previewImage, setPreviewImage] = useState<string>("");
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
@ -147,7 +148,6 @@ export function RegisterPage() {
|
||||||
toast.error(data.message || "خطایی رخ داد");
|
toast.error(data.message || "خطایی رخ داد");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success("ثبت نام با موفقیت انجام شد");
|
toast.success("ثبت نام با موفقیت انجام شد");
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["userProfile"],
|
queryKey: ["userProfile"],
|
||||||
|
|
@ -188,7 +188,7 @@ export function RegisterPage() {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
|
|
@ -199,9 +199,10 @@ export function RegisterPage() {
|
||||||
registerMutation.mutate(formData);
|
registerMutation.mutate(formData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logOut = () => {
|
const logOut = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
navigate(`${AUTH_ROUTE.sub}/${AUTH_ROUTE.LOGIN}`);
|
window.location.href = `${AUTH_ROUTE.sub}/${AUTH_ROUTE.LOGIN}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -331,13 +332,14 @@ export function RegisterPage() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
label="عکس پروفایل (اختیاری)"
|
label="عکس پروفایل "
|
||||||
previewImage={previewImage}
|
previewImage={previewImage}
|
||||||
onImageChange={(file) => {
|
onImageChange={(file) => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
setPreviewImage("");
|
setPreviewImage("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setFormData((prev) => ({ ...prev, image: file }));
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
setPreviewImage(reader.result as string);
|
setPreviewImage(reader.result as string);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export const dashboardRoutes: AppRoute[] = [
|
||||||
element: <CampaignsPage />,
|
element: <CampaignsPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "campaigns/:id",
|
path: `${DASHBOARD_ROUTE.campaigns}/:id`,
|
||||||
element: <CampaignDetailPage />,
|
element: <CampaignDetailPage />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
import { API_ADDRESS } from "@/core/service/api-address";
|
import { API_ADDRESS } from "@/core/service/api-address";
|
||||||
import api from "@/core/service/axios";
|
import api from "@/core/service/axios";
|
||||||
import { userInfoService } from "@/core/service/user-info.service";
|
import { userInfoService } from "@/core/service/user-info.service";
|
||||||
import to from "await-to-js";
|
import { uploadImage } from "@/core/utils";
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import type {
|
import type {
|
||||||
Campaign,
|
Campaign,
|
||||||
|
CommentsItem,
|
||||||
CreateCampaignData,
|
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 (
|
export const getCampaignsService = async (): Promise<Campaign[]> => {
|
||||||
tab: string,
|
|
||||||
search?: string
|
|
||||||
): Promise<Campaign[]> => {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append("tab", tab);
|
|
||||||
if (search) params.append("search", search);
|
|
||||||
const userStr = userInfoService.getUserInfo();
|
const userStr = userInfoService.getUserInfo();
|
||||||
const query = {
|
const query = {
|
||||||
ProcessName: "campaign",
|
ProcessName: "campaign",
|
||||||
|
|
@ -28,11 +25,11 @@ export const getCampaignsService = async (
|
||||||
"status",
|
"status",
|
||||||
"school_code",
|
"school_code",
|
||||||
"signature_count",
|
"signature_count",
|
||||||
// "comment_count",
|
"comment_count",
|
||||||
],
|
],
|
||||||
conditions: [
|
conditions: [
|
||||||
["school_code", "=", userStr.school_code, "or"],
|
["school_code", "=", userStr.school_code, "or"],
|
||||||
["user_id", "=", "", "and"],
|
// ["user_id", "=", "", "and"],
|
||||||
["status", "!=", "حذف شده", "and"],
|
["status", "!=", "حذف شده", "and"],
|
||||||
["status", "!=", "غیر فعال"],
|
["status", "!=", "غیر فعال"],
|
||||||
],
|
],
|
||||||
|
|
@ -47,35 +44,147 @@ export const getCampaignsService = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.data.resultType !== 0) {
|
if (res.data.resultType !== 0) {
|
||||||
toast.error("خطا در دریافت کمپینها");
|
toast.error("خطا در دریافت کارزارها");
|
||||||
throw new Error("خطا در دریافت کمپینها");
|
throw new Error("خطا در دریافت کارزارها");
|
||||||
}
|
}
|
||||||
const data = JSON.parse(res.data.data);
|
const data = JSON.parse(res.data.data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCampaignDetailService = async (
|
export const getSignsCampaignService = async (
|
||||||
campaignId: string
|
campaignId: string
|
||||||
): Promise<Campaign> => {
|
): Promise<SignatureItem[]> => {
|
||||||
const [err, res] = await to(api.get(`/campaigns/${campaignId}`));
|
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) {
|
if (err) {
|
||||||
throw 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 (
|
export const createCampaignService = async (
|
||||||
data: CreateCampaignData
|
data: CreateCampaignData
|
||||||
): Promise<Campaign> => {
|
): Promise<Campaign> => {
|
||||||
const user = userInfoService.getUserInfo();
|
const user = userInfoService.getUserInfo();
|
||||||
|
|
||||||
|
let saveImage = null;
|
||||||
|
if (data.image) {
|
||||||
|
saveImage = await uploadImage({
|
||||||
|
file: data.image as File,
|
||||||
|
name: "profile_picture",
|
||||||
|
});
|
||||||
|
}
|
||||||
const body = {
|
const body = {
|
||||||
ProcessName: "campaign",
|
ProcessName: "campaign",
|
||||||
campaign: {
|
campaign: {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
image: data.image,
|
image: saveImage ? saveImage.data.data : undefined,
|
||||||
user_id: user.username, // ورکفلو آی دی شخص
|
user_id: user.username,
|
||||||
|
status: "فعال",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const [err, res] = await to(api.post(API_ADDRESS.select, body));
|
const [err, res] = await to(api.post(API_ADDRESS.select, body));
|
||||||
|
|
@ -93,18 +202,66 @@ export const createCampaignService = async (
|
||||||
|
|
||||||
export const signCampaignService = async (
|
export const signCampaignService = async (
|
||||||
campaignId: string
|
campaignId: string
|
||||||
): Promise<Campaign> => {
|
): Promise<void> => {
|
||||||
const response = await api.post(`/campaigns/${campaignId}/sign`);
|
const user = userInfoService.getUserInfo();
|
||||||
return response.data;
|
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 (
|
export const addCommentService = async (
|
||||||
campaignId: string,
|
campaignId: string,
|
||||||
text: string
|
text: string
|
||||||
): Promise<Campaign> => {
|
): Promise<void> => {
|
||||||
const response = await api.post(`/campaigns/${campaignId}/comments`, {
|
const user = userInfoService.getUserInfo();
|
||||||
text,
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
return response.data;
|
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_ADDRESS } from "@/core/service/api-address";
|
||||||
import api from "@/core/service/axios";
|
import api from "@/core/service/axios";
|
||||||
|
import { uploadImage } from "@/core/utils";
|
||||||
import type { RegistrationFormData } from "@modules/dashboard/pages/profile/profile.type";
|
import type { RegistrationFormData } from "@modules/dashboard/pages/profile/profile.type";
|
||||||
import { to } from "await-to-js";
|
import { to } from "await-to-js";
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ export const fetchUserProfile = async () => {
|
||||||
"invitor",
|
"invitor",
|
||||||
"nationalcode",
|
"nationalcode",
|
||||||
],
|
],
|
||||||
conditions: [["username", "=", person.ID]],
|
conditions: [["username", "=", person.ID ? person.ID : person.username]],
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await api.post(API_ADDRESS.select, query);
|
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];
|
const user = JSON.parse(res.data.data)[0];
|
||||||
if (user) localStorage.setItem("person", JSON.stringify(user));
|
if (user) localStorage.setItem("person", JSON.stringify(user));
|
||||||
return {
|
return {
|
||||||
username: user.username,
|
stageID: user?.ValueP1224S1943StageID,
|
||||||
name: user.name,
|
username: user?.username,
|
||||||
family: user.family,
|
name: user?.name,
|
||||||
educationLevel: user.education_level,
|
family: user?.family,
|
||||||
base: user.base,
|
educationLevel: user?.education_level,
|
||||||
userType: user.account_type,
|
base: user?.base,
|
||||||
nickname: user.nickname,
|
userType: user?.account_type,
|
||||||
schoolCode: user.school_code,
|
nickname: user?.nickname,
|
||||||
invitor: user.invitor,
|
schoolCode: user?.school_code,
|
||||||
nationalcode: user.nationalcode,
|
invitor: user?.invitor,
|
||||||
|
nationalcode: user?.nationalcode,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -52,21 +54,29 @@ export const updateUserProfile = async (data: RegistrationFormData) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const person = JSON.parse(personStr);
|
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 = {
|
let payload = {
|
||||||
|
...(nationalCode && { WorkflowID: person.WorkflowID }),
|
||||||
user: {
|
user: {
|
||||||
username: String(person.ID),
|
username: person.ID ? String(person.ID) : person.username,
|
||||||
name: data.name.trim(),
|
name: data.name.trim(),
|
||||||
family: data.family.trim(),
|
family: data.family.trim(),
|
||||||
nickname: data.nickname.trim() || undefined,
|
nickname: data.nickname.trim() || undefined,
|
||||||
education_level: data.education_level,
|
education_level: data.education_level,
|
||||||
base: data.base,
|
base: data.base,
|
||||||
|
image: saveImage?.data?.data,
|
||||||
account_type: "عادی",
|
account_type: "عادی",
|
||||||
nationalcode: data.nationalcode,
|
nationalcode: data.nationalcode,
|
||||||
|
...(saveImage?.data?.data && { image: saveImage?.data?.data }),
|
||||||
...(data.school_code && { school_code: data.school_code.trim() }),
|
...(data.school_code && { school_code: data.school_code.trim() }),
|
||||||
...(data.invitor && { invitor: data.invitor.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