Initial commit

This commit is contained in:
payacom
2026-07-09 15:26:46 +03:30
parent ff2009c402
commit edb094e323
65 changed files with 4083 additions and 803 deletions

View File

@@ -0,0 +1,50 @@
export const UPSTREAM_API =
process.env.UPSTREAM_API_URL ?? "https://api.modstagram.ir";
type ProxyOptions = {
stripAuth?: boolean;
};
export async function proxyToUpstream(
req: Request,
upstreamPath: string,
options?: ProxyOptions
): Promise<Response> {
const url = new URL(upstreamPath, UPSTREAM_API);
const incoming = new URL(req.url);
url.search = incoming.search;
const headers = new Headers();
const contentType = req.headers.get("content-type");
if (contentType) headers.set("content-type", contentType);
if (!options?.stripAuth) {
const auth = req.headers.get("authorization");
if (auth) headers.set("authorization", auth);
}
const method = req.method;
const body =
method !== "GET" && method !== "HEAD" ? await req.arrayBuffer() : undefined;
const upstream = await fetch(url.toString(), {
method,
headers,
body,
cache: "no-store",
});
const responseHeaders = new Headers();
const upstreamType = upstream.headers.get("content-type");
if (upstreamType) responseHeaders.set("content-type", upstreamType);
return new Response(upstream.body, {
status: upstream.status,
headers: responseHeaders,
});
}
export function isPublicAuthPath(pathSegments: string[]): boolean {
const path = pathSegments.join("/");
return path.startsWith("login") || path.startsWith("register");
}

82
src/lib/auth/postLogin.ts Normal file
View File

@@ -0,0 +1,82 @@
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import type { IVerifyOtp } from "@/types/types";
import { setAuthSession } from "@/lib/auth/session";
import { AUTH_SUCCESS_DELAY_MS, pause } from "@/lib/auth/feedback";
export function getSafeRedirectPath(): string {
if (typeof window === "undefined") return "/";
const redirect = new URLSearchParams(window.location.search).get("redirect");
if (redirect && redirect.startsWith("/") && !redirect.startsWith("//")) {
return redirect;
}
return "/";
}
export async function navigateIncompleteRegistration(
router: AppRouterInstance,
step?: string
) {
switch (step) {
case "user_name":
router.push("/register/username");
break;
case "password":
router.push("/register/password");
break;
case "first_name":
router.push("/register/fullname");
break;
case "user_type":
router.push("/register/usertype");
break;
default:
router.push("/verify/avatar");
break;
}
}
export async function completeLogin(
router: AppRouterInstance,
response: IVerifyOtp,
options?: { redirectTo?: string }
): Promise<boolean> {
if (!response?.token) return false;
await setAuthSession(response.token, {
id: response.id,
user_type: response.user_type,
step: response.step,
});
await pause(AUTH_SUCCESS_DELAY_MS);
router.refresh();
if (response.page === "home") {
router.push(options?.redirectTo ?? getSafeRedirectPath());
return true;
}
if (response.page === "auth-page") {
await navigateIncompleteRegistration(router, response.step);
return true;
}
return false;
}
export function getAxiosErrorMessage(error: unknown): string {
const data = (
error as {
response?: { data?: { message?: string; en_message?: string } };
}
)?.response?.data;
if (data?.en_message === "Not allowed by CORS") {
return "خطا در اتصال به سرور. صفحه را رفرش کنید و دوباره تلاش کنید.";
}
if (data?.message) return data.message;
if ((error as Error)?.message === "Network Error") {
return "خطا در ارتباط با سرور. اتصال اینترنت را بررسی کنید.";
}
return "نام کاربری یا کلمه عبور اشتباه است.";
}

98
src/lib/coldStartFeed.ts Normal file
View File

