+
{Array.from({ length }).map((_, index) => (
{
- inputsRef.current[index] = el!;
+ inputsRef.current[index] = el;
}}
onChange={(e) => handleChange(e, index)}
onKeyDown={(e) => handleKeyDown(e, index)}
- className="w-12 h-12 text-center text-xl border rounded-md focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
+ className="w-12 h-12 text-center text-xl font-medium border-2 rounded-lg focus:border-blue-500 focus:outline-none focus:ring-4 focus:ring-blue-100 transition-all duration-200"
+ style={{ caretColor: "transparent" }} // اختیاری: برای زیبایی بیشتر
/>
))}
diff --git a/src/modules/auth/pages/login/index.tsx b/src/modules/auth/pages/login/index.tsx
index 8474696..5ab1392 100644
--- a/src/modules/auth/pages/login/index.tsx
+++ b/src/modules/auth/pages/login/index.tsx
@@ -14,19 +14,21 @@ import {
DialogTitle,
} from "@/core/components/base/dialog";
import { CustomInput } from "@/core/components/base/input";
-import { API_ADDRESS } from "@/core/service/api-address";
-import API from "@/core/service/axios";
import { OTPDialog } from "@modules/auth/components/otp/opt-dialog";
import { toast } from "react-toastify";
-import to from "await-to-js";
+import { DASHBOARD_ROUTE } from "@/modules/dashboard/routes/route.constant";
+import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { sendOtpService, verifyOtpService } from "../../service/auth.service";
export function LoginPage() {
+ const navigate = useNavigate();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [otpDialog, setOtpDialog] = useState(false);
- const [phoneNumber, setPhoneNumber] = useState("");
- const [otp, setOtp] = useState("");
+ const [phoneNumber, setPhoneNumber] = useState
("");
+ const [otp, setOtp] = useState("");
const [error, setError] = useState("");
const [submitLoading, setSubmitLoading] = useState(false);
@@ -43,28 +45,65 @@ export function LoginPage() {
return true;
};
- const handleSubmit = async () => {
- if (validatePhoneNumber(phoneNumber)) {
- setSubmitLoading(true);
- const [err, res] = await to(API.post(API_ADDRESS.auth.otp, phoneNumber));
+ const sendOtpMutation = useMutation({
+ mutationFn: sendOtpService,
+
+ onSuccess: (data) => {
setSubmitLoading(false);
-
- if (res?.data.resultType !== 0) {
- toast.error(res?.data.message);
- }
-
- if (res?.data.resultType === 0) {
- toast.success("ورود با موفقیت انجام شد");
- localStorage.setItem("token", res.data.data.token);
- }
-
- if (err) {
+ if (data.resultType !== 0) {
+ toast.error(data.message);
return;
}
+
+ toast.success("کد یکبار مصرف ارسال شد");
setIsDialogOpen(false);
setOtpDialog(true);
- setPhoneNumber("");
- }
+ // setPhoneNumber("");
+ },
+
+ onError: (error: any) => {
+ setSubmitLoading(false);
+ toast.error("مشکلی رخ داد");
+ console.log(error);
+ },
+ });
+
+ const verifyOtpMutation = useMutation({
+ mutationFn: verifyOtpService,
+
+ onSuccess: (data) => {
+ setSubmitLoading(false);
+ if (data.resultType !== 0) {
+ toast.error(data.message);
+ return;
+ }
+ toast.success("ورود با موفقیت انجام شد");
+ const person = JSON.parse(data.data).Person;
+ const token = JSON.parse(data.data).Token;
+ localStorage.setItem("token", JSON.stringify(token));
+ localStorage.setItem("person", JSON.stringify(person));
+ setOtpDialog(false);
+ if (person.NationalCode === "") {
+ navigate(`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.profile}`, {
+ replace: true,
+ });
+ } else {
+ navigate(`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.campaigns}`, {
+ replace: true,
+ });
+ }
+ },
+
+ onError: () => {
+ setSubmitLoading(false);
+ toast.error("خطا در تایید کد");
+ },
+ });
+
+ const handleSubmit = () => {
+ if (!validatePhoneNumber(phoneNumber)) return;
+ setSubmitLoading(true);
+ sendOtpMutation.mutate(phoneNumber);
};
const handleCancel = () => {
@@ -72,25 +111,12 @@ export function LoginPage() {
setError("");
};
- const handleOtpSubmit = async () => {
+ const handleOtpSubmit = () => {
setSubmitLoading(true);
- const [err, res] = await to(
- API.post(API_ADDRESS.auth.verifyOtp, { phoneNumber, otp })
- );
- if (res?.data.resultType !== 0) {
- toast.error(res?.data.message);
- }
-
- if (res?.data.resultType === 0) {
- toast.success("ورود با موفقیت انجام شد");
- localStorage.setItem("token", res.data.data.token);
- }
- setSubmitLoading(false);
-
- if (err) {
- return;
- }
- setOtpDialog(false);
+ verifyOtpMutation.mutate({
+ mobile: phoneNumber,
+ code: otp,
+ });
};
return (
@@ -99,12 +125,12 @@ export function LoginPage() {
- تماس با ما
+ ورود به سامانه
- برای ارتباط با تیم پشتیبانی، شماره تلفن خود را وارد کنید
+ برای ورود به سامانه بر روی دکمه ورود کلیک نمایید.{" "}
setIsDialogOpen(true)}
>
- وارد کردن شماره تلفن
+ ورود
diff --git a/src/modules/auth/pages/register/index.tsx b/src/modules/auth/pages/register/index.tsx
deleted file mode 100644
index ea7e6dc..0000000
--- a/src/modules/auth/pages/register/index.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-const RegisterPage = () => {
- return Register
;
-};
-
-export default RegisterPage;
diff --git a/src/modules/auth/routes/route.constant.ts b/src/modules/auth/routes/route.constant.ts
index 2670b1c..2dc22c1 100644
--- a/src/modules/auth/routes/route.constant.ts
+++ b/src/modules/auth/routes/route.constant.ts
@@ -1,4 +1,5 @@
export const AUTH_ROUTE = {
+ sub: "/auth",
LOGIN: "login",
REGISTER: "register",
};
diff --git a/src/modules/auth/routes/router.tsx b/src/modules/auth/routes/router.tsx
index 907ee48..0f59ec0 100644
--- a/src/modules/auth/routes/router.tsx
+++ b/src/modules/auth/routes/router.tsx
@@ -1,6 +1,6 @@
import type { AppRoute } from "@core/types/router.type";
+import RegisterPage from "../../dashboard/pages/profile";
import LoginPage from "../pages/login";
-import RegisterPage from "../pages/register";
import { AUTH_ROUTE } from "./route.constant";
export const authRoutes: AppRoute[] = [
diff --git a/src/modules/auth/service/auth.service.ts b/src/modules/auth/service/auth.service.ts
new file mode 100644
index 0000000..b3aa466
--- /dev/null
+++ b/src/modules/auth/service/auth.service.ts
@@ -0,0 +1,18 @@
+import { API_ADDRESS } from "@/core/service/api-address";
+import api from "@/core/service/axios";
+
+export const sendOtpService = async (phoneNumber: string) => {
+ const res = await api.post(API_ADDRESS.auth.otp, phoneNumber);
+ return res.data;
+};
+
+export const verifyOtpService = async ({
+ mobile,
+ code,
+}: {
+ mobile: string;
+ code: string;
+}) => {
+ const res = await api.post(API_ADDRESS.auth.verifyOtp, { mobile, code });
+ return res.data;
+};
diff --git a/src/modules/dashboard/components/campaign-card.tsx b/src/modules/dashboard/components/campaign-card.tsx
new file mode 100644
index 0000000..e7ac515
--- /dev/null
+++ b/src/modules/dashboard/components/campaign-card.tsx
@@ -0,0 +1,54 @@
+import { Heart } from "lucide-react";
+import { useNavigate } from "react-router-dom";
+import type { Campaign } from "../pages/campaigns/campaigns.type";
+
+interface CampaignCardProps {
+ campaign: Campaign;
+}
+
+export function CampaignCard({ campaign }: CampaignCardProps) {
+ const navigate = useNavigate();
+
+ return (
+ navigate(`/dashboard/campaigns/${campaign.user_id}`)}
+ className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-md hover:shadow-lg transition-shadow cursor-pointer h-full"
+ >
+ {/* Image */}
+ {/*
+

+
*/}
+
+ {/* Content */}
+
+ {/* Title */}
+
+ {campaign.title}
+
+
+ {/* Creator */}
+
+ از طرف: {campaign.user_id}
+
+
+ {/* Stats */}
+
+
+
+ {campaign.title}
+
+ {/*
+
+
+ {campaign.comments.length}
+
+
*/}
+
+
+
+ );
+}
diff --git a/src/modules/dashboard/components/create-campaign-modal.tsx b/src/modules/dashboard/components/create-campaign-modal.tsx
new file mode 100644
index 0000000..6eac89b
--- /dev/null
+++ b/src/modules/dashboard/components/create-campaign-modal.tsx
@@ -0,0 +1,178 @@
+import { CustomButton } from "@/core/components/base/button";
+import {
+ Dialog,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/core/components/base/dialog";
+import { ImageUploader } from "@/core/components/base/image-uploader";
+import { CustomInput } from "@/core/components/base/input";
+import TextAreaField from "@/core/components/base/text-area";
+import { useMutation } from "@tanstack/react-query";
+import { useState } from "react";
+import { toast } from "react-toastify";
+import type { CreateCampaignData } from "../pages/campaigns/campaigns.type";
+import { createCampaignService } from "../service/campaigns.service";
+
+interface CreateCampaignModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onSuccess: () => void;
+}
+
+export function CreateCampaignModal({
+ isOpen,
+ onClose,
+ onSuccess,
+}: CreateCampaignModalProps) {
+ const [title, setTitle] = useState("");
+ const [description, setDescription] = useState("");
+ const [imageFile, setImageFile] = useState(null);
+ const [previewImage, setPreviewImage] = useState("");
+ const [errors, setErrors] = useState>({});
+
+ const validateForm = (): boolean => {
+ const newErrors: Record = {};
+
+ if (!title.trim()) {
+ newErrors.title = "عنوان کمپین الزامی است";
+ }
+
+ if (!description.trim()) {
+ newErrors.description = "توضیحات الزامی است";
+ } else if (description.length < 20) {
+ newErrors.description = "توضیحات باید حداقل 20 کاراکتر باشد";
+ }
+
+ if (!imageFile) {
+ newErrors.image = "تصویر الزامی است";
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const createMutation = useMutation({
+ mutationFn: (data: CreateCampaignData) => createCampaignService(data),
+ onSuccess: () => {
+ toast.success("کمپین با موفقیت ایجاد شد");
+ handleClose();
+ onSuccess();
+ },
+ onError: (error: any) => {
+ toast.error(error?.message || "خطا در ایجاد کمپین");
+ },
+ });
+
+ const removeImage = () => {
+ setImageFile(null);
+ setPreviewImage("");
+ };
+
+ const handleClose = () => {
+ setTitle("");
+ setDescription("");
+ setImageFile(null);
+ setPreviewImage("");
+ setErrors({});
+ onClose();
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!validateForm()) {
+ toast.error("لطفاً تمام فیلدهای الزامی را پر کنید");
+ return;
+ }
+
+ if (imageFile) {
+ createMutation.mutate({
+ title,
+ description,
+ image: imageFile,
+ });
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/modules/dashboard/components/dashboard-card.tsx b/src/modules/dashboard/components/dashboard-card.tsx
new file mode 100644
index 0000000..a0929f3
--- /dev/null
+++ b/src/modules/dashboard/components/dashboard-card.tsx
@@ -0,0 +1,47 @@
+import type { LucideIcon } from "lucide-react";
+import React from "react";
+
+interface DashboardCardProps {
+ icon: LucideIcon;
+ label: string;
+ onClick: () => void;
+ variant?: "default" | "danger";
+ className?: string;
+}
+
+const DashboardCard: React.FC = ({
+ icon: Icon,
+ label,
+ onClick,
+ variant = "default",
+ className = "",
+}) => {
+ const baseStyles =
+ "flex flex-col items-center justify-center gap-4 p-6 rounded-lg shadow-md transition-all duration-200 active:scale-95 h-40 w-full";
+
+ const variantStyles = {
+ default:
+ "bg-white border border-blue-100 hover:shadow-lg hover:border-blue-200",
+ danger:
+ "bg-rose-50 border border-rose-200 hover:shadow-lg hover:border-rose-300",
+ };
+
+ const textColor = variant === "danger" ? "text-rose-700" : "text-slate-700";
+ const iconColor = variant === "danger" ? "text-rose-600" : "text-blue-500";
+
+ return (
+
+ );
+};
+
+export default DashboardCard;
diff --git a/src/modules/dashboard/components/profile-card.tsx b/src/modules/dashboard/components/profile-card.tsx
new file mode 100644
index 0000000..c7a6021
--- /dev/null
+++ b/src/modules/dashboard/components/profile-card.tsx
@@ -0,0 +1,61 @@
+import { User } from "lucide-react";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import type { UserProfile } from "../types/dashboard.type";
+
+interface ProfileCardProps {
+ profile: UserProfile;
+}
+
+const ProfileCard: React.FC = ({ profile }) => {
+ const { t } = useTranslation();
+
+ const userTypeLabel =
+ profile?.userType === "student"
+ ? t("dashboard.profile.student")
+ : t("dashboard.profile.school");
+
+ const userInfo =
+ profile?.userType === "student" ? profile.groupName : profile.schoolName;
+
+ return (
+
+
+ {/* Profile Image */}
+
+ {profile.profileImage ? (
+

+ ) : (
+
+
+
+ )}
+
+
+ {/* Profile Info */}
+
+
+ {profile.fullName}
+
+
+ {userTypeLabel}
+
+ {userInfo && (
+
+ {profile.userType === "student"
+ ? t("dashboard.profile.groupName")
+ : t("dashboard.profile.schoolName")}
+ : {userInfo}
+
+ )}
+
+
+
+ );
+};
+
+export default ProfileCard;
diff --git a/src/modules/dashboard/layouts/index.tsx b/src/modules/dashboard/layouts/index.tsx
new file mode 100644
index 0000000..7c44d08
--- /dev/null
+++ b/src/modules/dashboard/layouts/index.tsx
@@ -0,0 +1,14 @@
+// layouts/DashboardLayout.tsx
+import { MobileNavbar } from "@/core/components/others";
+import { Outlet } from "react-router-dom";
+
+export function DashboardLayout() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/modules/dashboard/pages/campaigns/campaigns.type.ts b/src/modules/dashboard/pages/campaigns/campaigns.type.ts
new file mode 100644
index 0000000..a7a9790
--- /dev/null
+++ b/src/modules/dashboard/pages/campaigns/campaigns.type.ts
@@ -0,0 +1,37 @@
+export interface Campaign {
+ ValueP1226S1951StageID: Number;
+ ValueP1226S1951ValueID: Number;
+ WorkflowID: Number;
+ description: String;
+ image: String;
+ status: String;
+ title: String;
+ user_id: String;
+ volume: String;
+ school_code: String;
+ nickname?: String;
+ signature_count: Number;
+ comment_count?: Number;
+}
+
+export interface Signer {
+ id: string;
+ nickname: string;
+ avatar?: string;
+}
+
+export interface Comment {
+ id: string;
+ authorNickname: string;
+ authorId: string;
+ text: string;
+ createdAt: string;
+}
+
+export interface CreateCampaignData {
+ title: string;
+ description: string;
+ image: File;
+}
+
+export type CampaignTab = "all" | "my" | "top" | "group";
diff --git a/src/modules/dashboard/pages/campaigns/detail.tsx b/src/modules/dashboard/pages/campaigns/detail.tsx
new file mode 100644
index 0000000..5408fbe
--- /dev/null
+++ b/src/modules/dashboard/pages/campaigns/detail.tsx
@@ -0,0 +1,264 @@
+"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 {
+ addCommentService,
+ getCampaignDetailService,
+ signCampaignService,
+} from "../../service/campaigns.service";
+
+export function CampaignDetailPage() {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const [commentText, setCommentText] = useState("");
+ const [hasSignedCampaign, setHasSignedCampaign] = useState(false);
+
+ const {
+ data: campaign,
+ isLoading,
+ refetch,
+ } = useQuery({
+ queryKey: ["campaign", id],
+ queryFn: () => getCampaignDetailService(id!),
+ enabled: !!id,
+ });
+
+ const signMutation = useMutation({
+ mutationFn: () => signCampaignService(id!),
+ onSuccess: () => {
+ toast.success("با موفقیت امضا کردید");
+ setHasSignedCampaign(true);
+ refetch();
+ },
+ onError: (error: any) => {
+ toast.error(error?.message || "خطا در امضای کمپین");
+ },
+ });
+
+ const commentMutation = useMutation({
+ mutationFn: (text: string) => addCommentService(id!, text),
+ onSuccess: () => {
+ toast.success("نظر شما اضافه شد");
+ setCommentText("");
+ refetch();
+ },
+ onError: (error: any) => {
+ toast.error(error?.message || "خطا در افزودن نظر");
+ },
+ });
+
+ const handleSignCampaign = () => {
+ if (hasSignedCampaign) {
+ toast.info("شما قبلاً این کمپین را امضا کردهاید");
+ return;
+ }
+ signMutation.mutate();
+ };
+
+ const handleAddComment = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!commentText.trim()) {
+ toast.error("لطفاً نظری بنویسید");
+ return;
+ }
+ commentMutation.mutate(commentText);
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!campaign) {
+ return (
+
+
کمپین یافت نشد
+
navigate("/dashboard/campaigns")}>
+ بازگشت به کمپینها
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Back Button */}
+
+
+ {/* Campaign Image */}
+
+

+
+
+ {/* Campaign Header */}
+
+ {/* Title */}
+
+ {campaign.title}
+
+
+ {/* Creator Info */}
+
+ توسط:{" "}
+ {campaign.creatorNickname}
+
+
+ {/* Stats */}
+
+
+
+ {campaign.signatures}
+
+
+
+ امضا
+
+
+
+
+ {campaign.comments.length}
+
+
+
+ نظر
+
+
+
+
+ {/* Description */}
+
+ {campaign.description}
+
+
+ {/* Sign Campaign Button */}
+
+ {signMutation.isPending
+ ? "در حال امضا..."
+ : hasSignedCampaign
+ ? "شما امضا کردهاید ✓"
+ : "امضای کمپین"}
+
+
+
+ {/* Signers Section */}
+
+
+ امضاکنندگان
+
+
+ {campaign.signers.length > 0 ? (
+
+ {campaign.signers.map((signer) => (
+
+
+ {signer.avatar ? (
+

+ ) : (
+ signer.nickname.charAt(0).toUpperCase()
+ )}
+
+
+ {signer.nickname}
+
+
+ ))}
+
+ ) : (
+
+ هنوز کسی امضا نکرده است. شما میتوانید اولین نفر باشید!
+
+ )}
+
+
+ {/* Comments Section */}
+
+
+ نظرات
+
+
+ {/* Add Comment Form */}
+
+
+ {/* Comments List */}
+
+ {campaign.comments.length > 0 ? (
+ campaign.comments.map((comment) => (
+
+
+
+ {new Date(comment.createdAt).toLocaleDateString("fa-IR")}
+
+
+ {comment.authorNickname}
+
+
+
{comment.text}
+
+ ))
+ ) : (
+
+ هنوز نظری وجود ندارد. اولین نظر را بنویسید!
+
+ )}
+
+
+
+
+ );
+}
+
+export default CampaignDetailPage;
diff --git a/src/modules/dashboard/pages/campaigns/index.tsx b/src/modules/dashboard/pages/campaigns/index.tsx
new file mode 100644
index 0000000..6d19364
--- /dev/null
+++ b/src/modules/dashboard/pages/campaigns/index.tsx
@@ -0,0 +1,195 @@
+"use client";
+
+import { CustomButton } from "@/core/components/base/button";
+import { CustomInput } from "@/core/components/base/input";
+import { userInfoService } from "@/core/service/user-info.service";
+import { useQuery } from "@tanstack/react-query";
+import { Loader, Plus, Search } from "lucide-react";
+import { useEffect, useState } 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("all");
+ const [searchQuery, setSearchQuery] = useState("");
+ const [currentCampaign, setCurrentCampaign] = useState>([]);
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
+
+ const {
+ data: campaigns = [],
+ isLoading,
+ refetch,
+ } = useQuery({
+ queryKey: ["campaigns", searchQuery],
+ queryFn: () => getCampaignsService(activeTab, searchQuery),
+ });
+
+ useEffect(() => {
+ if (campaigns) {
+ setCurrentCampaign(campaigns);
+ }
+ }, [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: "کمپینهای گروه" },
+ ];
+
+ const handleSearchChange = (e: React.ChangeEvent) => {
+ setSearchQuery(e.target.value);
+ };
+
+ const handleTabChange = (tab: CampaignTab) => {
+ setActiveTab(tab);
+ const user = userInfoService.getUserInfo();
+ switch (tab) {
+ case "all":
+ setCurrentCampaign(campaigns);
+ break;
+ case "my":
+ setCurrentCampaign(
+ campaigns.filter(
+ (campaign) =>
+ Number(campaign.WorkflowID) === Number(user.WorkflowID)
+ )
+ );
+ break;
+ case "top":
+ setCurrentCampaign(
+ [...campaigns].sort(
+ (a, b) => (b.signature_count || 0) - (a.signature_count || 0)
+ )
+ );
+ break;
+ case "group":
+ setCurrentCampaign(
+ campaigns.filter(
+ (campaign) => campaign.school_code === user.school_code
+ )
+ );
+ break;
+ default:
+ setCurrentCampaign(campaigns);
+ }
+
+ setSearchQuery("");
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+ کمپینها
+
+
+ برای تغییر جهان، کمپین ایجاد کنید و دیگران را دعوت کنید
+
+
+
+ {/* Top Bar: Search and Create Button */}
+
+ {/* Search Box */}
+
+
+ {/* Create Campaign Button */}
+
setIsCreateModalOpen(true)}
+ className="flex items-center gap-2"
+ >
+
+ ایجاد کمپین
+
+
+
+ {/* Tabs */}
+
+ {tabs.map((tab) => (
+
+ ))}
+
+
+ {/* Loading State */}
+ {isLoading && (
+
+
+
+ )}
+
+ {/* Campaigns Grid */}
+ {!isLoading && campaigns.length > 0 && (
+
+ {currentCampaign.length > 0 ? (
+ currentCampaign.map((campaign) => (
+
+ ))
+ ) : (
+
کمپینی یافت نشد
+ )}
+
+ )}
+
+ {/* Empty State */}
+ {!isLoading && campaigns.length === 0 && (
+
+
+ {activeTab === "my"
+ ? "هنوز کمپینی ایجاد نکردهاید"
+ : "کمپینی یافت نشد"}
+
+ {activeTab === "my" && (
+
setIsCreateModalOpen(true)}
+ >
+ ایجاد اولین کمپین خود
+
+ )}
+
+ )}
+
+
+ {/* Create Campaign Modal */}
+
setIsCreateModalOpen(false)}
+ onSuccess={() => refetch()}
+ />
+
+ );
+}
+
+export default CampaignsPage;
diff --git a/src/modules/dashboard/pages/main-page/index.tsx b/src/modules/dashboard/pages/main-page/index.tsx
new file mode 100644
index 0000000..6009bf0
--- /dev/null
+++ b/src/modules/dashboard/pages/main-page/index.tsx
@@ -0,0 +1,134 @@
+import { useQuery } from "@tanstack/react-query";
+import {
+ Activity,
+ BarChart3,
+ Edit2,
+ Loader,
+ LogOut,
+ Users,
+} from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import DashboardCard from "../../components/dashboard-card";
+import { fetchUserProfile } from "../../service/user.service";
+
+const DashboardPage = () => {
+ const navigate = useNavigate();
+ const { t } = useTranslation();
+
+ const { data, isLoading, error } = useQuery({
+ queryKey: ["userProfile"],
+ queryFn: fetchUserProfile,
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ });
+
+ const handleEditInfo = () => {
+ // Navigate to edit profile page
+ navigate("/dashboard/edit-profile");
+ };
+
+ const handleMyGroup = () => {
+ if (data?.userType === "student") {
+ navigate("/dashboard/my-group");
+ } else {
+ navigate("/dashboard/school-students");
+ }
+ };
+
+ const handleActivities = () => {
+ navigate("/dashboard/activities");
+ };
+
+ const handleReports = () => {
+ navigate("/dashboard/reports");
+ };
+
+ const handleLogout = () => {
+ localStorage.removeItem("token");
+ navigate("/auth/login", { replace: true });
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+ {t("dashboard.loading") || "در حال بارگذاری..."}
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Main Content */}
+
+ {/* Page Title */}
+
+ {t("dashboard.title")}
+
+
+ {/* Profile Card */}
+ {/*
*/}
+
+ {/* Dashboard Cards Grid */}
+
+ {/* Edit Information */}
+
+
+ {/* My Group / School Students */}
+
+
+ {/* Activities */}
+
+
+ {/* Reports */}
+
+
+
+ {/* Logout Button */}
+
+
+ {/* Safe spacing at bottom */}
+
+
+
+ );
+};
+
+export default DashboardPage;
diff --git a/src/modules/dashboard/pages/profile/index.tsx b/src/modules/dashboard/pages/profile/index.tsx
new file mode 100644
index 0000000..7167136
--- /dev/null
+++ b/src/modules/dashboard/pages/profile/index.tsx
@@ -0,0 +1,374 @@
+"use client";
+
+import { BaseDropdown } from "@/core/components/base/base-drop-down";
+import { CustomButton } from "@/core/components/base/button";
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/core/components/base/card";
+import { ImageUploader } from "@/core/components/base/image-uploader";
+import { CustomInput } from "@/core/components/base/input";
+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 { useNavigate } from "react-router-dom";
+import { toast } from "react-toastify";
+import type { RegistrationFormData } from "./profile.type";
+
+export function RegisterPage() {
+ const navigate = useNavigate();
+
+ const queryClient = useQueryClient();
+
+ const { data } = useQuery({
+ queryKey: ["userProfile"],
+ queryFn: fetchUserProfile,
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ });
+
+ const [formData, setFormData] = useState({
+ name: data?.name || "",
+ family: data?.family || "",
+ nickname: data?.nickname || "",
+ school_code: data?.schoolCode || "",
+ education_level: data?.educationLevel || "",
+ invitor: data?.invitor || "",
+ image: undefined,
+ nationalcode: data?.nationalcode || "",
+ base: data?.base || "",
+ });
+
+ useEffect(() => {
+ if (data) {
+ setFormData((prev) => ({
+ ...prev,
+ name: data.name || "",
+ family: data.family || "",
+ nickname: data.nickname || "",
+ school_code: data.schoolCode || "",
+ education_level: data.educationLevel || "",
+ invitor: data.invitor || "",
+ nationalcode: data.nationalcode || "",
+ base: data.base || "",
+ }));
+ }
+ }, [data]);
+
+ const [errors, setErrors] = useState>({});
+ const [previewImage, setPreviewImage] = useState("");
+
+ const validateForm = (): boolean => {
+ const newErrors: Record = {};
+
+ // نام
+ if (!formData.name?.trim()) {
+ newErrors.name = "نام الزامی است";
+ }
+
+ // نام خانوادگی
+ if (!formData.family?.trim()) {
+ newErrors.family = "نام خانوادگی الزامی است";
+ }
+
+ // کد ملی (10 رقم + الگوریتم رسمی ایران)
+ const nationalCode = formData.nationalcode?.trim();
+ if (!nationalCode) {
+ newErrors.nationalcode = "کد ملی الزامی است";
+ } else if (!/^\d{10}$/.test(nationalCode)) {
+ newErrors.nationalcode = "کد ملی باید دقیقاً ۱۰ رقم باشد";
+ } else if (!isValidIranianNationalCode(nationalCode)) {
+ newErrors.nationalcode = "کد ملی وارد شده معتبر نیست";
+ }
+
+ // نام مستعار
+ if (!formData.nickname?.trim()) {
+ newErrors.nickname = "نام مستعار الزامی است";
+ } else if (formData.nickname.length < 3) {
+ newErrors.nickname = "نام مستعار باید حداقل ۳ کاراکتر باشد";
+ }
+
+ // کد مدرسه
+ if (!formData.school_code?.trim()) {
+ newErrors.school_code = "کد مدرسه الزامی است";
+ } else if (!/^\d{5,10}$/.test(formData.school_code)) {
+ newErrors.school_code = "کد مدرسه باید بین ۵ تا ۱۰ رقم باشد";
+ }
+
+ // مقطع تحصیلی
+ if (!formData.education_level) {
+ newErrors.education_level = "لطفاً مقطع تحصیلی را انتخاب کنید";
+ }
+
+ // پایه تحصیلی
+ if (!formData.base) {
+ newErrors.base = "لطفاً پایه تحصیلی را انتخاب کنید";
+ } else if (
+ formData.education_level === "متوسطه اول" &&
+ !["هفتم", "هشتم", "نهم"].includes(formData.base)
+ ) {
+ newErrors.base = "پایه انتخابشده با مقطع متوسطه اول سازگار نیست";
+ } else if (
+ formData.education_level === "متوسطه دوم" &&
+ !["دهم", "یازدهم", "دوازدهم"].includes(formData.base)
+ ) {
+ newErrors.base = "پایه انتخابشده با مقطع متوسطه دوم سازگار نیست";
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const isValidIranianNationalCode = (input: string): boolean => {
+ if (!/^\d{10}$/.test(input)) return false;
+
+ const code = input.split("").map(Number);
+ const checkDigit = code[9];
+ const sum = code
+ .slice(0, 9)
+ .reduce((acc, digit, index) => acc + digit * (10 - index), 0);
+ const remainder = sum % 11;
+
+ return remainder < 2
+ ? checkDigit === remainder
+ : checkDigit === 11 - remainder;
+ };
+
+ const registerMutation = useMutation({
+ mutationFn: updateUserProfile,
+ onSuccess: (data) => {
+ if (data.resultType !== 0) {
+ toast.error(data.message || "خطایی رخ داد");
+ return;
+ }
+
+ toast.success("ثبت نام با موفقیت انجام شد");
+ queryClient.invalidateQueries({
+ queryKey: ["userProfile"],
+ });
+ },
+ onError: (error: any) => {
+ console.error("Registration error:", error);
+ toast.error(
+ "خطا در ثبت نام: " + (error?.message || "لطفاً دوباره تلاش کنید")
+ );
+ },
+ });
+
+ const handleInputChange = (
+ e: React.ChangeEvent
+ ) => {
+ const { name, value } = e.target;
+
+ setFormData((prev) => {
+ if (name === "education_level") {
+ return {
+ ...prev,
+ education_level: value as "متوسطه اول" | "متوسطه دوم" | "",
+ base: "",
+ };
+ }
+
+ return {
+ ...prev,
+ [name]: value,
+ };
+ });
+
+ setErrors((prev) => ({
+ ...prev,
+ [name]: "",
+ ...(name === "education_level" && { base: "" }),
+ }));
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!validateForm()) {
+ toast.error("لطفاً تمام فیلدهای الزامی را پر کنید");
+ return;
+ }
+
+ registerMutation.mutate(formData);
+ };
+
+ const logOut = () => {
+ localStorage.clear();
+ navigate(`${AUTH_ROUTE.sub}/${AUTH_ROUTE.LOGIN}`);
+ };
+
+ return (
+
+
+
+
+ ثبت نام
+
+
+ لطفاً اطلاعات خود را وارد کنید
+
+
+
+
+
+
+
+
+ );
+}
+
+export default RegisterPage;
diff --git a/src/modules/dashboard/pages/profile/profile.type.ts b/src/modules/dashboard/pages/profile/profile.type.ts
new file mode 100644
index 0000000..5a2498b
--- /dev/null
+++ b/src/modules/dashboard/pages/profile/profile.type.ts
@@ -0,0 +1,19 @@
+export interface RegistrationFormData {
+ username?: string;
+ WorkflowID?: string;
+ name: string;
+ family: string;
+ nickname: string;
+ school_code: string;
+ education_level: string;
+ invitor: string;
+ image?: File | null;
+ nationalcode: string;
+ base: string;
+}
+
+export interface RegistrationResponse {
+ resultType: number;
+ message: string;
+ data?: string;
+}
diff --git a/src/modules/dashboard/routes/route.constant.ts b/src/modules/dashboard/routes/route.constant.ts
new file mode 100644
index 0000000..5ea2a1a
--- /dev/null
+++ b/src/modules/dashboard/routes/route.constant.ts
@@ -0,0 +1,6 @@
+export const DASHBOARD_ROUTE = {
+ sub: "/dashboard",
+ dashboard: "main",
+ profile: "profile",
+ campaigns: "campaigns",
+};
diff --git a/src/modules/dashboard/routes/router.tsx b/src/modules/dashboard/routes/router.tsx
new file mode 100644
index 0000000..85c454b
--- /dev/null
+++ b/src/modules/dashboard/routes/router.tsx
@@ -0,0 +1,32 @@
+import type { AppRoute } from "@core/types/router.type";
+import { DashboardLayout } from "../layouts";
+import CampaignsPage from "../pages/campaigns";
+import CampaignDetailPage from "../pages/campaigns/detail";
+import DashboardPage from "../pages/main-page";
+import ProfilePage from "../pages/profile";
+import { DASHBOARD_ROUTE } from "./route.constant";
+
+export const dashboardRoutes: AppRoute[] = [
+ {
+ path: DASHBOARD_ROUTE.sub,
+ element: ,
+ children: [
+ {
+ path: DASHBOARD_ROUTE.dashboard,
+ element: ,
+ },
+ {
+ path: DASHBOARD_ROUTE.profile,
+ element: ,
+ },
+ {
+ path: DASHBOARD_ROUTE.campaigns,
+ element: ,
+ },
+ {
+ path: "campaigns/:id",
+ element: ,
+ },
+ ],
+ },
+];
diff --git a/src/modules/dashboard/service/campaigns.service.ts b/src/modules/dashboard/service/campaigns.service.ts
new file mode 100644
index 0000000..646a7c1
--- /dev/null
+++ b/src/modules/dashboard/service/campaigns.service.ts
@@ -0,0 +1,110 @@
+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 type {
+ Campaign,
+ CreateCampaignData,
+} from "../pages/campaigns/campaigns.type";
+
+export const getCampaignsService = async (
+ tab: string,
+ search?: string
+): Promise => {
+ const params = new URLSearchParams();
+ params.append("tab", tab);
+ if (search) params.append("search", search);
+ const userStr = userInfoService.getUserInfo();
+ const query = {
+ ProcessName: "campaign",
+ OutputFields: [
+ "title",
+ "description",
+ "image",
+ "user_id",
+ "user_id.nickname",
+ "volume",
+ "status",
+ "school_code",
+ "signature_count",
+ // "comment_count",
+ ],
+ conditions: [
+ ["school_code", "=", userStr.school_code, "or"],
+ ["user_id", "=", "", "and"],
+ ["status", "!=", "حذف شده", "and"],
+ ["status", "!=", "غیر فعال"],
+ ],
+ };
+ const [err, res] = await to(api.post(API_ADDRESS.select, query));
+ if (err) {
+ throw err;
+ }
+
+ if (!res?.data?.data || res.data.data.length === 0) {
+ return [];
+ }
+
+ if (res.data.resultType !== 0) {
+ toast.error("خطا در دریافت کمپینها");
+ throw new Error("خطا در دریافت کمپینها");
+ }
+ const data = JSON.parse(res.data.data);
+
+ return data;
+};
+
+export const getCampaignDetailService = async (
+ campaignId: string
+): Promise => {
+ const [err, res] = await to(api.get(`/campaigns/${campaignId}`));
+ if (err) {
+ throw err;
+ }
+ return res?.data;
+};
+
+export const createCampaignService = async (
+ data: CreateCampaignData
+): Promise => {
+ const user = userInfoService.getUserInfo();
+ const body = {
+ ProcessName: "campaign",
+ campaign: {
+ title: data.title,
+ description: data.description,
+ image: data.image,
+ 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("خطا در ثبت کارزار");
+ }
+
+ return res.data;
+};
+
+export const signCampaignService = async (
+ campaignId: string
+): Promise => {
+ const response = await api.post(`/campaigns/${campaignId}/sign`);
+ return response.data;
+};
+
+export const addCommentService = async (
+ campaignId: string,
+ text: string
+): Promise => {
+ const response = await api.post(`/campaigns/${campaignId}/comments`, {
+ text,
+ });
+
+ return response.data;
+};
diff --git a/src/modules/dashboard/service/user.service.ts b/src/modules/dashboard/service/user.service.ts
new file mode 100644
index 0000000..a589aff
--- /dev/null
+++ b/src/modules/dashboard/service/user.service.ts
@@ -0,0 +1,81 @@
+import { API_ADDRESS } from "@/core/service/api-address";
+import api from "@/core/service/axios";
+import type { RegistrationFormData } from "@modules/dashboard/pages/profile/profile.type";
+import { to } from "await-to-js";
+
+export const fetchUserProfile = async () => {
+ const person = JSON.parse(localStorage.getItem("person") || "{}");
+ const query = {
+ ProcessName: "user",
+ OutputFields: [
+ "username",
+ "name",
+ "family",
+ "education_level",
+ "base",
+ "account_type",
+ "nickname",
+ "school_code",
+ "school_code.title",
+ "invitor",
+ "nationalcode",
+ ],
+ conditions: [["username", "=", person.ID]],
+ };
+
+ const res = await api.post(API_ADDRESS.select, query);
+
+ if (!res.data || res.data.length === 0) {
+ throw new Error("User not found");
+ }
+
+ 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,
+ };
+};
+
+export const updateUserProfile = async (data: RegistrationFormData) => {
+ const personStr = localStorage.getItem("person");
+ if (!personStr) {
+ throw new Error("کاربر وارد سیستم نشده است");
+ }
+
+ const person = JSON.parse(personStr);
+ const natinalCode = person.NationalCode;
+
+ let payload = {
+ user: {
+ username: String(person.ID),
+ name: data.name.trim(),
+ family: data.family.trim(),
+ nickname: data.nickname.trim() || undefined,
+ education_level: data.education_level,
+ base: data.base,
+ account_type: "عادی",
+ nationalcode: data.nationalcode,
+ ...(data.school_code && { school_code: data.school_code.trim() }),
+ ...(data.invitor && { invitor: data.invitor.trim() }),
+ ...(natinalCode && { WorkflowID: person.ID }),
+ },
+ };
+
+ const [error, response] = await to(api.post(API_ADDRESS.save, payload));
+
+ if (error) {
+ console.error("خطا در ارسال اطلاعات پروفایل:", error.message || error);
+ throw error;
+ }
+
+ return response?.data;
+};
diff --git a/src/modules/dashboard/types/dashboard.type.ts b/src/modules/dashboard/types/dashboard.type.ts
new file mode 100644
index 0000000..9c00355
--- /dev/null
+++ b/src/modules/dashboard/types/dashboard.type.ts
@@ -0,0 +1,18 @@
+export interface UserProfile {
+ id: string;
+ fullName: string;
+ userType: "student" | "school";
+ groupName?: string;
+ schoolName?: string;
+ profileImage?: string;
+ email?: string;
+ phone?: string;
+}
+
+export interface DashboardCard {
+ id: string;
+ label: string;
+ icon: string;
+ action: () => void;
+ variant?: "default" | "danger";
+}
diff --git a/src/router/rootRoutes.ts b/src/router/rootRoutes.ts
index 7da71ab..fe4af93 100644
--- a/src/router/rootRoutes.ts
+++ b/src/router/rootRoutes.ts
@@ -1,4 +1,5 @@
import type { AppRoute } from "@/core/types/router.type";
import { authRoutes } from "@/modules/auth/routes/router";
+import { dashboardRoutes } from "@/modules/dashboard/routes/router";
-export const rootRoutes: AppRoute[] = [...authRoutes];
+export const rootRoutes: AppRoute[] = [...authRoutes, ...dashboardRoutes];