Hamdast1/src/app/components/ChallengeSelectionPage.tsx
reza7321 213c0a70f0 امروز ۳۱ اردیبهشت
چت بات عمومی خرابه
2026-05-21 14:50:03 +03:30

365 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useNavigate, useParams } from "react-router-dom";
import { motion } from "motion/react";
import { useState, useEffect, useCallback } from "react";
import { ChevronLeft, Clock3, Star } from "lucide-react";
import challengeIcon from "figma:asset/c11973053d8410ffeb3c76aa4d1da6991076e7e1.png";
import { getTopicConfig } from "../../config/topicConfig";
import { loadMissions, getMissionImageUrl, MissionData } from "../../services/feedService";
import { usePageTracking } from "../../hooks/usePageTracking";
import { AppHeader } from "./AppHeader";
import { BottomNav } from "./BottomNav";
import { AppBackground } from "./shared/AppBackground";
import { backgroundImages } from "../../config/backgroundConfig";
import coinImage from "../../assets/coin-star.png";
const PERSIAN_NUMBERS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"] as const;
const toPersianNumber = (input: string | number | undefined, fallback = "") => {
const value = input === undefined || input === null || input === "" ? fallback : String(input);
return value.replace(/\d/g, (digit) => PERSIAN_NUMBERS[parseInt(digit, 10)]);
};
const formatMissionDuration = (duration: MissionData["duration"]) => {
const value = duration === undefined || duration === null || duration === "" ? "15" : String(duration);
return value.includes("دقیقه") ? toPersianNumber(value) : `${toPersianNumber(value)} دقیقه`;
};
function DifficultySignalIcon() {
const bars = [
{ height: "h-[5px]", color: "#D7B6FF" },
{ height: "h-[8px]", color: "#F2A8D8" },
{ height: "h-[11px]", color: "#F7C47D" },
];
return (
<span className="inline-flex h-4 w-4 -translate-y-[2px] items-end justify-center gap-[2px] align-middle" aria-hidden="true">
{bars.map((bar, index) => (
<span
key={index}
className={`w-[3px] rounded-full ${bar.height}`}
style={{ backgroundColor: bar.color }}
/>
))}
</span>
);
}
const glassPanelStyle = {
backgroundImage: `
linear-gradient(180deg, rgba(46, 27, 61, 0.82) 0%, rgba(35, 24, 62, 0.8) 100%),
linear-gradient(128deg, rgba(255, 164, 222, 0.92) 0%, rgba(168, 120, 255, 0.88) 38%, rgba(249, 115, 22, 0.78) 72%, rgba(250, 204, 21, 0.68) 100%)
`,
border: "1px solid transparent",
backgroundOrigin: "border-box",
backgroundClip: "padding-box, border-box",
boxShadow:
"0 -7px 20px rgba(7, 0, 18, 0.52), 0 10px 22px rgba(5, 2, 12, 0.38), 0 0 14px rgba(255, 121, 207, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.16), inset 0 2px 5px rgba(255, 222, 255, 0.06), inset 0 -2px 0 rgba(12, 7, 27, 0.74), inset 0 -8px 14px rgba(8, 4, 18, 0.38), inset 0 0 0 1px rgba(255, 255, 255, 0.03), inset 0 0 0 2px rgba(17, 10, 35, 0.36)",
backdropFilter: "blur(14px) saturate(118%)",
WebkitBackdropFilter: "blur(14px) saturate(118%)",
} as const;
const cardInnerGlassStyle = {
background:
"linear-gradient(132deg, rgba(255, 255, 255, 0.07) 0%, rgba(255, 255, 255, 0.018) 28%, rgba(255, 121, 207, 0.065) 58%, rgba(124, 58, 237, 0.055) 100%), linear-gradient(180deg, rgba(46, 27, 61, 0.72) 0%, rgba(35, 24, 62, 0.8) 100%)",
border: "1px solid rgba(255, 191, 229, 0.2)",
boxShadow:
"inset 0 1px 0 rgba(255,255,255,0.15), inset 0 0 18px rgba(255,121,207,0.05), inset 0 -12px 20px rgba(8,4,18,0.34)",
} as const;
const imageFrameStyle = {
backgroundImage: `
linear-gradient(180deg, rgba(46, 27, 61, 0.78) 0%, rgba(35, 24, 62, 0.78) 100%),
linear-gradient(135deg, rgba(124, 58, 237, 0.95) 0%, rgba(255, 121, 207, 0.72) 48%, rgba(249, 115, 22, 0.88) 100%)
`,
backgroundOrigin: "border-box",
backgroundClip: "padding-box, border-box",
boxShadow:
"0 10px 22px rgba(0,0,0,0.38), 0 0 14px rgba(255,121,207,0.16), inset 0 1px 0 rgba(255,255,255,0.2)",
} as const;
const arrowButtonStyle = {
backgroundImage: `
linear-gradient(145deg, rgba(46, 27, 61, 0.82) 0%, rgba(35, 24, 62, 0.88) 100%),
linear-gradient(132deg, rgba(255, 121, 207, 0.74) 0%, rgba(124, 58, 237, 0.7) 48%, rgba(249, 115, 22, 0.64) 100%)
`,
backgroundOrigin: "border-box",
backgroundClip: "padding-box, border-box",
boxShadow:
"0 8px 18px rgba(0,0,0,0.34), 0 0 12px rgba(255,121,207,0.14), inset 0 1px 0 rgba(255,255,255,0.16), inset 0 -4px 8px rgba(8,4,18,0.38)",
} as const;
const headerBackButtonStyle = {
backgroundImage: `
linear-gradient(180deg, #2E1B3D 0%, #23183E 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.5), 0 6px 14px rgba(5, 2, 12, 0.26), inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 2px 5px rgba(255, 222, 255, 0.09), inset 0 -2px 0 rgba(12, 7, 27, 0.72), inset 0 -8px 14px rgba(8, 4, 18, 0.34), inset 0 0 0 1px rgba(255, 255, 255, 0.045), inset 0 0 0 2px rgba(17, 10, 35, 0.32)",
backdropFilter: "blur(14px)",
WebkitBackdropFilter: "blur(14px)",
} as const;
export function ChallengeSelectionPage() {
const navigate = useNavigate();
// Support both /challenges/:topicId and legacy /challenges (defaults to topic 1)
const { topicId = "1" } = useParams<{ topicId: string }>();
const topicConfig = getTopicConfig(topicId);
usePageTracking(`انتخاب چالش ${topicConfig.title}`);
const handleChallengeSelectCallback = useCallback((mission: MissionData) => {
const missionId = mission.mission_workflowID;
localStorage.setItem("current_mission_type", topicConfig.title);
localStorage.setItem("current_mission_id", missionId);
localStorage.setItem("current_mission_title", mission.title);
const params = new URLSearchParams({
continueMode: "true",
missionId,
missionType: topicConfig.title,
});
navigate(`/chatbot/${topicId}?${params.toString()}`, {
state: {
selectedMissionTitle: mission.title,
missionType: topicConfig.title,
},
});
}, [topicConfig.title, topicId, navigate]);
const [missions, setMissions] = useState<MissionData[]>([]);
const [loading, setLoading] = useState(true);
const [autoNavigating, setAutoNavigating] = useState(false);
const handleBack = useCallback(() => {
navigate(`/feed/${topicId}`);
}, [navigate, topicId]);
useEffect(() => {
const fetchMissions = async () => {
setLoading(true);
const response = await loadMissions(topicConfig.title);
setMissions(response.missions);
setLoading(false);
// برای دفترچه یادداشت (topicId === "3"): به طور خودکار چالش را انتخاب کن
if (topicId === "3") {
if (response.missions.length > 0) {
// اگر چالشی وجود داشت، به طور خودکار آن را انتخاب کن
setAutoNavigating(true);
handleChallengeSelectCallback(response.missions[0]);
} else {
// اگر چالشی وجود نداشت، پیام خطا نمایش بده
alert("چالشی برای این بخش وجود ندارد");
// بازگشت به فید
navigate(`/feed/${topicId}`);
}
}
};
fetchMissions();
}, [topicConfig.title, topicId, navigate, handleChallengeSelectCallback]);
return (
<div className="fixed inset-0 w-full h-screen overflow-hidden">
<AppBackground position="fixed" zIndex={0} imageUrl={backgroundImages.challenges} />
{/* Auto-navigation Loading Overlay - برای دفترچه یادداشت */}
{autoNavigating && (
<div
className="fixed inset-0 z-[100] flex items-center justify-center backdrop-blur-sm"
style={{
background:
"radial-gradient(120% 120% at 50% 0%, rgba(124, 58, 237, 0.36) 0%, rgba(46, 27, 61, 0.9) 55%, rgba(35, 24, 62, 0.96) 100%)",
}}
>
<div className="text-center">
<div className="inline-block w-12 h-12 border-4 border-[#ffd6f0]/30 border-t-[#ff79cf] rounded-full animate-spin mb-3" />
<p className="text-white text-base font-bold">در حال بارگذاری چالش...</p>
</div>
</div>
)}
{/* Content */}
<div className="relative z-10 max-w-md mx-auto h-full flex flex-col">
{/* Header */}
<AppHeader showBack onBack={handleBack} centerTitle={topicConfig.title} centerSubtitle="انتخاب چالش" />
{/* Main Content - Scrollable */}
<div
className="mt-2 flex-1 overflow-y-auto px-4 pb-20"
style={{
scrollbarWidth: "none",
msOverflowStyle: "none",
}}
>
<div className="pt-7">
<div
className="mb-5 rounded-[22px] border-[0.5px] border-transparent px-4 py-3 text-right"
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.46), 0 6px 14px rgba(5, 2, 12, 0.24), inset 0 1px 0 rgba(255, 255, 255, 0.16), inset 0 -2px 0 rgba(12, 7, 27, 0.66)",
backdropFilter: "blur(14px)",
WebkitBackdropFilter: "blur(14px)",
}}
>
<p className="text-[13px] font-bold text-[#FBE7F5]">چالش مناسب رو انتخاب کن و شروع کن</p>
<p className="mt-1 text-[11px] leading-5 text-[#EED3EC]/90">
امتیاز، سختی و زمان هر چالش رو ببین و مستقیم وارد مرحله گفتگو با ربات شو.
</p>
</div>
{/* Challenges List */}
<div className="w-full space-y-4" dir="rtl">
{loading ? (
/* Loading State */
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-10"
>
<div className="inline-block w-10 h-10 border-4 border-[#ffd6f0]/30 border-t-[#ff79cf] rounded-full animate-spin mb-3" />
<p className="text-[#ffd6f0] text-xs">در حال بارگذاری چالشها...</p>
</motion.div>
) : missions.length === 0 ? (
/* Empty State */
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-10"
>
<p className="text-teal-200 text-xs">هیچ چالشی یافت نشد</p>
</motion.div>
) : (
/* Missions from Server */
missions.map((mission, index) => (
<motion.div
key={mission.mission_workflowID}
initial={{ x: -80, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.3 + index * 0.1, duration: 0.4 }}
className="relative"
>
<motion.button
type="button"
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
onClick={() => handleChallengeSelectCallback(mission)}
className="block w-full rounded-[20px] border-[0.5px] border-transparent p-[1px] text-right"
style={glassPanelStyle}
aria-label={`شروع چالش ${mission.title}`}
>
<div
className="relative flex min-h-[132px] items-center gap-3 overflow-hidden rounded-[19px] px-3 py-3"
style={cardInnerGlassStyle}
>
<div
className="pointer-events-none absolute inset-0"
style={{
background:
"linear-gradient(115deg, rgba(255,255,255,0.055) 0%, transparent 34%, rgba(255,121,207,0.05) 62%, transparent 100%)",
}}
/>
<div
className="pointer-events-none absolute -left-8 top-0 h-20 w-36 rotate-[-18deg] rounded-full blur-2xl"
style={{ background: "rgba(255, 121, 207, 0.12)" }}
/>
<div
className="pointer-events-none absolute bottom-0 right-8 h-16 w-32 rounded-full blur-2xl"
style={{ background: "rgba(249, 115, 22, 0.1)" }}
/>
<div
className="relative z-10 h-[104px] w-[104px] flex-shrink-0 overflow-hidden rounded-[16px] border-[0.5px] border-transparent p-[1px]"
style={imageFrameStyle}
>
<img
src={getMissionImageUrl(mission.StageID)}
alt={mission.title}
className="h-full w-full rounded-[15px] object-cover"
loading="eager"
onError={(e) => {
// Fallback به آیکون پیش‌فرض در صورت خطا
e.currentTarget.src = challengeIcon;
e.currentTarget.style.objectFit = "contain";
}}
/>
</div>
<div className="relative z-10 min-w-0 flex-1 text-right">
<div className="mb-1 flex items-center justify-start gap-2">
<h3 className="truncate text-[18px] font-extrabold leading-7 text-white">
{mission.title}
</h3>
<span
className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full border-[0.5px] border-transparent text-[#ffb7dd]"
style={arrowButtonStyle}
>
<Star size={13} fill="currentColor" strokeWidth={1.5} />
</span>
</div>
<p
className="mb-3 text-[12px] font-medium leading-5 text-[#F4EAF6]/88"
style={{
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{mission.description}
</p>
<div className="flex w-full flex-nowrap items-center justify-between gap-1 text-[10px] font-normal leading-none text-white/90">
<span className="inline-flex h-6 shrink-0 items-center gap-1 whitespace-nowrap px-0">
<span className="leading-none">{toPersianNumber(mission.coin_count, "250")}</span>
<img src={coinImage} alt="سکه" className="h-4 w-4 shrink-0 object-contain" />
</span>
<span className="inline-flex h-6 shrink-0 items-center gap-1 whitespace-nowrap px-0">
<span className="leading-none">{mission.difficulty || "متوسط"}</span>
<DifficultySignalIcon />
</span>
<span className="inline-flex h-6 shrink-0 items-center gap-1 whitespace-nowrap px-0">
<Clock3 size={13} className="shrink-0 text-[#ffb7dd]" strokeWidth={2.1} />
<span className="leading-none">{formatMissionDuration(mission.duration)}</span>
</span>
</div>
</div>
<span
className="relative z-10 flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full border-[0.5px] border-transparent text-white"
style={headerBackButtonStyle}
aria-hidden="true"
>
<ChevronLeft size={22} color="#ffffff" />
</span>
</div>
</motion.button>
</motion.div>
))
)}
</div>
</div>
</div>
</div>
{/* Bottom Navigation */}
<BottomNav />
<style>{`
.flex-1.overflow-y-auto::-webkit-scrollbar {
display: none;
}
`}</style>
</div>
);
}