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 && ( + + )} + +