nahaie 2 khordad
This commit is contained in:
parent
a535e43e81
commit
a1118cb2fc
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/node_modules
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB |
1
dist/assets/index-B5jzgFDg.css
vendored
1
dist/assets/index-B5jzgFDg.css
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-BINX1_Nm.css
vendored
Normal file
1
dist/assets/index-BINX1_Nm.css
vendored
Normal file
File diff suppressed because one or more lines are too long
397
dist/assets/index-Dvo_8Nz_.js
vendored
397
dist/assets/index-Dvo_8Nz_.js
vendored
File diff suppressed because one or more lines are too long
381
dist/assets/index-zxxbrZom.js
vendored
Normal file
381
dist/assets/index-zxxbrZom.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
|
|
@ -16,8 +16,8 @@
|
|||
background: #23183E;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-Dvo_8Nz_.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B5jzgFDg.css">
|
||||
<script type="module" crossorigin src="/assets/index-zxxbrZom.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BINX1_Nm.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export function AnimatedOutlet() {
|
|||
const location = useLocation();
|
||||
const outlet = useOutlet();
|
||||
const isPublicChatPage = location.pathname === "/public-chat";
|
||||
const isEditProfilePage = location.pathname === "/edit-profile";
|
||||
const reduceMotion = useReducedMotion();
|
||||
const previousPathRef = useRef(location.pathname);
|
||||
const pageRef = useRef<HTMLDivElement | null>(null);
|
||||
|
|
@ -75,6 +76,11 @@ export function AnimatedOutlet() {
|
|||
paddingRight: "0px",
|
||||
paddingBottom: "0px",
|
||||
}
|
||||
: isEditProfilePage
|
||||
? {
|
||||
willChange: "opacity, transform",
|
||||
paddingBottom: "0px",
|
||||
}
|
||||
: { willChange: "opacity, transform" }
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ChevronRight, Plus } from "lucide-react";
|
||||
import { ChevronRight, Plus, SquarePen } from "lucide-react";
|
||||
import { useProfile } from "../context/ProfileContext";
|
||||
import { getCachedProfile } from "../../services/profileService";
|
||||
import { getProfileImageUrl } from "../../services/feedService";
|
||||
|
|
@ -13,6 +13,8 @@ interface AppHeaderProps {
|
|||
onBack?: () => void;
|
||||
centerTitle?: string;
|
||||
centerSubtitle?: string;
|
||||
useNewChatAction?: boolean;
|
||||
onNewChatClick?: () => void;
|
||||
}
|
||||
|
||||
const toPersianNumber = (num: number | null | undefined): string => {
|
||||
|
|
@ -21,7 +23,14 @@ const toPersianNumber = (num: number | null | undefined): string => {
|
|||
return String(num).replace(/\d/g, (digit) => persianDigits[parseInt(digit, 10)]);
|
||||
};
|
||||
|
||||
export function AppHeader({ showBack = false, onBack, centerTitle, centerSubtitle }: AppHeaderProps) {
|
||||
export function AppHeader({
|
||||
showBack = false,
|
||||
onBack,
|
||||
centerTitle,
|
||||
centerSubtitle,
|
||||
useNewChatAction = false,
|
||||
onNewChatClick,
|
||||
}: AppHeaderProps) {
|
||||
const navigate = useNavigate();
|
||||
const { profile } = useProfile();
|
||||
|
||||
|
|
@ -44,6 +53,9 @@ export function AppHeader({ showBack = false, onBack, centerTitle, centerSubtitl
|
|||
return profileFallbackImage;
|
||||
}, [hasCustomAvatar, profile?.image, profile?.user_stage_id]);
|
||||
|
||||
const displayName = `${profile?.name || ""} ${profile?.family || ""}`.trim() || profile?.username || "همراه";
|
||||
const displayGrade = profile?.base ? `پایه ${profile.base}` : profile?.education_level || "";
|
||||
|
||||
const navLikePanelStyle = {
|
||||
backgroundImage: `
|
||||
linear-gradient(180deg, #2E1B3D 0%, #23183E 100%),
|
||||
|
|
@ -58,11 +70,43 @@ export function AppHeader({ showBack = false, onBack, centerTitle, centerSubtitl
|
|||
} as const;
|
||||
|
||||
return (
|
||||
<div className="relative z-20 flex items-center justify-between px-4 pt-3 pb-0" dir="rtl">
|
||||
<div className="relative z-20 h-[68px] min-h-[68px] px-4" dir="rtl">
|
||||
{useNewChatAction ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNewChatClick}
|
||||
className="absolute left-4 top-1/2 z-20 flex h-10 w-fit -translate-y-1/2 items-center gap-1.5 rounded-full border-[0.5px] border-transparent px-2.5 text-xs font-bold text-white"
|
||||
style={navLikePanelStyle}
|
||||
aria-label="چت جدید"
|
||||
>
|
||||
<SquarePen size={15} color="#facc15" />
|
||||
<span className="leading-none">چت جدید</span>
|
||||
</button>
|
||||
) : (
|
||||
<div
|
||||
className="absolute left-4 top-1/2 z-20 flex h-10 w-fit -translate-y-1/2 items-center gap-[6px] rounded-full border-[0.5px] border-transparent px-2.5"
|
||||
style={navLikePanelStyle}
|
||||
>
|
||||
<div
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full border-[0.5px] border-transparent"
|
||||
style={navLikePanelStyle}
|
||||
>
|
||||
<Plus size={11} color="#ffd6f0" strokeWidth={2.25} />
|
||||
</div>
|
||||
<span
|
||||
className="font-semibold leading-none text-white tracking-tight"
|
||||
style={{ fontSize: 16 }}
|
||||
>
|
||||
{toPersianNumber(displayCoins)}
|
||||
</span>
|
||||
<img src={coinImage} alt="سکه" className="-my-0.5 h-7 w-7 object-contain" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBack ? (
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="w-11 h-11 rounded-full flex items-center justify-center border-[0.5px] border-transparent"
|
||||
className="absolute right-4 top-1/2 z-20 flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full border-[0.5px] border-transparent"
|
||||
aria-label="بازگشت"
|
||||
style={navLikePanelStyle}
|
||||
>
|
||||
|
|
@ -71,39 +115,56 @@ export function AppHeader({ showBack = false, onBack, centerTitle, centerSubtitl
|
|||
) : (
|
||||
<button
|
||||
onClick={() => navigate("/profile")}
|
||||
className="relative w-12 h-12 rounded-full p-[2px] border-[0.5px] border-transparent overflow-hidden"
|
||||
className="absolute right-4 top-1/2 z-20 flex max-w-[148px] -translate-y-1/2 items-center gap-2 text-right"
|
||||
aria-label="پروفایل"
|
||||
>
|
||||
<span
|
||||
className="h-12 w-12 shrink-0 overflow-hidden rounded-full border-[0.5px] border-transparent p-[2px]"
|
||||
style={navLikePanelStyle}
|
||||
>
|
||||
{hasCustomAvatar ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt="پروفایل"
|
||||
className="w-full h-full rounded-full object-cover"
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
onError={(event) => {
|
||||
event.currentTarget.src = profileFallbackImage;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="w-full h-full rounded-full flex items-center justify-center"
|
||||
<span
|
||||
className="flex h-full w-full items-center justify-center rounded-full"
|
||||
style={navLikePanelStyle}
|
||||
>
|
||||
<img
|
||||
src={profileFallbackImage}
|
||||
alt="پروفایل پیشفرض"
|
||||
className=" object-cover rounded-full"
|
||||
className="rounded-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span className="min-w-0 leading-none">
|
||||
<span className="block max-w-[86px] truncate text-[12px] font-extrabold leading-4 text-[#FBE7F5]">
|
||||
{displayName}
|
||||
</span>
|
||||
{displayGrade && (
|
||||
<span className="mt-0.5 block max-w-[86px] truncate text-[10px] font-medium leading-3 text-[#F2DFF0]/72">
|
||||
{displayGrade}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-0 pointer-events-none flex items-center justify-center text-center translate-y-1">
|
||||
{centerTitle ? (
|
||||
<div className="leading-none">
|
||||
<div
|
||||
className="font-extrabold text-[22px]"
|
||||
className="pointer-events-none absolute left-1/2 top-1/2 z-10 flex w-[min(170px,42vw)] -translate-x-1/2 -translate-y-1/2 items-center justify-center text-center"
|
||||
>
|
||||
{centerTitle ? (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className="mx-auto max-w-full truncate font-extrabold text-[18px] leading-6"
|
||||
style={{
|
||||
display: "inline-block",
|
||||
background:
|
||||
|
|
@ -119,7 +180,7 @@ export function AppHeader({ showBack = false, onBack, centerTitle, centerSubtitl
|
|||
</div>
|
||||
{centerSubtitle && (
|
||||
<div
|
||||
className="mt-1 text-[11px] font-medium"
|
||||
className="-mt-2 text-[11px] font-medium"
|
||||
style={{
|
||||
color: "#ffb7dd",
|
||||
textShadow: "0 1px 6px rgba(255, 119, 202, 0.35)",
|
||||
|
|
@ -138,25 +199,6 @@ export function AppHeader({ showBack = false, onBack, centerTitle, centerSubtitl
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="relative h-10 w-fit rounded-full px-2.5 flex items-center gap-[6px] border-[0.5px] border-transparent"
|
||||
style={navLikePanelStyle}
|
||||
>
|
||||
<div
|
||||
className="w-5 h-5 rounded-full flex items-center justify-center border-[0.5px] border-transparent"
|
||||
style={navLikePanelStyle}
|
||||
>
|
||||
<Plus size={11} color="#ffd6f0" strokeWidth={2.25} />
|
||||
</div>
|
||||
<span
|
||||
className="font-semibold leading-none text-white tracking-tight"
|
||||
style={{ fontSize: 16 }}
|
||||
>
|
||||
{toPersianNumber(displayCoins)}
|
||||
</span>
|
||||
<img src={coinImage} alt="سکه" className="w-7 h-7 object-contain -my-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowRight, Save } from "lucide-react";
|
||||
import { Save, ChevronDown } from "lucide-react";
|
||||
import { getUserProfile, saveUserProfile, getCachedProfile, type UserProfile } from "../../services/profileService";
|
||||
import { getUsername } from "../../utils/auth";
|
||||
import { usePageTracking } from "../../hooks/usePageTracking";
|
||||
|
|
@ -164,138 +164,104 @@ export function EditProfilePage() {
|
|||
// Loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen flex items-center justify-center"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(120% 120% at 50% 0%, rgba(124, 58, 237, 0.32) 0%, rgba(46, 27, 61, 0.98) 52%, rgba(35, 24, 62, 1) 100%)",
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-center"
|
||||
>
|
||||
<motion.div
|
||||
className="w-16 h-16 border-4 rounded-full mx-auto mb-4"
|
||||
style={{
|
||||
borderColor: "rgba(255, 214, 240, 0.28)",
|
||||
borderTopColor: "#ff79cf",
|
||||
}}
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
<p
|
||||
className="text-lg font-bold"
|
||||
style={{
|
||||
color: "#ffd6f0",
|
||||
textShadow: "0 2px 4px rgba(0, 0, 0, 0.6)",
|
||||
}}
|
||||
dir="rtl"
|
||||
>
|
||||
در حال بارگذاری پروفایل...
|
||||
</p>
|
||||
</motion.div>
|
||||
<div className="flex h-full min-h-0 items-center justify-center" dir="rtl">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-3 h-10 w-10 animate-spin rounded-full border-4 border-[#ffd6f0]/30 border-t-[#ff79cf]" />
|
||||
<p className="text-sm font-bold text-[#FBE7F5]">در حال بارگذاری پروفایل...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto px-6 py-8" dir="rtl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => navigate("/profile")}
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center"
|
||||
<div className="relative h-full min-h-0 overflow-hidden" dir="rtl">
|
||||
<main
|
||||
className="h-full overflow-y-auto px-4 pb-4 pt-4"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(32, 76, 106, 0.6) 0%, rgba(20, 40, 60, 0.6) 100%)",
|
||||
border: "2px solid rgba(138, 206, 224, 0.3)",
|
||||
}}
|
||||
>
|
||||
<ArrowRight className="text-white" size={20} />
|
||||
</motion.button>
|
||||
<h1
|
||||
className="text-2xl font-bold text-white"
|
||||
style={{
|
||||
textShadow: "0 4px 12px rgba(138, 206, 224, 0.6), 0 2px 4px rgba(0, 0, 0, 0.8)",
|
||||
maxHeight: "100%",
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-4 text-center">
|
||||
<h1 className="text-[20px] font-extrabold text-[#FBE7F5]">
|
||||
{userProfile?.user_workflowID ? "ویرایش پروفایل" : "تکمیل پروفایل"}
|
||||
</h1>
|
||||
<p className="mt-1 text-xs text-[#EED3EC]/82">اطلاعات پایه حساب کاربری را ثبت یا بهروزرسانی کنید</p>
|
||||
</div>
|
||||
|
||||
{/* Success/Info Message */}
|
||||
{successMessage && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-6 p-4 rounded-xl text-center font-bold"
|
||||
className="mb-4 rounded-2xl px-4 py-3 text-center text-sm font-bold"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(76, 175, 80, 0.2) 0%, rgba(56, 142, 60, 0.2) 100%)",
|
||||
border: "2px solid rgba(76, 175, 80, 0.4)",
|
||||
color: "#A7F3D0",
|
||||
textShadow: "0 2px 4px rgba(0, 0, 0, 0.6)",
|
||||
backgroundImage:
|
||||
"linear-gradient(180deg, rgba(24, 67, 46, 0.88) 0%, rgba(16, 56, 39, 0.92) 100%), linear-gradient(120deg, #34d399 0%, #10b981 100%)",
|
||||
backgroundOrigin: "border-box",
|
||||
backgroundClip: "padding-box, border-box",
|
||||
border: "1px solid rgba(167, 243, 208, 0.4)",
|
||||
color: "#D1FAE5",
|
||||
}}
|
||||
>
|
||||
{successMessage}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Name */}
|
||||
<form id="edit-profile-form" onSubmit={handleSubmit} className="space-y-4">
|
||||
<div
|
||||
className="rounded-[22px] border-[0.5px] border-transparent px-4 py-4"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(180deg, rgba(46, 27, 61, 0.92) 0%, rgba(35, 24, 62, 0.94) 100%), linear-gradient(120deg, #7c3aed 0%, #f97316 58%, #facc15 100%)",
|
||||
backgroundOrigin: "border-box",
|
||||
backgroundClip: "padding-box, border-box",
|
||||
boxShadow:
|
||||
"0 -7px 20px rgba(7, 0, 18, 0.36), 0 8px 16px rgba(5, 2, 12, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.12)",
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-white mb-2 text-sm font-bold">نام</label>
|
||||
<label className="mb-2 block text-xs font-semibold text-[#FBE7F5]/92">نام</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleChange("name", e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-xl text-white font-bold"
|
||||
className="w-full rounded-xl px-3 py-2.5 text-sm font-bold text-white outline-none"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(32, 76, 106, 0.4) 0%, rgba(20, 40, 60, 0.4) 100%)",
|
||||
border: errors.name ? "2px solid rgba(220, 53, 69, 0.6)" : "2px solid rgba(138, 206, 224, 0.3)",
|
||||
outline: "none",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
border: errors.name ? "1px solid rgba(248, 113, 113, 0.62)" : "1px solid rgba(255,255,255,0.14)",
|
||||
}}
|
||||
placeholder="نام خود را وارد کنید"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-red-400 text-xs mt-1">{errors.name}</p>
|
||||
)}
|
||||
{errors.name && <p className="mt-1 text-xs text-red-300">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
{/* Family */}
|
||||
<div>
|
||||
<label className="block text-white mb-2 text-sm font-bold">نام خانوادگی</label>
|
||||
<label className="mb-2 block text-xs font-semibold text-[#FBE7F5]/92">نام خانوادگی</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.family}
|
||||
onChange={(e) => handleChange("family", e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-xl text-white font-bold"
|
||||
className="w-full rounded-xl px-3 py-2.5 text-sm font-bold text-white outline-none"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(32, 76, 106, 0.4) 0%, rgba(20, 40, 60, 0.4) 100%)",
|
||||
border: errors.family ? "2px solid rgba(220, 53, 69, 0.6)" : "2px solid rgba(138, 206, 224, 0.3)",
|
||||
outline: "none",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
border: errors.family ? "1px solid rgba(248, 113, 113, 0.62)" : "1px solid rgba(255,255,255,0.14)",
|
||||
}}
|
||||
placeholder="نام خانوادگی خود را وارد کنید"
|
||||
/>
|
||||
{errors.family && (
|
||||
<p className="text-red-400 text-xs mt-1">{errors.family}</p>
|
||||
)}
|
||||
{errors.family && <p className="mt-1 text-xs text-red-300">{errors.family}</p>}
|
||||
</div>
|
||||
|
||||
{/* Education Level */}
|
||||
<div>
|
||||
<label className="block text-white mb-2 text-sm font-bold">مقطع تحصیلی</label>
|
||||
<label className="mb-2 block text-xs font-semibold text-[#FBE7F5]/92">مقطع تحصیلی</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={formData.education_level}
|
||||
onChange={(e) => handleChange("education_level", e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-xl text-white font-bold"
|
||||
className="w-full appearance-none rounded-xl px-3 py-2.5 pl-10 text-sm font-bold text-white outline-none"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(32, 76, 106, 0.4) 0%, rgba(20, 40, 60, 0.4) 100%)",
|
||||
border: errors.education_level ? "2px solid rgba(220, 53, 69, 0.6)" : "2px solid rgba(138, 206, 224, 0.3)",
|
||||
outline: "none",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
border: errors.education_level ? "1px solid rgba(248, 113, 113, 0.62)" : "1px solid rgba(255,255,255,0.14)",
|
||||
}}
|
||||
>
|
||||
<option value="">انتخاب کنید</option>
|
||||
|
|
@ -303,22 +269,23 @@ export function EditProfilePage() {
|
|||
<option value="متوسطه اول">متوسطه اول</option>
|
||||
<option value="متوسطه دوم">متوسطه دوم</option>
|
||||
</select>
|
||||
{errors.education_level && (
|
||||
<p className="text-red-400 text-xs mt-1">{errors.education_level}</p>
|
||||
)}
|
||||
<ChevronDown
|
||||
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#F2DFF0]/72"
|
||||
/>
|
||||
</div>
|
||||
{errors.education_level && <p className="mt-1 text-xs text-red-300">{errors.education_level}</p>}
|
||||
</div>
|
||||
|
||||
{/* Base (Grade) */}
|
||||
<div>
|
||||
<label className="block text-white mb-2 text-sm font-bold">پایه تحصیلی</label>
|
||||
<label className="mb-2 block text-xs font-semibold text-[#FBE7F5]/92">پایه تحصیلی</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={formData.base}
|
||||
onChange={(e) => handleChange("base", e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-xl text-white font-bold"
|
||||
className="w-full appearance-none rounded-xl px-3 py-2.5 pl-10 text-sm font-bold text-white outline-none"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(32, 76, 106, 0.4) 0%, rgba(20, 40, 60, 0.4) 100%)",
|
||||
border: errors.base ? "2px solid rgba(220, 53, 69, 0.6)" : "2px solid rgba(138, 206, 224, 0.3)",
|
||||
outline: "none",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
border: errors.base ? "1px solid rgba(248, 113, 113, 0.62)" : "1px solid rgba(255,255,255,0.14)",
|
||||
}}
|
||||
>
|
||||
<option value="">انتخاب کنید</option>
|
||||
|
|
@ -347,33 +314,39 @@ export function EditProfilePage() {
|
|||
</>
|
||||
)}
|
||||
</select>
|
||||
{errors.base && (
|
||||
<p className="text-red-400 text-xs mt-1">{errors.base}</p>
|
||||
)}
|
||||
<ChevronDown
|
||||
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#F2DFF0]/72"
|
||||
/>
|
||||
</div>
|
||||
{errors.base && <p className="mt-1 text-xs text-red-300">{errors.base}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<motion.button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
whileHover={{ scale: isSaving ? 1 : 1.02 }}
|
||||
whileTap={{ scale: isSaving ? 1 : 0.98 }}
|
||||
className="w-full flex items-center justify-center gap-3 px-8 py-4 rounded-2xl font-bold text-lg"
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full text-sm font-bold text-white"
|
||||
style={{
|
||||
background: isSaving
|
||||
? "linear-gradient(135deg, rgba(60, 140, 150, 0.7) 0%, rgba(40, 100, 110, 0.7) 100%)"
|
||||
: "linear-gradient(135deg, rgba(76, 175, 80, 0.9) 0%, rgba(56, 142, 60, 0.9) 100%)",
|
||||
boxShadow: "0 12px 32px rgba(76, 175, 80, 0.4), 0 4px 12px rgba(0, 0, 0, 0.4)",
|
||||
color: "#FFFFFF",
|
||||
textShadow: "0 2px 4px rgba(0, 0, 0, 0.5)",
|
||||
backgroundImage: isSaving
|
||||
? "linear-gradient(145deg, rgba(72, 58, 105, 0.72) 0%, rgba(42, 35, 77, 0.76) 100%)"
|
||||
: "linear-gradient(90deg, rgba(255,139,91,1) 0%, rgba(238,91,166,1) 45%, rgba(147,78,255,1) 100%), linear-gradient(120deg, #7c3aed 0%, #f97316 58%, #facc15 100%)",
|
||||
backgroundOrigin: "border-box",
|
||||
backgroundClip: "padding-box, border-box",
|
||||
border: "1px solid transparent",
|
||||
boxShadow: isSaving
|
||||
? "inset 0 1px 0 rgba(255,255,255,0.12)"
|
||||
: "0 10px 24px rgba(196, 87, 255, 0.32), inset 0 1px 0 rgba(255,255,255,0.3)",
|
||||
opacity: isSaving ? 0.7 : 1,
|
||||
cursor: isSaving ? "not-allowed" : "pointer",
|
||||
}}
|
||||
>
|
||||
<Save size={24} />
|
||||
<Save size={16} />
|
||||
<span>{isSaving ? "در حال ذخیره..." : "ذخیره اطلاعات"}</span>
|
||||
</motion.button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,15 +22,22 @@ const headerActionStyle = {
|
|||
WebkitBackdropFilter: "blur(14px)",
|
||||
} as const;
|
||||
|
||||
export function Header({ showBack = false, onBack, action, onActionClick }: HeaderProps) {
|
||||
export function Header({ title, showBack = false, onBack, action, onActionClick }: HeaderProps) {
|
||||
const shouldShowCenterTitle =
|
||||
title === "اعلانها" || title === "پروفایل" || title === "کیف جادویی";
|
||||
|
||||
return (
|
||||
<header className="app-header relative">
|
||||
<AppHeader showBack={showBack} onBack={onBack} />
|
||||
<AppHeader
|
||||
showBack={showBack}
|
||||
onBack={onBack}
|
||||
centerTitle={shouldShowCenterTitle ? title : undefined}
|
||||
/>
|
||||
{action === "history" && onActionClick && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onActionClick}
|
||||
className="absolute right-[64px] top-3 h-10 px-3 rounded-full text-white text-xs font-bold border-[0.5px] border-transparent z-30"
|
||||
className="absolute right-[60px] top-1/2 z-30 flex h-10 -translate-y-1/2 items-center rounded-full border-[0.5px] border-transparent px-3 text-[11px] font-bold text-white"
|
||||
style={headerActionStyle}
|
||||
aria-label="تاریخچه چت"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { ShoppingBag, Package, Lock, X, Download } from "lucide-react";
|
||||
import coinImage from "figma:asset/f7664d355c12b1003ad460ff44c8f22cfb1bbf5a.png";
|
||||
import coinImage from "../../assets/coin-star.png";
|
||||
import itemImage from "figma:asset/0469c3ac6223dede16e9f8943a3cac9943835707.png";
|
||||
import {
|
||||
loadMagicBagMissions,
|
||||
|
|
@ -105,7 +105,7 @@ const purchasedItems: Item[] = [
|
|||
export function MagicBagPage() {
|
||||
usePageTracking("کیف جادویی");
|
||||
|
||||
const [activeTab, setActiveTab] = useState<"shop" | "owned">("shop");
|
||||
const [activeTab, setActiveTab] = useState<"shop" | "owned">("owned");
|
||||
const [missionItems, setMissionItems] = useState<MagicBagMissionItem[]>([]);
|
||||
const [loadingMissions, setLoadingMissions] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<{ url: string; title: string } | null>(null);
|
||||
|
|
@ -134,54 +134,51 @@ export function MagicBagPage() {
|
|||
return (
|
||||
<div className="pt-6 pb-2" dir="rtl">
|
||||
{/* Tab Switcher */}
|
||||
<div className="mb-4 flex gap-2">
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => setActiveTab("shop")}
|
||||
className="flex-1 py-2.5 rounded-2xl font-bold text-sm flex items-center justify-center gap-1.5"
|
||||
<div
|
||||
className="mb-4 flex rounded-2xl p-1"
|
||||
style={{
|
||||
background: activeTab === "shop"
|
||||
? "linear-gradient(135deg, rgba(255, 183, 0, 0.95) 0%, rgba(255, 140, 0, 0.95) 100%)"
|
||||
: "linear-gradient(135deg, rgba(50, 107, 118, 0.6) 0%, rgba(32, 76, 106, 0.6) 100%)",
|
||||
boxShadow: activeTab === "shop"
|
||||
? "0 6px 18px rgba(255, 165, 0, 0.5)"
|
||||
: "0 3px 10px rgba(0, 0, 0, 0.3)",
|
||||
border: activeTab === "shop"
|
||||
? "1.5px solid rgba(255, 200, 50, 0.5)"
|
||||
: "1.5px solid rgba(138, 206, 224, 0.3)",
|
||||
color: activeTab === "shop" ? "#5A3800" : "#FFFFFF",
|
||||
textShadow: activeTab === "shop"
|
||||
? "0 1px 0 rgba(255, 255, 255, 0.2)"
|
||||
: "none",
|
||||
backgroundImage:
|
||||
"linear-gradient(180deg, rgba(46, 27, 61, 0.92) 0%, rgba(35, 24, 62, 0.94) 100%), linear-gradient(120deg, #7c3aed 0%, #f97316 58%, #facc15 100%)",
|
||||
backgroundOrigin: "border-box",
|
||||
backgroundClip: "padding-box, border-box",
|
||||
border: "0.5px solid transparent",
|
||||
}}
|
||||
>
|
||||
<ShoppingBag className="w-4 h-4" />
|
||||
فروشگاه
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => setActiveTab("owned")}
|
||||
className="flex-1 py-2.5 rounded-2xl font-bold text-sm flex items-center justify-center gap-1.5"
|
||||
className="flex-1 flex items-center justify-center gap-1.5 rounded-xl py-2.5 text-sm font-bold transition-all duration-300"
|
||||
style={{
|
||||
background: activeTab === "owned"
|
||||
? "linear-gradient(135deg, rgba(255, 183, 0, 0.95) 0%, rgba(255, 140, 0, 0.95) 100%)"
|
||||
: "linear-gradient(135deg, rgba(50, 107, 118, 0.6) 0%, rgba(32, 76, 106, 0.6) 100%)",
|
||||
? "linear-gradient(135deg, rgba(174, 117, 255, 0.96) 0%, rgba(138, 82, 238, 0.95) 46%, rgba(102, 55, 204, 0.94) 100%)"
|
||||
: "transparent",
|
||||
boxShadow: activeTab === "owned"
|
||||
? "0 6px 18px rgba(255, 165, 0, 0.5)"
|
||||
: "0 3px 10px rgba(0, 0, 0, 0.3)",
|
||||
border: activeTab === "owned"
|
||||
? "1.5px solid rgba(255, 200, 50, 0.5)"
|
||||
: "1.5px solid rgba(138, 206, 224, 0.3)",
|
||||
color: activeTab === "owned" ? "#5A3800" : "#FFFFFF",
|
||||
textShadow: activeTab === "owned"
|
||||
? "0 1px 0 rgba(255, 255, 255, 0.2)"
|
||||
? "0 0 18px rgba(155,108,241,0.4)"
|
||||
: "none",
|
||||
color: activeTab === "owned" ? "#FFFFFF" : "rgba(251,231,245,0.72)",
|
||||
}}
|
||||
>
|
||||
<Package className="w-4 h-4" />
|
||||
ایتمهای من
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => setActiveTab("shop")}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 rounded-xl py-2.5 text-sm font-bold transition-all duration-300"
|
||||
style={{
|
||||
background: activeTab === "shop"
|
||||
? "linear-gradient(135deg, rgba(174, 117, 255, 0.96) 0%, rgba(138, 82, 238, 0.95) 46%, rgba(102, 55, 204, 0.94) 100%)"
|
||||
: "transparent",
|
||||
boxShadow: activeTab === "shop"
|
||||
? "0 0 18px rgba(155,108,241,0.4)"
|
||||
: "none",
|
||||
color: activeTab === "shop" ? "#FFFFFF" : "rgba(251,231,245,0.72)",
|
||||
}}
|
||||
>
|
||||
<ShoppingBag className="w-4 h-4" />
|
||||
فروشگاه
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{/* Shop Tab */}
|
||||
|
|
@ -235,20 +232,20 @@ export function MagicBagPage() {
|
|||
className="absolute top-0 left-0 flex items-center gap-1 px-2 py-1 rounded-full"
|
||||
style={{
|
||||
background: canAfford
|
||||
? "linear-gradient(135deg, rgba(255, 193, 7, 0.95) 0%, rgba(255, 152, 0, 0.95) 100%)"
|
||||
: "linear-gradient(135deg, rgba(107, 114, 128, 0.95) 0%, rgba(75, 85, 99, 0.95) 100%)",
|
||||
border: `1.5px solid ${canAfford ? "rgba(255, 200, 50, 0.8)" : "rgba(107, 114, 128, 0.8)"}`,
|
||||
? "linear-gradient(135deg, rgba(44, 34, 66, 0.96) 0%, rgba(30, 22, 50, 0.98) 100%)"
|
||||
: "linear-gradient(135deg, rgba(68, 74, 88, 0.96) 0%, rgba(53, 58, 71, 0.98) 100%)",
|
||||
border: `1.5px solid ${canAfford ? "rgba(255, 214, 240, 0.45)" : "rgba(148, 163, 184, 0.45)"}`,
|
||||
boxShadow: canAfford
|
||||
? "0 2px 8px rgba(255, 193, 7, 0.6)"
|
||||
? "0 2px 8px rgba(15, 10, 25, 0.6)"
|
||||
: "0 2px 8px rgba(0, 0, 0, 0.4)",
|
||||
}}
|
||||
>
|
||||
<img src={coinImage} alt="سکه" className="w-3.5 h-3.5" />
|
||||
<img src={coinImage} alt="سکه" className="w-4 h-4" />
|
||||
<span
|
||||
className="text-[10px] font-bold"
|
||||
style={{
|
||||
color: canAfford ? "#5A3800" : "rgba(255, 255, 255, 0.7)",
|
||||
textShadow: canAfford ? "0 1px 0 rgba(255, 255, 255, 0.3)" : "none",
|
||||
color: canAfford ? "#FFF3CC" : "rgba(255, 255, 255, 0.74)",
|
||||
textShadow: "none",
|
||||
}}
|
||||
>
|
||||
{item.price}
|
||||
|
|
@ -419,14 +416,7 @@ export function MagicBagPage() {
|
|||
|
||||
{/* Divider */}
|
||||
{missionItems.length > 0 && purchasedItems.length > 0 && (
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div
|
||||
className="flex-1 h-[1px]"
|
||||
style={{
|
||||
background: "linear-gradient(90deg, transparent 0%, rgba(138, 206, 224, 0.3) 50%, transparent 100%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="my-5 h-px w-full bg-white/20" />
|
||||
)}
|
||||
|
||||
{/* Section: خریداری شده */}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,23 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "motion/react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { usePageTracking } from "../../hooks/usePageTracking";
|
||||
import { useInbox } from "../context/InboxContext";
|
||||
import { FeedHeader } from "./feed/FeedHeader";
|
||||
import { getMessageStyle } from "../../utils/messageUtils";
|
||||
import { AppBackground } from "./shared/AppBackground";
|
||||
import { backgroundImages } from "../../config/backgroundConfig";
|
||||
import { BottomNav } from "./BottomNav";
|
||||
|
||||
const cardStyle = {
|
||||
backgroundImage:
|
||||
"linear-gradient(180deg, rgba(46, 27, 61, 0.9) 0%, rgba(35, 24, 62, 0.94) 100%), linear-gradient(120deg, #7c3aed 0%, #f97316 58%, #facc15 100%)",
|
||||
backgroundOrigin: "border-box",
|
||||
backgroundClip: "padding-box, border-box",
|
||||
border: "0.5px solid transparent",
|
||||
boxShadow:
|
||||
"0 -5px 16px rgba(7, 0, 18, 0.35), 0 8px 14px rgba(5, 2, 12, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.12)",
|
||||
} as const;
|
||||
|
||||
export function MessagesPage() {
|
||||
const navigate = useNavigate();
|
||||
usePageTracking("پیامها");
|
||||
const { messages, loading, markMessagesAsRead } = useInbox();
|
||||
const hasMarkedAsReadRef = useRef(false);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
navigate("/");
|
||||
}, [navigate]);
|
||||
|
||||
// Mark all unread messages as read when page is opened (only once)
|
||||
useEffect(() => {
|
||||
if (!loading && messages.length > 0 && !hasMarkedAsReadRef.current) {
|
||||
const unreadStageIds = messages
|
||||
|
|
@ -34,123 +32,49 @@ export function MessagesPage() {
|
|||
}, [loading, messages, markMessagesAsRead]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full relative overflow-hidden">
|
||||
<AppBackground position="fixed" zIndex={0} imageUrl={backgroundImages.messages} />
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 max-w-md mx-auto">
|
||||
{/* Header */}
|
||||
<FeedHeader topicTitle="پیامها" onBack={handleBack} />
|
||||
|
||||
{/* Messages List - Scrollable */}
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 bottom-0 max-w-md mx-auto overflow-hidden"
|
||||
style={{
|
||||
paddingTop: "110px",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-full overflow-y-auto relative px-[20px] pt-[48px] pb-[128px]"
|
||||
style={{
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
maskImage: "linear-gradient(to bottom, transparent 0%, black 60px)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, transparent 0%, black 60px)",
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full min-h-0 flex-col pb-3 pt-4">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-white text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto mb-4"></div>
|
||||
<p>در حال بارگذاری پیامها...</p>
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-3 h-10 w-10 animate-spin rounded-full border-4 border-[#ffd6f0]/30 border-t-[#ff79cf]" />
|
||||
<p className="text-sm font-bold text-[#FBE7F5]">در حال بارگذاری پیامها...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-white text-center">
|
||||
<p className="text-lg">پیامی وجود ندارد</p>
|
||||
</div>
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm font-bold text-[#FBE7F5]/88">پیامی وجود ندارد</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4" dir="rtl">
|
||||
<div className="space-y-3" dir="rtl">
|
||||
{messages.map((message, index) => {
|
||||
const messageStyle = getMessageStyle(message.kind);
|
||||
const isUnread = message.status === "خوانده نشده";
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<motion.article
|
||||
key={`${message.user_id}-${index}`}
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.1, duration: 0.4 }}
|
||||
className="relative"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: Math.min(index * 0.05, 0.25), duration: 0.26 }}
|
||||
className="relative overflow-hidden rounded-2xl px-4 py-3"
|
||||
style={cardStyle}
|
||||
>
|
||||
<div
|
||||
className="rounded-3xl p-5 backdrop-blur-sm"
|
||||
<span
|
||||
className="absolute inset-y-0 right-0 w-1.5"
|
||||
style={{
|
||||
background: messageStyle.gradient,
|
||||
border: `1.5px solid ${messageStyle.border}`,
|
||||
boxShadow: messageStyle.shadow,
|
||||
opacity: isUnread ? 1 : 0.85,
|
||||
}}
|
||||
>
|
||||
{/* Unread Badge */}
|
||||
{isUnread && (
|
||||
<div
|
||||
className="absolute top-3 left-3 w-3 h-3 rounded-full"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #FF4444 0%, #CC0000 100%)",
|
||||
boxShadow: "0 0 8px rgba(255, 68, 68, 0.8)",
|
||||
background: isUnread
|
||||
? "linear-gradient(180deg, rgba(255,97,152,0.95) 0%, rgba(234,71,170,0.88) 100%)"
|
||||
: "linear-gradient(180deg, rgba(148,163,184,0.35) 0%, rgba(100,116,139,0.28) 100%)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Icon */}
|
||||
<div
|
||||
className="flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center text-white"
|
||||
style={{
|
||||
background: messageStyle.gradient,
|
||||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
|
||||
}}
|
||||
>
|
||||
{messageStyle.icon}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1">
|
||||
<h3 className="text-white font-bold text-base mb-2">{message.title}</h3>
|
||||
<p className="text-white/80 text-sm leading-relaxed">{message.Message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
<h3 className="pr-3 text-sm font-extrabold text-[#FBE7F5]">{message.title}</h3>
|
||||
<p className="mt-1.5 pr-3 text-xs leading-6 text-[#F2DFF0]/88">{message.Message}</p>
|
||||
</motion.article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom fade gradient */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 h-16 pointer-events-none"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, transparent 0%, rgba(46, 27, 61, 0.82) 50%, rgba(35, 24, 62, 1) 100%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { LogOut, Edit2, CheckCircle2, Clock, XCircle, TrendingUp, Play, Camera } from "lucide-react";
|
||||
import { LogOut, Edit2, CheckCircle2, ShieldCheck, Clock, XCircle, Play, Camera } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion } from "motion/react";
|
||||
import profileIcon from "../../assets/image 5.png";
|
||||
import coinImage from "figma:asset/f7664d355c12b1003ad460ff44c8f22cfb1bbf5a.png";
|
||||
import coinImage from "../../assets/coin-star.png";
|
||||
import { logout, getUserInfo } from "../../utils/auth";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getUserProfile, getCachedProfile, saveUserProfile, getUserProfileData, type UserProfile, type ProfileChallenge, type ProfileCoinTransaction, type ProfilePost } from "../../services/profileService";
|
||||
import { PostCard } from "./PostCard";
|
||||
import { AvatarSelectionModal } from "./AvatarSelectionModal";
|
||||
import { getProfileImageUrl, getFeedImageUrl, getVideoUrl, getAudioUrl, isImageFile, getMagicBagFileUrl, getAvatarUrl, bumpAvatarCacheBust } from "../../services/feedService";
|
||||
import { getProfileImageUrl, getFeedImageUrl, getVideoUrl, getAudioUrl, isImageFile, getMagicBagFileUrl, getAvatarUrl } from "../../services/feedService";
|
||||
import { ImageWithFallback } from "./figma/ImageWithFallback";
|
||||
import { usePageTracking } from "../../hooks/usePageTracking";
|
||||
import { getMissionTypeToTopicId } from "../../utils/topicMapper";
|
||||
|
|
@ -19,21 +19,21 @@ const getStatusBadge = (status: string) => {
|
|||
return {
|
||||
icon: <CheckCircle2 className="w-4 h-4" />,
|
||||
text: "انجام شده",
|
||||
gradient: "linear-gradient(135deg, rgba(34, 197, 94, 0.9) 0%, rgba(22, 163, 74, 0.9) 100%)",
|
||||
border: "rgba(34, 197, 94, 0.5)",
|
||||
shadow: "0 2px 8px rgba(34, 197, 94, 0.4)",
|
||||
gradient: "linear-gradient(135deg, rgba(32, 201, 151, 0.95) 0%, rgba(16, 185, 129, 0.95) 100%)",
|
||||
border: "rgba(110, 231, 183, 0.6)",
|
||||
shadow: "0 2px 8px rgba(16, 185, 129, 0.35)",
|
||||
};
|
||||
} else if (status === "تایید شده") {
|
||||
return {
|
||||
icon: <CheckCircle2 className="w-4 h-4" />,
|
||||
icon: <ShieldCheck className="w-4 h-4" />,
|
||||
text: "تایید شده",
|
||||
gradient: "linear-gradient(135deg, rgba(59, 130, 246, 0.9) 0%, rgba(37, 99, 235, 0.9) 100%)",
|
||||
border: "rgba(59, 130, 246, 0.5)",
|
||||
shadow: "0 2px 8px rgba(59, 130, 246, 0.4)",
|
||||
gradient: "linear-gradient(135deg, rgba(59, 130, 246, 0.95) 0%, rgba(37, 99, 235, 0.95) 100%)",
|
||||
border: "rgba(147, 197, 253, 0.62)",
|
||||
shadow: "0 2px 8px rgba(37, 99, 235, 0.35)",
|
||||
};
|
||||
} else if (status === "در حال انجام") {
|
||||
return {
|
||||
icon: <Play className="w-4 h-4" />,
|
||||
icon: <Play className="w-4 h-4 rotate-180" />,
|
||||
text: "در حال انجام",
|
||||
gradient: "linear-gradient(135deg, rgba(255, 193, 7, 0.9) 0%, rgba(255, 160, 0, 0.9) 100%)",
|
||||
border: "rgba(255, 193, 7, 0.5)",
|
||||
|
|
@ -61,7 +61,7 @@ const getStatusBadge = (status: string) => {
|
|||
export function ProfilePage() {
|
||||
const navigate = useNavigate();
|
||||
usePageTracking("پروفایل");
|
||||
const { refreshProfile } = useProfile();
|
||||
const { profile, refreshProfile } = useProfile();
|
||||
|
||||
const [userInfo, setUserInfo] = useState<{ Name: string; Family: string; Username: string } | null>(null);
|
||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||
|
|
@ -81,6 +81,12 @@ export function ProfilePage() {
|
|||
loadProfile();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile) {
|
||||
setUserProfile(profile);
|
||||
}
|
||||
}, [profile]);
|
||||
|
||||
const loadProfile = async () => {
|
||||
setIsLoadingProfile(true);
|
||||
try {
|
||||
|
|
@ -138,6 +144,28 @@ export function ProfilePage() {
|
|||
return String(num).replace(/\d/g, (digit) => persianDigits[parseInt(digit)]);
|
||||
};
|
||||
|
||||
const toPersianDigitsInText = (value: string | null | undefined): string => {
|
||||
if (!value) return "";
|
||||
const persianDigits = "۰۱۲۳۴۵۶۷۸۹";
|
||||
return value.replace(/\d/g, (digit) => persianDigits[parseInt(digit, 10)]);
|
||||
};
|
||||
|
||||
const getChallengeAccent = (status: string): string => {
|
||||
if (status === "انجام شده") {
|
||||
return "linear-gradient(180deg, #7EF7C5 0%, #4BCF9F 100%)";
|
||||
}
|
||||
if (status === "تایید شده") {
|
||||
return "linear-gradient(180deg, #87D6FF 0%, #5DAEFF 100%)";
|
||||
}
|
||||
if (status === "در حال انجام") {
|
||||
return "linear-gradient(180deg, #FFD994 0%, #F5B14A 100%)";
|
||||
}
|
||||
if (status === "رد شده") {
|
||||
return "linear-gradient(180deg, #FF8A94 0%, #EF4444 100%)";
|
||||
}
|
||||
return "linear-gradient(180deg, #F6A6DA 0%, #D777BE 100%)";
|
||||
};
|
||||
|
||||
const handleAvatarSelect = async (imageFilename: string) => {
|
||||
// imageFilename از AvatarSelectionModal نام فایل آپلود شده به سرور است
|
||||
console.log("handleAvatarSelect called with:", imageFilename);
|
||||
|
|
@ -165,9 +193,6 @@ export function ProfilePage() {
|
|||
console.log("Saving profile with data:", JSON.stringify(saveData));
|
||||
const result = await saveUserProfile(saveData);
|
||||
console.log("Save profile result:", result);
|
||||
if (result) {
|
||||
bumpAvatarCacheBust();
|
||||
}
|
||||
|
||||
// بارگذاری مجدد پروفایل از سرور
|
||||
console.log("Reloading profile from server...");
|
||||
|
|
@ -200,6 +225,14 @@ export function ProfilePage() {
|
|||
WebkitBackdropFilter: "blur(14px)",
|
||||
} as const;
|
||||
|
||||
const segmentedControlStyle = {
|
||||
backgroundImage:
|
||||
"linear-gradient(180deg, rgba(46, 27, 61, 0.92) 0%, rgba(35, 24, 62, 0.94) 100%), linear-gradient(120deg, #7c3aed 0%, #f97316 58%, #facc15 100%)",
|
||||
backgroundOrigin: "border-box",
|
||||
backgroundClip: "padding-box, border-box",
|
||||
border: "0.5px solid transparent",
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pt-6 pb-2"
|
||||
|
|
@ -212,20 +245,45 @@ export function ProfilePage() {
|
|||
className="flex flex-col items-center mb-4"
|
||||
>
|
||||
{/* Avatar with Edit Button */}
|
||||
<div className="relative mb-3">
|
||||
<div className="mb-3 grid w-full grid-cols-3 items-center">
|
||||
<div className="flex justify-center">
|
||||
<motion.button
|
||||
whileTap={{ scale: isLoggingOut ? 1 : 0.92 }}
|
||||
onClick={handleLogout}
|
||||
disabled={isLoggingOut}
|
||||
aria-label={isLoggingOut ? "در حال خروج" : "خروج"}
|
||||
className="flex h-12 min-w-[92px] items-center justify-center gap-1.5 rounded-full px-3"
|
||||
style={{
|
||||
background: isLoggingOut
|
||||
? "linear-gradient(145deg, rgba(226, 84, 142, 0.72) 0%, rgba(196, 57, 109, 0.72) 100%)"
|
||||
: "linear-gradient(145deg, rgba(255, 93, 143, 0.95) 0%, rgba(236, 63, 122, 0.95) 52%, rgba(207, 40, 98, 0.95) 100%)",
|
||||
boxShadow: isLoggingOut
|
||||
? "0 6px 12px rgba(170, 34, 79, 0.3), inset 0 1px 0 rgba(255,255,255,0.18)"
|
||||
: "0 10px 18px rgba(207, 40, 98, 0.36), inset 0 1px 0 rgba(255,255,255,0.26)",
|
||||
border: "1px solid rgba(255, 188, 214, 0.5)",
|
||||
opacity: isLoggingOut ? 0.7 : 1,
|
||||
cursor: isLoggingOut ? "not-allowed" : "pointer",
|
||||
}}
|
||||
>
|
||||
<LogOut size={16} color="#fff" />
|
||||
<span className="text-xs font-bold text-white">{isLoggingOut ? "خروج..." : "خروج"}</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", duration: 0.5 }}
|
||||
className="w-24 h-24 rounded-full p-[2px]"
|
||||
className="h-24 w-24 rounded-full p-[2px]"
|
||||
style={navLikePanelStyle}
|
||||
>
|
||||
<div className="w-full h-full rounded-full overflow-hidden flex items-center justify-center" style={navLikePanelStyle}>
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden rounded-full" style={navLikePanelStyle}>
|
||||
{userProfile?.image ? (
|
||||
<ImageWithFallback
|
||||
src={getProfileImageUrl(userProfile.image, userProfile.user_stage_id)}
|
||||
alt="پروفایل"
|
||||
className="w-full h-full rounded-full object-cover"
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
fallbackSrc={profileIcon}
|
||||
style={{
|
||||
filter: "drop-shadow(0 3px 6px rgba(138, 206, 224, 0.6))",
|
||||
|
|
@ -235,7 +293,7 @@ export function ProfilePage() {
|
|||
<img
|
||||
src={profileIcon}
|
||||
alt="پروفایل"
|
||||
className="w-[84px] h-[84px] object-cover rounded-full"
|
||||
className="h-[84px] w-[84px] rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -246,17 +304,40 @@ export function ProfilePage() {
|
|||
whileTap={{ scale: 0.9 }}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
onClick={() => setShowAvatarModal(true)}
|
||||
className="absolute bottom-0 right-0 w-8 h-8 rounded-full flex items-center justify-center"
|
||||
className="absolute bottom-0 right-0 flex h-8 w-8 items-center justify-center rounded-full"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(255, 193, 7, 0.95) 0%, rgba(255, 152, 0, 0.95) 100%)",
|
||||
boxShadow: "0 3px 10px rgba(255, 193, 7, 0.6), 0 0 16px rgba(255, 193, 7, 0.4)",
|
||||
border: "2px solid rgba(255, 255, 255, 0.9)",
|
||||
}}
|
||||
>
|
||||
<Camera className="w-3.5 h-3.5" style={{ color: "#5A3800" }} />
|
||||
<Camera className="h-3.5 w-3.5" style={{ color: "#5A3800" }} />
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
{(userProfile?.user_workflowID || !isLoadingProfile) && (
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.92 }}
|
||||
onClick={() => navigate("/edit-profile")}
|
||||
aria-label={userProfile?.user_workflowID ? "ویرایش پروفایل" : "تکمیل پروفایل"}
|
||||
className="flex h-12 min-w-[92px] items-center justify-center gap-1.5 rounded-full px-3"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(255, 211, 92, 0.98) 0%, rgba(255, 180, 64, 0.97) 55%, rgba(244, 148, 34, 0.96) 100%)",
|
||||
backgroundOrigin: "border-box",
|
||||
backgroundClip: "padding-box, border-box",
|
||||
boxShadow: "0 10px 20px rgba(245, 158, 11, 0.32), inset 0 1px 0 rgba(255,255,255,0.4)",
|
||||
border: "1px solid rgba(255, 236, 167, 0.55)",
|
||||
}}
|
||||
>
|
||||
<Edit2 size={16} color="#fff" />
|
||||
<span className="text-xs font-bold text-white">ویرایش</span>
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<h2
|
||||
className="text-white text-lg font-bold mb-0.5"
|
||||
|
|
@ -282,161 +363,36 @@ export function ProfilePage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex gap-2.5 mb-3">
|
||||
{[
|
||||
{ label: "چالشها", value: toPersianNumber(challenges.filter(c => c.status === "انجام شده").length) },
|
||||
{ label: "سکهها", value: toPersianNumber(userProfile?.coin_count) },
|
||||
{ label: "پستها", value: toPersianNumber(posts.length) },
|
||||
].map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="px-4 py-2 rounded-2xl"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(32, 76, 106, 0.6) 0%, rgba(20, 40, 60, 0.6) 100%)",
|
||||
border: "1.5px solid rgba(138, 206, 224, 0.3)",
|
||||
boxShadow: "inset 0 1.5px 6px rgba(0, 0, 0, 0.4), 0 3px 8px rgba(0, 0, 0, 0.3)",
|
||||
}}
|
||||
>
|
||||
<div className="text-white font-bold text-sm mb-0.5">{stat.value}</div>
|
||||
<div style={{ color: "#8ACEE0" }} className="text-[9px]">
|
||||
{stat.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 w-full">
|
||||
{/* Edit Profile Button */}
|
||||
{userProfile?.user_workflowID ? (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => navigate("/edit-profile")}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-4 py-2 rounded-2xl font-bold text-xs"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(76, 175, 80, 0.9) 0%, rgba(56, 142, 60, 0.9) 100%)",
|
||||
boxShadow: "0 3px 12px rgba(76, 175, 80, 0.4)",
|
||||
color: "#FFFFFF",
|
||||
textShadow: "0 1px 3px rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
<span>ویرایش</span>
|
||||
</motion.button>
|
||||
) : !isLoadingProfile ? (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => navigate("/edit-profile")}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-4 py-2 rounded-2xl font-bold text-xs"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(255, 193, 7, 0.9) 0%, rgba(255, 160, 0, 0.9) 100%)",
|
||||
boxShadow: "0 3px 12px rgba(255, 193, 7, 0.4)",
|
||||
color: "#FFFFFF",
|
||||
textShadow: "0 1px 3px rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
<span>تکمیل پروفایل</span>
|
||||
</motion.button>
|
||||
) : null}
|
||||
|
||||
{/* Logout Button */}
|
||||
<motion.button
|
||||
whileHover={{ scale: isLoggingOut ? 1 : 1.03 }}
|
||||
whileTap={{ scale: isLoggingOut ? 1 : 0.97 }}
|
||||
onClick={handleLogout}
|
||||
disabled={isLoggingOut}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-4 py-2 rounded-2xl font-bold text-xs"
|
||||
style={{
|
||||
background: isLoggingOut
|
||||
? "linear-gradient(135deg, rgba(120, 30, 40, 0.7) 0%, rgba(96, 24, 32, 0.7) 100%)"
|
||||
: "linear-gradient(135deg, rgba(220, 53, 69, 0.9) 0%, rgba(176, 42, 55, 0.9) 100%)",
|
||||
boxShadow: "0 3px 12px rgba(220, 53, 69, 0.4)",
|
||||
color: "#FFFFFF",
|
||||
textShadow: "0 1px 3px rgba(0, 0, 0, 0.5)",
|
||||
opacity: isLoggingOut ? 0.7 : 1,
|
||||
cursor: isLoggingOut ? "not-allowed" : "pointer",
|
||||
}}
|
||||
>
|
||||
<LogOut size={14} />
|
||||
<span>{isLoggingOut ? "خروج..." : "خروج"}</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Tab Switcher */}
|
||||
<div className="mb-3 flex gap-2">
|
||||
<div className="mb-3 flex rounded-2xl p-1" style={segmentedControlStyle}>
|
||||
{(
|
||||
[
|
||||
{ id: "challenges", label: "سابقه چالشها" },
|
||||
{ id: "coins", label: "سابقه سکهها" },
|
||||
{ id: "posts", label: "پستها" },
|
||||
] as const
|
||||
).map((tab) => (
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => setActiveTab("challenges")}
|
||||
className="flex-1 py-2 rounded-2xl font-bold text-[11px] flex items-center justify-center gap-1"
|
||||
style={{
|
||||
background: activeTab === "challenges"
|
||||
? "linear-gradient(135deg, rgba(255, 183, 0, 0.95) 0%, rgba(255, 140, 0, 0.95) 100%)"
|
||||
: "linear-gradient(135deg, rgba(50, 107, 118, 0.6) 0%, rgba(32, 76, 106, 0.6) 100%)",
|
||||
boxShadow: activeTab === "challenges"
|
||||
? "0 3px 12px rgba(255, 165, 0, 0.5)"
|
||||
: "0 2px 6px rgba(0, 0, 0, 0.3)",
|
||||
border: activeTab === "challenges"
|
||||
? "1.5px solid rgba(255, 200, 50, 0.5)"
|
||||
: "1.5px solid rgba(138, 206, 224, 0.3)",
|
||||
color: activeTab === "challenges" ? "#5A3800" : "#FFFFFF",
|
||||
textShadow: activeTab === "challenges"
|
||||
? "0 1px 0 rgba(255, 255, 255, 0.2)"
|
||||
: "none",
|
||||
}}
|
||||
key={tab.id}
|
||||
whileTap={{ scale: 0.96 }}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className="flex flex-1 items-center justify-center gap-1 rounded-xl py-2.5 text-[11px] font-bold transition-all duration-300"
|
||||
style={
|
||||
activeTab === tab.id
|
||||
? {
|
||||
background:
|
||||
"linear-gradient(135deg, rgba(174, 117, 255, 0.96) 0%, rgba(138, 82, 238, 0.95) 46%, rgba(102, 55, 204, 0.94) 100%)",
|
||||
color: "#FFFFFF",
|
||||
boxShadow: "0 0 18px rgba(155,108,241,0.4)",
|
||||
}
|
||||
: { color: "rgba(251,231,245,0.72)" }
|
||||
}
|
||||
>
|
||||
سابقه چالشها
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => setActiveTab("coins")}
|
||||
className="flex-1 py-2 rounded-2xl font-bold text-[11px] flex items-center justify-center gap-1"
|
||||
style={{
|
||||
background: activeTab === "coins"
|
||||
? "linear-gradient(135deg, rgba(255, 183, 0, 0.95) 0%, rgba(255, 140, 0, 0.95) 100%)"
|
||||
: "linear-gradient(135deg, rgba(50, 107, 118, 0.6) 0%, rgba(32, 76, 106, 0.6) 100%)",
|
||||
boxShadow: activeTab === "coins"
|
||||
? "0 3px 12px rgba(255, 165, 0, 0.5)"
|
||||
: "0 2px 6px rgba(0, 0, 0, 0.3)",
|
||||
border: activeTab === "coins"
|
||||
? "1.5px solid rgba(255, 200, 50, 0.5)"
|
||||
: "1.5px solid rgba(138, 206, 224, 0.3)",
|
||||
color: activeTab === "coins" ? "#5A3800" : "#FFFFFF",
|
||||
textShadow: activeTab === "coins"
|
||||
? "0 1px 0 rgba(255, 255, 255, 0.2)"
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
سابقه سکهها
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => setActiveTab("posts")}
|
||||
className="flex-1 py-2 rounded-2xl font-bold text-[11px] flex items-center justify-center gap-1"
|
||||
style={{
|
||||
background: activeTab === "posts"
|
||||
? "linear-gradient(135deg, rgba(255, 183, 0, 0.95) 0%, rgba(255, 140, 0, 0.95) 100%)"
|
||||
: "linear-gradient(135deg, rgba(50, 107, 118, 0.6) 0%, rgba(32, 76, 106, 0.6) 100%)",
|
||||
boxShadow: activeTab === "posts"
|
||||
? "0 3px 12px rgba(255, 165, 0, 0.5)"
|
||||
: "0 2px 6px rgba(0, 0, 0, 0.3)",
|
||||
border: activeTab === "posts"
|
||||
? "1.5px solid rgba(255, 200, 50, 0.5)"
|
||||
: "1.5px solid rgba(138, 206, 224, 0.3)",
|
||||
color: activeTab === "posts" ? "#5A3800" : "#FFFFFF",
|
||||
textShadow: activeTab === "posts"
|
||||
? "0 1px 0 rgba(255, 255, 255, 0.2)"
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
پستها
|
||||
{tab.label}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Challenges Tab */}
|
||||
|
|
@ -448,9 +404,11 @@ export function ProfilePage() {
|
|||
<div className="text-center text-white/60 py-8">هنوز چالشی ثبت نشده است</div>
|
||||
) : (
|
||||
challenges.map((challenge, index) => {
|
||||
const statusBadge = getStatusBadge(challenge.status);
|
||||
const coins = parseInt(challenge.coin_count || "0");
|
||||
const isInProgress = challenge.status === "در حال انجام";
|
||||
const statusBadge = getStatusBadge(challenge.status);
|
||||
const showEarnedCoins = !isInProgress &&
|
||||
(challenge.status === "انجام شده" || challenge.status === "تایید شده") && coins > 0;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
|
@ -464,78 +422,60 @@ export function ProfilePage() {
|
|||
navigate(`/chatbot/${topicId}?missionId=${challenge.mission_id}&missionType=${encodeURIComponent(challenge.mission_type)}&continueMode=true`);
|
||||
}
|
||||
}}
|
||||
className={`rounded-2xl p-4 ${isInProgress ? 'cursor-pointer' : ''}`}
|
||||
className={`relative overflow-hidden rounded-2xl px-4 py-3 ${isInProgress ? "cursor-pointer" : ""}`}
|
||||
style={{
|
||||
background: isInProgress
|
||||
? "linear-gradient(135deg, rgba(255, 193, 7, 0.15) 0%, rgba(255, 152, 0, 0.15) 100%)"
|
||||
: "linear-gradient(135deg, rgba(32, 76, 106, 0.5) 0%, rgba(20, 40, 60, 0.5) 100%)",
|
||||
backgroundImage:
|
||||
"linear-gradient(180deg, rgba(46, 27, 61, 0.92) 0%, rgba(35, 24, 62, 0.94) 100%), linear-gradient(120deg, #7c3aed 0%, #f97316 58%, #facc15 100%)",
|
||||
backgroundOrigin: "border-box",
|
||||
backgroundClip: "padding-box, border-box",
|
||||
border: isInProgress
|
||||
? "1.5px solid rgba(255, 193, 7, 0.4)"
|
||||
: "1.5px solid rgba(138, 206, 224, 0.3)",
|
||||
? "1px solid rgba(255, 205, 122, 0.58)"
|
||||
: "0.5px solid rgba(255, 255, 255, 0.16)",
|
||||
boxShadow: isInProgress
|
||||
? "0 4px 16px rgba(255, 193, 7, 0.3)"
|
||||
: "0 4px 12px rgba(0, 0, 0, 0.3)",
|
||||
? "0 10px 22px rgba(255, 183, 77, 0.24), inset 0 1px 0 rgba(255,255,255,0.18)"
|
||||
: "0 10px 22px rgba(10, 6, 22, 0.28), inset 0 1px 0 rgba(255,255,255,0.14)",
|
||||
opacity: 0.9,
|
||||
}}
|
||||
whileHover={isInProgress ? { scale: 1.02, y: -2 } : {}}
|
||||
whileTap={isInProgress ? { scale: 0.98 } : {}}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<h3 className={`font-bold text-sm ${isInProgress ? 'text-yellow-300' : 'text-white'}`}>
|
||||
<span
|
||||
className="absolute bottom-0 right-0 top-0 w-1.5"
|
||||
style={{
|
||||
background: getChallengeAccent(challenge.status),
|
||||
opacity: 0.92,
|
||||
}}
|
||||
/>
|
||||
<div className="mb-1.5 flex items-start justify-between gap-2">
|
||||
<h3 className="flex-1 text-sm font-extrabold text-[#FBE7F5]">
|
||||
{challenge.mission_title}
|
||||
</h3>
|
||||
{isInProgress && (
|
||||
<motion.div
|
||||
animate={{ x: [0, 4, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5 }}
|
||||
>
|
||||
<Play className="w-3.5 h-3.5 text-yellow-300" />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-white text-[10px] font-bold"
|
||||
className="flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10px] font-bold text-white"
|
||||
style={{
|
||||
background: statusBadge.gradient,
|
||||
border: `1px solid ${statusBadge.border}`,
|
||||
boxShadow: statusBadge.shadow,
|
||||
}}
|
||||
>
|
||||
{statusBadge.icon}
|
||||
<span>{statusBadge.text}</span>
|
||||
{statusBadge.icon}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white/60 text-xs">{challenge.datetime1}</span>
|
||||
{isInProgress && (
|
||||
<span className="text-white/50 text-[10px] px-2 py-0.5 rounded-full" style={{
|
||||
background: "rgba(138, 206, 224, 0.2)",
|
||||
border: "1px solid rgba(138, 206, 224, 0.3)"
|
||||
}}>
|
||||
<div className="mb-0 text-[11px] font-bold text-[#F2DFF0]/72">
|
||||
{challenge.mission_type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{(challenge.status === "انجام شده" || challenge.status === "تایید شده") && coins > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<img src={coinImage} alt="سکه" className="w-4 h-4" />
|
||||
<span className="text-yellow-400 font-bold text-xs">
|
||||
+{toPersianNumber(coins)}
|
||||
|
||||
<div className="mt0 flex items-center justify-between text-[11px]">
|
||||
<span className="text-[#F2DFF0]/72">
|
||||
{toPersianDigitsInText(challenge.datetime1)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isInProgress ? (
|
||||
<span className="text-yellow-300 text-[10px] font-bold">
|
||||
برای ادامه کلیک کنید ←
|
||||
<div className="flex items-center gap-2 rounded-full px-2 py-1">
|
||||
<img src={coinImage} alt="سکه" className="h-5 w-5" />
|
||||
<span className="text-base font-extrabold leading-none text-[#FFD873]">
|
||||
{showEarnedCoins ? toPersianNumber(coins) : "۰"}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-white/50 text-[10px]">
|
||||
{challenge.mission_type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
@ -552,24 +492,26 @@ export function ProfilePage() {
|
|||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="rounded-2xl p-4 mb-4"
|
||||
className="mb-4 rounded-2xl p-4"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(255, 193, 7, 0.2) 0%, rgba(255, 152, 0, 0.2) 100%)",
|
||||
border: "2px solid rgba(255, 193, 7, 0.4)",
|
||||
boxShadow: "0 4px 16px rgba(255, 193, 7, 0.3)",
|
||||
backgroundImage:
|
||||
"linear-gradient(180deg, rgba(46, 27, 61, 0.92) 0%, rgba(35, 24, 62, 0.96) 100%), linear-gradient(120deg, rgba(250, 204, 21, 0.28) 0%, rgba(245, 158, 11, 0.24) 50%, rgba(251, 113, 133, 0.26) 100%)",
|
||||
backgroundOrigin: "border-box",
|
||||
backgroundClip: "padding-box, border-box",
|
||||
border: "1px solid transparent",
|
||||
boxShadow:
|
||||
"0 12px 24px rgba(25, 12, 46, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.18)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white/70 text-xs mb-1">مجموع سکههای دریافتی</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={coinImage} alt="سکه" className="w-8 h-8" />
|
||||
<span className="text-yellow-300 font-bold text-2xl">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<img src={coinImage} alt="سکه" className="h-9 w-9" />
|
||||
<span className="text-[32px] font-black leading-none text-[#FFD166]">
|
||||
{toPersianNumber(userProfile?.coin_count)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<TrendingUp className="w-8 h-8 text-yellow-300" />
|
||||
<p className="mt-2 text-xs font-bold text-[#FFF4D6]/92">مجموع کل سکهها</p>
|
||||
<div className="mx-auto mt-3 h-px w-[72%] rounded-full bg-gradient-to-r from-transparent via-[#FFD166] to-transparent opacity-85" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
|
|
@ -590,36 +532,57 @@ export function ProfilePage() {
|
|||
initial={{ opacity: 0, x: -30 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.08, duration: 0.3 }}
|
||||
className="rounded-2xl p-4"
|
||||
className="relative overflow-hidden rounded-2xl px-4 py-3"
|
||||
style={{
|
||||
background: isNegative
|
||||
? "linear-gradient(135deg, rgba(76, 29, 29, 0.5) 0%, rgba(60, 20, 20, 0.5) 100%)"
|
||||
: "linear-gradient(135deg, rgba(32, 76, 106, 0.5) 0%, rgba(20, 40, 60, 0.5) 100%)",
|
||||
border: isNegative
|
||||
? "1.5px solid rgba(239, 68, 68, 0.4)"
|
||||
: "1.5px solid rgba(138, 206, 224, 0.3)",
|
||||
backgroundImage: isNegative
|
||||
? "linear-gradient(180deg, rgba(46, 24, 41, 0.9) 0%, rgba(34, 18, 34, 0.94) 100%), linear-gradient(120deg, #fb7185 0%, #ef4444 100%)"
|
||||
: "linear-gradient(180deg, rgba(46, 27, 61, 0.92) 0%, rgba(35, 24, 62, 0.96) 100%), linear-gradient(120deg, rgba(250, 204, 21, 0.22) 0%, rgba(245, 158, 11, 0.18) 42%, rgba(251, 113, 133, 0.24) 100%)",
|
||||
backgroundOrigin: "border-box",
|
||||
backgroundClip: "padding-box, border-box",
|
||||
border: "1px solid transparent",
|
||||
boxShadow: isNegative
|
||||
? "0 4px 12px rgba(239, 68, 68, 0.2)"
|
||||
: "0 4px 12px rgba(0, 0, 0, 0.3)",
|
||||
? "0 8px 16px rgba(239, 68, 68, 0.18), inset 0 1px 0 rgba(255, 221, 226, 0.2)"
|
||||
: "0 8px 16px rgba(25, 12, 46, 0.22), inset 0 1px 0 rgba(255, 244, 200, 0.12)",
|
||||
opacity: 0.92,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="text-white font-bold text-sm flex-1">
|
||||
<span
|
||||
className="absolute bottom-0 right-0 top-0 w-1.5"
|
||||
style={{
|
||||
background: isNegative
|
||||
? "linear-gradient(180deg, #FB7185 0%, #EF4444 100%)"
|
||||
: "linear-gradient(180deg, #FFD76A 0%, #F59E0B 100%)",
|
||||
opacity: 0.95,
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="line-clamp-2 text-xs font-semibold leading-5 text-[#FFF4D6]/92">
|
||||
{item.description}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5">
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] text-[#FFE9A8]/60">تراکنش سکه</p>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-1.5 rounded-full px-2.5 py-1"
|
||||
style={{
|
||||
background: isNegative ? "rgba(239, 68, 68, 0.18)" : "rgba(255, 209, 102, 0.12)",
|
||||
border: isNegative
|
||||
? "1px solid rgba(251, 113, 133, 0.42)"
|
||||
: "1px solid rgba(255, 224, 140, 0.34)",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={coinImage}
|
||||
alt="سکه"
|
||||
className="w-5 h-5"
|
||||
className="h-4 w-4"
|
||||
style={{
|
||||
filter: isNegative ? "grayscale(100%) brightness(0.8)" : "none"
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="font-bold text-sm"
|
||||
className="text-sm font-extrabold leading-none"
|
||||
style={{
|
||||
color: isNegative ? "#ef4444" : "#fcd34d"
|
||||
color: isNegative ? "#FF9CA8" : "#FFD166"
|
||||
}}
|
||||
>
|
||||
{isNegative ? "-" : "+"}{toPersianNumber(absCoins)}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export function PublicChatPage() {
|
|||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [showChatHistory, setShowChatHistory] = useState(false);
|
||||
const [historyItems, setHistoryItems] = useState<ChatListItem[]>([]);
|
||||
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
||||
const [currentChatWorkflowID, setCurrentChatWorkflowID] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
|
|
@ -124,13 +125,15 @@ export function PublicChatPage() {
|
|||
};
|
||||
|
||||
const handleSendMessage = async (message: string) => {
|
||||
const trimmedText = message.trim();
|
||||
if (!trimmedText || isSending) return;
|
||||
const displayText = message.trim();
|
||||
const serverText = displayText.replace(/\r?\n+/g, " ").trim();
|
||||
|
||||
if (!serverText || isSending) return;
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
id: createMessageId(),
|
||||
type: "user",
|
||||
content: trimmedText,
|
||||
content: displayText,
|
||||
timestamp: new Date().toLocaleTimeString("fa-IR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
|
|
@ -151,7 +154,7 @@ export function PublicChatPage() {
|
|||
setIsSending(true);
|
||||
|
||||
try {
|
||||
const result = await sendPublicChatMessage(trimmedText, currentChatWorkflowID);
|
||||
const result = await sendPublicChatMessage(serverText, currentChatWorkflowID);
|
||||
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== loadingMessageId));
|
||||
|
||||
|
|
@ -186,15 +189,34 @@ export function PublicChatPage() {
|
|||
|
||||
const handleHistoryClick = useCallback(async () => {
|
||||
setShowChatHistory(true);
|
||||
|
||||
setIsLoadingHistory(true);
|
||||
try {
|
||||
const result = await loadChatList();
|
||||
|
||||
if (result.success) {
|
||||
setHistoryItems(result.data);
|
||||
const sortedItems = [...result.data].sort((a, b) => {
|
||||
const timeA = Date.parse(a.datetime1 || "");
|
||||
const timeB = Date.parse(b.datetime1 || "");
|
||||
|
||||
if (!Number.isNaN(timeA) && !Number.isNaN(timeB)) {
|
||||
return timeB - timeA;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const allDatesInvalid = sortedItems.every((item) =>
|
||||
Number.isNaN(Date.parse(item.datetime1 || "")),
|
||||
);
|
||||
|
||||
setHistoryItems(allDatesInvalid ? [...result.data].reverse() : sortedItems);
|
||||
} else {
|
||||
console.error("Failed to load chat list:", result.message);
|
||||
alert(result.message || "خطا در بارگذاری تاریخچه");
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingHistory(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -202,9 +224,24 @@ export function PublicChatPage() {
|
|||
void handleHistoryClick();
|
||||
};
|
||||
|
||||
const onNewChatRequest = () => {
|
||||
if (isSending || isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShowChatHistory(false);
|
||||
setCurrentChatWorkflowID("");
|
||||
setMessages([]);
|
||||
setShouldAutoScroll(true);
|
||||
};
|
||||
|
||||
window.addEventListener("public-chat:history", onHistoryRequest);
|
||||
return () => window.removeEventListener("public-chat:history", onHistoryRequest);
|
||||
}, [handleHistoryClick]);
|
||||
window.addEventListener("public-chat:new-chat", onNewChatRequest);
|
||||
return () => {
|
||||
window.removeEventListener("public-chat:history", onHistoryRequest);
|
||||
window.removeEventListener("public-chat:new-chat", onNewChatRequest);
|
||||
};
|
||||
}, [handleHistoryClick, isLoading, isSending]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full min-h-0 overflow-hidden">
|
||||
|
|
@ -285,6 +322,7 @@ export function PublicChatPage() {
|
|||
<ChatHistoryModal
|
||||
isOpen={showChatHistory}
|
||||
onClose={() => setShowChatHistory(false)}
|
||||
isLoading={isLoadingHistory}
|
||||
historyItems={historyItems.map((item) => ({
|
||||
id: item.chatlist_workflowID,
|
||||
title: item.title || "چت عمومی",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useRef, useEffect, type KeyboardEvent } from "react";
|
||||
import { Send } from "lucide-react";
|
||||
import { Send, Square } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
interface ChatInputBarProps {
|
||||
|
|
@ -21,19 +21,18 @@ export function ChatInputBar({ onSendMessage, disabled = false }: ChatInputBarPr
|
|||
// Reset height after send and refocus
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.style.height = "auto";
|
||||
inputRef.current.style.height = "24px";
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key !== "Enter" || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) {
|
||||
if (e.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
// Keep Enter behavior as newline (same as Shift+Enter).
|
||||
// Sending is intentionally limited to the send button.
|
||||
};
|
||||
|
||||
// Auto-resize textarea based on content
|
||||
|
|
@ -73,14 +72,15 @@ export function ChatInputBar({ onSendMessage, disabled = false }: ChatInputBarPr
|
|||
dir="rtl"
|
||||
disabled={disabled}
|
||||
aria-label="پیام خود را بنویسید"
|
||||
className="chat-input-textarea flex-1 bg-transparent text-white placeholder:text-[rgba(207,168,212,0.7)] resize-none outline-none text-right disabled:opacity-50"
|
||||
className="chat-input-textarea block flex-1 resize-none bg-transparent py-0 text-right text-white outline-none placeholder:text-[rgba(207,168,212,0.7)] disabled:opacity-50"
|
||||
style={{
|
||||
fontFamily: "Alibaba, sans-serif",
|
||||
textAlign: "right",
|
||||
minHeight: "36px",
|
||||
minHeight: "24px",
|
||||
maxHeight: "96px",
|
||||
lineHeight: "1.4",
|
||||
lineHeight: "24px",
|
||||
overflow: "hidden",
|
||||
padding: 0,
|
||||
fontSize: "16px", // حداقل 16px برای جلوگیری از zoom در iOS Safari
|
||||
}}
|
||||
/>
|
||||
|
|
@ -103,7 +103,11 @@ export function ChatInputBar({ onSendMessage, disabled = false }: ChatInputBarPr
|
|||
border: canSend ? "1px solid rgba(255, 189, 228, 0.5)" : "1px solid rgba(198, 111, 177, 0.22)",
|
||||
}}
|
||||
>
|
||||
{disabled ? (
|
||||
<Square className="h-4 w-4 text-[#CFA8D4]/80" />
|
||||
) : (
|
||||
<Send className={`h-5 w-5 ${canSend ? "text-white" : "text-[#CFA8D4]/70"}`} />
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { MessageCircle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface ChatHistoryItem {
|
||||
|
|
@ -12,38 +11,43 @@ interface ChatHistoryItem {
|
|||
interface ChatHistoryModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
isLoading?: boolean;
|
||||
historyItems: ChatHistoryItem[];
|
||||
onSelectChat: (chatId: string) => void;
|
||||
}
|
||||
|
||||
const modalStyles = {
|
||||
overlay: {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.72)",
|
||||
},
|
||||
container: {
|
||||
background: "linear-gradient(180deg, rgba(32, 76, 106, 0.98) 0%, rgba(20, 40, 60, 0.98) 100%)",
|
||||
border: "2px solid rgba(138, 206, 224, 0.3)",
|
||||
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.3)",
|
||||
maxHeight: "85vh",
|
||||
backgroundImage:
|
||||
"linear-gradient(180deg, rgba(46, 27, 61, 0.95) 0%, rgba(35, 24, 62, 0.98) 100%), linear-gradient(120deg, #7c3aed 0%, #f97316 58%, #facc15 100%)",
|
||||
backgroundOrigin: "border-box",
|
||||
backgroundClip: "padding-box, border-box",
|
||||
border: "0.5px solid transparent",
|
||||
boxShadow:
|
||||
"0 -7px 20px rgba(7, 0, 18, 0.4), 0 10px 22px rgba(5, 2, 12, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.16), inset 0 -2px 0 rgba(12, 7, 27, 0.62)",
|
||||
backdropFilter: "blur(14px)",
|
||||
WebkitBackdropFilter: "blur(14px)",
|
||||
},
|
||||
chatItem: {
|
||||
background: "linear-gradient(135deg, rgba(50, 107, 118, 0.6) 0%, rgba(32, 76, 106, 0.6) 100%)",
|
||||
border: "1.5px solid rgba(138, 206, 224, 0.3)",
|
||||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
|
||||
},
|
||||
iconContainer: {
|
||||
background: "linear-gradient(135deg, rgba(138, 206, 224, 0.3) 0%, rgba(76, 127, 137, 0.3) 100%)",
|
||||
background: "rgba(255, 255, 255, 0.07)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.12)",
|
||||
boxShadow: "0 8px 18px rgba(8, 4, 20, 0.24), inset 0 1px 0 rgba(255,255,255,0.1)",
|
||||
},
|
||||
closeButton: {
|
||||
background: "linear-gradient(135deg, rgba(96, 147, 157, 0.6) 0%, rgba(76, 127, 137, 0.6) 100%)",
|
||||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
|
||||
border: "1.5px solid rgba(138, 206, 224, 0.3)",
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(240, 110, 168, 0.95) 0%, rgba(201, 87, 156, 0.94) 52%, rgba(138, 79, 207, 0.93) 100%)",
|
||||
boxShadow: "0 0 16px rgba(240, 110, 168, 0.32), inset 0 1px 0 rgba(255,255,255,0.22)",
|
||||
border: "1px solid rgba(255, 189, 228, 0.5)",
|
||||
},
|
||||
};
|
||||
|
||||
export function ChatHistoryModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
isLoading = false,
|
||||
historyItems,
|
||||
onSelectChat,
|
||||
}: ChatHistoryModalProps) {
|
||||
|
|
@ -65,69 +69,93 @@ export function ChatHistoryModal({
|
|||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center p-4"
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center p-3"
|
||||
style={modalStyles.overlay}
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, y: 20 }}
|
||||
animate={{ scale: 1, y: 0 }}
|
||||
exit={{ scale: 0.9, y: 20 }}
|
||||
initial={{ opacity: 0, y: 14 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-full max-w-md rounded-3xl overflow-hidden"
|
||||
style={modalStyles.container}
|
||||
className="flex w-full max-w-md min-h-0 flex-col overflow-hidden rounded-3xl"
|
||||
style={{ ...modalStyles.container, maxHeight: "min(78dvh, 620px)" }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b" style={{ borderColor: "rgba(138, 206, 224, 0.2)" }}>
|
||||
<div className="border-b px-5 py-4" style={{ borderColor: "rgba(255, 255, 255, 0.12)" }}>
|
||||
<h3
|
||||
className="text-lg font-bold text-center text-white"
|
||||
style={{ textShadow: "0 2px 4px rgba(0, 0, 0, 0.3)" }}
|
||||
className="text-center text-base font-extrabold text-[#FBE7F5]"
|
||||
style={{ textShadow: "0 2px 8px rgba(255, 119, 202, 0.3)" }}
|
||||
>
|
||||
تاریخچه گفتگوها
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="overflow-y-auto p-6" style={{ maxHeight: "calc(85vh - 120px)" }}>
|
||||
<div
|
||||
className="min-h-0 flex-1 overflow-y-auto px-4 py-4"
|
||||
style={{
|
||||
maxHeight:
|
||||
"calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 180px)",
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex h-40 items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-3 h-10 w-10 animate-spin rounded-full border-4 border-[#ffd6f0]/30 border-t-[#ff79cf]" />
|
||||
<p className="text-xs font-bold text-[#FBE7F5]">در حال بارگذاری...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : historyItems.length === 0 ? (
|
||||
<div className="flex h-40 items-center justify-center text-center">
|
||||
<p className="text-sm font-bold text-[#F2DFF0]/72">تاریخچهای پیدا نشد</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{historyItems.map((chat, index) => (
|
||||
<motion.button
|
||||
key={chat.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05, duration: 0.2 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onSelectChat(chat.id);
|
||||
}}
|
||||
className="w-full p-4 rounded-2xl text-right"
|
||||
className="w-full rounded-2xl p-3.5 text-right"
|
||||
style={modalStyles.chatItem}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center"
|
||||
style={modalStyles.iconContainer}
|
||||
>
|
||||
<MessageCircle className="w-5 h-5" style={{ color: "#8ACEE0" }} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-white font-bold text-sm mb-1">{chat.title}</h4>
|
||||
<p className="text-white/60 text-xs mb-1">{chat.lastMessage}</p>
|
||||
<p className="text-white/40 text-xs">{chat.date}</p>
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h4 className="line-clamp-1 text-sm font-bold text-[#FBE7F5]">
|
||||
{chat.title}
|
||||
</h4>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-[#ff78c6]" />
|
||||
</div>
|
||||
{!!chat.lastMessage && (
|
||||
<p className="mt-1 line-clamp-1 text-xs text-[#F2DFF0]/74">
|
||||
{chat.lastMessage}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-1 text-[11px] text-[#F2DFF0]/62">{chat.date}</p>
|
||||
<div className="mt-2 h-px w-full bg-white/8" />
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t" style={{ borderColor: "rgba(138, 206, 224, 0.2)" }}>
|
||||
<div
|
||||
className="border-t px-5 pb-[calc(env(safe-area-inset-bottom,0px)+12px)] pt-3"
|
||||
style={{ borderColor: "rgba(255, 255, 255, 0.12)" }}
|
||||
>
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.95 }}
|
||||
whileTap={{ scale: 0.96 }}
|
||||
onClick={onClose}
|
||||
className="w-full py-3 rounded-full font-bold text-white"
|
||||
className="w-full rounded-full py-3 text-sm font-bold text-white"
|
||||
style={modalStyles.closeButton}
|
||||
>
|
||||
بستن
|
||||
|
|
@ -136,6 +164,11 @@ export function ChatHistoryModal({
|
|||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
<style>{`
|
||||
.min-h-0.flex-1.overflow-y-auto::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ body,
|
|||
z-index: 20;
|
||||
height: 68px;
|
||||
min-height: 68px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.page-frame {
|
||||
|
|
@ -94,6 +94,6 @@ body,
|
|||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 30;
|
||||
height: calc(94px + env(safe-area-inset-bottom));
|
||||
min-height: calc(94px + env(safe-area-inset-bottom));
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user