full update

This commit is contained in:
payacom
2026-07-18 14:29:05 +03:30
parent 17f334d0f1
commit af5b27b610
239 changed files with 10283 additions and 3041 deletions

View File

@@ -0,0 +1,262 @@
"use client";
import { useCallback, useState } from "react";
import Image from "next/image";
import { Post } from "@/types/types";
import useAxios from "@/hooks/useAxios";
import toast from "react-hot-toast";
import { AxiosError } from "axios";
import { useRequireAuth } from "@/lib/auth/useRequireAuth";
import { getStoredUserId } from "@/lib/auth/session";
import useInfiniteScroll from "@/hooks/useInfiniteScroll";
import { buildPostPublicUrl } from "@/lib/postSlug";
import Cookies from "js-cookie";
import { trackExploreInteraction } from "@/api/trackExploreInteraction";
export function usePostSocialState(postData: Post) {
const {
_id: postId,
user_id,
likesCount,
is_liked,
savesCount: initialSaves = 0,
sendsCount: initialSends = 0,
is_saved: initialSaved = false,
is_following: initialFollowing = false,
type: postType,
} = postData;
const [liked, setLiked] = useState(is_liked || false);
const [likesCountState, setLikesCount] = useState(likesCount || 0);
const [saved, setSaved] = useState(initialSaved);
const [savesCount, setSavesCount] = useState(initialSaves);
const [sendsCount, setSendsCount] = useState(initialSends);
const [following, setFollowing] = useState(initialFollowing);
const { request, loading } = useAxios();
const requireAuth = useRequireAuth();
const currentUserId = getStoredUserId();
const isOwnPost = Boolean(currentUserId && user_id && currentUserId === String(user_id));
const shareUrl = buildPostPublicUrl(postId, postData);
const { data } = useInfiniteScroll({
endpoint: "/users/comments",
queryKey: ["comments", user_id, postId || "all"],
params: postId ? { user_id, post_id: postId } : { user_id },
});
const commentCount =
data?.pages
?.flatMap((p) => p?.comments || [])
.filter((c: { post_id?: string; postId?: string }) =>
postId
? c.post_id === postId || c.postId === postId || (!c.post_id && !c.postId)
: true
).length ?? 0;
const toggleLike = useCallback(async () => {
if (!requireAuth()) return;
if (!user_id || !postId) {
toast.error("لطفاً وارد حساب کاربری خود شوید");
return;
}
if (loading) return;
const newLiked = !liked;
const newLikesCount = newLiked ? likesCountState + 1 : likesCountState - 1;
setLiked(newLiked);
setLikesCount(newLikesCount);
try {
const response = await request<{ likesCount?: number; is_liked?: boolean }>(
"POST",
"/posts/like",
{ postId },
{ noToast: true }
);
setLikesCount(response?.likesCount ?? newLikesCount);
setLiked(response?.is_liked ?? newLiked);
const token = Cookies.get("token") || "";
if (token && newLiked) {
trackExploreInteraction(
{
targetType: "post",
targetId: postId,
authorId: user_id,
contentType: postType === "video" ? "video" : "image",
action: "like",
},
token
);
}
} catch (error: unknown) {
let errorMessage = "خطا در ارتباط با سرور";
if (error instanceof Error) errorMessage = error.message;
else if (error instanceof AxiosError)
errorMessage = error.response?.data?.message ?? error.message;
toast.error(errorMessage);
setLiked(!newLiked);
setLikesCount(likesCountState);
}
}, [
requireAuth,
user_id,
postId,
loading,
liked,
likesCountState,
request,
postType,
]);
const toggleSave = useCallback(async () => {
if (!requireAuth()) return;
if (!postId || loading) return;
const newSaved = !saved;
const newCount = newSaved ? savesCount + 1 : Math.max(0, savesCount - 1);
setSaved(newSaved);
setSavesCount(newCount);
try {
const response = await request<{ savesCount?: number; is_saved?: boolean }>(
"POST",
"/posts/bookmark",
{ postId },
{ noToast: true }
);
setSavesCount(response?.savesCount ?? newCount);
setSaved(response?.is_saved ?? newSaved);
} catch (error: unknown) {
toast.error("خطا در ذخیره پست");
setSaved(!newSaved);
setSavesCount(savesCount);
}
}, [requireAuth, postId, loading, saved, savesCount, request]);
const toggleFollow = useCallback(async () => {
if (!requireAuth()) return;
if (!user_id || isOwnPost || loading) return;
const newFollowing = !following;
setFollowing(newFollowing);
try {
const response = await request<{ is_following?: boolean }>(
"POST",
"/posts/follow",
{ userId: user_id },
{ noToast: true }
);
setFollowing(response?.is_following ?? newFollowing);
toast.success(newFollowing ? "فالو شد" : "آنفالو شد");
} catch {
toast.error("خطا در فالو");
setFollowing(!newFollowing);
}
}, [requireAuth, user_id, isOwnPost, loading, following, request]);
const trackShare = useCallback(() => {
const token = Cookies.get("token") || "";
if (token) {
trackExploreInteraction(
{
targetType: "post",
targetId: postId,
authorId: user_id,
contentType: postType === "video" ? "video" : "image",
action: "share",
},
token
);
}
}, [postId, user_id, postType]);
return {
liked,
likesCountState,
saved,
savesCount,
sendsCount,
setSendsCount,
following,
commentCount,
shareUrl,
currentUserId,
isOwnPost,
toggleLike,
toggleSave,
toggleFollow,
trackShare,
};
}
export type PostSocialIconSize = "sm" | "md";
export const POST_SOCIAL_ICONS = {
like: (liked: boolean, size: PostSocialIconSize) => ({
src: liked ? "/images/icons/red-heart.svg" : "/images/icons/heart.svg",
w: size === "sm" ? 20 : 28,
className: liked
? "drop-shadow-lg"
: size === "sm"
? "dark:invert drop-shadow-lg"
: "invert drop-shadow-lg",
}),
comment: (size: PostSocialIconSize) => ({
src: "/images/icons/message-text.svg",
w: size === "sm" ? 20 : 28,
className: size === "sm" ? "dark:invert" : "invert drop-shadow-lg",
}),
share: (size: PostSocialIconSize) => ({
src: "/images/icons/share.svg",
w: size === "sm" ? 20 : 28,
className: size === "sm" ? "dark:invert" : "invert drop-shadow-lg",
}),
send: (size: PostSocialIconSize) => ({
src: "/images/icons/send-post.svg",
w: size === "sm" ? 20 : 28,
className: size === "sm" ? "dark:invert" : "invert drop-shadow-lg",
}),
save: (saved: boolean, size: PostSocialIconSize) => ({
src: "/images/icons/bookmark-post.svg",
w: size === "sm" ? 20 : 28,
className: saved
? "drop-shadow-lg"
: size === "sm"
? "dark:invert"
: "invert drop-shadow-lg",
}),
follow: (following: boolean, size: PostSocialIconSize) => ({
src: "/images/icons/follow-user.svg",
w: size === "sm" ? 20 : 28,
className: following
? "opacity-60 drop-shadow-lg"
: size === "sm"
? "dark:invert"
: "invert drop-shadow-lg",
}),
collaboration: (size: PostSocialIconSize) => ({
src: "/images/icons/collaboration-request.svg",
w: size === "sm" ? 20 : 28,
className: size === "sm" ? "dark:invert" : "invert drop-shadow-lg",
}),
} as const;
export function PostSocialIcon({
src,
w,
className,
alt = "",
}: {
src: string;
w: number;
className?: string;
alt?: string;
}) {
return (
<Image width={w} height={w} alt={alt} src={src} className={className} />
);
}