From ce4c33d46dd26d0fe0a4488b7fb682cda55eb953 Mon Sep 17 00:00:00 2001 From: MehrdadAdabi <126083584+mehrdadAdabi@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:10:30 +0330 Subject: [PATCH] feat: Implement dashboard profile card and campaigns management - Added ProfileCard component to display user profile information. - Created DashboardLayout for consistent layout structure. - Defined Campaign and related types for campaign management. - Developed CampaignDetailPage for viewing individual campaign details. - Implemented CampaignsPage for listing and filtering campaigns. - Enhanced DashboardPage with user profile fetching and navigation. - Built RegisterPage for user profile registration and updates. - Added user service for fetching and updating user profiles. - Established campaigns service for managing campaign data and interactions. - Updated routing constants and router configuration for new pages. --- index.html | 22 +- package-lock.json | 27 ++ package.json | 1 + public/locales/fa.json | 102 ++++- src/core/components/base/base-drop-down.tsx | 80 ++++ src/core/components/base/button.tsx | 2 +- src/core/components/base/card.tsx | 2 +- src/core/components/base/image-uploader.tsx | 147 +++++++ src/core/components/base/input.tsx | 2 +- src/core/components/base/text-area.tsx | 45 +++ src/core/components/others/index.ts | 1 + .../components/others/mobile-navbar.demo.tsx | 212 ++++++++++ src/core/components/others/mobile-navbar.tsx | 133 +++++++ src/core/layouts/root-layout.example.tsx | 81 ++++ src/core/service/api-address.ts | 2 + src/core/service/axios.ts | 8 +- src/core/service/user-info.service.ts | 17 + src/index.css | 27 +- src/main.tsx | 9 +- .../auth/components/otp/opt-dialog.tsx | 2 +- .../auth/components/otp/otp-receiver.tsx | 61 ++- src/modules/auth/pages/login/index.tsx | 112 ++++-- src/modules/auth/pages/register/index.tsx | 5 - src/modules/auth/routes/route.constant.ts | 1 + src/modules/auth/routes/router.tsx | 2 +- src/modules/auth/service/auth.service.ts | 18 + .../dashboard/components/campaign-card.tsx | 54 +++ .../components/create-campaign-modal.tsx | 178 +++++++++ .../dashboard/components/dashboard-card.tsx | 47 +++ .../dashboard/components/profile-card.tsx | 61 +++ src/modules/dashboard/layouts/index.tsx | 14 + .../pages/campaigns/campaigns.type.ts | 37 ++ .../dashboard/pages/campaigns/detail.tsx | 264 +++++++++++++ .../dashboard/pages/campaigns/index.tsx | 195 +++++++++ .../dashboard/pages/main-page/index.tsx | 134 +++++++ src/modules/dashboard/pages/profile/index.tsx | 374 ++++++++++++++++++ .../dashboard/pages/profile/profile.type.ts | 19 + .../dashboard/routes/route.constant.ts | 6 + src/modules/dashboard/routes/router.tsx | 32 ++ .../dashboard/service/campaigns.service.ts | 110 ++++++ src/modules/dashboard/service/user.service.ts | 81 ++++ src/modules/dashboard/types/dashboard.type.ts | 18 + src/router/rootRoutes.ts | 3 +- 43 files changed, 2670 insertions(+), 78 deletions(-) create mode 100644 src/core/components/base/base-drop-down.tsx create mode 100644 src/core/components/base/image-uploader.tsx create mode 100644 src/core/components/base/text-area.tsx create mode 100644 src/core/components/others/index.ts create mode 100644 src/core/components/others/mobile-navbar.demo.tsx create mode 100644 src/core/components/others/mobile-navbar.tsx create mode 100644 src/core/layouts/root-layout.example.tsx create mode 100644 src/core/service/user-info.service.ts delete mode 100644 src/modules/auth/pages/register/index.tsx create mode 100644 src/modules/auth/service/auth.service.ts create mode 100644 src/modules/dashboard/components/campaign-card.tsx create mode 100644 src/modules/dashboard/components/create-campaign-modal.tsx create mode 100644 src/modules/dashboard/components/dashboard-card.tsx create mode 100644 src/modules/dashboard/components/profile-card.tsx create mode 100644 src/modules/dashboard/layouts/index.tsx create mode 100644 src/modules/dashboard/pages/campaigns/campaigns.type.ts create mode 100644 src/modules/dashboard/pages/campaigns/detail.tsx create mode 100644 src/modules/dashboard/pages/campaigns/index.tsx create mode 100644 src/modules/dashboard/pages/main-page/index.tsx create mode 100644 src/modules/dashboard/pages/profile/index.tsx create mode 100644 src/modules/dashboard/pages/profile/profile.type.ts create mode 100644 src/modules/dashboard/routes/route.constant.ts create mode 100644 src/modules/dashboard/routes/router.tsx create mode 100644 src/modules/dashboard/service/campaigns.service.ts create mode 100644 src/modules/dashboard/service/user.service.ts create mode 100644 src/modules/dashboard/types/dashboard.type.ts diff --git a/index.html b/index.html index ce9339c..058bb45 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,26 @@ - - + + - yari-garan + + + + + + + + + یاری گران - داشبورد
diff --git a/package-lock.json b/package-lock.json index f167a61..cee3a19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.1.17", + "@tanstack/react-query": "^5.90.10", "await-to-js": "^3.0.0", "axios": "^1.13.2", "clsx": "^2.1.1", @@ -2914,6 +2915,32 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.10.tgz", + "integrity": "sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.10.tgz", + "integrity": "sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", diff --git a/package.json b/package.json index a51cec9..e362489 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.17", + "@tanstack/react-query": "^5.90.10", "await-to-js": "^3.0.0", "axios": "^1.13.2", "clsx": "^2.1.1", diff --git a/public/locales/fa.json b/public/locales/fa.json index 0967ef4..9651864 100644 --- a/public/locales/fa.json +++ b/public/locales/fa.json @@ -1 +1,101 @@ -{} +{ + "dashboard": { + "title": "داشبورد کاربری", + "editInfo": "ویرایش اطلاعات", + "myGroup": "گروه من", + "schoolStudents": "دانش‌آموزان مدرسه", + "activities": "فعالیت‌ها", + "reports": "گزارش‌ها", + "logout": "خروج از حساب", + "profile": { + "fullName": "نام کامل", + "userType": "نوع کاربر", + "groupName": "نام گروه", + "schoolName": "نام مدرسه", + "student": "دانش‌آموز", + "school": "مدرسه" + } + }, + "registration": { + "title": "ثبت نام", + "subtitle": "لطفاً اطلاعات خود را وارد کنید", + "firstName": "نام", + "lastName": "نام خانوادگی", + "nationalId": "شناسه ملی", + "nickname": "نام مستعار", + "schoolCode": "کد مدرسه", + "gradeLevel": "پایه و مقطع", + "referrerNickname": "نام مستعار معرف (اختیاری)", + "profilePicture": "عکس پروفایل (اختیاری)", + "submit": "ثبت نام", + "submitting": "در حال ثبت نام...", + "back": "بازگشت", + "errors": { + "firstNameRequired": "نام الزامی است", + "lastNameRequired": "نام خانوادگی الزامی است", + "nationalIdRequired": "شناسه ملی الزامی است", + "nationalIdInvalid": "شناسه ملی باید 10 تا 12 رقم باشد", + "nicknameRequired": "نام مستعار الزامی است", + "schoolCodeRequired": "کد مدرسه الزامی است", + "gradeLevelRequired": "پایه و مقطع الزامی است", + "invalidImage": "لطفاً فقط فایل تصویری انتخاب کنید", + "imageTooLarge": "حجم تصویر نباید بیشتر از 5 مگابایت باشد" + } + }, + "campaigns": { + "title": "کمپین‌ها", + "subtitle": "برای تغییر جهان، کمپین ایجاد کنید و دیگران را دعوت کنید", + "search": "جستجوی کمپین...", + "create": "ایجاد کمپین", + "tabs": { + "all": "تمام کمپین‌ها", + "my": "کمپین‌های من", + "top": "کمپین‌های برتر", + "group": "کمپین‌های گروه" + }, + "createModal": { + "title": "ایجاد کمپین جدید", + "titleLabel": "عنوان کمپین", + "titlePlaceholder": "عنوان کمپین را وارد کنید", + "description": "توضیحات", + "descriptionPlaceholder": "توضیحات کمپین را وارد کنید (حداقل 20 کاراکتر)", + "image": "تصویر کمپین", + "upload": "کلیک کنید یا تصویر را بکشید", + "cancel": "لغو", + "submit": "ایجاد کمپین", + "submitting": "در حال ایجاد...", + "errors": { + "titleRequired": "عنوان کمپین الزامی است", + "descriptionRequired": "توضیحات الزامی است", + "descriptionMinLength": "توضیحات باید حداقل 20 کاراکتر باشد", + "imageRequired": "تصویر الزامی است" + } + }, + "detail": { + "back": "بازگشت به کمپین‌ها", + "by": "توسط", + "signatures": "امضا", + "comments": "نظر", + "sign": "امضای کمپین", + "signing": "در حال امضا...", + "signed": "شما امضا کرده‌اید ✓", + "signers": "امضاکنندگان", + "noSigners": "هنوز کسی امضا نکرده است. شما می‌توانید اولین نفر باشید!", + "noComments": "هنوز نظری وجود ندارد. اولین نظر را بنویسید!", + "addComment": "نظر خود را بنویسید...", + "send": "ارسال", + "sending": "ارسال...", + "notFound": "کمپین یافت نشد" + }, + "empty": { + "my": "هنوز کمپینی ایجاد نکرده‌اید", + "noResults": "کمپینی یافت نشد", + "createFirst": "ایجاد اولین کمپین خود" + }, + "notifications": { + "created": "کمپین با موفقیت ایجاد شد", + "signed": "با موفقیت امضا کردید", + "commentAdded": "نظر شما اضافه شد", + "alreadySigned": "شما قبلاً این کمپین را امضا کرده‌اید", + "emptyComment": "لطفاً نظری بنویسید" + } diff --git a/src/core/components/base/base-drop-down.tsx b/src/core/components/base/base-drop-down.tsx new file mode 100644 index 0000000..70ae937 --- /dev/null +++ b/src/core/components/base/base-drop-down.tsx @@ -0,0 +1,80 @@ +// components/ui/BaseDropdown.tsx +import { cn } from "@/core/lib/utils"; +import { ChevronDown } from "lucide-react"; +import { type SelectHTMLAttributes, forwardRef } from "react"; + +type BaseDropdownProps = SelectHTMLAttributes & { + label?: string; + error?: string; + variant?: "primary" | "error"; + options: { value: string; label: string }[]; + placeholder?: string; +}; + +export const BaseDropdown = forwardRef( + ( + { + label, + error, + variant = "primary", + options, + placeholder = "انتخاب کنید", + className, + ...props + }, + ref + ) => { + const hasError = !!error; + + return ( +
+ {label && ( + + )} + +
+ + + {/* آیکون پایین */} +
+ +
+
+ + {hasError && ( +

+ {error} +

+ )} +
+ ); + } +); + +BaseDropdown.displayName = "BaseDropdown"; diff --git a/src/core/components/base/button.tsx b/src/core/components/base/button.tsx index a7bff98..82b6251 100644 --- a/src/core/components/base/button.tsx +++ b/src/core/components/base/button.tsx @@ -23,7 +23,7 @@ const CustomButton = React.forwardRef( variant === "info" && !disabled, // Error variant - "bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-600 active:bg-red-800": + "bg-red-400 text-white hover:bg-red-700 focus-visible:ring-red-600 active:bg-red-800": variant === "error" && !disabled, // Disabled state diff --git a/src/core/components/base/card.tsx b/src/core/components/base/card.tsx index d0772df..2cb8530 100644 --- a/src/core/components/base/card.tsx +++ b/src/core/components/base/card.tsx @@ -9,7 +9,7 @@ export const Card = forwardRef( ref={ref} data-slot="card" className={cn( - "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", + "bg-card text-card-foreground flex flex-col gap-4 rounded-xl border py-6 shadow-sm", className )} {...props} diff --git a/src/core/components/base/image-uploader.tsx b/src/core/components/base/image-uploader.tsx new file mode 100644 index 0000000..a2731ba --- /dev/null +++ b/src/core/components/base/image-uploader.tsx @@ -0,0 +1,147 @@ +import { cn } from "@/core/lib/utils"; +import { Upload, X } from "lucide-react"; +import { type ChangeEvent, useRef, useState } from "react"; + +type ImageUploaderProps = { + label?: string; + previewImage?: string | null; + onImageChange: (file: File | null) => void; + onRemove?: () => void; + className?: string; + error?: string; + required?: boolean; + imageSize?: "sm" | "md" | "lg"; +}; + +export function ImageUploader({ + label = "عکس پروفایل (اختیاری)", + previewImage, + onImageChange, + onRemove, + className, + error, + required = false, + imageSize = "md", +}: ImageUploaderProps) { + const fileInputRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + + const sizeClasses = { + sm: "w-24 h-24", + md: "w-32 h-32", + lg: "w-48 h-48", + }; + + const handleFileChange = (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + validateAndSetFile(file); + } + }; + + const validateAndSetFile = (file: File) => { + if (!file.type.startsWith("image/")) { + return; + } + + if (file.size > 5 * 1024 * 1024) { + return; + } + + onImageChange(file); + }; + + const handleRemove = () => { + onImageChange(null); + onRemove?.(); + if (fileInputRef.current) fileInputRef.current.value = ""; + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = () => { + setIsDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const file = e.dataTransfer.files?.[0]; + if (file) { + validateAndSetFile(file); + } + }; + + return ( +
+ {label && ( + + )} + + {previewImage ? ( +
+ پیش‌نمایش تصویر + +
+ ) : ( + + )} + + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/src/core/components/base/input.tsx b/src/core/components/base/input.tsx index 65b2c5f..4e9690b 100644 --- a/src/core/components/base/input.tsx +++ b/src/core/components/base/input.tsx @@ -48,7 +48,7 @@ const CustomInput = React.forwardRef( {...props} /> {error && ( -

+

{error}

)} diff --git a/src/core/components/base/text-area.tsx b/src/core/components/base/text-area.tsx new file mode 100644 index 0000000..1c8fd02 --- /dev/null +++ b/src/core/components/base/text-area.tsx @@ -0,0 +1,45 @@ +import { cn } from "@/core/lib/utils"; +import { forwardRef, type ComponentProps } from "react"; + +interface TextAreaFieldProps extends ComponentProps<"textarea"> { + label?: string; + error?: string; + minLength?: number; +} + +const TextAreaField = forwardRef( + ({ label, error, minLength = 40, className, placeholder, ...props }, ref) => { + return ( +
+ {label && ( + + )} + +