Browse Source
feat: scaffold marriage feature with new pages, components, hooks, and localization support
front-test-2
feat: scaffold marriage feature with new pages, components, hooks, and localization support
front-test-2
47 changed files with 2034 additions and 708 deletions
-
1src/app/[lang]/marriage-advisors/page.tsx
-
36src/app/candidate-contact/page.tsx
-
2src/app/finding-match/page.tsx
-
35src/app/globals.css
-
14src/app/intro/page.tsx
-
234src/app/marriage-advisors/page.tsx
-
36src/app/new-match/page.tsx
-
72src/app/providers.tsx
-
6src/app/questions-list/[slug]/page.tsx
-
236src/app/questions-list/[slug]/question-detail-client.tsx
-
24src/app/questions-list/page.tsx
-
7src/app/request-accepted/page.tsx
-
12src/app/request-sent/page.tsx
-
2src/components/Componentes/call-result-sheet.tsx
-
12src/components/Componentes/dismiss-reason-sheet.tsx
-
4src/components/Componentes/female-outcome-sheet.test.tsx
-
9src/components/Componentes/help-modal.tsx
-
4src/components/Componentes/information-sheet.tsx
-
11src/components/Componentes/loading-border-spinner.tsx
-
20src/components/Componentes/loading-icon-spinner.tsx
-
6src/components/Componentes/loading-select-spinner.tsx
-
4src/components/Componentes/loading-skeleton.tsx
-
5src/components/Componentes/loading-three-dot.tsx
-
26src/components/Componentes/navigation-button.tsx
-
12src/components/Componentes/page-header.tsx
-
54src/components/Componentes/page-loading-skeleton.tsx
-
22src/components/Componentes/progress-helper.ts
-
15src/components/Componentes/question-birthplace.tsx
-
4src/components/Componentes/question-file.tsx
-
10src/components/Componentes/question-number.tsx
-
8src/components/Componentes/question-section-flow.tsx
-
3src/components/Componentes/question-text.tsx
-
8src/components/Componentes/question-title.tsx
-
8src/components/Componentes/required-steps-card.tsx
-
61src/components/Componentes/silent-reloader.tsx
-
23src/components/Componentes/support-access.ts
-
6src/components/Componentes/support-sheet.tsx
-
6src/components/Componentes/swipe-button.tsx
-
30src/components/Componentes/test-intro-page.tsx
-
177src/components/Componentes/test-questions-flow.tsx
-
2src/data/cattell-fallback.ts
-
2src/hooks/marriage/use-marriage-config.ts
-
4src/lib/get-submit-path.ts
-
48src/translations/locales/en.json
-
48src/translations/locales/fa.json
-
1043src/translations/locales/ru.json
@ -0,0 +1 @@ |
|||
export { default } from "@/app/marriage-advisors/page"; |
|||
@ -0,0 +1,234 @@ |
|||
"use client"; |
|||
|
|||
import { useState } from "react"; |
|||
import Image from "next/image"; |
|||
import { FiMessageSquare, FiPhone } from "react-icons/fi"; |
|||
import { FaStar } from "react-icons/fa"; |
|||
import StickyHeader from "@/components/Componentes/sticky-header"; |
|||
import NavigationButton from "@/components/Componentes/navigation-button"; |
|||
import { PageBackground } from "@/components/Componentes/page-background"; |
|||
import SupportSheet from "@/components/Componentes/support-sheet"; |
|||
import { useI18n } from "@/translations/provider"; |
|||
|
|||
export default function MarriageAdvisorsPage() { |
|||
const { dictionary: t } = useI18n(); |
|||
const [isSupportOpen, setIsSupportOpen] = useState(false); |
|||
|
|||
const mainStyle = { |
|||
backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`, |
|||
backgroundColor: "#F5F5F5", |
|||
backgroundRepeat: "repeat-y", |
|||
backgroundSize: "100% auto", |
|||
}; |
|||
|
|||
const advisors = [ |
|||
{ |
|||
id: "hasti-masoudi", |
|||
name: t["Dr. Hasti Masoudi"] || "Dr. Hasti Masoudi", |
|||
specialty: |
|||
t["Specialist in clinical psychology"] || |
|||
"Specialist in clinical psychology", |
|||
avatar: "/assets/images/female_avatar.svg", |
|||
rating: "3.2", |
|||
tagsRow1: [ |
|||
t["Personal Development"] || "Personal Development", |
|||
t["Team Manager"] || "Team Manager", |
|||
t["Science"] || "Science", |
|||
], |
|||
tagsRow2: [ |
|||
t["Manager"] || "Manager", |
|||
t["Team Manager"] || "Team Manager", |
|||
t["Science"] || "Science", |
|||
], |
|||
online: true, |
|||
}, |
|||
{ |
|||
id: "ali-rezaei", |
|||
name: t["Dr. Ali Rezaei"] || "Dr. Ali Rezaei", |
|||
specialty: |
|||
t["Family & marriage counselor"] || "Family & marriage counselor", |
|||
avatar: "/assets/images/Avatar Image.png", |
|||
rating: "4.9", |
|||
tagsRow1: [ |
|||
t["Premarital Counselling"] || "Premarital Counselling", |
|||
t["Conflict Resolution"] || "Conflict Resolution", |
|||
], |
|||
tagsRow2: [ |
|||
t["Behavioral Therapy"] || "Behavioral Therapy", |
|||
t["Self-Awareness"] || "Self-Awareness", |
|||
], |
|||
online: true, |
|||
}, |
|||
{ |
|||
id: "sara-alavi", |
|||
name: t["Dr. Sara Alavi"] || "Dr. Sara Alavi", |
|||
specialty: |
|||
t["Relationship & intimacy coach"] || "Relationship & intimacy coach", |
|||
avatar: "/assets/images/Ellipse 370.png", |
|||
rating: "4.7", |
|||
tagsRow1: [ |
|||
t["Emotional Health"] || "Emotional Health", |
|||
t["Communication Skills"] || "Communication Skills", |
|||
], |
|||
tagsRow2: [ |
|||
t["Relationship Coach"] || "Relationship Coach", |
|||
t["Personal Development"] || "Personal Development", |
|||
], |
|||
online: false, |
|||
}, |
|||
]; |
|||
|
|||
return ( |
|||
<> |
|||
<PageBackground /> |
|||
<main |
|||
className="-mx-[17px] flex min-h-screen flex-col pb-10" |
|||
style={mainStyle} |
|||
> |
|||
<StickyHeader> |
|||
<div className="flex items-center justify-between gap-3"> |
|||
<NavigationButton |
|||
variant="transparent" |
|||
icon="back" |
|||
iconLabel={t["Go back"]} |
|||
/> |
|||
<h1 |
|||
className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white" |
|||
style={{ fontFamily: "'Segoe UI', sans-serif" }} |
|||
> |
|||
{t["Marriage advisors"]} |
|||
</h1> |
|||
<div className="size-10 shrink-0" /> |
|||
</div> |
|||
</StickyHeader> |
|||
|
|||
<section className="px-[17px] pb-32 pt-5 flex flex-col items-center gap-4"> |
|||
{advisors.map((advisor) => ( |
|||
<div |
|||
key={advisor.id} |
|||
className="flex flex-col items-center py-[13px] gap-[13px] w-[343px] bg-white rounded-[15px] shadow-[0px_4px_12px_rgba(0,0,0,0.05)] border border-[#EBEBEB]" |
|||
> |
|||
{/* Top Frame containing Left (Avatar + Info) and Right (Rating) and Actions */} |
|||
<div className="flex flex-col items-start p-0 gap-[11px] w-[317px]"> |
|||
{/* Header Row */} |
|||
<div className="flex flex-row items-start justify-between w-full h-[45px]"> |
|||
{/* Left part: Avatar + Info */} |
|||
<div className="flex flex-row items-center gap-2.5 h-[45px]"> |
|||
{/* Avatar Container */} |
|||
<div className="relative w-[45px] h-[45px] shrink-0"> |
|||
<Image |
|||
src={advisor.avatar} |
|||
alt={advisor.name} |
|||
width={45} |
|||
height={45} |
|||
className="w-full h-full rounded-full object-cover" |
|||
/> |
|||
{/* Status indicator dot */} |
|||
{advisor.online && ( |
|||
<div className="absolute top-0 right-0 w-[12px] h-[12px] rounded-full bg-white flex items-center justify-center select-none pointer-events-none"> |
|||
<span className="absolute w-[10px] h-[10px] rounded-full bg-[#31CB2E] opacity-20 animate-ping" /> |
|||
<span className="w-[6px] h-[6px] rounded-full bg-[#31CB2E]" /> |
|||
</div> |
|||
)} |
|||
</div> |
|||
|
|||
{/* Name + Title Info */} |
|||
<div className="flex flex-col items-start gap-[2px] h-[35px]"> |
|||
<h3 |
|||
className="font-bold text-[16px] leading-[20px] text-[#111111] text-start font-sans truncate w-[170px]" |
|||
style={{ fontFamily: "'Segoe UI', sans-serif" }} |
|||
> |
|||
{advisor.name} |
|||
</h3> |
|||
<p |
|||
className="font-semibold text-[10px] leading-[13px] text-[#8B8B8B] text-start font-sans truncate w-[170px]" |
|||
style={{ fontFamily: "'Segoe UI', sans-serif" }} |
|||
> |
|||
{advisor.specialty} |
|||
</p> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Right part: Rating */} |
|||
<div className="flex flex-row justify-center items-center gap-[4px] h-[20px] shrink-0"> |
|||
<FaStar className="size-[14px] text-[#ECA533] shrink-0" /> |
|||
<span |
|||
className="font-semibold text-[10px] leading-[13px] text-[#ECA533] font-sans text-center" |
|||
style={{ fontFamily: "'Segoe UI', sans-serif" }} |
|||
> |
|||
{advisor.rating} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Actions Row */} |
|||
<div className="flex flex-row items-center gap-[7px] h-[22px]"> |
|||
{/* Text Contact Button */} |
|||
<button |
|||
type="button" |
|||
onClick={() => setIsSupportOpen(true)} |
|||
className="flex flex-row justify-center items-center px-2 py-0.5 gap-1.5 h-[22px] bg-[rgba(14,177,60,0.1)] rounded-[20px] text-[#0EB13C] font-semibold text-[10px] leading-[13px] transition-transform active:scale-95 cursor-pointer border-none" |
|||
> |
|||
<FiMessageSquare className="size-3 text-[#0EB13C] shrink-0" /> |
|||
<span style={{ fontFamily: "'Segoe UI', sans-serif" }}> |
|||
{t["Text"] || "Text"} |
|||
</span> |
|||
</button> |
|||
|
|||
{/* Voice Call Contact Button */} |
|||
<button |
|||
type="button" |
|||
onClick={() => setIsSupportOpen(true)} |
|||
className="flex flex-row justify-center items-center px-2 py-0.5 gap-1.5 h-[22px] bg-[rgba(14,177,60,0.1)] rounded-[20px] text-[#0EB13C] font-semibold text-[10px] leading-[13px] transition-transform active:scale-95 cursor-pointer border-none" |
|||
> |
|||
<FiPhone className="size-3 text-[#0EB13C] shrink-0" /> |
|||
<span style={{ fontFamily: "'Segoe UI', sans-serif" }}> |
|||
{t["Voice Call"] || "Voice Call"} |
|||
</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Divider */} |
|||
<div className="w-[343px] h-[1px] bg-[#EBEBEB]" /> |
|||
|
|||
{/* Tags Section */} |
|||
<div className="flex flex-col items-start gap-[9px] w-[317px]"> |
|||
{/* Row 1 tags */} |
|||
<div className="flex flex-row items-center gap-[9px] w-full flex-wrap"> |
|||
{advisor.tagsRow1.map((tag, index) => ( |
|||
<span |
|||
key={index} |
|||
className="flex flex-row justify-center items-center py-[2px] px-[8px] bg-[#F5F5F5] rounded-[15px] text-[#8B8B8B] font-semibold text-[10px] leading-[13px] shrink-0 font-sans" |
|||
style={{ fontFamily: "'Segoe UI', sans-serif" }} |
|||
> |
|||
{tag} |
|||
</span> |
|||
))} |
|||
</div> |
|||
|
|||
{/* Row 2 tags */} |
|||
<div className="flex flex-row items-center gap-[9px] w-full flex-wrap"> |
|||
{advisor.tagsRow2.map((tag, index) => ( |
|||
<span |
|||
key={index} |
|||
className="flex flex-row justify-center items-center py-[2px] px-[8px] bg-[#F5F5F5] rounded-[15px] text-[#8B8B8B] font-semibold text-[10px] leading-[13px] shrink-0 font-sans" |
|||
style={{ fontFamily: "'Segoe UI', sans-serif" }} |
|||
> |
|||
{tag} |
|||
</span> |
|||
))} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
))} |
|||
</section> |
|||
</main> |
|||
|
|||
<SupportSheet |
|||
isOpen={isSupportOpen} |
|||
onClose={() => setIsSupportOpen(false)} |
|||
/> |
|||
</> |
|||
); |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
"use client"; |
|||
|
|||
import { useQueryClient } from "@tanstack/react-query"; |
|||
import { type ReactNode, useEffect } from "react"; |
|||
|
|||
type SilentReloaderProps = { |
|||
children: ReactNode; |
|||
}; |
|||
|
|||
export default function SilentReloader({ children }: SilentReloaderProps) { |
|||
const queryClient = useQueryClient(); |
|||
|
|||
useEffect(() => { |
|||
if (typeof window !== "undefined") { |
|||
(window as any).__queryClient = queryClient; |
|||
} |
|||
|
|||
let lastReloadTime = 0; |
|||
const handleReload = () => { |
|||
const now = Date.now(); |
|||
if (now - lastReloadTime < 5000) { |
|||
return; |
|||
} |
|||
lastReloadTime = now; |
|||
// Invalidate all active queries so fresh data is reloaded from backend
|
|||
queryClient.invalidateQueries(); |
|||
}; |
|||
|
|||
const handleVisibilityChange = () => { |
|||
if (document.visibilityState === "visible") { |
|||
handleReload(); |
|||
} |
|||
}; |
|||
|
|||
const handlePageShow = () => { |
|||
handleReload(); |
|||
}; |
|||
|
|||
window.addEventListener("focus", handleReload); |
|||
window.addEventListener("pageshow", handlePageShow); |
|||
document.addEventListener("visibilitychange", handleVisibilityChange); |
|||
|
|||
// Listen for Flutter response events if available
|
|||
const win = window as any; |
|||
const unbindFlutter = |
|||
typeof win.addFlutterResponseListener === "function" |
|||
? win.addFlutterResponseListener(() => handleReload()) |
|||
: null; |
|||
|
|||
return () => { |
|||
window.removeEventListener("focus", handleReload); |
|||
window.removeEventListener("pageshow", handlePageShow); |
|||
document.removeEventListener("visibilitychange", handleVisibilityChange); |
|||
if (typeof unbindFlutter === "function") { |
|||
unbindFlutter(); |
|||
} |
|||
}; |
|||
}, [queryClient]); |
|||
|
|||
return <>{children}</>; |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
import type { MarriageProfile } from "@/hooks/marriage/types"; |
|||
|
|||
/** |
|||
* Support is unavailable until a user has finished their profile. Women gain |
|||
* access on the waiting screen; men additionally need a currently valid |
|||
* subscription. |
|||
*/ |
|||
export function hasSupportAccess(profile?: MarriageProfile): boolean { |
|||
if (!profile || !["waiting", "in_case", "matched"].includes(profile.status)) { |
|||
return false; |
|||
} |
|||
|
|||
if (profile.gender === "female") { |
|||
return true; |
|||
} |
|||
|
|||
const subscription = profile.active_subscription; |
|||
return ( |
|||
profile.gender === "male" && |
|||
subscription?.is_active === true && |
|||
subscription.is_valid === true |
|||
); |
|||
} |
|||
1043
src/translations/locales/ru.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue