This commit is contained in:
mahmoodsht 2026-05-23 18:55:00 +03:30
parent 388d3da866
commit caef00bb5c
17 changed files with 152 additions and 48 deletions

4
dist/index.html vendored
View File

@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>new hamdast</title> <title>همدست</title>
<meta name="theme-color" content="#23183E" /> <meta name="theme-color" content="#23183E" />
<style> <style>
html, html,
@ -16,7 +16,7 @@
background: #23183E; background: #23183E;
} }
</style> </style>
<script type="module" crossorigin src="/assets/index-Ccau0Eyo.js"></script> <script type="module" crossorigin src="/assets/index-C0TqKym5.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-xouV2fxu.css"> <link rel="stylesheet" crossorigin href="/assets/index-xouV2fxu.css">
</head> </head>

17
dist/web.config vendored Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="SPA Routes" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="/index.html" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

View File

@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>new hamdast</title> <title>همدست</title>
<meta name="theme-color" content="#23183E" /> <meta name="theme-color" content="#23183E" />
<style> <style>
html, html,

17
public/web.config Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="SPA Routes" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="/index.html" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

View File

@ -196,7 +196,7 @@ export function CommentsModal({
<span className="text-xs text-gray-400">{comment.timestamp}</span> <span className="text-xs text-gray-400">{comment.timestamp}</span>
</div> </div>
<p className="text-sm text-white leading-relaxed mb-2">{comment.text}</p> <p className="text-sm text-white leading-relaxed mb-2 whitespace-pre-line">{comment.text}</p>
{/* Actions */} {/* Actions */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">

View File

@ -1,4 +1,4 @@
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useNavigate, useLocation, Navigate } from "react-router-dom"; import { useNavigate, useLocation, Navigate } from "react-router-dom";
import { ChevronDown, ChevronRight, ShieldCheck } from "lucide-react"; import { ChevronDown, ChevronRight, ShieldCheck } from "lucide-react";
@ -110,6 +110,20 @@ export function LoginPage() {
return input.replace(/\d/g, (digit) => PERSIAN_NUMBERS[parseInt(digit, 10)]); return input.replace(/\d/g, (digit) => PERSIAN_NUMBERS[parseInt(digit, 10)]);
}; };
const parsePhoneNumber = (input: string) => {
const digits = normalizeNumber(input).replace(/\D/g, "").slice(0, 11);
if (/^09\d{9}$/.test(digits)) {
return { error: "", serverMobile: digits };
}
if (/^9\d{9}$/.test(digits)) {
return { error: "", serverMobile: `0${digits}` };
}
return { error: "شماره موبایل معتبر نیست", serverMobile: "" };
};
const clearTimer = () => { const clearTimer = () => {
if (timerRef.current) { if (timerRef.current) {
clearInterval(timerRef.current); clearInterval(timerRef.current);
@ -148,12 +162,16 @@ export function LoginPage() {
const handleSendCode = async (e: React.FormEvent) => { const handleSendCode = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const { error: phoneError, serverMobile } = parsePhoneNumber(phoneNumber);
if (phoneError) {
setError(phoneError);
return;
}
setIsLoading(true); setIsLoading(true);
setError(""); setError("");
try { try {
const normalizedPhone = normalizeNumber(phoneNumber); const { response, result } = await requestSmsCode(serverMobile);
const { response, result } = await requestSmsCode(normalizedPhone);
if (response.ok && result.state === 0) { if (response.ok && result.state === 0) {
setStep("code"); setStep("code");
@ -173,11 +191,15 @@ export function LoginPage() {
const handleVerifyCode = async (e?: React.FormEvent) => { const handleVerifyCode = async (e?: React.FormEvent) => {
e?.preventDefault(); e?.preventDefault();
const { error: phoneError, serverMobile } = parsePhoneNumber(phoneNumber);
if (phoneError) {
setError(phoneError);
return;
}
setIsLoading(true); setIsLoading(true);
setError(""); setError("");
try { try {
const normalizedPhone = normalizeNumber(phoneNumber);
const normalizedCode = normalizeNumber(code); const normalizedCode = normalizeNumber(code);
const response = await fetch(`${API_BASE_URL}/api/verifyloginbysms`, { const response = await fetch(`${API_BASE_URL}/api/verifyloginbysms`, {
@ -186,7 +208,7 @@ export function LoginPage() {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
mobile: normalizedPhone, mobile: serverMobile,
code: normalizedCode, code: normalizedCode,
}), }),
}); });
@ -223,12 +245,16 @@ export function LoginPage() {
}; };
const handleResendCode = async () => { const handleResendCode = async () => {
const { error: phoneError, serverMobile } = parsePhoneNumber(phoneNumber);
if (phoneError) {
setError(phoneError);
return;
}
setIsLoading(true); setIsLoading(true);
setError(""); setError("");
try { try {
const normalizedPhone = normalizeNumber(phoneNumber); const { response, result } = await requestSmsCode(serverMobile);
const { response, result } = await requestSmsCode(normalizedPhone);
if (response.ok && result.state === 0) { if (response.ok && result.state === 0) {
setCode(""); setCode("");
@ -394,7 +420,8 @@ export function LoginPage() {
inputMode="numeric" inputMode="numeric"
autoFocus={step === "phone"} autoFocus={step === "phone"}
value={toPersianNumber(phoneNumber)} value={toPersianNumber(phoneNumber)}
onChange={(e) => setPhoneNumber(normalizeNumber(e.target.value))} onChange={(e) => { const nextValue = normalizeNumber(e.target.value).replace(/\D/g, "").slice(0, 11); setPhoneNumber(nextValue); if (error) setError(""); }}
maxLength={11}
className="w-full bg-transparent text-left text-lg text-white outline-none placeholder:text-lg placeholder:text-white/45" className="w-full bg-transparent text-left text-lg text-white outline-none placeholder:text-lg placeholder:text-white/45"
placeholder="۹۱۲ ۱۲۳ ۴۵۶۷" placeholder="۹۱۲ ۱۲۳ ۴۵۶۷"
required required
@ -535,3 +562,4 @@ export function LoginPage() {
</div> </div>
); );
} }

View File

@ -9,6 +9,7 @@ import { useNavigate } from "react-router-dom";
import reactionIconShared from "../../assets/reaction-icon-shared.svg"; import reactionIconShared from "../../assets/reaction-icon-shared.svg";
import commentIconShared from "../../assets/comment-icon-shared.svg"; import commentIconShared from "../../assets/comment-icon-shared.svg";
import avatarFallbackImage from "../../assets/image 5.png"; import avatarFallbackImage from "../../assets/image 5.png";
import { convertBrToNewlines } from "../../utils/textFormat";
// PostCard با طراحی جدید - قسمت پایین بازطراحی شده + آیکون مشارکت‌کنندگان // PostCard با طراحی جدید - قسمت پایین بازطراحی شده + آیکون مشارکت‌کنندگان
interface PostCardProps { interface PostCardProps {
@ -59,6 +60,7 @@ export function PostCard({
teamMemberIds, teamMemberIds,
preloadedTeamMembers, preloadedTeamMembers,
}: PostCardProps) { }: PostCardProps) {
const normalizedCaption = convertBrToNewlines(caption || "");
const COMMENTS_PAGE_SIZE = 25; const COMMENTS_PAGE_SIZE = 25;
const postChromeStyle = { const postChromeStyle = {
border: "1px solid transparent", border: "1px solid transparent",
@ -419,11 +421,12 @@ export function PostCard({
WebkitLineClamp: expandedCaption ? "unset" : 4, WebkitLineClamp: expandedCaption ? "unset" : 4,
WebkitBoxOrient: "vertical", WebkitBoxOrient: "vertical",
overflow: "hidden", overflow: "hidden",
whiteSpace: "pre-line",
}} }}
> >
{caption} {normalizedCaption}
</p> </p>
{caption.length > 150 && ( {normalizedCaption.length > 150 && (
<button <button
onClick={() => setExpandedCaption(!expandedCaption)} onClick={() => setExpandedCaption(!expandedCaption)}
className="text-xs mt-1" className="text-xs mt-1"

View File

@ -126,7 +126,7 @@ export function PublicChatPage() {
const handleSendMessage = async (message: string) => { const handleSendMessage = async (message: string) => {
const displayText = message.trim(); const displayText = message.trim();
const serverText = displayText.replace(/\r?\n+/g, " ").trim(); const serverText = displayText;
if (!serverText || isSending) return; if (!serverText || isSending) return;
@ -194,22 +194,8 @@ export function PublicChatPage() {
const result = await loadChatList(); const result = await loadChatList();
if (result.success) { if (result.success) {
const sortedItems = [...result.data].sort((a, b) => { // Server returns oldest -> newest. UI should show newest first.
const timeA = Date.parse(a.datetime1 || ""); setHistoryItems([...result.data].reverse());
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 { } else {
console.error("Failed to load chat list:", result.message); console.error("Failed to load chat list:", result.message);
alert(result.message || "خطا در بارگذاری تاریخچه"); alert(result.message || "خطا در بارگذاری تاریخچه");
@ -244,7 +230,13 @@ export function PublicChatPage() {
}, [handleHistoryClick, isLoading, isSending]); }, [handleHistoryClick, isLoading, isSending]);
return ( return (
<div className="relative h-full min-h-0 overflow-hidden"> <div
className="relative h-full min-h-0 overflow-hidden"
style={{
overscrollBehaviorY: "none",
touchAction: "pan-y",
}}
>
<div className="grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto]"> <div className="grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto]">
<main className="relative min-h-0 overflow-hidden"> <main className="relative min-h-0 overflow-hidden">
{isLoading ? ( {isLoading ? (

View File

@ -99,6 +99,9 @@ export function ChatHistoryModal({
"calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 180px)", "calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 180px)",
scrollbarWidth: "none", scrollbarWidth: "none",
msOverflowStyle: "none", msOverflowStyle: "none",
overscrollBehaviorY: "contain",
WebkitOverflowScrolling: "touch",
touchAction: "pan-y",
}} }}
> >
{isLoading ? ( {isLoading ? (

View File

@ -54,8 +54,13 @@ export function ChatMessages({
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className="flex-1 overflow-y-auto px-4 pb-20" className="h-full min-h-0 overflow-y-auto px-4 pb-20"
dir="rtl" dir="rtl"
style={{
overscrollBehaviorY: "contain",
WebkitOverflowScrolling: "touch",
touchAction: "pan-y",
}}
> >
<div className="space-y-3"> <div className="space-y-3">
{messages.map((message) => { {messages.map((message) => {

View File

@ -1,3 +1,4 @@
// API Configuration // API Configuration
export const API_BASE_URL = "http://141.11.1.189"; export const API_BASE_URL = "http://141.11.1.189";
// export const API_BASE_URL = "https://localhost:44362";
//export const API_BASE_URL = "https://hamdast-back2.sepehrdata.com"; //export const API_BASE_URL = "https://hamdast-back2.sepehrdata.com";

View File

@ -50,7 +50,7 @@ export const useChatFlow = ({ workflowId, onMissionEnd }: UseChatFlowOptions): U
}); });
const displayMessage = messageText.trim(); const displayMessage = messageText.trim();
const normalizedMessage = displayMessage.replace(/\r?\n+/g, " ").trim(); const normalizedMessage = displayMessage;
if (!normalizedMessage || !workflowId) { if (!normalizedMessage || !workflowId) {
console.log("sendMessage aborted:", { console.log("sendMessage aborted:", {

View File

@ -1,4 +1,5 @@
import { API_BASE_URL } from "../config/api"; import { API_BASE_URL } from "../config/api";
import { convertBrToNewlines, convertNewlinesToBr } from "../utils/textFormat";
const AVATAR_CACHE_BUST_KEY = "avatarCacheBust"; const AVATAR_CACHE_BUST_KEY = "avatarCacheBust";
@ -82,7 +83,10 @@ const formatErrorMessage = (result: ApiResponse<any>): string => {
const parseFeedData = (data: string): FeedItem[] => { const parseFeedData = (data: string): FeedItem[] => {
try { try {
const parsed = JSON.parse(data); const parsed = JSON.parse(data);
const feedArray = JSON.parse(parsed.feed || "[]"); const feedArray = JSON.parse(parsed.feed || "[]").map((item: FeedItem) => ({
...item,
description: convertBrToNewlines(item.description || ""),
}));
return feedArray; return feedArray;
} catch (error) { } catch (error) {
console.error("Error parsing feed data:", error); console.error("Error parsing feed data:", error);
@ -280,7 +284,7 @@ export const saveComment = async (
save_comment_function: { save_comment_function: {
mission_type: missionType, mission_type: missionType,
mission_done_workflowID: missionDoneWorkflowID, mission_done_workflowID: missionDoneWorkflowID,
text: text, text: convertNewlinesToBr(text || ""),
replay_workflowID: replayWorkflowID, replay_workflowID: replayWorkflowID,
}, },
}), }),
@ -373,7 +377,10 @@ export const loadComments = async (
if (response.ok && result.state === 0) { if (response.ok && result.state === 0) {
const data = JSON.parse(result.data); const data = JSON.parse(result.data);
const comments = JSON.parse(data.comments); const comments = JSON.parse(data.comments).map((comment: CommentData) => ({
...comment,
comment_text: convertBrToNewlines(comment.comment_text || ""),
}));
return { comments }; return { comments };
} else { } else {
console.error("Error loading comments:", result.message); console.error("Error loading comments:", result.message);
@ -519,7 +526,11 @@ export const startMission = async (
if (response.ok && result.state === 0) { if (response.ok && result.state === 0) {
const data = JSON.parse(result.data); const data = JSON.parse(result.data);
const doingMissionArray = JSON.parse(data.doing_mission); const doingMissionArray = JSON.parse(data.doing_mission);
const chatsArray = JSON.parse(data.chats); const chatsArray = JSON.parse(data.chats).map((chat: ChatMessage) => ({
...chat,
question: convertBrToNewlines(chat.question || ""),
answer: convertBrToNewlines(chat.answer || ""),
}));
return { return {
doing_mission: doingMissionArray.length > 0 ? doingMissionArray[0] : null, doing_mission: doingMissionArray.length > 0 ? doingMissionArray[0] : null,
@ -561,7 +572,7 @@ export const sendChatMessage = async (
try { try {
const payload = { const payload = {
chat_service_function: { chat_service_function: {
user_message: userMessage, user_message: convertNewlinesToBr(userMessage || ""),
mission_done_workflowID: missionDoneWorkflowID, mission_done_workflowID: missionDoneWorkflowID,
}, },
}; };
@ -587,7 +598,7 @@ export const sendChatMessage = async (
const chatResponse = JSON.parse(data[keys[0]]); const chatResponse = JSON.parse(data[keys[0]]);
return { return {
success: true, success: true,
message: chatResponse.message, message: convertBrToNewlines(chatResponse.message || ""),
actions: chatResponse.actions, actions: chatResponse.actions,
is_mission_end: chatResponse.is_mission_end, is_mission_end: chatResponse.is_mission_end,
}; };
@ -760,7 +771,7 @@ export const submitMission = async (
title: data.title || "", title: data.title || "",
mission_type: data.mission_type || "", mission_type: data.mission_type || "",
mission_done_workflowID: data.mission_done_workflowID || "", mission_done_workflowID: data.mission_done_workflowID || "",
description: data.description || "", description: convertNewlinesToBr(data.description || ""),
film: data.film || "", film: data.film || "",
image: data.image || "", image: data.image || "",
audio: data.audio || "", audio: data.audio || "",

View File

@ -1,6 +1,7 @@
// Profile Service - برای مدیریت پروفایل کاربر // Profile Service - برای مدیریت پروفایل کاربر
import { API_BASE_URL } from "../config/api"; import { API_BASE_URL } from "../config/api";
import { getAccessToken } from "../utils/auth"; import { getAccessToken } from "../utils/auth";
import { convertBrToNewlines } from "../utils/textFormat";
export interface UserProfile { export interface UserProfile {
username: string; username: string;
@ -242,8 +243,18 @@ export const getUserProfileData = async (): Promise<UserProfileData | null> => {
return { return {
challenges: parsedData.challenges ? JSON.parse(parsedData.challenges) : [], challenges: parsedData.challenges ? JSON.parse(parsedData.challenges) : [],
coin_transaction: parsedData.coin_transaction ? JSON.parse(parsedData.coin_transaction) : [], coin_transaction: parsedData.coin_transaction
posts: parsedData.posts ? JSON.parse(parsedData.posts) : [], ? JSON.parse(parsedData.coin_transaction).map((item: ProfileCoinTransaction) => ({
...item,
description: convertBrToNewlines(item.description || ""),
}))
: [],
posts: parsedData.posts
? JSON.parse(parsedData.posts).map((post: ProfilePost) => ({
...post,
description: convertBrToNewlines(post.description || ""),
}))
: [],
}; };
} }
@ -252,4 +263,4 @@ export const getUserProfileData = async (): Promise<UserProfileData | null> => {
if (import.meta.env.DEV) console.warn("خطا در دریافت داده‌های پروفایل:", error); if (import.meta.env.DEV) console.warn("خطا در دریافت داده‌های پروفایل:", error);
return null; return null;
} }
}; };

View File

@ -1,4 +1,5 @@
import { API_BASE_URL } from "../config/api"; import { API_BASE_URL } from "../config/api";
import { convertBrToNewlines, convertNewlinesToBr } from "../utils/textFormat";
const getAuthToken = (): string | null => { const getAuthToken = (): string | null => {
const accessToken = localStorage.getItem("accessToken"); const accessToken = localStorage.getItem("accessToken");
@ -129,10 +130,15 @@ export const loadChat = async (
// Parse the nested JSON string // Parse the nested JSON string
const parsedData = JSON.parse(result.data); const parsedData = JSON.parse(result.data);
const chats = JSON.parse(parsedData.chats); const chats = JSON.parse(parsedData.chats);
const normalizedChats = chats.map((chat: PublicChatMessage) => ({
...chat,
question: convertBrToNewlines(chat.question || ""),
answer: convertBrToNewlines(chat.answer || ""),
}));
return { return {
success: true, success: true,
data: chats, data: normalizedChats,
}; };
} else { } else {
return { return {
@ -177,7 +183,7 @@ export const sendPublicChatMessage = async (
body: JSON.stringify({ body: JSON.stringify({
public_caht_function: { public_caht_function: {
chatlist_workflowID: chatlistWorkflowID, chatlist_workflowID: chatlistWorkflowID,
question: question, question: convertNewlinesToBr(question || ""),
}, },
}), }),
}); });
@ -191,7 +197,7 @@ export const sendPublicChatMessage = async (
return { return {
success: true, success: true,
answer: messageData.answer, answer: convertBrToNewlines(messageData.answer || ""),
newChatlistWorkflowID: messageData.chatlist_workflowID2, newChatlistWorkflowID: messageData.chatlist_workflowID2,
}; };
} else { } else {

View File

@ -1,4 +1,5 @@
import { FeedItem, getAvatarUrl, getFeedImageUrl, getVideoUrl, getAudioUrl, isOwnFeed, type TeamMember } from "../services/feedService"; import { FeedItem, getAvatarUrl, getFeedImageUrl, getVideoUrl, getAudioUrl, isOwnFeed, type TeamMember } from "../services/feedService";
import { convertBrToNewlines } from "./textFormat";
export interface PostCardModel { export interface PostCardModel {
id: string; id: string;
@ -63,7 +64,7 @@ export const mapFeedItemToPostCardModel = (
authorAvatar: getAvatarUrl(item.person_stage_id), authorAvatar: getAvatarUrl(item.person_stage_id),
image: coverImage, image: coverImage,
title: item.title, title: item.title,
caption: item.description, caption: convertBrToNewlines(item.description || ""),
likes: item.like_count, likes: item.like_count,
dislikes: item.dislike_count, dislikes: item.dislike_count,
comments: item.comment_count, comments: item.comment_count,

9
src/utils/textFormat.ts Normal file
View File

@ -0,0 +1,9 @@
export const convertNewlinesToBr = (value: string): string => {
if (!value) return "";
return value.replace(/\r\n|\r|\n/g, "<br>");
};
export const convertBrToNewlines = (value: string): string => {
if (!value) return "";
return value.replace(/<br\s*\/?>/gi, "\n");
};