Initial commit
This commit is contained in:
50
src/lib/api/upstreamProxy.ts
Normal file
50
src/lib/api/upstreamProxy.ts
Normal 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
82
src/lib/auth/postLogin.ts
Normal 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
98
src/lib/coldStartFeed.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
44
src/lib/explore/buildPostFilters.ts
Normal file
44
src/lib/explore/buildPostFilters.ts
Normal 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 {};
|
||||
}
|
||||
}
|
||||
12
src/lib/getCreateContentPath.ts
Normal file
12
src/lib/getCreateContentPath.ts
Normal 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
23
src/lib/reelsSeedPost.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user