@ -1,11 +1,26 @@
"use client" ;
"use client" ;
import { useCallback , useEffect , useId , useMemo , useRef , useState } from "react" ;
import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler" ;
import {
useCallback ,
useEffect ,
useId ,
useMemo ,
useRef ,
useState ,
} from "react" ;
import { localeDirections } from "@/translations/config" ;
import { useI18n } from "@/translations/provider" ;
import { useI18n } from "@/translations/provider" ;
import { useSheetScrollLock } from "./use-sheet-scroll-lock" ;
const EXIT_ANIMATION_MS = 200 ;
const EXIT_ANIMATION_MS = 200 ;
// Parity with Flutter DraggableScrollableSheet:
// initialChildSize: 0.75, minChildSize: 0.6, maxChildSize: 1.0, shouldCloseOnMinExtent: true
const SHEET_INITIAL_SIZE = 0.75 ;
const SHEET_MIN_SIZE = 0.6 ;
const SHEET_MAX_SIZE = 1 ;
const GESTURE_ENGAGE_PX = 10 ;
export type FemaleOutcomeSheetProps = {
export type FemaleOutcomeSheetProps = {
closeOnOutside? : boolean ;
closeOnOutside? : boolean ;
onClose ? : ( ) = > void ;
onClose ? : ( ) = > void ;
@ -17,13 +32,25 @@ export function FemaleOutcomeSheet({
onClose ,
onClose ,
onSubmit ,
onSubmit ,
} : FemaleOutcomeSheetProps ) {
} : FemaleOutcomeSheetProps ) {
const { dictionary : t } = useI18n ( ) ;
const { dictionary : t , locale } = useI18n ( ) ;
const direction = localeDirections [ locale ] ? ? "ltr" ;
const groupId = useId ( ) ;
const groupId = useId ( ) ;
const [ isVisible , setIsVisible ] = useState ( true ) ;
const [ isVisible , setIsVisible ] = useState ( true ) ;
const [ isClosing , setIsClosing ] = useState ( false ) ;
const [ isClosing , setIsClosing ] = useState ( false ) ;
const isClosingRef = useRef ( false ) ;
const isMountedRef = useRef ( true ) ;
const isMountedRef = useRef ( true ) ;
// Outcome selection state: "ongoing" (Option A) or "canceled" (Option B)
const [ outcome , setOutcome ] = useState < "ongoing" | "canceled" > ( "ongoing" ) ;
const isExpanded = outcome === "canceled" ;
const sheetRef = useRef < HTMLElement > ( null ) ;
const headerRef = useRef < HTMLDivElement > ( null ) ;
const contentRef = useRef < HTMLDivElement > ( null ) ;
const sheetSizeRef = useRef ( SHEET_INITIAL_SIZE ) ;
const restingStateRef = useRef < number > ( SHEET_INITIAL_SIZE ) ;
useEffect ( ( ) = > {
useEffect ( ( ) = > {
isMountedRef . current = true ;
isMountedRef . current = true ;
return ( ) = > {
return ( ) = > {
@ -31,9 +58,6 @@ export function FemaleOutcomeSheet({
} ;
} ;
} , [ ] ) ;
} , [ ] ) ;
// Outcome selection state: "ongoing" (Option A) or "canceled" (Option B)
const [ outcome , setOutcome ] = useState < "ongoing" | "canceled" > ( "ongoing" ) ;
// Rejection/Cancellation reason states
// Rejection/Cancellation reason states
const reasons = useMemo (
const reasons = useMemo (
( ) = > [
( ) = > [
@ -64,36 +88,302 @@ export function FemaleOutcomeSheet({
} , [ selectedReasons , reasons ] ) ;
} , [ selectedReasons , reasons ] ) ;
const closeSheet = useCallback ( ( ) = > {
const closeSheet = useCallback ( ( ) = > {
if ( isClosing ) {
if ( isClosingRef . current ) {
return ;
return ;
}
}
isClosingRef . current = true ;
setIsClosing ( true ) ;
setIsClosing ( true ) ;
window . setTimeout ( ( ) = > {
window . setTimeout ( ( ) = > {
if ( isMountedRef . current ) {
if ( isMountedRef . current ) {
setIsVisible ( false ) ;
setIsVisible ( false ) ;
}
}
isClosingRef . current = false ;
onClose ? . ( ) ;
onClose ? . ( ) ;
} , EXIT_ANIMATION_MS ) ;
} , EXIT_ANIMATION_MS ) ;
} , [ isClosing , onClose ] ) ;
} , [ onClose ] ) ;
useHardwareBackHandler ( closeSheet , isVisible && ! isClosing ) ;
useSheetScrollLock ( isVisible && ! isClosing , { onBack : closeSheet } ) ;
// Handle escape key
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( ! isVisible ) {
return ;
if ( ! isVisible ) return ;
const handleKeyDown = ( e : KeyboardEvent ) = > {
if ( e . key === "Escape" ) {
closeSheet ( ) ;
}
} ;
window . addEventListener ( "keydown" , handleKeyDown ) ;
return ( ) = > window . removeEventListener ( "keydown" , handleKeyDown ) ;
} , [ isVisible , closeSheet ] ) ;
// Adaptive height transition when switching between ongoing and canceled states
useEffect ( ( ) = > {
if ( ! sheetRef . current ) return ;
if ( isExpanded ) {
sheetSizeRef . current = SHEET_INITIAL_SIZE ;
restingStateRef . current = SHEET_INITIAL_SIZE ;
sheetRef . current . style . transition =
"height 260ms cubic-bezier(0.16, 1, 0.3, 1)" ;
sheetRef . current . style . setProperty (
"--sheet-size" ,
SHEET_INITIAL_SIZE . toFixed ( 4 ) ,
) ;
const timer = window . setTimeout ( ( ) = > {
if ( sheetRef . current ) {
sheetRef . current . style . transition = "" ;
}
} , 270 ) ;
return ( ) = > clearTimeout ( timer ) ;
} else {
sheetRef . current . style . transition =
"height 260ms cubic-bezier(0.16, 1, 0.3, 1)" ;
sheetRef . current . style . removeProperty ( "--sheet-size" ) ;
const timer = window . setTimeout ( ( ) = > {
if ( sheetRef . current ) {
sheetRef . current . style . transition = "" ;
}
} , 270 ) ;
return ( ) = > clearTimeout ( timer ) ;
}
}
} , [ isExpanded ] ) ;
// Drag-to-resize gesture handlers matching Flutter DraggableScrollableSheet
// Active only when isExpanded is true (option 2 chosen)
useEffect ( ( ) = > {
if ( ! isVisible || isClosing || ! isExpanded ) return ;
const content = contentRef . current ;
const sheet = sheetRef . current ;
const header = headerRef . current ;
if ( ! sheet ) return ;
let active = false ;
let engaged = false ;
let closing = false ;
let isHeaderDrag = false ;
let startX = 0 ;
let startY = 0 ;
let lastY = 0 ;
let snapTimeout : ReturnType < typeof setTimeout > | null = null ;
let wheelSnapTimeout : ReturnType < typeof setTimeout > | null = null ;
const contentOverflows = ( ) = >
content ? content . scrollHeight > content . clientHeight + 1 : false ;
const snapTo = ( targetSize : number ) = > {
sheetSizeRef . current = targetSize ;
restingStateRef . current = targetSize ;
sheet . style . transition = "height 260ms cubic-bezier(0.16, 1, 0.3, 1)" ;
sheet . style . setProperty ( "--sheet-size" , targetSize . toFixed ( 4 ) ) ;
if ( snapTimeout ) clearTimeout ( snapTimeout ) ;
snapTimeout = setTimeout ( ( ) = > {
if ( sheet ) {
sheet . style . transition = "" ;
}
} , 270 ) ;
} ;
const resize = ( deltaPx : number ) = > {
const viewportHeight = window . innerHeight || 1 ;
const nextSize = Math . min (
SHEET_MAX_SIZE ,
Math . max (
SHEET_MIN_SIZE ,
sheetSizeRef . current + deltaPx / viewportHeight ,
) ,
) ;
sheetSizeRef . current = nextSize ;
sheet . style . setProperty ( "--sheet-size" , nextSize . toFixed ( 4 ) ) ;
} ;
const handleTouchStart = ( event : TouchEvent , fromHeader : boolean ) = > {
if ( event . touches . length !== 1 ) {
active = false ;
return ;
}
if ( snapTimeout ) {
clearTimeout ( snapTimeout ) ;
snapTimeout = null ;
}
sheet . style . transition = "none" ;
active = true ;
engaged = fromHeader ;
isHeaderDrag = fromHeader ;
startX = event . touches [ 0 ] . clientX ;
startY = event . touches [ 0 ] . clientY ;
lastY = startY ;
} ;
const handleTouchMove = ( event : TouchEvent ) = > {
if ( ! active || closing || event . touches . length !== 1 ) return ;
const touch = event . touches [ 0 ] ;
const deltaY = lastY - touch . clientY ; // finger up = grow sheet
const pulled = startY - touch . clientY ; // cumulative distance
lastY = touch . clientY ;
if ( ! engaged ) {
if ( Math . abs ( pulled ) < GESTURE_ENGAGE_PX ) return ;
if ( Math . abs ( touch . clientX - startX ) >= Math . abs ( pulled ) ) return ;
const atTop = ! content || content . scrollTop <= 0 ;
const canGrow =
pulled > 0 &&
atTop &&
contentOverflows ( ) &&
sheetSizeRef . current < SHEET_MAX_SIZE - 0.001 ;
const canShrink = pulled < 0 && atTop ;
if ( ! canGrow && ! canShrink ) return ;
engaged = true ;
}
const atTop = ! content || content . scrollTop <= 0 ;
const size = sheetSizeRef . current ;
const consume = isHeaderDrag
? true
: deltaY > 0
? atTop && size < SHEET_MAX_SIZE - 0.001
: atTop ;
if ( ! consume ) return ; // at ceiling or scrolling inside content
if ( event . cancelable ) {
event . preventDefault ( ) ;
}
if ( deltaY > 0 ) {
resize ( deltaY ) ;
} else if ( size <= SHEET_MIN_SIZE + 0.001 ) {
closing = true ; // shouldCloseOnMinExtent: true
closeSheet ( ) ;
} else {
resize ( deltaY ) ;
}
} ;
const handleTouchEnd = ( ) = > {
if ( ! active || closing ) {
active = false ;
engaged = false ;
isHeaderDrag = false ;
return ;
}
active = false ;
engaged = false ;
isHeaderDrag = false ;
const currentSize = sheetSizeRef . current ;
const baseline = restingStateRef . current ;
const totalPulled = startY - lastY ; // > 0 finger pulled up, < 0 finger pulled down
// Flutter shouldCloseOnMinExtent: true when dragged near or below 60% floor
if ( currentSize <= SHEET_MIN_SIZE + 0.01 ) {
closing = true ;
closeSheet ( ) ;
return ;
}
// Snapping between only two states: 0.75 (default) and 1.0 (expanded)
if ( baseline === SHEET_MAX_SIZE ) {
// Dragging down from 1.0: if pulled down >= 30px or size <= 0.94, snap to 0.75
if ( totalPulled <= - 30 || currentSize <= 0.94 ) {
snapTo ( SHEET_INITIAL_SIZE ) ;
} else {
snapTo ( SHEET_MAX_SIZE ) ;
}
} else {
// Dragging from 0.75:
// If pulled down strongly, close
if ( totalPulled <= - 60 ) {
closing = true ;
closeSheet ( ) ;
} else if ( totalPulled >= 30 || currentSize >= 0.78 ) {
// Dragged up enough to expand to 1.0
snapTo ( SHEET_MAX_SIZE ) ;
} else {
snapTo ( SHEET_INITIAL_SIZE ) ;
}
}
} ;
const handleWheel = ( event : WheelEvent ) = > {
if ( closing ) return ;
const delta = event . deltaY ;
const atTop = ! content || content . scrollTop <= 0 ;
const size = sheetSizeRef . current ;
const consume =
delta > 0
? atTop && contentOverflows ( ) && size < SHEET_MAX_SIZE - 0.001
: atTop ;
if ( ! consume ) return ;
if ( event . cancelable ) {
event . preventDefault ( ) ;
}
if ( delta > 0 ) {
resize ( delta ) ;
} else if ( size <= SHEET_MIN_SIZE + 0.001 ) {
closing = true ;
closeSheet ( ) ;
} else {
resize ( delta ) ;
}
const previousBodyOverflow = document . body . style . overflow ;
const previousHtmlOverflow = document . documentElement . style . overflow ;
if ( wheelSnapTimeout ) clearTimeout ( wheelSnapTimeout ) ;
wheelSnapTimeout = setTimeout ( ( ) = > {
if ( closing ) return ;
const current = sheetSizeRef . current ;
if ( current <= SHEET_MIN_SIZE + 0.01 ) {
closing = true ;
closeSheet ( ) ;
} else if ( current >= 0.82 ) {
snapTo ( SHEET_MAX_SIZE ) ;
} else {
snapTo ( SHEET_INITIAL_SIZE ) ;
}
} , 160 ) ;
} ;
document . body . style . overflow = "hidden" ;
document . documentElement . style . overflow = "hidden" ;
const onHeaderTouchStart = ( e : TouchEvent ) = > handleTouchStart ( e , true ) ;
const onContentTouchStart = ( e : TouchEvent ) = > handleTouchStart ( e , false ) ;
if ( header ) {
header . addEventListener ( "touchstart" , onHeaderTouchStart , {
passive : true ,
} ) ;
}
if ( content ) {
content . addEventListener ( "touchstart" , onContentTouchStart , {
passive : true ,
} ) ;
content . addEventListener ( "touchmove" , handleTouchMove , {
passive : false ,
} ) ;
content . addEventListener ( "touchend" , handleTouchEnd , { passive : true } ) ;
content . addEventListener ( "touchcancel" , handleTouchEnd , { passive : true } ) ;
content . addEventListener ( "wheel" , handleWheel , { passive : false } ) ;
}
sheet . addEventListener ( "touchmove" , handleTouchMove , { passive : false } ) ;
sheet . addEventListener ( "touchend" , handleTouchEnd , { passive : true } ) ;
sheet . addEventListener ( "touchcancel" , handleTouchEnd , { passive : true } ) ;
return ( ) = > {
return ( ) = > {
document . body . style . overflow = previousBodyOverflow ;
document . documentElement . style . overflow = previousHtmlOverflow ;
if ( snapTimeout ) clearTimeout ( snapTimeout ) ;
if ( wheelSnapTimeout ) clearTimeout ( wheelSnapTimeout ) ;
if ( header ) {
header . removeEventListener ( "touchstart" , onHeaderTouchStart ) ;
}
if ( content ) {
content . removeEventListener ( "touchstart" , onContentTouchStart ) ;
content . removeEventListener ( "touchmove" , handleTouchMove ) ;
content . removeEventListener ( "touchend" , handleTouchEnd ) ;
content . removeEventListener ( "touchcancel" , handleTouchEnd ) ;
content . removeEventListener ( "wheel" , handleWheel ) ;
}
sheet . removeEventListener ( "touchmove" , handleTouchMove ) ;
sheet . removeEventListener ( "touchend" , handleTouchEnd ) ;
sheet . removeEventListener ( "touchcancel" , handleTouchEnd ) ;
} ;
} ;
} , [ isVisible ] ) ;
} , [ isVisible , isClosing , isExpanded , closeSheet ] ) ;
if ( ! isVisible ) {
if ( ! isVisible ) {
return null ;
return null ;
@ -115,6 +405,7 @@ export function FemaleOutcomeSheet({
role = "dialog"
role = "dialog"
aria - modal = "true"
aria - modal = "true"
aria - label = { t [ "What was the outcome of your contact?" ] }
aria - label = { t [ "What was the outcome of your contact?" ] }
dir = { direction }
tabIndex = { - 1 }
tabIndex = { - 1 }
onClick = { ( event ) = > {
onClick = { ( event ) = > {
if ( closeOnOutside && event . target === event . currentTarget ) {
if ( closeOnOutside && event . target === event . currentTarget ) {
@ -133,20 +424,39 @@ export function FemaleOutcomeSheet({
} }
} }
>
>
< section
< section
ref = { sheetRef }
style = { {
height : isExpanded
? "calc(var(--sheet-size, 0.75) * (100svh - max(var(--safe-top, 0px), env(safe-area-inset-top, 0px))))"
: undefined ,
maxHeight :
"calc(100svh - max(var(--safe-top, 0px), env(safe-area-inset-top, 0px)))" ,
} }
className = { [
className = { [
"flex max-h-[85vh] w-full max-w-[834px] sm:max-w-[540px] flex-col overflow-hidden rounded-t-[22px] bg-white p-4 text-center shadow-[0_20px_60px_rgba(15,23,42,0.08)]" ,
"flex w-full max-w-[834px] sm:max-w-[540px] flex-col overflow-hidden rounded-t-[22px] bg-white p-4 pt-2.5 text-start shadow-[0_20px_60px_rgba(15,23,42,0.08)]" ,
isExpanded ? "flutter-draggable-sheet" : "h-auto max-h-[82svh]" ,
isClosing ? "flutter-sheet-surface-exit" : "flutter-sheet-surface" ,
isClosing ? "flutter-sheet-surface-exit" : "flutter-sheet-surface" ,
]
]
. filter ( Boolean )
. filter ( Boolean )
. join ( " " ) }
. join ( " " ) }
>
>
< div className = "mx-auto flex h-full min-h-0 w-full flex-col items-center" >
< div className = "flex min-h-0 w-full flex-1 flex-col items-center" >
< h2 className = "text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]" >
{ /* Drag handle */ }
< div
ref = { headerRef }
className = "flex flex-col select-none touch-none shrink-0"
>
< div className = "flex justify-center pt-0 pb-2.5 cursor-grab active:cursor-grabbing" >
< div className = "h-1.2 w-10 rounded-full bg-[#D0D5DD]" / >
< / div >
< / div >
< div className = "mx-auto flex h-full min-h-0 w-full flex-col" >
< div className = "flex min-h-0 w-full flex-1 flex-col" >
< h2 className = "w-full text-start text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]" >
{ t [ "What was the outcome of your contact?" ] }
{ t [ "What was the outcome of your contact?" ] }
< / h2 >
< / h2 >
< p className = "mt-3.5 w-full text-start text-[14px] leading-[1.45] text-[#2C2C2C] dir-auto font-medium" >
< p className = "mt-3.5 w-full text-start text-[14px] leading-[1.45] text-[#2C2C2C] font-medium" >
{
{
t [
t [
"We hope the acquaintance process is going well. Please let us know if you want to continue the acquaintance process or if the match has been canceled."
"We hope the acquaintance process is going well. Please let us know if you want to continue the acquaintance process or if the match has been canceled."
@ -162,11 +472,14 @@ export function FemaleOutcomeSheet({
{ t [ "What was the outcome of your contact?" ] }
{ t [ "What was the outcome of your contact?" ] }
< / legend >
< / legend >
< div className = "flex min-h-0 flex-1 flex-col gap-[14px] overflow-y-auto pr-1" >
< div
ref = { contentRef }
className = "flex min-h-0 flex-1 flex-col gap-[14px] overflow-y-auto pe-1"
>
{ /* Option A: Ongoing acquaintance */ }
{ /* Option A: Ongoing acquaintance */ }
< label
< label
className = { [
className = { [
"flex cursor-pointer items-center gap-3 rounded-[14px] px-[14px] py-[18px] text-lef t border transition-all" ,
"flex cursor-pointer items-center gap-3 rounded-[14px] px-[14px] py-[18px] text-star t border transition-all" ,
outcome === "ongoing"
outcome === "ongoing"
? "bg-[#FFECEF] border-[#F0445B]/30"
? "bg-[#FFECEF] border-[#F0445B]/30"
: "bg-[#ECECEC] border-transparent" ,
: "bg-[#ECECEC] border-transparent" ,
@ -195,7 +508,7 @@ export function FemaleOutcomeSheet({
< span className = "h-3 w-3 rounded-full bg-[#F0445B]" / >
< span className = "h-3 w-3 rounded-full bg-[#F0445B]" / >
) : null }
) : null }
< / span >
< / span >
< span className = "text-[15px] leading-snug font-bold text-[#262626] flex-1" >
< span className = "text-[15px] leading-snug font-bold text-[#262626] flex-1 text-start " >
{
{
t [
t [
"We are in the acquaintance/proposal process and nothing is finalized yet"
"We are in the acquaintance/proposal process and nothing is finalized yet"
@ -207,7 +520,7 @@ export function FemaleOutcomeSheet({
{ /* Option B: Canceled / Decline */ }
{ /* Option B: Canceled / Decline */ }
< label
< label
className = { [
className = { [
"flex cursor-pointer items-center gap-3 rounded-[14px] px-[14px] py-[18px] text-lef t border transition-all" ,
"flex cursor-pointer items-center gap-3 rounded-[14px] px-[14px] py-[18px] text-star t border transition-all" ,
outcome === "canceled"
outcome === "canceled"
? "bg-[#FFECEF] border-[#F0445B]/30"
? "bg-[#FFECEF] border-[#F0445B]/30"
: "bg-[#ECECEC] border-transparent" ,
: "bg-[#ECECEC] border-transparent" ,
@ -242,20 +555,18 @@ export function FemaleOutcomeSheet({
< span className = "h-3 w-3 rounded-full bg-[#F0445B]" / >
< span className = "h-3 w-3 rounded-full bg-[#F0445B]" / >
) : null }
) : null }
< / span >
< / span >
< span className = "text-[15px] leading-snug font-bold text-[#262626] flex-1" >
{
t [
"We did not reach the agreement needed to continue acquaintance and decided not to proceed"
] ||
< span className = "text-[15px] leading-snug font-bold text-[#262626] flex-1 text-start" >
{ t [
"We did not reach the agreement needed to continue acquaintance and decided not to proceed"
"We did not reach the agreement needed to continue acquaintance and decided not to proceed"
}
] ||
"We did not reach the agreement needed to continue acquaintance and decided not to proceed" }
< / span >
< / span >
< / label >
< / label >
{ /* Reasons List (if Canceled is chosen) */ }
{ /* Reasons List (if Canceled is chosen) */ }
{ outcome === "canceled" ? (
{ outcome === "canceled" ? (
< div className = "mt-3 flex flex-col gap-3 pl -2 text-start animate-fadeIn" >
< p className = "text-[13px] font-bold text-[#555] mb-1" >
< div className = "mt-3 flex flex-col gap-3 ps -2 text-start animate-fadeIn" >
< p className = "text-[13px] font-bold text-[#555] mb-1 text-start " >
{ t [ "Please select the reason for cancellation:" ] }
{ t [ "Please select the reason for cancellation:" ] }
< / p >
< / p >
< div className = "flex flex-col gap-[10px]" >
< div className = "flex flex-col gap-[10px]" >
@ -317,8 +628,8 @@ export function FemaleOutcomeSheet({
) : null }
) : null }
< / span >
< / span >
< span className = "min-w-0 flex-1" >
< span className = "text-[14px] leading-none font-bold" >
< span className = "min-w-0 flex-1 text-start " >
< span className = "text-[14px] leading-none font-bold text-start " >
{ option }
{ option }
< / span >
< / span >
< / span >
< / span >
@ -327,7 +638,7 @@ export function FemaleOutcomeSheet({
{ isReasonChecked && isOtherReason ? (
{ isReasonChecked && isOtherReason ? (
< textarea
< textarea
ref = { textareaRef }
ref = { textareaRef }
className = "mt-1 h-[90px] w-full resize-none rounded-[12px] border border-[#BFBFBF] bg-transparent px-3 py-2 text-[14px] text-[#171717] outline-none placeholder:text-[#AAAAAA]"
className = "mt-1 h-[90px] w-full resize-none rounded-[12px] border border-[#BFBFBF] bg-transparent px-3 py-2 text-[14px] text-[#171717] outline-none placeholder:text-[#AAAAAA] text-start "
placeholder = {
placeholder = {
t [
t [
"Feel free to briefly explain your decision..."
"Feel free to briefly explain your decision..."
@ -377,7 +688,9 @@ export function FemaleOutcomeSheet({
onClick = { ( ) = > {
onClick = { ( ) = > {
const activeReasons = selectedReasons . map ( ( r ) = > {
const activeReasons = selectedReasons . map ( ( r ) = > {
if ( r === reasons [ reasons . length - 1 ] ) {
if ( r === reasons [ reasons . length - 1 ] ) {
return reasonText . trim ( ) ? ` ${ r } : ${ reasonText . trim ( ) } ` : r ;
return reasonText . trim ( )
? ` ${ r } : ${ reasonText . trim ( ) } `
: r ;
}
}
return r ;
return r ;
} ) ;
} ) ;