Browse Source

feat: implement question flow system with dynamic components and custom styling

front-test-2
ghorbani 4 weeks ago
parent
commit
a3bda4ecd3
  1. 27
      src/app/globals.css
  2. 18
      src/app/intro/page.tsx
  3. 21
      src/app/new-match/page.tsx
  4. 5
      src/app/new-match/profile/page.tsx
  5. 6
      src/app/questions-list/[slug]/question-detail-client.tsx
  6. 1
      src/app/questions-list/page.tsx
  7. 10
      src/app/request-accepted/page.tsx
  8. 94
      src/components/questions/question-date.tsx
  9. 1
      src/components/questions/question-section-flow.tsx
  10. 2
      src/components/questions/question-snap-list.tsx
  11. 7
      src/components/questions/test-loading-screen.tsx
  12. 57
      src/components/sliders/slider-page.tsx
  13. 4
      src/components/sliders/slider-slide-one.tsx
  14. 2
      src/components/sliders/slider-slide-two.tsx
  15. 18
      src/components/ui/button.tsx
  16. 4
      src/data/questions/en.json
  17. 2
      src/lib/get-submit-path.ts

27
src/app/globals.css

@ -160,3 +160,30 @@ body[data-page-background="custom"] .app-shell {
0 2px 7px rgb(0 0 0 / 22%),
0 0 0 1px rgb(0 0 0 / 8%);
}
/* 3 Dots Pulsing Wave Loader for Buttons & App */
@keyframes button-dot-slide {
0%, 100% {
transform: scale(0.35);
opacity: 0.35;
}
40% {
transform: scale(1.25);
opacity: 1;
}
}
.animate-dots-slide-1 {
animation: button-dot-slide 0.9s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
animation-delay: 0s;
}
.animate-dots-slide-2 {
animation: button-dot-slide 0.9s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
animation-delay: 0.18s;
}
.animate-dots-slide-3 {
animation: button-dot-slide 0.9s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
animation-delay: 0.36s;
}

18
src/app/intro/page.tsx

