Browse Source
feat: implement marriage case contact and outcome management flows with related UI and translations
front-test-2
feat: implement marriage case contact and outcome management flows with related UI and translations
front-test-2
14 changed files with 507 additions and 156 deletions
-
104src/app/candidate-contact/page.tsx
-
122src/app/questions-list/[slug]/question-detail-client.tsx
-
8src/app/request-accepted/page.tsx
-
217src/components/Componentes/outcome-selection-sheet.tsx
-
23src/components/Componentes/question-answer-storage.tsx
-
6src/components/Componentes/question-renderer.tsx
-
10src/components/Componentes/question-text.tsx
-
20src/data/question-data.ts
-
2src/hooks/marriage/types.ts
-
44src/hooks/marriage/use-contact-status.ts
-
31src/hooks/marriage/use-section-data.ts
-
4src/lib/get-submit-path.ts
-
9src/translations/locales/en.json
-
9src/translations/locales/fa.json
@ -0,0 +1,217 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import type { HTMLAttributes } from "react"; |
||||
|
import { useEffect, useId, useState } from "react"; |
||||
|
import { useI18n } from "@/translations/provider"; |
||||
|
|
||||
|
const EXIT_ANIMATION_MS = 220; |
||||
|
|
||||
|
export type OutcomeSelectionSheetProps = Omit< |
||||
|
HTMLAttributes<HTMLDivElement>, |
||||
|
"title" | "onSubmit" |
||||
|
> & { |
||||
|
closeOnOutside?: boolean; |
||||
|
onClose?: () => void; |
||||
|
onSubmit?: (status: "success" | "failure") => void; |
||||
|
}; |
||||
|
|
||||
|
export function OutcomeSelectionSheet({ |
||||
|
closeOnOutside = true, |
||||
|
onClose, |
||||
|
onSubmit, |
||||
|
className, |
||||
|
...props |
||||
|
}: OutcomeSelectionSheetProps) { |
||||
|
const { dictionary: t } = useI18n(); |
||||
|
const groupId = useId(); |
||||
|
const [isVisible, setIsVisible] = useState(true); |
||||
|
const [isEntering, setIsEntering] = useState(true); |
||||
|
const [isClosing, setIsClosing] = useState(false); |
||||
|
const [selectedStatus, setSelectedStatus] = useState<"success" | "failure">( |
||||
|
"success", |
||||
|
); |
||||
|
|
||||
|
const closeSheet = () => { |
||||
|
if (isClosing) { |
||||
|
return; |
||||
|
} |
||||
|
setIsClosing(true); |
||||
|
}; |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const frameId = window.requestAnimationFrame(() => { |
||||
|
setIsEntering(false); |
||||
|
}); |
||||
|
|
||||
|
return () => { |
||||
|
window.cancelAnimationFrame(frameId); |
||||
|
}; |
||||
|
}, []); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!isVisible) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const previousBodyOverflow = document.body.style.overflow; |
||||
|
const previousHtmlOverflow = document.documentElement.style.overflow; |
||||
|
|
||||
|
document.body.style.overflow = "hidden"; |
||||
|
document.documentElement.style.overflow = "hidden"; |
||||
|
|
||||
|
return () => { |
||||
|
document.body.style.overflow = previousBodyOverflow; |
||||
|
document.documentElement.style.overflow = previousHtmlOverflow; |
||||
|
}; |
||||
|
}, [isVisible]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!isClosing) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const timeoutId = window.setTimeout(() => { |
||||
|
setIsVisible(false); |
||||
|
onClose?.(); |
||||
|
}, EXIT_ANIMATION_MS); |
||||
|
|
||||
|
return () => { |
||||
|
window.clearTimeout(timeoutId); |
||||
|
}; |
||||
|
}, [isClosing, onClose]); |
||||
|
|
||||
|
if (!isVisible) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const options = [ |
||||
|
{ value: "success" as const, label: t.candidateContact.marriageSuccess }, |
||||
|
{ value: "failure" as const, label: t.candidateContact.marriageFailure }, |
||||
|
]; |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
className={[ |
||||
|
"fixed inset-0 z-50 flex items-end justify-center transition-all duration-[220ms]", |
||||
|
isClosing || isEntering |
||||
|
? "bg-[#171717]/0 opacity-0" |
||||
|
: "bg-[#171717]/55 opacity-100", |
||||
|
] |
||||
|
.filter(Boolean) |
||||
|
.join(" ")} |
||||
|
role="dialog" |
||||
|
aria-modal="true" |
||||
|
aria-label={t.candidateContact.outcomeTitle} |
||||
|
tabIndex={-1} |
||||
|
onClick={(event) => { |
||||
|
if (closeOnOutside && event.target === event.currentTarget) { |
||||
|
closeSheet(); |
||||
|
} |
||||
|
}} |
||||
|
onKeyDown={(event) => { |
||||
|
if ( |
||||
|
closeOnOutside && |
||||
|
event.target === event.currentTarget && |
||||
|
(event.key === "Escape" || event.key === "Enter" || event.key === " ") |
||||
|
) { |
||||
|
event.preventDefault(); |
||||
|
closeSheet(); |
||||
|
} |
||||
|
}} |
||||
|
> |
||||
|
<section |
||||
|
{...props} |
||||
|
className={[ |
||||
|
"w-full sm:max-w-[375px] rounded-t-[34px] bg-[#F9F8F8] p-3.5 text-center shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out will-change-transform", |
||||
|
isClosing || isEntering ? "translate-y-full" : "translate-y-0", |
||||
|
className, |
||||
|
] |
||||
|
.filter(Boolean) |
||||
|
.join(" ")} |
||||
|
> |
||||
|
<div className="mx-auto flex flex-col items-center"> |
||||
|
<h2 className="text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]"> |
||||
|
{t.candidateContact.outcomeTitle} |
||||
|
</h2> |
||||
|
|
||||
|
<div |
||||
|
className="mt-5 w-full" |
||||
|
role="radiogroup" |
||||
|
aria-labelledby={groupId} |
||||
|
> |
||||
|
<span id={groupId} className="sr-only"> |
||||
|
{t.candidateContact.outcomeTitle} |
||||
|
</span> |
||||
|
|
||||
|
<div className="flex flex-col gap-[14px]"> |
||||
|
{options.map((option) => { |
||||
|
const checked = selectedStatus === option.value; |
||||
|
|
||||
|
return ( |
||||
|
<label |
||||
|
key={option.value} |
||||
|
className="flex cursor-pointer items-center gap-3 rounded-[14px] bg-[#ECECEC] px-[14px] py-[18px] text-left" |
||||
|
> |
||||
|
<input |
||||
|
checked={checked} |
||||
|
className="sr-only" |
||||
|
name="outcome-status" |
||||
|
type="radio" |
||||
|
value={option.value} |
||||
|
onChange={() => { |
||||
|
setSelectedStatus(option.value); |
||||
|
}} |
||||
|
/> |
||||
|
<span |
||||
|
aria-hidden="true" |
||||
|
className={[ |
||||
|
"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border", |
||||
|
checked ? "border-[#F0445B]" : "border-[#9E9E9E]", |
||||
|
] |
||||
|
.filter(Boolean) |
||||
|
.join(" ")} |
||||
|
> |
||||
|
{checked ? ( |
||||
|
<span className="h-3 w-3 rounded-full bg-[#F0445B]" /> |
||||
|
) : null} |
||||
|
</span> |
||||
|
<span className="text-[16px] leading-none font-semibold text-[#262626]"> |
||||
|
{option.label} |
||||
|
</span> |
||||
|
</label> |
||||
|
); |
||||
|
})} |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div className="mt-8 grid w-full grid-cols-[1fr_2fr] gap-3"> |
||||
|
<button |
||||
|
type="button" |
||||
|
className="appearance-none border-0 bg-transparent p-0 text-left" |
||||
|
onClick={closeSheet} |
||||
|
> |
||||
|
<div className="inline-flex w-full items-center justify-center rounded-[14px] border border-[#9A9A9A] bg-[#F7F7F7] px-4 py-[18px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90"> |
||||
|
{t.common.cancel} |
||||
|
</div> |
||||
|
</button> |
||||
|
|
||||
|
<button |
||||
|
type="button" |
||||
|
className="appearance-none border-0 bg-transparent p-0 text-left" |
||||
|
onClick={() => { |
||||
|
onSubmit?.(selectedStatus); |
||||
|
closeSheet(); |
||||
|
}} |
||||
|
> |
||||
|
<div className="inline-flex w-full items-center justify-center rounded-[14px] bg-[#F0445B] px-4 py-[18px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90"> |
||||
|
{t.common.submit} |
||||
|
</div> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default OutcomeSelectionSheet; |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue