Browse Source
feat: implement comprehensive marriage matching system with multi-language support and core UI components
front-test-2
feat: implement comprehensive marriage matching system with multi-language support and core UI components
front-test-2
46 changed files with 3487 additions and 547 deletions
-
BINFAMINELA.OTF
-
BINFAMINELA.TTF
-
41src/app/api/dev-reset-profile/route.ts
-
11src/app/finding-match/page.tsx
-
2src/app/globals.css
-
63src/app/intro/page.tsx
-
41src/app/new-match/page.tsx
-
67src/app/new-match/profile/page.tsx
-
6src/app/providers.tsx
-
10src/app/questions-list/page.tsx
-
30src/components/Componentes/navigation-button.tsx
-
147src/components/Componentes/payment-swipe-modal.tsx
-
26src/components/Componentes/question-answer-storage.tsx
-
92src/components/Componentes/question-section-flow.tsx
-
17src/components/Componentes/question-snap-list.tsx
-
162src/components/Componentes/swipe-button.tsx
-
169src/components/Componentes/token-switcher.tsx
-
614src/data/questions/en.json
-
614src/data/questions/fa.json
-
2src/hooks/marriage/types.ts
-
23src/hooks/marriage/use-marriage-config.ts
-
25src/lib/auth-bridge.ts
-
78src/translations/locales/ar.json
-
78src/translations/locales/az.json
-
78src/translations/locales/bn.json
-
78src/translations/locales/da.json
-
78src/translations/locales/de.json
-
78src/translations/locales/en.json
-
78src/translations/locales/es.json
-
78src/translations/locales/fa.json
-
78src/translations/locales/fr.json
-
78src/translations/locales/gu.json
-
78src/translations/locales/ha.json
-
78src/translations/locales/he.json
-
78src/translations/locales/hi.json
-
78src/translations/locales/id.json
-
78src/translations/locales/ks.json
-
78src/translations/locales/pt.json
-
78src/translations/locales/ru.json
-
78src/translations/locales/sw.json
-
78src/translations/locales/tg.json
-
78src/translations/locales/tr.json
-
78src/translations/locales/ul.json
-
78src/translations/locales/ur.json
-
78src/translations/locales/uz.json
-
78src/translations/locales/zh.json
@ -0,0 +1,41 @@ |
|||||
|
import type { NextRequest } from "next/server"; |
||||
|
import { exec } from "child_process"; |
||||
|
import { promisify } from "util"; |
||||
|
|
||||
|
const execAsync = promisify(exec); |
||||
|
|
||||
|
export const dynamic = "force-dynamic"; |
||||
|
export const runtime = "nodejs"; |
||||
|
|
||||
|
export async function POST(request: NextRequest) { |
||||
|
if (process.env.NODE_ENV !== "development") { |
||||
|
return Response.json({ error: "Forbidden in production" }, { status: 403 }); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const { userId } = await request.json(); |
||||
|
if (!userId) { |
||||
|
return Response.json({ error: "userId is required" }, { status: 400 }); |
||||
|
} |
||||
|
|
||||
|
const scriptPath = "C:\\Users\\User\\.gemini\\antigravity-ide\\brain\\5015ea2d-f1fc-4e3d-9372-4bb0670449cc\\scratch\\reset_user.py"; |
||||
|
// Construct the command safely with numeric userId
|
||||
|
const command = `python "${scriptPath}" ${Number(userId)}`; |
||||
|
const { stdout, stderr } = await execAsync(command); |
||||
|
|
||||
|
if (stderr) { |
||||
|
console.error("Stderr from reset script:", stderr); |
||||
|
} |
||||
|
|
||||
|
console.log("Stdout from reset script:", stdout); |
||||
|
|
||||
|
if (stdout.includes("SUCCESS:")) { |
||||
|
return Response.json({ success: true, message: stdout.trim() }); |
||||
|
} else { |
||||
|
return Response.json({ error: stdout.trim() }, { status: 500 }); |
||||
|
} |
||||
|
} catch (error: any) { |
||||
|
console.error("Failed to reset profile:", error); |
||||
|
return Response.json({ error: error.message || "Internal Server Error" }, { status: 500 }); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,147 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import Image from "next/image"; |
||||
|
import { authBridge } from "@/lib/auth-bridge"; |
||||
|
import { useI18n } from "@/translations/provider"; |
||||
|
import { DotsLoader } from "./button"; |
||||
|
import SwipeButton from "./swipe-button"; |
||||
|
|
||||
|
type PaymentSwipeModalProps = { |
||||
|
onSuccess: () => void; |
||||
|
isPaymentPending: boolean; |
||||
|
errorMessage: string | null; |
||||
|
}; |
||||
|
|
||||
|
export function PaymentSwipeModal({ |
||||
|
onSuccess, |
||||
|
isPaymentPending, |
||||
|
errorMessage, |
||||
|
}: PaymentSwipeModalProps) { |
||||
|
const { dictionary: t } = useI18n(); |
||||
|
const modalT = (t as any).paymentModal || {}; |
||||
|
|
||||
|
const currentCoins = authBridge.getCoins(); |
||||
|
|
||||
|
const handleCloseService = () => { |
||||
|
if ( |
||||
|
typeof window !== "undefined" && |
||||
|
(window as any).HabibApp?.postMessage |
||||
|
) { |
||||
|
(window as any).HabibApp.postMessage( |
||||
|
JSON.stringify({ action: "close_service" }), |
||||
|
); |
||||
|
} else { |
||||
|
window.history.back(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
className="fixed inset-0 z-50 flex items-end justify-center bg-[#171717]/55 backdrop-blur-xs transition-opacity duration-300" |
||||
|
role="dialog" |
||||
|
aria-modal="true" |
||||
|
> |
||||
|
<section className="w-full sm:max-w-[375px] rounded-t-[24px] bg-[#FAF9F9] px-5 pt-6 pb-8 text-center shadow-[0_-8px_30px_rgb(0,0,0,0.12)] animate-slide-up"> |
||||
|
{/* Diamond Icon Indicator */} |
||||
|
<div className="mx-auto flex h-[64px] w-[64px] items-center justify-center rounded-full bg-[#FFECEF] shadow-sm mb-3"> |
||||
|
<Image |
||||
|
src="/assets/images/icon-park-outline_diamond.svg" |
||||
|
alt="Diamond" |
||||
|
width={36} |
||||
|
height={36} |
||||
|
className="shrink-0" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
{/* Modal Title */} |
||||
|
<h2 className="text-[18px] font-bold text-[#1C1C1E] leading-tight"> |
||||
|
{modalT.title || "Verification & Subscription"} |
||||
|
</h2> |
||||
|
|
||||
|
{/* Verification Info Panel */} |
||||
|
<div className="mt-4 rounded-[16px] bg-white border border-[#E5E5EA] p-4 text-right"> |
||||
|
<p className="text-[13px] leading-[1.6] font-medium text-[#3A3A3C]"> |
||||
|
{modalT.verificationText || |
||||
|
"This payment acts as a verification process for your account and activates a 3-month subscription to receive new case introductions. The cost is 50 Habib Coins."} |
||||
|
</p> |
||||
|
|
||||
|
<div className="mt-3 flex items-center justify-between border-t border-[#F2F2F7] pt-2.5"> |
||||
|
<span className="text-[12px] font-semibold text-[#8E8E93]"> |
||||
|
{modalT.activeFor3Months || "Validity duration"} |
||||
|
</span> |
||||
|
<span className="text-[13px] font-bold text-[#F0445B]"> |
||||
|
3 {(t.common as any)?.months || "Months"} |
||||
|
</span> |
||||
|
</div> |
||||
|
|
||||
|
<div className="mt-1.5 flex items-center justify-between"> |
||||
|
<span className="text-[12px] font-semibold text-[#8E8E93]"> |
||||
|
{modalT.cost || "Subscription fee"} |
||||
|
</span> |
||||
|
<span className="inline-flex items-center gap-1 text-[13px] font-bold text-[#F0445B]"> |
||||
|
<span>50</span> |
||||
|
<span className="text-[11px] font-medium text-[#8E8E93]"> |
||||
|
{(t.common as any)?.coins || "Coins"} |
||||
|
</span> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{/* Quantitative Disclaimer Box */} |
||||
|
<div className="mt-3 rounded-[16px] bg-[#FFF2F4] border border-[#FFCCD4] p-3 text-right"> |
||||
|
<p className="text-[11px] leading-[1.5] font-semibold text-[#FF4F67]"> |
||||
|
{modalT.disclaimerText || |
||||
|
"Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users."} |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
{/* User Balance Status */} |
||||
|
<div className="mt-4 flex items-center justify-between px-1.5 text-sm"> |
||||
|
<span className="font-semibold text-[#8E8E93]"> |
||||
|
{(t.common as any)?.balance || "Your balance"}: |
||||
|
</span> |
||||
|
<span |
||||
|
className={`font-bold ${currentCoins < 50 ? "text-[#FF4F67]" : "text-[#1C1C1E]"}`} |
||||
|
> |
||||
|
{currentCoins} {(t.common as any)?.coins || "Coins"} |
||||
|
</span> |
||||
|
</div> |
||||
|
|
||||
|
{/* Error Message Box */} |
||||
|
{errorMessage && ( |
||||
|
<div className="mt-3 rounded-[12px] border border-[#FF3B30]/30 bg-[#FF3B30]/10 p-3 text-right"> |
||||
|
<p className="text-[12px] font-semibold text-[#FF3B30]"> |
||||
|
{errorMessage} |
||||
|
</p> |
||||
|
</div> |
||||
|
)} |
||||
|
|
||||
|
{/* Swipe Button / Pending Indicator */} |
||||
|
<div className="mt-5"> |
||||
|
{isPaymentPending ? ( |
||||
|
<div className="flex h-[56px] w-full items-center justify-center rounded-full bg-[#FFECEF]"> |
||||
|
<DotsLoader /> |
||||
|
</div> |
||||
|
) : ( |
||||
|
<SwipeButton |
||||
|
onSuccess={onSuccess} |
||||
|
text={modalT.swipeToPay || "Swipe to pay 50 Habib Coins"} |
||||
|
disabled={currentCoins < 50} |
||||
|
/> |
||||
|
)} |
||||
|
</div> |
||||
|
|
||||
|
{/* Close/Exit link */} |
||||
|
<button |
||||
|
type="button" |
||||
|
onClick={handleCloseService} |
||||
|
className="mt-4 inline-block text-[14px] font-bold text-[#8E8E93] hover:text-[#48484A] transition-colors" |
||||
|
> |
||||
|
{modalT.close || "Exit"} |
||||
|
</button> |
||||
|
</section> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default PaymentSwipeModal; |
||||
@ -0,0 +1,162 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import type React from "react"; |
||||
|
import { useEffect, useRef, useState } from "react"; |
||||
|
import { GoChevronLeft, GoChevronRight } from "react-icons/go"; |
||||
|
import { useI18n } from "@/translations/provider"; |
||||
|
|
||||
|
type SwipeButtonProps = { |
||||
|
onSuccess: () => void; |
||||
|
text: string; |
||||
|
disabled?: boolean; |
||||
|
}; |
||||
|
|
||||
|
export function SwipeButton({ |
||||
|
onSuccess, |
||||
|
text, |
||||
|
disabled = false, |
||||
|
}: SwipeButtonProps) { |
||||
|
const { locale } = useI18n(); |
||||
|
const isRtl = |
||||
|
locale === "fa" || |
||||
|
locale === "ar" || |
||||
|
locale === "ur" || |
||||
|
locale === "he" || |
||||
|
locale === "ks"; |
||||
|
|
||||
|
const [dragX, setDragX] = useState(0); |
||||
|
const [isDragging, setIsDragging] = useState(false); |
||||
|
const [swiped, setSwiped] = useState(false); |
||||
|
|
||||
|
const containerRef = useRef<HTMLDivElement>(null); |
||||
|
const startX = useRef(0); |
||||
|
|
||||
|
const getContainerWidth = () => { |
||||
|
return containerRef.current ? containerRef.current.clientWidth : 0; |
||||
|
}; |
||||
|
|
||||
|
const handleStart = (clientX: number) => { |
||||
|
if (disabled || swiped) return; |
||||
|
setIsDragging(true); |
||||
|
startX.current = clientX; |
||||
|
}; |
||||
|
|
||||
|
const handleMove = (clientX: number) => { |
||||
|
if (!isDragging || swiped) return; |
||||
|
const delta = isRtl ? startX.current - clientX : clientX - startX.current; |
||||
|
const containerWidth = getContainerWidth(); |
||||
|
const handleWidth = 48; // width of the handle button (w-12 = 48px)
|
||||
|
const padding = 8; // padding around the handle (p-1 = 4px on each side -> 8px total)
|
||||
|
const maxDrag = containerWidth - handleWidth - padding; |
||||
|
const newDragX = Math.max(0, Math.min(delta, maxDrag)); |
||||
|
setDragX(newDragX); |
||||
|
}; |
||||
|
|
||||
|
const handleEnd = () => { |
||||
|
if (!isDragging) return; |
||||
|
setIsDragging(false); |
||||
|
|
||||
|
const containerWidth = getContainerWidth(); |
||||
|
const handleWidth = 48; |
||||
|
const padding = 8; |
||||
|
const maxDrag = containerWidth - handleWidth - padding; |
||||
|
|
||||
|
if (dragX >= maxDrag * 0.85) { |
||||
|
const finalX = maxDrag; |
||||
|
setDragX(finalX); |
||||
|
setSwiped(true); |
||||
|
onSuccess(); |
||||
|
} else { |
||||
|
setDragX(0); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Touch handlers
|
||||
|
const onTouchStart = (e: React.TouchEvent) => { |
||||
|
handleStart(e.touches[0].clientX); |
||||
|
}; |
||||
|
|
||||
|
const onTouchMove = (e: React.TouchEvent) => { |
||||
|
handleMove(e.touches[0].clientX); |
||||
|
}; |
||||
|
|
||||
|
// Mouse handlers
|
||||
|
const onMouseDown = (e: React.MouseEvent) => { |
||||
|
handleStart(e.clientX); |
||||
|
}; |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!isDragging) return; |
||||
|
|
||||
|
const onGlobalMouseMove = (e: MouseEvent) => { |
||||
|
handleMove(e.clientX); |
||||
|
}; |
||||
|
|
||||
|
const onGlobalMouseUp = () => { |
||||
|
handleEnd(); |
||||
|
}; |
||||
|
|
||||
|
window.addEventListener("mousemove", onGlobalMouseMove); |
||||
|
window.addEventListener("mouseup", onGlobalMouseUp); |
||||
|
|
||||
|
return () => { |
||||
|
window.removeEventListener("mousemove", onGlobalMouseMove); |
||||
|
window.removeEventListener("mouseup", onGlobalMouseUp); |
||||
|
}; |
||||
|
}, [isDragging, handleEnd, handleMove]); |
||||
|
|
||||
|
const handleStyle = isRtl |
||||
|
? { |
||||
|
transform: `translateX(${-dragX}px)`, |
||||
|
transition: isDragging ? "none" : "transform 0.2s ease-out", |
||||
|
right: "4px", |
||||
|
} |
||||
|
: { |
||||
|
transform: `translateX(${dragX}px)`, |
||||
|
transition: isDragging ? "none" : "transform 0.2s ease-out", |
||||
|
left: "4px", |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
ref={containerRef} |
||||
|
className={`relative flex h-[56px] w-full items-center justify-center rounded-full bg-[#FFECEF] p-1 select-none overflow-hidden ${ |
||||
|
disabled ? "opacity-60 cursor-not-allowed" : "cursor-pointer" |
||||
|
}`}
|
||||
|
onTouchStart={onTouchStart} |
||||
|
onTouchMove={onTouchMove} |
||||
|
onTouchEnd={handleEnd} |
||||
|
onMouseDown={onMouseDown} |
||||
|
> |
||||
|
{/* Background sliding fill */} |
||||
|
<div |
||||
|
className="absolute inset-y-0 bg-[#FFCCD4]/30" |
||||
|
style={{ |
||||
|
left: isRtl ? "auto" : 0, |
||||
|
right: isRtl ? 0 : "auto", |
||||
|
width: `${dragX + 48}px`, |
||||
|
transition: isDragging ? "none" : "width 0.2s ease-out", |
||||
|
}} |
||||
|
/> |
||||
|
|
||||
|
{/* Slide text */} |
||||
|
<span className="pointer-events-none z-10 text-[14px] font-bold text-[#F0445B] animate-pulse"> |
||||
|
{swiped ? "..." : text} |
||||
|
</span> |
||||
|
|
||||
|
{/* Slide handle */} |
||||
|
<div |
||||
|
style={handleStyle} |
||||
|
className="absolute top-1 bottom-1 flex aspect-square items-center justify-center rounded-full bg-[#F0445B] text-white shadow-lg z-20 cursor-grab active:cursor-grabbing" |
||||
|
> |
||||
|
{isRtl ? ( |
||||
|
<GoChevronLeft className="size-6 shrink-0" /> |
||||
|
) : ( |
||||
|
<GoChevronRight className="size-6 shrink-0" /> |
||||
|
)} |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default SwipeButton; |
||||
614
src/data/questions/en.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
614
src/data/questions/fa.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,23 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useQuery } from "@tanstack/react-query"; |
||||
|
import { http } from "@/lib/http"; |
||||
|
|
||||
|
export type MarriageConfig = { |
||||
|
intro_video_url: string; |
||||
|
intro_video_thumbnail_url: string; |
||||
|
}; |
||||
|
|
||||
|
export async function getMarriageConfig() { |
||||
|
const { data } = await http.get<MarriageConfig>("/api/marriage/config/"); |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
export function useMarriageConfigQuery() { |
||||
|
return useQuery({ |
||||
|
queryKey: ["marriage", "config"], |
||||
|
queryFn: getMarriageConfig, |
||||
|
staleTime: 5 * 60 * 1000, // 5 minutes cache
|
||||
|
refetchOnWindowFocus: false, |
||||
|
}); |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue