Initial commit - modstagram-next

This commit is contained in:
root
2026-07-05 13:59:41 +03:30
commit 11e73da693
859 changed files with 117640 additions and 0 deletions

24
src/hooks/getUserById.tsx Normal file
View File

@@ -0,0 +1,24 @@
// useUserById.ts
import { useState, useEffect } from "react";
import useAxios from "./useAxios";
import { User } from "@/types/types";
export const useUserById = (_id: string) => {
const [user, setUser] = useState<User>();
const { request } = useAxios();
useEffect(() => {
const fetchUser = async () => {
try {
const response = await request<{ user: User }>("GET", `/profile/${_id}`);
setUser(response.user);
} catch (error) {
console.error("Failed to fetch user:", error);
}
};
if (_id) fetchUser();
}, [_id, request]);
return user;
};

19
src/hooks/use-mobile.ts Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

150
src/hooks/useAxios.tsx Normal file
View File

@@ -0,0 +1,150 @@
/* eslint-disable @typescript-eslint/no-empty-object-type */
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import { useState, useCallback } from "react";
import axios, {
AxiosRequestConfig,
AxiosError,
InternalAxiosRequestConfig,
} from "axios";
import toast from "react-hot-toast";
import { BASE_URL as base } from "@/components/main/BaseUrl";
import Cookies from "js-cookie";
const BASE_URL = base;
const axiosInstance = axios.create({
baseURL: BASE_URL,
timeout: 900000, // 5 دقیقه (300,000 میلی‌ثانیه)
maxContentLength: Infinity,
maxBodyLength: Infinity,
});
const getToken = (): string => {
if (typeof window !== "undefined") {
const token = Cookies.get("token");
return token ? `Bearer ${token}` : "";
}
return "";
};
// اضافه کردن توکن به هر درخواست
axiosInstance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
if (config.headers) {
config.headers["Authorization"] = getToken();
}
return config;
},
(error) => Promise.reject(error)
);
// بررسی noToast در headers و جلوگیری از نمایش toast در پاسخ‌ها
axiosInstance.interceptors.response.use(
(response) => {
if (!response.config.headers?.["X-No-Toast"] && response.data?.message) {
// toast.success(response.data.message);
}
return response;
},
(error: AxiosError) => {
const { response, config } = error;
if (response) {
const { status, data } = response;
const noToast = config?.headers?.["X-No-Toast"];
if (status === 401 && !noToast) {
handleUnauthorized();
} else if (status === 422) {
const errors = (data as { [key: string]: string[] }).errors || {};
if (!noToast) {
if (
typeof data === "object" &&
(data as { message?: string }).message
) {
toast.error((data as { message: string }).message);
} else {
Object.values(errors).forEach((messages) => {
(messages as unknown as string[]).forEach((message) =>
toast.error(message)
);
});
}
}
} else if ([429, 500, 503].includes(status)) {
if (!noToast) {
// toast.error("An unexpected error occurred. Please try again later.");
}
} else {
if (!noToast) {
// toast.error((data as any)?.message || "An error occurred.");
}
}
}
return Promise.reject(error);
}
);
const handleUnauthorized = (): void => {
if (typeof window !== "undefined") {
// toast.error("برای انجام عملیات لطفا وارد حساب کاربری خود شوید");
}
};
// تایپ اصلاح‌شده برای درخواست‌ها
interface RequestConfig extends AxiosRequestConfig {
noToast?: boolean;
}
const useAxios = () => {
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<AxiosError | null>(null);
const request = useCallback(
async <T,>(
method: AxiosRequestConfig["method"],
url: string,
data: unknown = null,
config: RequestConfig = {}
): Promise<T> => {
setLoading(true);
setError(null);
try {
const isFormData = data instanceof FormData;
const response = await axiosInstance({
method,
url,
data,
...config,
headers: {
...config.headers,
...(isFormData ? {} : { "Content-Type": "application/json" }), // حذف Content-Type برای FormData
...(config.noToast ? { "X-No-Toast": "true" } : {}), // ارسال noToast به عنوان یک هدر
},
});
return response.data;
} catch (err) {
setError(err as AxiosError);
throw err;
} finally {
setLoading(false);
}
},
[]
);
return {
request,
loading,
error,
};
};
export default useAxios;