@ -35,11 +35,9 @@ export default function Intro() {
return;
}
const shouldRedirect =
authBridge.isAuthenticated() &&
window.sessionStorage.getItem(REDIRECT_SESSION_KEY) === "true";
const isAuthenticated = authBridge.isAuthenticated();
if (!shouldRedirect) {
if (!isAuthenticated) {
if (!isCancelled) {
setIsCheckingRedirect(false);
}
@ -52,16 +50,24 @@ export default function Intro() {
setIsCheckingRedirect(true);
}
try {
const profileResponse = profile ?? (await refetch()).data;
const nextPath = localizePath(getSubmitPath(profileResponse), locale);
if (typeof window !== "undefined") {
window.sessionStorage.removeItem(REDIRECT_SESSION_KEY);
}
if (!isCancelled) {
router.replace(nextPath);
return;
}
} catch {
if (!isCancelled) {
setIsCheckingRedirect(false);
}
} finally {
isRedirectingRef.current = false;
}
};
void redirectIfNeeded();
@ -208,7 +214,7 @@ export default function Intro() {
style={{ paddingBottom: "calc(20px + var(--safe-bottom))" }}
className="pointer-events-auto px-[17px] pt-3"
>
<Button onClick={handleSubmit} disabled={isSubmitting}>
<Button onClick={handleSubmit} disabled={isSubmitting} isLoading={isSubmitting}>
{t.common.submit}
</Button>
</div>

21
src/app/new-match/page.tsx

@ -1,6 +1,7 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useMemo } from "react";
import { useRouter } from "next/navigation";
import { localizePath } from "@/translations/config";
@ -8,6 +9,7 @@ import { getSubmitPath } from "@/lib/get-submit-path";
import { FaLock } from "react-icons/fa6";
import AdvisorActionsCard from "@/components/ui/advisor-actions-card";
import NavigationButton from "@/components/ui/navigation-button";
import { DotsLoader } from "@/components/ui/button";
import { PageBackground } from "@/components/utils/page-background";
import type {
MarriageField,
@ -250,9 +252,9 @@ export default function NewMatchPage() {
<div className="flex h-full flex-col justify-between">
<section className="mt-[36px] rounded-[15px] bg-[linear-gradient(180deg,#F0445B_0%,#F4556E_100%)] px-[17px] pt-[18px] pb-[17px] text-white shadow-[0_18px_38px_rgba(240,68,91,0.25)]">
{isLoading ? (
<p className="py-8 text-[13px] font-semibold">
Loading match summary...
</p>
<div className="flex items-center justify-center py-8">
<DotsLoader />
</div>
) : isError ? (
<p className="py-8 text-[13px] font-semibold">
Unable to load match summary.
@ -264,7 +266,7 @@ export default function NewMatchPage() {
<span>{matchDisplay.name}</span>
</h2>
<div className="mt-[3px] min-h-[68px]">
<div className="mt-[3px] min-h-[48px]">
{matchDisplay.occupation ? (
<FieldLine field={matchDisplay.occupation} />
) : null}
@ -288,17 +290,14 @@ export default function NewMatchPage() {
{matchDisplay.cityPreference ? (
<FieldLine field={matchDisplay.cityPreference} />
) : null}
{matchDisplay.extraFields.map((field) => (
<FieldLine key={field.id} field={field} />
))}
</div>
<a
href="/new-match/profile"
className="mt-[15px] inline-flex w-full items-center justify-center rounded-[10px] border-none bg-white py-[12px] text-[16px] font-semibold text-[#F0445B] no-underline shadow-none"
<Link
href={localizePath("/new-match/profile", locale)}
className="mt-[15px] inline-flex w-full items-center justify-center rounded-[10px] border-none bg-white py-[12px] text-[16px] font-semibold text-[#F0445B] no-underline shadow-none hover:bg-white/95 transition-colors"
>
View Profile
</a>
</Link>
</>
) : (
<p className="py-8 text-[13px] font-semibold">

5
src/app/new-match/profile/page.tsx

@ -3,7 +3,7 @@
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import Button from "@/components/ui/button";
import Button, { DotsLoader } from "@/components/ui/button";
import DismissReasonSheet from "@/components/ui/dismiss-reason-sheet";
import FemaleConsentSheet from "@/components/ui/female-consent-sheet";
import InformationSheet from "@/components/ui/information-sheet";
@ -361,7 +361,8 @@ export default function NewMatchProfilePage() {
</Button>
<Button
className="py-[18px]"
disabled={!isAcceptProfileEnabled}
disabled={!isAcceptProfileEnabled || respondMutation.isPending}
isLoading={respondMutation.isPending}
onClick={async () => {
close();
if (!isAcceptProfileEnabled) {

6
src/app/questions-list/[slug]/question-detail-client.tsx

@ -7,7 +7,7 @@ import {
QuestionAnswersProvider,
useQuestionAnswers,
} from "@/components/questions/question-answer-storage";
import QuestionButton from "@/components/questions/question-button";
import QuestionButton, { DotsLoader } from "@/components/questions/question-button";
import { QuestionCheckbox } from "@/components/questions/question-checkbox";
import QuestionDate from "@/components/questions/question-date";
import QuestionDropdown from "@/components/questions/question-dropdown";
@ -508,7 +508,7 @@ export default function QuestionDetailClient({
<>
<PageBackground disabled />
<main className="-mx-[17px] flex h-svh flex-col items-center justify-center bg-[#F7F1F0]">
<div className="size-10 rounded-full border-[3px] border-[#DCDCE0] border-t-[#F2465F] border-r-[#F2465F] animate-spin" />
<DotsLoader className="text-[#F2465F] scale-150" />
</main>
</>
);
@ -529,7 +529,7 @@ export default function QuestionDetailClient({
<>
<PageBackground disabled />
<main className="-mx-[17px] flex h-svh flex-col items-center justify-center bg-[#F7F1F0]">
<div className="size-10 rounded-full border-[3px] border-[#DCDCE0] border-t-[#F2465F] border-r-[#F2465F] animate-spin" />
<DotsLoader className="text-[#F2465F] scale-150" />
</main>
</>
);

1
src/app/questions-list/page.tsx

@ -247,6 +247,7 @@ export default function QuestionsListPage() {
<Button
aria-label={t.questions.findMatches}
disabled={isStartMatchDisabled}
isLoading={startMatchMutation.isPending}
onClick={() => {
if (hasIncompleteOptionalSections) {
setIsOptionalInfoSheetOpen(true);

10
src/app/request-accepted/page.tsx

@ -10,6 +10,7 @@ import CallResultSheet from "@/components/ui/call-result-sheet";
import FemaleConsentSheet from "@/components/ui/female-consent-sheet";
import NavigationButton from "@/components/ui/navigation-button";
import SubscriptionRequiredSheet from "@/components/ui/subscription-required-sheet";
import { DotsLoader } from "@/components/ui/button";
import { PageBackground } from "@/components/utils/page-background";
import type { MarriageField, MarriagePhoneFieldValue } from "@/hooks/marriage/types";
import { useMarriageContactInfoQuery } from "@/hooks/marriage/use-contact-info";
@ -321,8 +322,8 @@ export default function RequestAcceptedPage() {
disabled={contactStatusMutation.isPending}
className="max-w-[212px] cursor-pointer appearance-none border-0 bg-transparent p-0 text-left"
>
<div className="bg-[#F5F5F5] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#36363C] hover:bg-[#EBEBEB] transition-colors">
{primaryActionText}
<div className="bg-[#F5F5F5] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#36363C] hover:bg-[#EBEBEB] transition-colors flex items-center justify-center min-h-[38px]">
{contactStatusMutation.isPending ? <DotsLoader /> : primaryActionText}
</div>
</button>
) : (
@ -338,10 +339,11 @@ export default function RequestAcceptedPage() {
onClick={() => {
void handleSecondaryAction();
}}
disabled={paymentMutation.isPending}
className="max-w-[212px] appearance-none border-0 bg-transparent p-0 text-left"
>
<div className="bg-linear-180 from-[#FE6F82] to-[#E03950] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#fff] shadow-md shadow-[#F2596E]/60">
{secondaryActionText}
<div className="bg-linear-180 from-[#FE6F82] to-[#E03950] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#fff] shadow-md shadow-[#F2596E]/60 flex items-center justify-center min-h-[38px]">
{paymentMutation.isPending ? <DotsLoader /> : secondaryActionText}
</div>
</button>
</div>

94
src/components/questions/question-date.tsx

@ -1,9 +1,10 @@
"use client";
import { useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import type { QuestionField } from "@/data/question-data";
import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title";
import { useI18n } from "@/translations/provider";
type QuestionDateProps = {
question: QuestionField;
@ -32,28 +33,48 @@ const DAYS = Array.from({ length: 31 }, (_, i) => {
return { value: val, label: `${num}` };
});
const MIN_AGE = 18;
const currentYear = new Date().getFullYear();
const YEARS = Array.from({ length: 90 }, (_, i) => (currentYear - i).toString());
const maxBirthYear = currentYear - MIN_AGE;
const YEARS = Array.from({ length: 80 }, (_, i) => (maxBirthYear - i).toString());
export function QuestionDate({
question,
questionIndex,
disabled,
}: QuestionDateProps) {
const { locale } = useI18n();
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const value = getAnswerValue(question, questionIndex);
const dateValue = typeof value === "string" ? value : "";
const [selectedYear, setSelectedYear] = useState(() => {
const parts = dateValue ? dateValue.split("-") : [];
const year = parts[0] || "";
const month = parts[1] || "";
const day = parts[2] || "";
return parts[0] || "";
});
const [selectedMonth, setSelectedMonth] = useState(() => {
const parts = dateValue ? dateValue.split("-") : [];
return parts[1] || "";
});
const [selectedDay, setSelectedDay] = useState(() => {
const parts = dateValue ? dateValue.split("-") : [];
return parts[2] || "";
});
useEffect(() => {
const parts = dateValue ? dateValue.split("-") : [];
if (parts.length === 3) {
setSelectedYear(parts[0] || "");
setSelectedMonth(parts[1] || "");
setSelectedDay(parts[2] || "");
}
}, [dateValue]);
const calculatedAge = useMemo(() => {
if (!year || !month || !day) return null;
const y = Number.parseInt(year, 10);
const m = Number.parseInt(month, 10);
const d = Number.parseInt(day, 10);
if (!selectedYear || !selectedMonth || !selectedDay) return null;
const y = Number.parseInt(selectedYear, 10);
const m = Number.parseInt(selectedMonth, 10);
const d = Number.parseInt(selectedDay, 10);
if (!y || !m || !d || Number.isNaN(y) || Number.isNaN(m) || Number.isNaN(d)) {
return null;
@ -70,19 +91,34 @@ export function QuestionDate({
}
return age >= 0 ? age : null;
}, [year, month, day]);
}, [selectedYear, selectedMonth, selectedDay]);
const isUnder18 = calculatedAge !== null && calculatedAge < MIN_AGE;
const updateDate = (y: string, m: string, d: string) => {
setSelectedYear(y);
setSelectedMonth(m);
setSelectedDay(d);
const handleSelectChange = (newYear: string, newMonth: string, newDay: string) => {
if (!newYear && !newMonth && !newDay) {
if (y && m && d) {
const formattedMonth = m.padStart(2, "0");
const formattedDay = d.padStart(2, "0");
setAnswerValue(question, questionIndex, `${y}-${formattedMonth}-${formattedDay}`);
} else {
setAnswerValue(question, questionIndex, "");
return;
}
};
const handleDayChange = (newDay: string) => {
updateDate(selectedYear, selectedMonth, newDay);
};
const y = newYear || year || "2000";
const m = (newMonth || month || "01").padStart(2, "0");
const d = (newDay || day || "01").padStart(2, "0");
const handleMonthChange = (newMonth: string) => {
updateDate(selectedYear, newMonth, selectedDay);
};
setAnswerValue(question, questionIndex, `${y}-${m}-${d}`);
const handleYearChange = (newYear: string) => {
updateDate(newYear, selectedMonth, selectedDay);
};
return (
@ -98,8 +134,8 @@ export function QuestionDate({
<div className="grid grid-cols-3 gap-2">
{/* Day / روز */}
<select
value={day}
onChange={(e) => handleSelectChange(year, month, e.target.value)}
value={selectedDay}
onChange={(e) => handleDayChange(e.target.value)}
disabled={disabled}
aria-label="Day"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
@ -114,8 +150,8 @@ export function QuestionDate({
{/* Month */}
<select
value={month}
onChange={(e) => handleSelectChange(year, e.target.value, day)}
value={selectedMonth}
onChange={(e) => handleMonthChange(e.target.value)}
disabled={disabled}
aria-label="Month"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
@ -130,8 +166,8 @@ export function QuestionDate({
{/* Year */}
<select
value={year}
onChange={(e) => handleSelectChange(e.target.value, month, day)}
value={selectedYear}
onChange={(e) => handleYearChange(e.target.value)}
disabled={disabled}
aria-label="Year"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
@ -155,8 +191,18 @@ export function QuestionDate({
disabled
readOnly
value={calculatedAge !== null ? calculatedAge : ""}
className="h-[54px] w-full rounded-[15px] border border-[#E7D8D5] bg-[#F5F2F1] px-4 group-12 font-medium text-[#7C7472] outline-none cursor-not-allowed disabled:bg-[#F5F2F1] disabled:text-[#7C7472]"
className={[
"h-[54px] w-full rounded-[15px] border px-4 group-12 font-medium outline-none cursor-not-allowed disabled:bg-[#F5F2F1] disabled:text-[#7C7472]",
isUnder18 ? "border-[#F2465F] text-[#F2465F]" : "border-[#E7D8D5] text-[#7C7472]",
].join(" ")}
/>
{isUnder18 ? (
<span className="block text-[11px] font-semibold text-[#F2465F]">
{locale === "fa"
? "حداقل سن برای ثبت‌نام ۱۸ سال می‌باشد."
: "Minimum age required for registration is 18 years."}
</span>
) : null}
</div>
</div>
);

1
src/components/questions/question-section-flow.tsx

@ -83,6 +83,7 @@ function SectionFlowContent({
: "!bg-linear-to-r !from-[#F2465F] !to-[#E03950] !text-white !opacity-100 shadow-[0_12px_28px_rgba(242,70,95,0.38)] cursor-pointer hover:brightness-105 active:scale-[0.99]",
].join(" ")}
disabled={isDisabled}
isLoading={isSaving || isLeaving}
onClick={() => void handleContinue()}
>
{continueLabel}

2
src/components/questions/question-snap-list.tsx

@ -247,7 +247,7 @@ export function QuestionSnapList({
containerStyles = "top-4 left-0 z-0 opacity-0 pointer-events-none scale-[0.98]";
wrapperStyles = "w-full max-h-[110px] overflow-hidden pointer-events-none";
} else if (isNext) {
containerStyles = "bottom-4 left-0 z-0 opacity-20 pointer-events-none scale-[0.98]";
containerStyles = "bottom-4 left-0 z-0 opacity-0 pointer-events-none scale-[0.98]";
wrapperStyles = "w-full max-h-[110px] overflow-hidden pointer-events-none";
} else {
containerStyles = "top-1/2 left-0 -translate-y-1/2 z-0 opacity-0 pointer-events-none scale-[0.95]";

7
src/components/questions/test-loading-screen.tsx

@ -1,5 +1,6 @@
"use client";
import { DotsLoader } from "@/components/ui/button";
import { PageBackground } from "@/components/utils/page-background";
export function AnalyzingIllustration({ className = "w-44 h-44" }: { className?: string }) {
@ -139,9 +140,9 @@ export default function TestLoadingScreen({
</p>
</div>
{/* Bottom Circular Loading Spinner */}
<div className="shrink-0 mb-4 flex justify-center items-center">
<div className="size-11 rounded-full border-[3.5px] border-[#DCDCE0] border-t-[#F2465F] border-r-[#F2465F] animate-spin transition-all" />
{/* Bottom 3-Dots Loading Animation */}
<div className="shrink-0 mb-4 flex justify-center items-center py-2">
<DotsLoader className="text-[#F2465F] scale-150" />
</div>
</main>
</>

57
src/components/sliders/slider-page.tsx

@ -45,7 +45,6 @@ export default function SliderPage() {
const [selectedGender, setSelectedGender] = useState<GenderAnswer>("woman");
const [selectedRegistration, setSelectedRegistration] =
useState<RegistrationAnswer>("other");
const [hasLoadedSavedAnswers, setHasLoadedSavedAnswers] = useState(false);
const [touchStartX, setTouchStartX] = useState<number | null>(null);
const updateProfileBasicMutation = useUpdateMarriageProfileBasicMutation();
const hasFinalNotice = selectedGender === "woman";
@ -59,46 +58,12 @@ export default function SliderPage() {
const navDotCount = displaySlideCount;
useEffect(() => {
const savedAnswers = window.localStorage.getItem(
SLIDER_ANSWERS_STORAGE_KEY,
);
if (!savedAnswers) {
setHasLoadedSavedAnswers(true);
return;
}
try {
const parsed = JSON.parse(savedAnswers) as Partial<SavedSliderAnswers>;
if (isGenderAnswer(parsed.gender)) {
setSelectedGender(parsed.gender);
}
if (isRegistrationAnswer(parsed.registration)) {
setSelectedRegistration(parsed.registration);
}
} catch {
// Clear any previous slider answers on mount so abandoned onboarding starts clean
if (typeof window !== "undefined") {
window.localStorage.removeItem(SLIDER_ANSWERS_STORAGE_KEY);
}
setHasLoadedSavedAnswers(true);
}, []);
useEffect(() => {
if (!hasLoadedSavedAnswers) {
return;
}
window.localStorage.setItem(
SLIDER_ANSWERS_STORAGE_KEY,
JSON.stringify({
gender: selectedGender,
registration: selectedRegistration,
}),
);
}, [hasLoadedSavedAnswers, selectedGender, selectedRegistration]);
useEffect(() => {
if (!hasFinalNotice && activeSlide >= FINAL_SLIDE_COUNT - 1) {
setActiveSlide(BASE_SLIDE_COUNT - 1);
@ -126,7 +91,11 @@ export default function SliderPage() {
} catch (error) {
console.warn("Failed to update profile basic details:", error);
} finally {
router.push(localizePath("/questions-list", locale));
if (typeof window !== "undefined") {
window.localStorage.removeItem(SLIDER_ANSWERS_STORAGE_KEY);
}
// Action Item 3: Immediately direct user into Card 1 (personal_info) after onboarding
router.push(localizePath("/questions-list/personal_info", locale));
}
};
@ -214,8 +183,8 @@ export default function SliderPage() {
<div className="pointer-events-none fixed bottom-0 left-1/2 z-20 w-full sm:max-w-[375px] -translate-x-1/2 bg-[#F5F5F5]">
<div
style={{ paddingBottom: `calc(1.5rem + var(--safe-bottom))` }}
className="mx-auto w-full sm:max-w-[375px] px-[17px] pt-2.5"
style={{ paddingBottom: `calc(1rem + var(--safe-bottom))` }}
className="mx-auto w-full sm:max-w-[375px] px-[17px] pt-4"
>
<div className="pointer-events-auto">
{activeSlide === 0 ? (
@ -252,7 +221,7 @@ type SliderStepActionsProps = {
function SliderStepActions({ onBack, onNext }: SliderStepActionsProps) {
return (
<div className="flex gap-3 [&>button]:flex-1">
<div className="flex items-center gap-3 [&>button]:flex-1">
<Button variant="outlined" arrowDirection="left" onClick={onBack}>
Back
</Button>
@ -275,7 +244,7 @@ function SliderFinalActions({
onFinish,
}: SliderFinalActionsProps) {
return (
<div className="flex gap-3 [&>button]:flex-1">
<div className="flex items-center gap-3 [&>button]:flex-1">
<Button
variant="outlined"
arrowDirection="left"
@ -284,8 +253,8 @@ function SliderFinalActions({
>
Back
</Button>
<Button disabled={isFinishing} onClick={onFinish}>
{isFinishing ? "Saving..." : "Finish"}
<Button disabled={isFinishing} isLoading={isFinishing} onClick={onFinish}>
Finish
</Button>
</div>
);

4
src/components/sliders/slider-slide-one.tsx

@ -1,7 +1,7 @@
import type { SliderSlideProps } from "@/components/sliders/slider-slide";
import Button from "../ui/button";
const ACTION_AREA_HEIGHT = 78;
const ACTION_AREA_HEIGHT = 80;
const CONTENT_MASK = `linear-gradient(to bottom, black 0, black calc(100% - ${ACTION_AREA_HEIGHT}px), transparent calc(100% - ${ACTION_AREA_HEIGHT}px), transparent 100%)`;
export function SliderSlideOne({ index }: SliderSlideProps) {
@ -116,7 +116,7 @@ export function SliderSlideOneActions({
onAccept,
}: SliderSlideOneActionsProps) {
return (
<div className="flex gap-3 [&>a]:flex-1">
<div className="flex items-center gap-3 [&>a]:flex-1 [&>button]:flex-1">
<Button variant="outlined" arrowDirection="left" href="/intro">
Back
</Button>

2
src/components/sliders/slider-slide-two.tsx

@ -7,6 +7,7 @@ export function SliderSlideTwo({ index }: SliderSlideProps) {
aria-label={`Slide ${index + 1}`}
className="flex h-full min-h-0 w-full shrink-0 flex-col"
>
<div className="min-h-0 flex-1 overflow-y-auto pb-[86px]">
<div className="text-center">
<p className="text-[#747474] group-10 font-semibold">Submit Process</p>
<p className="text-[#111111] group-16 font-bold">
@ -49,6 +50,7 @@ export function SliderSlideTwo({ index }: SliderSlideProps) {
confidential path for "permanent marriage" among Muslims
</p>
</div>
</div>
</section>
);
}

18
src/components/ui/button.tsx

@ -25,8 +25,19 @@ export type ButtonProps = Omit<
arrowDirection?: ArrowDirection;
countdownSeconds?: number;
href?: string;
isLoading?: boolean;
};
export function DotsLoader({ className = "" }: { className?: string }) {
return (
<span className={`inline-flex items-center justify-center gap-1.5 py-0.5 ${className}`}>
<span className="size-2 rounded-full bg-current animate-dots-slide-1" />
<span className="size-2 rounded-full bg-current animate-dots-slide-2" />
<span className="size-2 rounded-full bg-current animate-dots-slide-3" />
</span>
);
}
const FILLED_STROKE = "#FFFFFF";
const EMPTY_STROKE = "rgba(255, 255, 255, 0.5)";
const RADIUS = 18;
@ -39,6 +50,7 @@ export function Button({
arrowDirection,
countdownSeconds = 0,
disabled,
isLoading = false,
href,
className,
type = "button",
@ -99,7 +111,7 @@ export function Button({
}, [initialCountdown, isCountdown]);
const countdownLocked = isCountdown && remainingSeconds > 0;
const isDisabled = disabled || countdownLocked;
const isDisabled = disabled || countdownLocked || isLoading;
const progress = isCountdown ? animatedProgress : 1;
const widthClass = variant === "secondary" ? "w-1/2" : "w-full";
@ -149,6 +161,9 @@ export function Button({
aria-describedby={description && !isOutlined ? countdownId : undefined}
className={baseClassName}
>
{isLoading ? (
<DotsLoader />
) : (
<span className="flex w-full items-center justify-center gap-2">
{renderArrow("left")}
<span className="flex min-w-0 flex-col items-center justify-center">
@ -172,6 +187,7 @@ export function Button({
renderArrow("right")
)}
</span>
)}
</button>
);

4
src/data/questions/en.json

@ -10,12 +10,12 @@
"description": "Collects personal details to start the marriage application flow.",
"questions": [
{
"title": "Full Name",
"title": "First Name",
"type": "text",
"required": true,
"tooltip": "Use your passport or national ID spelling.",
"extras": {
"placeHolder": "e.g. Sara Ahmadi",
"placeHolder": "e.g. Sara",
"range": [
0,
0

2
src/lib/get-submit-path.ts

@ -52,7 +52,7 @@ export function getSubmitPath(profile: MarriageProfileResponse | undefined) {
return "/terms";
}
if (profile.status === "pending_info" && !isMatchSubmitted) {
if (profile.status === "pending_info") {
return "/questions-list";
}

Loading…
Cancel
Save