diff --git a/src/app/(main)/home/page.tsx b/src/app/(main)/home/page.tsx
index c1295a1..cdf7cb3 100644
--- a/src/app/(main)/home/page.tsx
+++ b/src/app/(main)/home/page.tsx
@@ -123,7 +123,9 @@ function DiscoverScreen() {
setError(null);
try {
const data = await getDiscoverFeed({
- maxDistanceKm: f.maxDistance,
+ // وقتی «بدون محدودیت فاصله» فعاله، اصلاً پارامتر فاصله رو نمیفرستیم تا
+ // بکاند همهی کاندیداها رو صرفنظر از فاصله برگردونه.
+ maxDistanceKm: f.unlimitedDistance ? undefined : f.maxDistance,
minAge: f.minAge,
maxAge: f.maxAge,
});
@@ -205,7 +207,7 @@ function DiscoverScreen() {
title="کسی با این فیلترها پیدا نشد"
description="محدودهی فاصله یا سن رو بازتر کن، یا بعداً دوباره سر بزن."
actionLabel="بازتنظیم فیلترها"
- onAction={() => setFilters(DEFAULT_FILTERS)}
+ onAction={() => setFilters({ ...DEFAULT_FILTERS })}
/>
) : (
diff --git a/src/app/(main)/profile/page.tsx b/src/app/(main)/profile/page.tsx
index 86ae4e8..8cc8405 100644
--- a/src/app/(main)/profile/page.tsx
+++ b/src/app/(main)/profile/page.tsx
@@ -25,7 +25,7 @@ import { ErrorBanner } from "@/components/ui/ErrorBanner";
import { useAuth } from "@/lib/auth-context";
import { getProfileMe, uploadPhoto, deletePhoto, updateBio, photoUrl, FullProfile, Photo } from "@/lib/api";
import { formatApiError } from "@/lib/hooks";
-import { calcAge } from "@/lib/utils";
+import { calcAge, validatePhotoSize } from "@/lib/utils";
function selectionTitles(profile: FullProfile, key: string): string[] {
return profile.selections.find((s) => s.category_key === key)?.titles ?? [];
@@ -251,6 +251,11 @@ function PhotosSection({ photos, onChange }: { photos: Photo[]; onChange: (photo
const file = e.target.files?.[0];
e.target.value = "";
if (!file) return;
+ const sizeError = validatePhotoSize(file);
+ if (sizeError) {
+ setError(sizeError);
+ return;
+ }
setUploading(true);
setError(null);
try {
diff --git a/src/app/(main)/settings/help/page.tsx b/src/app/(main)/settings/help/page.tsx
new file mode 100644
index 0000000..3f01c8f
--- /dev/null
+++ b/src/app/(main)/settings/help/page.tsx
@@ -0,0 +1,94 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { ChevronRight, ChevronDown, HelpCircle } from "lucide-react";
+import { Card } from "@/components/ui/Card";
+
+const FAQ: { q: string; a: string }[] = [
+ {
+ q: "چرا پروفایلم هنوز تایید نشده؟",
+ a: "بعد از تکمیل تمام مراحل ثبتنام، یکی از ادمینهای دلدل پروفایلت رو بررسی میکنه. معمولاً این کار زیاد طول نمیکشه؛ تا اون موقع میتونی هر وقت خواستی برگردی و اطلاعاتت رو ویرایش کنی.",
+ },
+ {
+ q: "چرا پروفایلم رد شد؟",
+ a: "دلیل رد دقیقاً توی صفحهی اصلی برات نمایش داده میشه (مثلاً عکس واضح نیست یا اطلاعات ناقصه). میتونی اطلاعاتت رو اصلاح کنی و دوباره برای بررسی بفرستی.",
+ },
+ {
+ q: "فاصلهای که کنار هر پروفایل میبینم دقیقه؟",
+ a: "نه، عمداً تقریبیه (گرد شده به نزدیکترین کیلومتر) تا موقعیت دقیق هیچکس لو نره.",
+ },
+ {
+ q: "چطور میتونم محدودهی سنی یا فاصله رو تغییر بدم؟",
+ a: "توی صفحهی کشف، روی آیکون فیلتر (کنار آیکون قلب) بزن. اونجا میتونی محدودهی سنی، حداکثر فاصله، یا حتی جستجوی بدون محدودیت فاصله رو فعال کنی.",
+ },
+ {
+ q: "چرا دیگه پروفایل جدیدی برام نمیاد؟",
+ a: "یعنی با فیلترهای فعلیت، پیشنهاد جدیدی نمونده. فیلترها رو بازتر کن یا بعداً دوباره سر بزن؛ تا اون موقع میتونی یه بازی کوچیک هم بزنی!",
+ },
+ {
+ q: "چطور با یکی چت کنم؟",
+ a: "وقتی دو نفر متقابلاً همدیگه رو لایک کنن، یه «مچ» ساخته میشه و بخش پیامها براتون باز میشه.",
+ },
+ {
+ q: "میتونم شماره موبایلم رو عوض کنم؟",
+ a: "فعلاً از داخل اپ امکانش نیست. با پشتیبانی در ارتباط باش.",
+ },
+];
+
+function FaqItem({ q, a }: { q: string; a: string }) {
+ const [open, setOpen] = useState(false);
+ return (
+
+
setOpen((o) => !o)}
+ className="w-full flex items-center justify-between gap-3 px-4 py-3.5 text-right"
+ >
+ {q}
+
+
+ {open && (
+
{a}
+ )}
+
+ );
+}
+
+export default function HelpSettingsPage() {
+ const router = useRouter();
+
+ return (
+
+
+ router.back()}
+ className="w-9 h-9 rounded-full flex items-center justify-center hover:bg-(--color-bg-secondary)"
+ aria-label="بازگشت"
+ >
+
+
+
+ راهنما و پشتیبانی 💬
+
+
+
+
+ {FAQ.map((item) => (
+
+ ))}
+
+
+
+
+
+ سوالت اینجا نبود؟ فعلاً کانال پشتیبانی مستقیم داخل اپ نداریم؛ بهزودی اضافه میشه.
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/notifications/page.tsx b/src/app/(main)/settings/notifications/page.tsx
new file mode 100644
index 0000000..ef44504
--- /dev/null
+++ b/src/app/(main)/settings/notifications/page.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { ChevronRight, Heart, MessageCircle, Star } from "lucide-react";
+import { Card } from "@/components/ui/Card";
+import { Switch } from "@/components/ui/Switch";
+import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
+import { ErrorBanner } from "@/components/ui/ErrorBanner";
+import { getProfileMe, updateNotificationSettings, NotificationSettings } from "@/lib/api";
+import { formatApiError } from "@/lib/hooks";
+
+const ITEMS: { key: keyof NotificationSettings; icon: React.ReactNode; label: string; hint: string }[] = [
+ {
+ key: "notify_matches",
+ icon: ,
+ label: "مچ جدید",
+ hint: "وقتی با کسی متقابلاً همدیگه رو لایک کردین",
+ },
+ {
+ key: "notify_messages",
+ icon: ,
+ label: "پیام جدید",
+ hint: "وقتی یکی از مچهات برات پیام میفرسته",
+ },
+ {
+ key: "notify_likes",
+ icon: ,
+ label: "لایک جدید",
+ hint: "وقتی یکی پروفایلت رو لایک میکنه",
+ },
+];
+
+export default function NotificationsSettingsPage() {
+ const router = useRouter();
+ const [settings, setSettings] = useState(null);
+ const [error, setError] = useState(null);
+ const [saving, setSaving] = useState(null);
+
+ useEffect(() => {
+ getProfileMe()
+ .then((p) =>
+ setSettings({
+ notify_matches: p.user.notify_matches,
+ notify_messages: p.user.notify_messages,
+ notify_likes: p.user.notify_likes,
+ })
+ )
+ .catch((err) => setError(formatApiError(err)));
+ }, []);
+
+ async function handleToggle(key: keyof NotificationSettings, value: boolean) {
+ if (!settings) return;
+ const next = { ...settings, [key]: value };
+ setSettings(next);
+ setSaving(key);
+ setError(null);
+ try {
+ await updateNotificationSettings(next);
+ } catch (err) {
+ setSettings(settings);
+ setError(formatApiError(err));
+ } finally {
+ setSaving(null);
+ }
+ }
+
+ return (
+
+
+ router.back()}
+ className="w-9 h-9 rounded-full flex items-center justify-center hover:bg-(--color-bg-secondary)"
+ aria-label="بازگشت"
+ >
+
+
+
اعلانها 🔔
+
+
+ {error &&
}
+
+ {!settings ? (
+
+ ) : (
+ <>
+
+ این تنظیمات همین الان ذخیره میشن. سیستم ارسال اعلان بیرون از اپ (پوش/پیامک) هنوز
+ راهاندازی نشده؛ بهمحض فعال شدنش، دقیقاً همین ترجیحات رو رعایت میکنه.
+
+
+ {ITEMS.map((item, i) => (
+
+ {item.icon}
+
+
+ {item.label}
+
+
{item.hint}
+
+
handleToggle(item.key, v)}
+ />
+
+ ))}
+
+ >
+ )}
+
+ );
+}
diff --git a/src/app/(main)/settings/page.tsx b/src/app/(main)/settings/page.tsx
index 6cbdf2d..70c7446 100644
--- a/src/app/(main)/settings/page.tsx
+++ b/src/app/(main)/settings/page.tsx
@@ -3,14 +3,13 @@
import { useRouter } from "next/navigation";
import { Bell, Shield, HelpCircle, LogOut, ChevronLeft } from "lucide-react";
import { Card } from "@/components/ui/Card";
-import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button";
import { useAuth } from "@/lib/auth-context";
-const comingSoonItems = [
- { icon: Bell, label: "اعلانها" },
- { icon: Shield, label: "حریم خصوصی و امنیت" },
- { icon: HelpCircle, label: "راهنما و پشتیبانی" },
+const settingsLinks = [
+ { icon: Bell, label: "اعلانها", href: "/settings/notifications" },
+ { icon: Shield, label: "حریم خصوصی و امنیت", href: "/settings/privacy" },
+ { icon: HelpCircle, label: "راهنما و پشتیبانی", href: "/settings/help" },
];
export default function SettingsPage() {
@@ -38,22 +37,20 @@ export default function SettingsPage() {
- {comingSoonItems.map((item, i) => {
+ {settingsLinks.map((item, i) => {
const Icon = item.icon;
return (
router.push(item.href)}
+ className={`w-full flex items-center gap-3 px-4 py-3.5 text-right hover:bg-(--color-bg-secondary) transition-colors ${
+ i !== settingsLinks.length - 1 ? "border-b border-(--color-border-subtle)" : ""
}`}
>
{item.label}
-
- بهزودی
-
);
diff --git a/src/app/(main)/settings/privacy/page.tsx b/src/app/(main)/settings/privacy/page.tsx
new file mode 100644
index 0000000..89a840b
--- /dev/null
+++ b/src/app/(main)/settings/privacy/page.tsx
@@ -0,0 +1,161 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { motion, AnimatePresence } from "framer-motion";
+import { ChevronRight, ShieldCheck, Phone, MapPin, Lock, Trash2 } from "lucide-react";
+import { Card } from "@/components/ui/Card";
+import { Button } from "@/components/ui/Button";
+import { ErrorBanner } from "@/components/ui/ErrorBanner";
+import { useAuth } from "@/lib/auth-context";
+import { deleteAccount } from "@/lib/api";
+import { formatApiError } from "@/lib/hooks";
+
+function ConfirmDeleteSheet({
+ open,
+ loading,
+ onConfirm,
+ onCancel,
+}: {
+ open: boolean;
+ loading: boolean;
+ onConfirm: () => void;
+ onCancel: () => void;
+}) {
+ return (
+
+ {open && (
+ <>
+
+
+
+ حذف حساب کاربری
+
+
+ مطمئنی میخوای حسابت رو حذف کنی؟ این کار رو نمیشه خودت برگردونی.
+
+
+
+ انصراف
+
+
+ بله، حذف کن
+
+
+
+ >
+ )}
+
+ );
+}
+
+const FACTS = [
+ {
+ icon: ,
+ text: "شماره موبایلت هیچوقت به کاربرهای دیگه نشون داده نمیشه.",
+ },
+ {
+ icon: ,
+ text: "موقعیت مکانی دقیقت هیچوقت ذخیره یا نشون داده نمیشه؛ فقط یه فاصلهی تقریبی و گردشده (مثلاً «۲۰ کیلومتر») محاسبه میشه.",
+ },
+ {
+ icon: ,
+ text: "ورود فقط با کد یکبارمصرف پیامکیه؛ رمز عبوری برای دزدیده شدن وجود نداره.",
+ },
+ {
+ icon: ,
+ text: "هر پروفایل قبل از دیده شدن توسط بقیه، توسط تیم پشتیبانی بررسی و تایید میشه.",
+ },
+];
+
+export default function PrivacySettingsPage() {
+ const router = useRouter();
+ const { logout } = useAuth();
+ const [confirmOpen, setConfirmOpen] = useState(false);
+ const [deleting, setDeleting] = useState(false);
+ const [error, setError] = useState(null);
+
+ async function handleDelete() {
+ setDeleting(true);
+ setError(null);
+ try {
+ await deleteAccount();
+ logout();
+ router.push("/login");
+ } catch (err) {
+ setError(formatApiError(err));
+ setConfirmOpen(false);
+ setDeleting(false);
+ }
+ }
+
+ return (
+
+
+ router.back()}
+ className="w-9 h-9 rounded-full flex items-center justify-center hover:bg-(--color-bg-secondary)"
+ aria-label="بازگشت"
+ >
+
+
+
+ حریم خصوصی و امنیت 🔒
+
+
+
+ {error &&
}
+
+
+ {FACTS.map((f, i) => (
+
+ ))}
+
+
+
+
+
حذف حساب کاربری
+
+ حسابت غیرفعال میشه و دیگه نمیتونی وارد بشی یا توی دوستیابی دیده بشی. برای
+ برگردوندنش باید با پشتیبانی تماس بگیری.
+
+
+ }
+ className="!text-(--color-status-error) !border-red-200 w-fit"
+ onClick={() => setConfirmOpen(true)}
+ >
+ حذف حساب من
+
+
+
+
setConfirmOpen(false)}
+ />
+
+ );
+}
diff --git a/src/components/discover/FiltersSheet.tsx b/src/components/discover/FiltersSheet.tsx
index f6ecf3c..f868388 100644
--- a/src/components/discover/FiltersSheet.tsx
+++ b/src/components/discover/FiltersSheet.tsx
@@ -9,6 +9,7 @@ export interface DiscoverFilters {
maxDistance: number;
minAge: number;
maxAge: number;
+ unlimitedDistance?: boolean;
}
export function FiltersSheet({
@@ -25,6 +26,7 @@ export function FiltersSheet({
const [maxDistance, setMaxDistance] = useState(initial.maxDistance);
const [minAge, setMinAge] = useState(initial.minAge);
const [maxAge, setMaxAge] = useState(initial.maxAge);
+ const [unlimitedDistance, setUnlimitedDistance] = useState(!!initial.unlimitedDistance);
return (
@@ -61,7 +63,7 @@ export function FiltersSheet({
حداکثر فاصله
- {maxDistance} کیلومتر
+ {unlimitedDistance ? "همهجا" : `${maxDistance} کیلومتر`}
setMaxDistance(Number(e.target.value))}
- className="w-full accent-(--color-brand-500)"
+ className="w-full accent-(--color-brand-500) disabled:opacity-40"
/>
+
+ setUnlimitedDistance(e.target.checked)}
+ className="w-4 h-4 accent-(--color-brand-500)"
+ />
+
+ بدون محدودیت فاصله (توی همهجا بگرد)
+
+
@@ -109,7 +123,7 @@ export function FiltersSheet({
isBlock
className="mt-8"
onClick={() => {
- onApply({ maxDistance, minAge, maxAge });
+ onApply({ maxDistance, minAge, maxAge, unlimitedDistance });
onClose();
}}
>
diff --git a/src/components/onboarding/steps/PhotosStep.tsx b/src/components/onboarding/steps/PhotosStep.tsx
index f920502..0e58a38 100644
--- a/src/components/onboarding/steps/PhotosStep.tsx
+++ b/src/components/onboarding/steps/PhotosStep.tsx
@@ -6,6 +6,7 @@ import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
import { ErrorBanner } from "@/components/ui/ErrorBanner";
import { confirmPhotosStep, uploadPhoto, deletePhoto, photoUrl, FullProfile, Photo } from "@/lib/api";
import { formatApiError } from "@/lib/hooks";
+import { validatePhotoSize } from "@/lib/utils";
const MAX_PHOTOS = 6;
@@ -26,6 +27,11 @@ export function PhotosStep({
const file = e.target.files?.[0];
e.target.value = "";
if (!file) return;
+ const sizeError = validatePhotoSize(file);
+ if (sizeError) {
+ setError(sizeError);
+ return;
+ }
setUploading(true);
setError(null);
try {
diff --git a/src/components/onboarding/steps/ProfileQuestionsStep.tsx b/src/components/onboarding/steps/ProfileQuestionsStep.tsx
index 67d84fe..5922cb4 100644
--- a/src/components/onboarding/steps/ProfileQuestionsStep.tsx
+++ b/src/components/onboarding/steps/ProfileQuestionsStep.tsx
@@ -12,10 +12,18 @@ import {
FullProfile,
} from "@/lib/api";
import { formatApiError } from "@/lib/hooks";
-import { Check } from "lucide-react";
+import { ChevronRight, Pencil } from "lucide-react";
const REQUIRED = 3;
+interface SlotAnswer {
+ questionId: number;
+ answer: string;
+}
+
+// این مرحله بهجای اینکه یهو ۳ تا سوال رو با هم جلوی کاربر بذاره (که خستهکننده
+// و شلوغه)، یکییکی جلو میبره: اول انتخاب سوال، بعد جواب همون سوال، بعد سوال
+// بعدی — دقیقاً مثل یه مصاحبهی کوتاه، نه یه فرم بلند.
export function ProfileQuestionsStep({
initialProfile,
onNext,
@@ -24,16 +32,20 @@ export function ProfileQuestionsStep({
onNext: () => void;
}) {
const [questions, setQuestions] = useState
(null);
- const [selectedIds, setSelectedIds] = useState(
- initialProfile?.profile_answers.map((a) => a.question_id) ?? []
- );
- const [answers, setAnswers] = useState>(() => {
- const map: Record = {};
- initialProfile?.profile_answers.forEach((a) => {
- map[a.question_id] = a.answer;
- });
- return map;
+ const [slots, setSlots] = useState<(SlotAnswer | null)[]>(() => {
+ const initial = initialProfile?.profile_answers.slice(0, REQUIRED) ?? [];
+ const arr: (SlotAnswer | null)[] = initial.map((a) => ({
+ questionId: a.question_id,
+ answer: a.answer,
+ }));
+ while (arr.length < REQUIRED) arr.push(null);
+ return arr;
});
+ const [activeSlot, setActiveSlot] = useState(() => {
+ const firstEmpty = slots.findIndex((s) => s === null);
+ return firstEmpty === -1 ? REQUIRED - 1 : firstEmpty;
+ });
+ const [draftAnswer, setDraftAnswer] = useState(slots[activeSlot]?.answer ?? "");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
@@ -43,25 +55,62 @@ export function ProfileQuestionsStep({
.catch((err) => setError(formatApiError(err)));
}, []);
- function toggleQuestion(id: number) {
- setSelectedIds((prev) => {
- if (prev.includes(id)) return prev.filter((x) => x !== id);
- if (prev.length >= REQUIRED) return prev;
- return [...prev, id];
- });
+ if (!questions) return ;
+
+ const usedQuestionIds = slots
+ .filter((s, i) => s && i !== activeSlot)
+ .map((s) => s!.questionId);
+ const currentQuestion = questions.find((q) => q.id === slots[activeSlot]?.questionId) ?? null;
+ const availableForSlot = questions.filter((q) => !usedQuestionIds.includes(q.id));
+
+ function saveDraftInto(list: (SlotAnswer | null)[]) {
+ const next = [...list];
+ if (next[activeSlot]) {
+ next[activeSlot] = { ...next[activeSlot]!, answer: draftAnswer.trim() };
+ }
+ return next;
}
- const allAnswered =
- selectedIds.length === REQUIRED &&
- selectedIds.every((id) => (answers[id] ?? "").trim().length > 0);
+ function pickQuestion(id: number) {
+ setSlots((prev) => {
+ const next = [...prev];
+ next[activeSlot] = { questionId: id, answer: "" };
+ return next;
+ });
+ setDraftAnswer("");
+ }
+
+ function clearCurrentQuestion() {
+ setSlots((prev) => {
+ const next = [...prev];
+ next[activeSlot] = null;
+ return next;
+ });
+ setDraftAnswer("");
+ }
+
+ function goToSlot(index: number) {
+ setSlots((prev) => saveDraftInto(prev));
+ setActiveSlot(index);
+ setDraftAnswer(slots[index]?.answer ?? "");
+ }
+
+ function goNext() {
+ setSlots((prev) => saveDraftInto(prev));
+ goToSlot(activeSlot + 1);
+ }
+
+ const isLastSlot = activeSlot === REQUIRED - 1;
+ const canAdvance = currentQuestion !== null && draftAnswer.trim().length > 0;
async function handleSubmit() {
- if (!allAnswered) return;
+ const finalSlots = saveDraftInto(slots);
+ if (finalSlots.some((s) => !s || !s.answer)) return;
setLoading(true);
setError(null);
try {
await submitProfileQuestions(
- selectedIds.map((id) => ({ question_id: id, answer: answers[id].trim() }))
+ finalSlots.map((s) => ({ question_id: s!.questionId, answer: s!.answer }))
);
onNext();
} catch (err) {
@@ -71,76 +120,87 @@ export function ProfileQuestionsStep({
}
}
- if (!questions) return ;
-
return (
-
- دقیقاً {REQUIRED} تا انتخاب کن ({selectedIds.length}/{REQUIRED})
+
+ {Array.from({ length: REQUIRED }).map((_, i) => (
+ (slots[i] || i < activeSlot) && goToSlot(i)}
+ className={cn(
+ "h-1.5 rounded-(--radius-full) transition-all",
+ i === activeSlot
+ ? "w-8 bg-(--color-brand-500)"
+ : slots[i]
+ ? "w-4 bg-(--color-brand-200)"
+ : "w-4 bg-(--color-bg-tertiary)"
+ )}
+ />
+ ))}
+
+
+ سوال {activeSlot + 1} از {REQUIRED}
-
- {questions.map((q) => {
- const selected = selectedIds.includes(q.id);
- const disabled = !selected && selectedIds.length >= REQUIRED;
- return (
+ {activeSlot > 0 && (
+
goToSlot(activeSlot - 1)}
+ className="flex items-center gap-1 text-[13px] text-(--color-content-tertiary) w-fit"
+ >
+
+ سوال قبلی
+
+ )}
+
+ {!currentQuestion ? (
+
+
+ یکی از این سوالها رو انتخاب کن:
+
+ {availableForSlot.map((q) => (
toggleQuestion(q.id)}
- disabled={disabled}
- className={cn(
- "flex items-center gap-3 text-right rounded-(--radius-md) border-2 px-4 py-3.5 transition-all",
- selected
- ? "border-(--color-brand-500) bg-(--color-brand-50)"
- : "border-(--color-border-default) bg-(--color-surface-default)",
- disabled && "opacity-40"
- )}
+ onClick={() => pickQuestion(q.id)}
+ className="text-right rounded-(--radius-md) border-2 border-(--color-border-default) bg-(--color-surface-default) px-4 py-3.5 text-[14px] text-(--color-content-primary) hover:border-(--color-brand-300) transition-colors"
>
-
- {selected && }
-
- {q.text}
+ {q.text}
- );
- })}
-
-
- {selectedIds.length > 0 && (
-
- {selectedIds.map((id) => {
- const q = questions.find((qq) => qq.id === id);
- if (!q) return null;
- return (
-
-
- {q.text}
-
-
- );
- })}
+ ))}
+
+ ) : (
+
+
+
+ {currentQuestion.text}
+
+
+
+ عوض کردن سوال
+
+
+
)}
-
- بریم مرحله بعد
+
+ {isLastSlot ? "ثبت و ادامه" : "سوال بعدی"}
);
diff --git a/src/components/ui/Switch.tsx b/src/components/ui/Switch.tsx
new file mode 100644
index 0000000..18641dc
--- /dev/null
+++ b/src/components/ui/Switch.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+export function Switch({
+ checked,
+ onChange,
+ disabled,
+}: {
+ checked: boolean;
+ onChange: (checked: boolean) => void;
+ disabled?: boolean;
+}) {
+ return (
+
onChange(!checked)}
+ className={`relative w-11 h-6 rounded-(--radius-full) transition-colors duration-200 shrink-0 disabled:opacity-50 cursor-pointer ${
+ checked ? "bg-(--color-brand-500)" : "bg-(--color-bg-tertiary)"
+ }`}
+ >
+ {/* از insetInlineStart استفاده میکنیم (نه left/right) تا توی حالت RTL درست جابهجا بشه */}
+
+
+ );
+}
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 8f50c34..8026d4f 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -48,6 +48,9 @@ export interface UserProfile {
is_admin: boolean;
bio?: string;
streak_count: number;
+ notify_matches: boolean;
+ notify_messages: boolean;
+ notify_likes: boolean;
}
export interface TokenPair {
@@ -440,6 +443,23 @@ export function checkIn() {
return authRequest
("/profile/checkin", { method: "POST" });
}
+export interface NotificationSettings {
+ notify_matches: boolean;
+ notify_messages: boolean;
+ notify_likes: boolean;
+}
+
+export function updateNotificationSettings(settings: NotificationSettings) {
+ return authRequest("/profile/notifications", {
+ method: "PATCH",
+ body: JSON.stringify(settings),
+ });
+}
+
+export function deleteAccount() {
+ return authRequest("/profile/me", { method: "DELETE" });
+}
+
// ---------------- دوستیابی ----------------
export function getDiscoverFeed(params?: {
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 79d39b7..d8acd9c 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -12,6 +12,18 @@ export function formatDistance(km: number): string {
return `${rounded} کیلومتر`;
}
+// سقف حجم عکس سمت کلاینت (موبایل/وب)؛ قبل از اینکه اصلاً آپلود شروع بشه
+// جلوی فایلهای خیلی بزرگ رو میگیره. بکاند هم خودش جداگانه سقف داره؛ این
+// فقط یه فیدبک فوریتر به کاربره، نه جایگزین اعتبارسنجی سمت سرور.
+export const MAX_PHOTO_SIZE_BYTES = 10 * 1024 * 1024;
+
+export function validatePhotoSize(file: File): string | null {
+ if (file.size > MAX_PHOTO_SIZE_BYTES) {
+ return "حجم عکس نباید بیشتر از ۱۰ مگابایت باشه؛ یه عکس کوچیکتر انتخاب کن.";
+ }
+ return null;
+}
+
export function calcAge(birthDate?: string): number | null {
if (!birthDate) return null;
const b = new Date(birthDate);