View File

@@ -0,0 +1,74 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useRef } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import useAxios from "@/hooks/useAxios";
interface FetchParams {
endpoint: string;
queryKey: (string | number)[];
params?: Record<string, any>;
limit?: number;
}
const useInfiniteScroll = ({
endpoint,
queryKey,
params = {},
limit = 10,
}: FetchParams) => {
const { request } = useAxios();
const containerRef = useRef<HTMLDivElement>(null);
const fetchData = async ({ pageParam = 1 }) => {
const queryParams = new URLSearchParams({
...params,
limit: limit.toString(),
page: pageParam.toString(),
});
const data = await request<any>(
"GET",
`${endpoint}?${queryParams.toString()}`
);
return data || [];
};
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } =
useInfiniteQuery({
queryKey,
queryFn: ({ pageParam }) => fetchData({ pageParam }),
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) =>
lastPage.totalPages > allPages.length ? allPages.length + 1 : undefined,
});
useEffect(() => {
const handleScroll = () => {
const container = containerRef.current || document.documentElement;
if (!container || isFetchingNextPage) return;
const { scrollTop, scrollHeight, clientHeight } = container;
if (scrollTop + clientHeight >= scrollHeight - 50 && hasNextPage) {
fetchNextPage();
}
};
const containerElement = containerRef.current;
if (containerElement) {
containerElement.addEventListener("scroll", handleScroll);
} else {
window.addEventListener("scroll", handleScroll);
}
return () => {
if (containerElement) {
containerElement.removeEventListener("scroll", handleScroll);
} else {
window.removeEventListener("scroll", handleScroll);
}
};
}, [hasNextPage, fetchNextPage, isFetchingNextPage]);
return { data, containerRef, isFetchingNextPage, isLoading, refetch };
};
export default useInfiniteScroll;

View File

@@ -0,0 +1,26 @@
"use client";
import { useEffect, useState } from "react";
export type NetworkState = "online" | "offline" | "syncing";
export function useNetworkStatus() {
const [state, setState] = useState<NetworkState>("online");
useEffect(() => {
const update = () =>
setState(navigator.onLine ? "online" : "offline");
update();
window.addEventListener("online", update);
window.addEventListener("offline", update);
return () => {
window.removeEventListener("online", update);
window.removeEventListener("offline", update);
};
}, []);
const startSync = () => setState("syncing");
const endSync = () => setState(navigator.onLine ? "online" : "offline");
return { state, startSync, endSync, isOffline: state === "offline", isSyncing: state === "syncing" };
}

View File

@@ -0,0 +1,39 @@
"use client";
import { useEffect, useRef, useState } from "react";
/** Returns true when user scrolls down past threshold (Safari-style compact header) */
export function useScrollDirection(threshold = 16) {
const [compact, setCompact] = useState(false);
const lastY = useRef(0);
const ticking = useRef(false);
useEffect(() => {
lastY.current = window.scrollY;
const update = () => {
const y = window.scrollY;
if (y <= threshold) {
setCompact(false);
} else if (y > lastY.current + 4) {
setCompact(true);
} else if (y < lastY.current - 4) {
setCompact(false);
}
lastY.current = y;
ticking.current = false;
};
const onScroll = () => {
if (!ticking.current) {
ticking.current = true;
requestAnimationFrame(update);
}
};
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, [threshold]);
return compact;
}

23
src/hooks/useUser.tsx Normal file
View File

@@ -0,0 +1,23 @@
"use client";
import { useState, useEffect } from "react";
import { User } from "@/types/types";
import useAxios from "./useAxios";
export const useUser = () => {
const [user, setUser] = useState<User>();
const { request } = useAxios();
const fetchUser = async () => {
try {
const response = await request<{ user: User }>("GET", "/profile");
setUser(response?.user);
} catch (error) {
console.error("Failed to fetch user:", error);
}
};
useEffect(() => {
fetchUser();
}, []);
return user;
};