@@ -0,0 +1,98 @@
import { Post } from "@/types/types";
const EXPERTISE_ALIASES: Record<string, string[]> = {
مدل: ["مدل", "مدلینگ"],
مدلینگ: ["مدل", "مدلینگ"],
عکاس: ["عکاس", "عکاسی"],
عکاسی: ["عکاس", "عکاسی"],
آرایشگر: ["آرایشگر", "زیبایی"],
زیبایی: ["آرایشگر", "زیبایی"],
};
function expertiseMatches(a?: string, b?: string): boolean {
if (!a || !b) return false;
const aliases = EXPERTISE_ALIASES[a] || [a];
return aliases.includes(b);
}
function shuffle<T>(items: T[], seed = Date.now()): T[] {
const copy = [...items];
let state = seed % 2147483647 || 1;
const random = () => {
state = (state * 16807) % 2147483647;
return (state - 1) / 2147483646;
};
for (let i = copy.length - 1; i > 0; i -= 1) {
const j = Math.floor(random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
}
function interleaveWeighted<T>(primary: T[], secondary: T[]): T[] {
if (!primary.length) return shuffle(secondary);
if (!secondary.length) return shuffle(primary);
const result: T[] = [];
let primaryIdx = 0;
let secondaryIdx = 0;
let primaryCount = 0;
while (primaryIdx < primary.length || secondaryIdx < secondary.length) {
if (
secondaryIdx < secondary.length &&
(primaryIdx >= primary.length || primaryCount >= 3)
) {
result.push(secondary[secondaryIdx]);
secondaryIdx += 1;
primaryCount = 0;
continue;
}
if (primaryIdx < primary.length) {
result.push(primary[primaryIdx]);
primaryIdx += 1;
primaryCount += 1;
} else if (secondaryIdx < secondary.length) {
result.push(secondary[secondaryIdx]);
secondaryIdx += 1;
}
}
return result;
}
function matchesSeedCategory(post: Post, seed: Post): boolean {
if (seed.expertise && expertiseMatches(seed.expertise, post.expertise)) {
return true;
}
if (
seed.sub_expertise?.length &&
post.sub_expertise?.some((item) => seed.sub_expertise?.includes(item))
) {
return true;
}
return false;
}
/** Cold-start reels order: random, but ~72% same category as clicked post */
export function reorderColdStartFeed(posts: Post[], seed: Post): Post[] {
const sameCategory: Post[] = [];
const others: Post[] = [];
posts.forEach((post) => {
if (matchesSeedCategory(post, seed)) {
sameCategory.push(post);
} else {
others.push(post);
}
});
const seedKey = String(seed._id || "");
return interleaveWeighted(
shuffle(sameCategory, seedKey.length * 997),
shuffle(others, seedKey.length * 499)
);
}

View File

@@ -0,0 +1,44 @@
import { ExploreFilterId } from "@/constants/exploreFilters";
import { USER_LEVELS } from "@/lib/userLevel";
export type PostFetchFilters = {
expertise?: string;
userLevel?: string;
exploreFilter?: string;
subExpertise?: string;
lat?: string;
lng?: string;
};
export function buildExplorePostFilters(
filter: ExploreFilterId,
coords?: { lat: number; lng: number } | null
): PostFetchFilters {
switch (filter) {
case "model":
return { exploreFilter: "model", expertise: "مدل" };
case "photographer":
return { exploreFilter: "photographer", expertise: "عکاس" };
case "hairstylist":
return { exploreFilter: "hairstylist", expertise: "آرایشگر" };
case "makeup":
return { exploreFilter: "makeup", subExpertise: "میکاپ" };
case "professional":
return { exploreFilter: "professional", userLevel: USER_LEVELS.PRO };
case "trending":
return { exploreFilter: "trending" };
case "best_month":
return { exploreFilter: "best_month" };
case "near_me":
if (coords) {
return {
exploreFilter: "near_me",
lat: String(coords.lat),
lng: String(coords.lng),
};
}
return { exploreFilter: "near_me" };
default:
return {};
}
}

View File

@@ -0,0 +1,12 @@
export function getCreatePostPath(): string {
return "/new-post";
}
export function getCreateProjectPath(): string {
return "/new-project";
}
/** @deprecated همه کاربران می‌توانند پست و پروژه ثبت کنند */
export function getCreateContentPath(_userType?: string | null): string {
return getCreatePostPath();
}

23
src/lib/reelsSeedPost.ts Normal file
View File

@@ -0,0 +1,23 @@
import { Post } from "@/types/types";
const KEY_PREFIX = "reels-seed-post-";
export function cacheReelsSeedPost(post: Post): void {
if (typeof window === "undefined") return;
try {
sessionStorage.setItem(`${KEY_PREFIX}${post._id}`, JSON.stringify(post));
} catch {
/* ignore quota errors */
}
}
export function readReelsSeedPost(id: string): Post | null {
if (typeof window === "undefined") return null;
try {
const raw = sessionStorage.getItem(`${KEY_PREFIX}${id}`);
if (!raw) return null;
return JSON.parse(raw) as Post;
} catch {
return null;
}
}