Initial commit: dating app web frontend (Next.js)
Phone+OTP auth, staged onboarding, admin-approval-gated home screen, and the full discovery/match/chat UI (swipe deck with drag gestures, match modal, profile detail, filters, messages/chat) wired to the real backend, styled after a Figma dating-app UI kit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
||||
import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
|
||||
import { OptionPicker } from "@/components/ui/OptionPicker";
|
||||
import { getOptions, submitEducation, Option, FullProfile } from "@/lib/api";
|
||||
import { formatApiError } from "@/lib/hooks";
|
||||
|
||||
function findSelection(profile: FullProfile | null, key: string): number[] {
|
||||
return profile?.selections.find((s) => s.category_key === key)?.option_ids ?? [];
|
||||
}
|
||||
|
||||
export function EducationStep({
|
||||
initialProfile,
|
||||
onNext,
|
||||
}: {
|
||||
initialProfile: FullProfile | null;
|
||||
onNext: () => void;
|
||||
}) {
|
||||
const [levelOptions, setLevelOptions] = useState<Option[] | null>(null);
|
||||
const [fieldOptions, setFieldOptions] = useState<Option[] | null>(null);
|
||||
const [levelId, setLevelId] = useState<number[]>(
|
||||
findSelection(initialProfile, "education_level")
|
||||
);
|
||||
const [fieldId, setFieldId] = useState<number[]>(
|
||||
findSelection(initialProfile, "field_of_study")
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([getOptions("education_level"), getOptions("field_of_study")])
|
||||
.then(([lvl, field]) => {
|
||||
setLevelOptions(lvl);
|
||||
setFieldOptions(field);
|
||||
})
|
||||
.catch((err) => setError(formatApiError(err)));
|
||||
}, []);
|
||||
|
||||
const isValid = levelId.length === 1 && fieldId.length === 1;
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!isValid) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await submitEducation({
|
||||
education_level_option_id: levelId[0],
|
||||
field_of_study_option_id: fieldId[0],
|
||||
});
|
||||
onNext();
|
||||
} catch (err) {
|
||||
setError(formatApiError(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!levelOptions || !fieldOptions) return <FullScreenLoader label="گزینهها رو میاریم..." />;
|
||||
|
||||
if (levelOptions.length === 0 || fieldOptions.length === 0) {
|
||||
return (
|
||||
<ErrorBanner message="هنوز گزینهای برای تحصیلات توی پنل ادمین ثبت نشده. اول باید ادمین اینها رو اضافه کنه." />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-7">
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="text-[13px] font-semibold text-(--color-content-secondary) px-1">
|
||||
سطح تحصیلات
|
||||
</label>
|
||||
<OptionPicker options={levelOptions} selectedIds={levelId} onChange={setLevelId} mode="single" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="text-[13px] font-semibold text-(--color-content-secondary) px-1">
|
||||
رشته تحصیلی
|
||||
</label>
|
||||
<OptionPicker options={fieldOptions} selectedIds={fieldId} onChange={setFieldId} mode="single" />
|
||||
</div>
|
||||
|
||||
<ErrorBanner message={error} />
|
||||
|
||||
<Button size="lg" isBlock disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
|
||||
بریم مرحله بعد
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user