import { tokenStore } from "./token-store"; export const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080/api/v1"; // عکس‌ها از مسیر نسبی (/uploads/...) برمی‌گردن، نه URL کامل؛ باید به origin // سرور (بدون /api/v1) وصل بشن. export const API_ORIGIN = API_BASE.replace(/\/api\/v1\/?$/, ""); export function photoUrl(path: string) { if (!path) return ""; if (path.startsWith("http")) return path; return `${API_ORIGIN}${path}`; } interface ApiEnvelope { success: boolean; message?: string; data?: T; errors?: unknown; } export class ApiError extends Error { errors?: unknown; status: number; constructor(message: string, status: number, errors?: unknown) { super(message); this.status = status; this.errors = errors; } } // ---------------- انواع ---------------- export type ApprovalStatus = "pending" | "approved" | "rejected"; export type UserState = "inactive" | "active" | "blocked"; export interface UserProfile { id: number; phone: string; first_name?: string; gender?: "male" | "female"; birth_date?: string; height_cm?: number; weight_kg?: number; current_step: number; is_onboarded: boolean; approval_status: ApprovalStatus; rejection_reason?: string; is_admin: boolean; state: UserState; blocked_reason?: string; blocked_at?: string; } export interface StepView { step: number; key: string; done: boolean; } export interface OnboardingStatus { current_step: number; current_step_key: string; is_onboarded: boolean; approval_status: ApprovalStatus; rejection_reason?: string; steps: StepView[]; } export interface SelectionSummary { category_key: string; option_ids: number[]; titles: string[]; } export interface ProfileAnswer { id: number; user_id: number; question_id: number; answer: string; question?: ProfileQuestion; } export interface UserPhoto { id: number; user_id: number; url: string; sort_order: number; } export interface FullProfile { user: UserProfile; onboarding_status: OnboardingStatus; selections: SelectionSummary[]; profile_answers: ProfileAnswer[]; photos: UserPhoto[]; } export interface TokenPair { access_token: string; refresh_token: string; } export interface AuthResponseData { tokens: TokenPair; user: UserProfile; is_new_user: boolean; onboarding_done: boolean; } export type CategoryKey = | "education_level" | "field_of_study" | "occupation" | "important_in_relationship" | "interest" | "looking_for"; export const ALL_CATEGORIES: { key: CategoryKey; fallbackTitle: string; emoji: string }[] = [ { key: "education_level", fallbackTitle: "سطح تحصیلات", emoji: "🎓" }, { key: "field_of_study", fallbackTitle: "رشته تحصیلی", emoji: "📚" }, { key: "occupation", fallbackTitle: "فعالیت‌ها", emoji: "💼" }, { key: "important_in_relationship", fallbackTitle: "اهمیت در رابطه", emoji: "❤️" }, { key: "interest", fallbackTitle: "علاقه‌مندی‌ها", emoji: "✨" }, { key: "looking_for", fallbackTitle: "دنبال چی می‌گردی", emoji: "🔍" }, ]; export interface Category { id: number; key: CategoryKey; title: string; min_select: number; max_select: number; created_at: string; updated_at: string; } export interface Option { id: number; category_id: number; title: string; sort_order: number; is_active: boolean; created_at: string; updated_at: string; } export interface ProfileQuestion { id: number; text: string; sort_order: number; is_active: boolean; created_at: string; updated_at: string; } // ---------------- هسته‌ی fetch ---------------- let refreshPromise: Promise | null = null; async function rawRequest( path: string, options: RequestInit = {}, auth = false ): Promise { const headers = new Headers(options.headers); headers.set("Content-Type", "application/json"); if (auth) { const token = tokenStore.getAccess(); if (token) headers.set("Authorization", `Bearer ${token}`); } const res = await fetch(`${API_BASE}${path}`, { ...options, headers }); let body: ApiEnvelope | null = null; try { body = await res.json(); } catch { // بدنه‌ی خالی } if (!res.ok) { throw new ApiError(body?.message ?? "خطای غیرمنتظره از سرور", res.status, body?.errors); } return (body?.data as T) ?? (undefined as T); } async function authRequest(path: string, options: RequestInit = {}): Promise { try { return await rawRequest(path, options, true); } catch (err) { if (err instanceof ApiError && err.status === 401) { const refreshed = await ensureRefreshed(); if (refreshed) return await rawRequest(path, options, true); } throw err; } } async function ensureRefreshed(): Promise { const refresh = tokenStore.getRefresh(); if (!refresh) return false; if (!refreshPromise) { refreshPromise = rawRequest("/auth/token/refresh", { method: "POST", body: JSON.stringify({ refresh_token: refresh }), }) .then((tokens) => { tokenStore.set(tokens.access_token, tokens.refresh_token); return true; }) .catch(() => { tokenStore.clear(); return false; }) .finally(() => { refreshPromise = null; }); } return refreshPromise; } // ---------------- احراز هویت ---------------- export function requestOtp(phone: string) { return rawRequest("/auth/request-otp", { method: "POST", body: JSON.stringify({ phone }), }); } export function verifyOtp(phone: string, otp: string) { return rawRequest("/auth/verify-otp", { method: "POST", body: JSON.stringify({ phone, otp }), }); } export function getProfileMe() { return authRequest<{ user: UserProfile }>("/profile/me"); } // ---------------- ادمین: دسته‌ها و گزینه‌ها ---------------- export function getCategory(key: CategoryKey) { return authRequest(`/admin/categories/${key}`); } export function updateCategory( key: CategoryKey, payload: { title?: string; min_select?: number; max_select?: number } ) { return authRequest(`/admin/categories/${key}`, { method: "PATCH", body: JSON.stringify(payload), }); } export function listOptions(key: CategoryKey) { return authRequest(`/admin/categories/${key}/options`); } export function createOption(key: CategoryKey, payload: { title: string; sort_order?: number }) { return authRequest