Browse Source

refactor: redesign marriage advisor card UI with enhanced status indicators and contact type visibility

master
mortezaei 5 days ago
parent
commit
da82dc378a
  1. 324
      src/app/marriage-advisors/page.tsx
  2. 30
      src/app/new-match/new-match-client.tsx
  3. 78
      src/app/new-match/profile/page.tsx
  4. 8
      src/translations/locales/ar.json
  5. 8
      src/translations/locales/az.json
  6. 8
      src/translations/locales/bn.json
  7. 8
      src/translations/locales/da.json
  8. 8
      src/translations/locales/de.json
  9. 7
      src/translations/locales/en.json
  10. 8
      src/translations/locales/es.json
  11. 7
      src/translations/locales/fa.json
  12. 8
      src/translations/locales/fr.json
  13. 8
      src/translations/locales/gu.json
  14. 8
      src/translations/locales/ha.json
  15. 8
      src/translations/locales/he.json
  16. 8
      src/translations/locales/hi.json
  17. 8
      src/translations/locales/id.json
  18. 8
      src/translations/locales/ks.json
  19. 8
      src/translations/locales/pt.json
  20. 8
      src/translations/locales/ru.json
  21. 8
      src/translations/locales/sw.json
  22. 8
      src/translations/locales/tg.json
  23. 8
      src/translations/locales/tr.json
  24. 8
      src/translations/locales/ul.json
  25. 8
      src/translations/locales/ur.json
  26. 8
      src/translations/locales/uz.json
  27. 8
      src/translations/locales/zh.json

324
src/app/marriage-advisors/page.tsx

@ -21,7 +21,9 @@ type MarriageAdvisorsPageProps = {
onClose?: () => void;
};
export default function MarriageAdvisorsPage({ onClose }: MarriageAdvisorsPageProps = {}) {
export default function MarriageAdvisorsPage({
onClose,
}: MarriageAdvisorsPageProps = {}) {
const { dictionary: t } = useI18n();
const [isSupportOpen, setIsSupportOpen] = useState(false);
@ -105,11 +107,6 @@ export default function MarriageAdvisorsPage({ onClose }: MarriageAdvisorsPagePr
key={advisor.username}
advisor={advisor}
onOpen={() => openConsultantPage(advisor.username)}
onContact={() => setIsSupportOpen(true)}
contactLabels={{
text: t["Text"] || "Text",
voice: t["Voice Call"] || "Voice Call",
}}
/>
))}
</section>
@ -126,17 +123,20 @@ export default function MarriageAdvisorsPage({ onClose }: MarriageAdvisorsPagePr
function AdvisorCard({
advisor,
onOpen,
onContact,
contactLabels,
}: {
advisor: MarriageConsultant;
onOpen: () => void;
onContact: () => void;
contactLabels: { text: string; voice: string };
}) {
const topics = advisor.topics.filter(Boolean);
const rating = Number(advisor.avg_rate ?? 0).toFixed(1);
const { dictionary: t, locale } = useI18n();
const isRtl = locale === "fa" || locale === "ar" || locale === "ur";
const unreadCount = advisor.unread_count ?? 0;
const isOnline = advisor.status === "online";
const rating = Number(advisor.avg_rate ?? 0);
const hasChat = advisor.contact_type?.includes("chat");
const hasVoice = advisor.contact_type?.includes("voice");
const hasVideo = advisor.contact_type?.includes("video");
const hasActiveContact = hasChat || hasVoice || hasVideo;
const topics = advisor.topics?.filter(Boolean) ?? [];
return (
<div
@ -150,127 +150,193 @@ function AdvisorCard({
onOpen();
}
}}
className="flex w-[343px] cursor-pointer flex-col items-center gap-[13px] rounded-[15px] border border-[#EBEBEB] bg-white py-[13px] shadow-[0px_4px_12px_rgba(0,0,0,0.05)] transition-transform active:scale-[0.98]"
className="relative w-full max-w-[343px] cursor-pointer transition-transform active:scale-[0.98] outline-none"
>
{/* Top Frame containing Left (Avatar + Info) and Right (Rating) and Actions */}
<div className="flex h-auto w-[317px] flex-col items-start gap-[11px] p-0">
{/* Header Row */}
<div className="flex h-[45px] w-full flex-row items-start justify-between">
{/* Left part: Avatar + Info */}
<div className="flex h-[45px] flex-row items-center gap-2.5">
{/* Avatar Container */}
<div className="relative size-[45px] shrink-0">
{/* Outer Card Container */}
<div
className={`w-full rounded-[15px] bg-white py-3 shadow-[0_4px_16px_rgba(0,0,0,0.06)] transition-all ${
unreadCount > 0
? "border border-[#ECA533]"
: "border border-[#EBEBEB]"
}`}
>
<div className="flex flex-col">
{/* Header Row: Avatar + Info + Rating */}
<div className="flex items-center gap-3 px-3">
{/* Avatar with Status Dot */}
<div className="relative size-12 shrink-0">
{advisor.avatar_url ? (
// Consultant avatars come from arbitrary media hosts, which
// are not all listed in next.config remotePatterns — a plain
// img with an onError fallback keeps them rendering.
// eslint-disable-next-line @next/next/no-img-element
<img
src={advisor.avatar_url}
alt={advisor.fullname ?? advisor.username}
className="size-full rounded-full object-cover"
className="size-full rounded-full object-cover bg-[#EBEBEB]"
onError={(event) => {
event.currentTarget.src = FALLBACK_AVATAR;
}}
/>
) : (
<Image
src={FALLBACK_AVATAR}
alt={advisor.fullname ?? advisor.username}
width={45}
height={45}
className="size-full rounded-full object-cover"
/>
)}
{/* Status indicator dot */}
{isOnline && (
<div className="pointer-events-none absolute top-0 right-0 flex size-[12px] select-none items-center justify-center rounded-full bg-white">
<span className="absolute size-[10px] animate-ping rounded-full bg-[#31CB2E] opacity-20" />
<span className="size-[6px] rounded-full bg-[#31CB2E]" />
<div className="size-full rounded-full bg-[#EBEBEB] flex items-center justify-center">
<svg
width="24"
height="24"
viewBox="0 0 33 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-[#8B8B8B]"
>
<path
d="M16.5 17.3333C15.4802 17.3333 14.4833 17.0401 13.6353 16.4907C12.7874 15.9413 12.1265 15.1604 11.7362 14.2467C11.346 13.3331 11.2439 12.3278 11.4428 11.3579C11.6418 10.388 12.1329 9.49706 12.854 8.7978C13.5751 8.09853 14.4939 7.62233 15.4941 7.4294C16.4943 7.23648 17.531 7.33549 18.4732 7.71393C19.4154 8.09237 20.2207 8.73323 20.7873 9.55548C21.3538 10.3777 21.6562 11.3444 21.6562 12.3333C21.6562 13.6594 21.113 14.9312 20.146 15.8689C19.179 16.8065 17.8675 17.3333 16.5 17.3333ZM16.5 9.33333C15.8881 9.33333 15.29 9.50928 14.7812 9.83892C14.2724 10.1686 13.8759 10.6371 13.6417 11.1853C13.4076 11.7335 13.3463 12.3367 13.4657 12.9186C13.5851 13.5005 13.8797 14.0351 14.3124 14.4547C14.7451 14.8742 15.2963 15.1599 15.8964 15.2757C16.4966 15.3914 17.1186 15.332 17.6839 15.105C18.2492 14.8779 18.7324 14.4934 19.0724 14C19.4123 13.5067 19.5938 12.9267 19.5938 12.3333C19.5938 11.5377 19.2678 10.7746 18.6876 10.212C18.1074 9.6494 17.3205 9.33333 16.5 9.33333Z"
fill="currentColor"
/>
</svg>
</div>
)}
{/* Status indicator dot badge (top-start) */}
<div className="absolute top-0 -left-0.5 rtl:-right-0.5 rtl:left-auto flex size-3 items-center justify-center rounded-full bg-white p-0.5">
<div
className={`flex size-2.5 items-center justify-center rounded-full ${
isOnline ? "bg-[#0EB13C]/20" : "bg-white"
}`}
>
<div
className={`size-1.5 rounded-full ${
isOnline ? "bg-[#0EB13C]" : "bg-[#747474]"
}`}
/>
</div>
</div>
</div>
{/* Name + Title Info */}
<div className="flex h-[35px] flex-col items-start gap-[2px]">
<h3
className="w-[170px] truncate text-start font-sans text-[16px] leading-[20px] font-bold text-[#111111]"
style={{ fontFamily: "'Segoe UI', sans-serif" }}
>
{advisor.fullname ?? advisor.username}
</h3>
<p
className="w-[170px] truncate text-start font-sans text-[10px] leading-[13px] font-semibold text-[#8B8B8B]"
style={{ fontFamily: "'Segoe UI', sans-serif" }}
>
{/* Consultant Info */}
<div className="min-w-0 flex-1 flex flex-col justify-center gap-1">
<div className="flex items-center gap-2">
{advisor.is_ai && (
<span className="inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#F45EA0] to-[#704EE9] text-[9px] font-bold text-white leading-none">
AI
</span>
)}
<h3 className="truncate font-sans text-[15px] font-bold text-[#151C31] leading-tight text-start">
{advisor.fullname ?? advisor.username}
</h3>
{rating > 0 && (
<div className="ms-auto flex shrink-0 items-center gap-1">
<svg
width="12"
height="12"
viewBox="0 0 24 23"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-3 text-[#F4C51D]"
>
<path
d="M12.0137 0.5C12.3276 0.5 12.6304 0.567504 12.9268 0.705078C13.2033 0.83346 13.4498 1.03558 13.6641 1.32812L13.6699 1.33594L16.8877 5.55078L16.9814 5.67285L17.127 5.72168L22.0283 7.36426V7.36523C22.5075 7.53628 22.8608 7.81149 23.1113 8.18945C23.3746 8.58681 23.5 8.99869 23.5 9.43457C23.5 9.64108 23.4672 9.85698 23.3975 10.083C23.3466 10.2477 23.2761 10.4069 23.1855 10.5615L23.0889 10.7139L19.9551 15.2051L19.8613 15.3398L19.8652 15.5029L19.9766 20.2646C19.9765 20.8763 19.7652 21.3933 19.3281 21.8408C18.8902 22.2891 18.3925 22.5 17.8125 22.5C17.8116 22.5 17.8078 22.4994 17.8008 22.499C17.7924 22.4985 17.7811 22.4984 17.7666 22.4971C17.737 22.4944 17.6975 22.4898 17.6484 22.4834C17.5546 22.4711 17.4318 22.4514 17.2783 22.4258L12.123 20.9561L11.9854 20.917L11.8477 20.9561L6.71484 22.4355L6.69043 22.4434L6.66699 22.4521C6.64021 22.4629 6.60211 22.4717 6.54785 22.4717H6.24316C5.63483 22.4717 5.11477 22.2631 4.66016 21.8291C4.22037 21.4092 4.00813 20.8947 4.02344 20.248L4.13477 15.5029L4.13867 15.3408L4.0459 15.207L0.910156 10.6846C0.770004 10.4806 0.668646 10.2734 0.601562 10.0635C0.533314 9.8498 0.500011 9.64082 0.5 9.43457C0.5 8.99869 0.625355 8.58681 0.888672 8.18945C1.13917 7.81149 1.49253 7.53628 1.97168 7.36523L1.9707 7.36426L6.84668 5.72168L6.99023 5.67285L7.08301 5.55273L10.3291 1.33789L10.3359 1.32812C10.5481 1.0385 10.7969 0.836235 11.082 0.707031C11.3901 0.567552 11.6997 0.500029 12.0137 0.5Z"
fill="#F4C51D"
/>
</svg>
<span className="font-sans text-[10px] font-semibold text-[#F4C51D]">
{rating.toFixed(1)}
</span>
</div>
)}
</div>
<p className="truncate font-sans text-[10px] font-semibold text-[#73889D] text-start">
{advisor.slogan ?? ""}
</p>
</div>
</div>
{/* Right part: Rating */}
<div className="flex h-[20px] shrink-0 flex-row items-center justify-center gap-[4px]">
<FaStar className="size-[14px] shrink-0 text-[#ECA533]" />
<span
className="text-center font-sans text-[10px] leading-[13px] font-semibold text-[#ECA533]"
style={{ fontFamily: "'Segoe UI', sans-serif" }}
>
{rating}
</span>
</div>
</div>
{/* Contact Chips */}
{hasActiveContact && (
<div className="flex flex-wrap items-center gap-2 pt-3 px-4">
{hasChat && (
<div className="inline-flex items-center gap-1 rounded-[16px] bg-[#0EB13C]/10 px-2 py-1 text-[10px] font-semibold text-[#0EB13C]">
<svg
width="12"
height="12"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-3 shrink-0 text-[#0EB13C]"
>
<path
d="M7.01331 0.333252C10.8 0.333252 13.6666 3.43791 13.6666 6.9899C13.6666 11.1094 10.3066 13.6666 6.99998 13.6666C5.90665 13.6666 4.69331 13.3728 3.71998 12.7986C3.37998 12.5916 3.09331 12.4381 2.72665 12.5583L1.37998 12.9589C1.03998 13.0657 0.733313 12.7986 0.833313 12.4381L1.27998 10.9425C1.35331 10.7355 1.33998 10.5152 1.23331 10.3416C0.65998 9.28668 0.333313 8.13162 0.333313 7.00993C0.333313 3.498 3.13998 0.333252 7.01331 0.333252ZM10.06 6.16199C9.58665 6.16199 9.20665 6.54257 9.20665 7.01661C9.20665 7.48398 9.58665 7.87122 10.06 7.87122C10.5333 7.87122 10.9133 7.48398 10.9133 7.01661C10.9133 6.54257 10.5333 6.16199 10.06 6.16199ZM6.98665 6.16199C6.51998 6.15532 6.13331 6.54257 6.13331 7.00993C6.13331 7.48398 6.51331 7.86455 6.98665 7.87122C7.45998 7.87122 7.83998 7.48398 7.83998 7.01661C7.83998 6.54257 7.45998 6.16199 6.98665 6.16199ZM3.91331 6.16199C3.43998 6.16199 3.05998 6.54257 3.05998 7.01661C3.05998 7.48398 3.44665 7.87122 3.91331 7.87122C4.38665 7.86455 4.76665 7.48398 4.76665 7.01661C4.76665 6.54257 4.38665 6.16199 3.91331 6.16199Z"
fill="currentColor"
/>
</svg>
<span>{t["Text Message"] || "Text Message"}</span>
</div>
)}
{/* Actions Row */}
<div className="flex h-[22px] flex-row items-center gap-[7px]">
{/* Text Contact Button */}
<button
type="button"
onClick={(event) => {
event.stopPropagation();
onContact();
}}
className="flex h-[22px] cursor-pointer flex-row items-center justify-center gap-1.5 rounded-[20px] border-none bg-[rgba(14,177,60,0.1)] px-2 py-0.5 text-[10px] leading-[13px] font-semibold text-[#0EB13C] transition-transform active:scale-95"
>
<FiMessageSquare className="size-3 shrink-0 text-[#0EB13C]" />
<span style={{ fontFamily: "'Segoe UI', sans-serif" }}>
{contactLabels.text}
</span>
</button>
{hasVoice && (
<div className="inline-flex items-center gap-1 rounded-[16px] bg-[#0EB13C]/10 px-2 py-1 text-[10px] font-semibold text-[#0EB13C]">
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-3 shrink-0 text-[#0EB13C]"
>
<path
d="M11.5643 0.576816L9.12681 0.0143159C8.86196 -0.0466215 8.59009 0.0916597 8.48228 0.340097L7.35728 2.9651C7.25884 3.19478 7.32446 3.46432 7.51899 3.62135L8.93931 4.78385C8.09556 6.5815 6.62134 8.07682 4.78618 8.93697L3.62368 7.51666C3.46431 7.32213 3.19712 7.2565 2.96743 7.35494L0.342432 8.47994C0.0916504 8.5901 -0.0466309 8.86197 0.0143066 9.12682L0.576807 11.5643C0.6354 11.8174 0.8604 12.0003 1.12524 12.0003C7.12759 12.0003 12.0002 7.13697 12.0002 1.12525C12.0002 0.862753 11.8198 0.63541 11.5643 0.576816Z"
fill="currentColor"
/>
</svg>
<span>{t["Voice Call"] || "Voice Call"}</span>
</div>
)}
{/* Voice Call Contact Button */}
<button
type="button"
onClick={(event) => {
event.stopPropagation();
onContact();
}}
className="flex h-[22px] cursor-pointer flex-row items-center justify-center gap-1.5 rounded-[20px] border-none bg-[rgba(14,177,60,0.1)] px-2 py-0.5 text-[10px] leading-[13px] font-semibold text-[#0EB13C] transition-transform active:scale-95"
>
<FiPhone className="size-3 shrink-0 text-[#0EB13C]" />
<span style={{ fontFamily: "'Segoe UI', sans-serif" }}>
{contactLabels.voice}
</span>
</button>
{hasVideo && (
<div className="inline-flex items-center gap-1 rounded-[16px] bg-[#0EB13C]/10 px-2 py-1 text-[10px] font-semibold text-[#0EB13C]">
<svg
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-3 shrink-0 text-[#0EB13C]"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.33398 3.33334C2.80355 3.33334 2.29484 3.54406 1.91977 3.91913C1.5447 4.2942 1.33398 4.80291 1.33398 5.33334V10.6667C1.33398 11.1971 1.5447 11.7058 1.91977 12.0809C2.29484 12.456 2.80355 12.6667 3.33398 12.6667H9.33398C9.86442 12.6667 10.3731 12.456 10.7482 12.0809C11.1233 11.7058 11.334 11.1971 11.334 10.6667V9.60934L12.9607 11.236C13.59 11.866 14.6673 11.4193 14.6673 10.5287V5.47134C14.6673 4.58068 13.59 4.13401 12.9607 4.76468L11.334 6.39001V5.33334C11.334 4.80291 11.1233 4.2942 10.7482 3.91913C10.3731 3.54406 9.86442 3.33334 9.33398 3.33334H3.33398Z"
fill="currentColor"
/>
</svg>
<span>{t["Video Call"] || "Video Call"}</span>
</div>
)}
</div>
)}
{/* Topics Badges */}
{topics.length > 0 && (
<>
<div className="my-2.5 border-t border-[#EBEBEB]" />
<div className="flex flex-wrap items-center gap-2 px-4 pb-1">
{topics.map((topic) => (
<span
key={topic}
className="inline-flex items-center rounded-[24px] bg-[#F4F4F8] px-2.5 py-1 font-sans text-[10px] font-semibold text-[#747474]"
>
{topic}
</span>
))}
</div>
</>
)}
</div>
</div>
{/* Divider */}
<div className="h-px w-[343px] bg-[#EBEBEB]" />
{/* Tags Section */}
{topics.length > 0 && (
<div className="flex w-[317px] flex-col items-start gap-[9px]">
<div className="flex w-full flex-row flex-wrap items-center gap-[9px]">
{topics.map((tag) => (
<span
key={tag}
className="flex shrink-0 flex-row items-center rounded-[15px] bg-[#F5F5F5] px-[8px] py-[2px] font-sans text-[10px] leading-[13px] font-semibold text-[#8B8B8B]"
style={{ fontFamily: "'Segoe UI', sans-serif" }}
>
{tag}
</span>
))}
</div>
{/* Floating Unread Badge at Top Corner */}
{unreadCount > 0 && (
<div className="absolute -top-2.5 end-0 z-10 flex items-center gap-1 rounded-[16px] bg-[#ECA533] px-2 py-0.5 text-white shadow-sm">
<span className="text-[10px] font-bold">{unreadCount}</span>
<span className="text-[8.5px] font-normal">
{t["New Message"] || "New Message"}
</span>
</div>
)}
</div>
@ -283,35 +349,27 @@ function AdvisorsSkeleton() {
{[0, 1, 2].map((index) => (
<div
key={index}
className="flex w-[343px] animate-pulse flex-col items-center gap-[13px] rounded-[15px] border border-[#EBEBEB] bg-white py-[13px] shadow-[0px_4px_12px_rgba(0,0,0,0.05)]"
className="w-full max-w-[343px] animate-pulse rounded-[15px] border border-[#EBEBEB] bg-white py-3 shadow-[0_4px_16px_rgba(0,0,0,0.04)]"
>
<div className="flex w-[317px] flex-col items-start gap-[11px]">
<div className="flex w-full flex-row items-start justify-between">
<div className="flex flex-row items-center gap-2.5">
<div className="size-[45px] shrink-0 rounded-full bg-[#F0F0F0]" />
<div className="flex flex-col gap-[6px]">
<div className="h-[14px] w-[150px] rounded bg-[#F0F0F0]" />
<div className="h-[10px] w-[110px] rounded bg-[#F0F0F0]" />
</div>
<div className="flex items-center gap-3 px-3">
<div className="size-12 shrink-0 rounded-full bg-[#F0F0F0]" />
<div className="min-w-0 flex-1 flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<div className="h-4 w-32 rounded bg-[#F0F0F0]" />
<div className="h-3 w-8 rounded bg-[#F0F0F0]" />
</div>
<div className="h-[12px] w-[34px] rounded bg-[#F0F0F0]" />
</div>
<div className="flex flex-row gap-[7px]">
<div className="h-[22px] w-[64px] rounded-[20px] bg-[#F0F0F0]" />
<div className="h-[22px] w-[88px] rounded-[20px] bg-[#F0F0F0]" />
<div className="h-3 w-44 rounded bg-[#F0F0F0]" />
</div>
</div>
<div className="h-px w-[343px] bg-[#F0F0F0]" />
<div className="flex w-[317px] flex-col gap-[9px]">
<div className="flex flex-row gap-[9px]">
<div className="h-[17px] w-[92px] rounded-[15px] bg-[#F0F0F0]" />
<div className="h-[17px] w-[112px] rounded-[15px] bg-[#F0F0F0]" />
<div className="h-[17px] w-[72px] rounded-[15px] bg-[#F0F0F0]" />
</div>
<div className="flex flex-row gap-[9px]">
<div className="h-[17px] w-[104px] rounded-[15px] bg-[#F0F0F0]" />
<div className="h-[17px] w-[84px] rounded-[15px] bg-[#F0F0F0]" />
</div>
<div className="flex gap-2 pt-3 px-4">
<div className="h-5 w-20 rounded-[16px] bg-[#F0F0F0]" />
<div className="h-5 w-20 rounded-[16px] bg-[#F0F0F0]" />
</div>
<div className="my-2.5 border-t border-[#F0F0F0]" />
<div className="flex gap-2 px-4 pb-1">
<div className="h-5 w-16 rounded-[24px] bg-[#F0F0F0]" />
<div className="h-5 w-20 rounded-[24px] bg-[#F0F0F0]" />
<div className="h-5 w-14 rounded-[24px] bg-[#F0F0F0]" />
</div>
</div>
))}

30
src/app/new-match/new-match-client.tsx

@ -152,6 +152,13 @@ function formatOptionValue(
value: MarriageFieldValue,
dictionary?: Record<string, string>,
): string | null {
if (Array.isArray(value)) {
const formattedItems = value
.map((item) => formatOptionValue(item as MarriageFieldValue, dictionary))
.filter(Boolean);
return formattedItems.length ? formattedItems.join(", ") : null;
}
const base = formatFieldValue(value);
if (!base) return null;
if (!dictionary) return base;
@ -201,7 +208,8 @@ function isMarriagePhoneFieldValue(
}
function titleFromKey(key: string) {
return key
const leafKey = key.includes(".") ? key.split(".").pop()! : key;
return leafKey
.replace(/^q\d+[_-]?/i, "")
.replace(/[_-]+/g, " ")
.replace(/\s+/g, " ")
@ -209,6 +217,24 @@ function titleFromKey(key: string) {
.replace(/\b\w/g, (letter) => letter.toUpperCase());
}
function formatFieldLabel(
field: MarriageField,
dictionary?: Record<string, string>,
): string {
const englishTitle = titleFromKey(field.key);
if (!dictionary) return field.label || englishTitle;
if (field.label && dictionary[field.label]) {
return dictionary[field.label];
}
if (dictionary[englishTitle]) {
return dictionary[englishTitle];
}
return field.label || englishTitle;
}
function toDisplayField(
field: MarriageField,
dictionary?: Record<string, string>,
@ -221,7 +247,7 @@ function toDisplayField(
return {
id: field.key || field.label || value,
label: field.label || titleFromKey(field.key),
label: formatFieldLabel(field, dictionary),
value,
};
}

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

@ -26,6 +26,34 @@ import { markMatchStarted } from "@/lib/match-start-grace";
import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider";
function formatOptionValue(
value: MarriageFieldValue,
dictionary?: Record<string, string>,
): string | null {
if (Array.isArray(value)) {
const formattedItems = value
.map((item) => formatOptionValue(item as MarriageFieldValue, dictionary))
.filter(Boolean);
return formattedItems.length ? formattedItems.join(", ") : null;
}
const base = formatFieldValue(value);
if (!base) return null;
if (!dictionary) return base;
if (dictionary[base]) return dictionary[base];
// Try replacing underscores with spaces: "single;_never_married" -> "single; never married"
const withSpaces = base.replace(/_/g, " ").trim();
if (dictionary[withSpaces]) return dictionary[withSpaces];
// Try capitalized first letter: "Single; never married"
const capitalized = withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
if (dictionary[capitalized]) return dictionary[capitalized];
return withSpaces;
}
function formatFieldValue(value: MarriageFieldValue) {
if (value === null || value === "") {
return null;
@ -58,7 +86,8 @@ function isMarriagePhoneFieldValue(
}
function titleFromKey(key: string) {
return key
const leafKey = key.includes(".") ? key.split(".").pop()! : key;
return leafKey
.replace(/^q\d+[_-]?/i, "")
.replace(/[_-]+/g, " ")
.replace(/\s+/g, " ")
@ -66,6 +95,24 @@ function titleFromKey(key: string) {
.replace(/\b\w/g, (letter) => letter.toUpperCase());
}
function formatFieldLabel(
field: MarriageField,
dictionary?: Record<string, string>,
): string {
const englishTitle = titleFromKey(field.key);
if (!dictionary) return field.label || englishTitle;
if (field.label && dictionary[field.label]) {
return dictionary[field.label];
}
if (dictionary[englishTitle]) {
return dictionary[englishTitle];
}
return field.label || englishTitle;
}
function isImageField(field: MarriageField) {
return /(avatar|image|photo|picture|portrait|upload)/i.test(
`${field.key} ${field.label}`,
@ -90,17 +137,19 @@ function canAcceptProfile(
function MatchField({
field,
isCandidateFemale,
dictionary,
}: {
field: MarriageField;
isCandidateFemale: boolean;
dictionary?: Record<string, string>;
}) {
const value = formatFieldValue(field.value);
const value = formatOptionValue(field.value, dictionary);
if (!value || isImageField(field)) {
return null;
}
const label = field.label || titleFromKey(field.key);
const label = formatFieldLabel(field, dictionary);
if (isCandidateFemale) {
return (
@ -132,9 +181,11 @@ function MatchField({
function MatchPublicProfileFields({
publicInfo,
isCandidateFemale,
dictionary,
}: {
publicInfo: MarriageField[] | null | undefined;
isCandidateFemale: boolean;
dictionary?: Record<string, string>;
}) {
const visibleFields = useMemo(() => {
if (!publicInfo) return [];
@ -153,7 +204,8 @@ function MatchPublicProfileFields({
return (
<div className="mt-6 rounded-[16px] bg-white/70 p-5 text-center shadow-xs">
<p className="group-12 font-medium text-[#747474]">
اطلاعات عمومی قابل نمایشی ثبت نشده است.
{dictionary?.["No public information is available to display."] ||
"No public information is available to display."}
</p>
</div>
);
@ -163,7 +215,12 @@ function MatchPublicProfileFields({
return (
<div className="flex flex-col w-full mt-6" style={{ gap: "14px" }}>
{visibleFields.map((field) => (
<MatchField key={field.key} field={field} isCandidateFemale={true} />
<MatchField
key={field.key}
field={field}
isCandidateFemale={true}
dictionary={dictionary}
/>
))}
</div>
);
@ -172,11 +229,17 @@ function MatchPublicProfileFields({
return (
<div className="mt-6 space-y-3 rounded-[18px] bg-white/80 p-4 shadow-xs">
<h3 className="border-b border-[#F0445B]/15 pb-2 text-right group-12 font-bold text-[#F0445B]">
اطلاعات عمومی و مشخصات فردی
{dictionary?.["General Information & Personal Details"] ||
"General Information & Personal Details"}
</h3>
<div className="space-y-2.5">
{visibleFields.map((field) => (
<MatchField key={field.key} field={field} isCandidateFemale={false} />
<MatchField
key={field.key}
field={field}
isCandidateFemale={false}
dictionary={dictionary}
/>
))}
</div>
</div>
@ -668,6 +731,7 @@ export default function NewMatchProfilePage({
<MatchPublicProfileFields
publicInfo={profile?.match_summary?.public_info}
isCandidateFemale={true}
dictionary={t}
/>
</section>

8
src/translations/locales/ar.json

@ -779,5 +779,11 @@
"Clear": "مسح",
"This field is required": "هذه الخانة مطلوبة",
"Habib Coins": "حبيب كوين",
"{} Habib Coins": "{} حبيب كوين"
"{} Habib Coins": "{} حبيب كوين",
"New Message": "رسالة جديدة",
"Text Message": "محادثة نصية",
"Voice Call": "مكالمة صوتية",
"Video Call": "مكالمة فيديو",
"No public information is available to display.": "لا توجد معلومات عامة متاحة للعرض.",
"General Information & Personal Details": "المعلومات العامة والتفاصيل الشخصية"
}

8
src/translations/locales/az.json

@ -779,5 +779,11 @@
"Clear": "Təmizlə",
"This field is required": "Bu sahə zəruridir",
"Habib Coins": "Həbib Koin",
"{} Habib Coins": "{} Həbib Koin"
"{} Habib Coins": "{} Həbib Koin",
"New Message": "Yeni Mesajlar",
"Text Message": "Mətn Mesajı",
"Voice Call": "Səsli Zəng",
"Video Call": "Video Zəng",
"No public information is available to display.": "Göstəriləcək heç bir ictimai məlumat qeyd edilməyib.",
"General Information & Personal Details": "Ümumi məlumat və fərdi xüsusiyyətlər"
}

8
src/translations/locales/bn.json

@ -779,5 +779,11 @@
"Clear": "মুছুন",
"This field is required": "এই ক্ষেত্রটি আবশ্যক",
"Habib Coins": "হাবিব কয়েন",
"{} Habib Coins": "{} হাবিব কয়েন"
"{} Habib Coins": "{} হাবিব কয়েন",
"New Message": "নতুন মেসেজ",
"Text Message": "টেক্সট বার্তা",
"Voice Call": "ভয়েস কল",
"Video Call": "ভিডিও কল",
"No public information is available to display.": "প্রদর্শনের জন্য কোনও সর্বজনীন তথ্য পাওয়া যায়নি।",
"General Information & Personal Details": "সাধারণ তথ্য এবং ব্যক্তিগত বিবরণ"
}

8
src/translations/locales/da.json

@ -779,5 +779,11 @@
"Clear": "Ryd",
"This field is required": "Dette felt er påkrævet",
"Habib Coins": "Habib-mønter",
"{} Habib Coins": "{} Habib-mønter"
"{} Habib Coins": "{} Habib-mønter",
"New Message": "Ny besked",
"Text Message": "Tekstbesked",
"Voice Call": "Taleopkald",
"Video Call": "Videoopkald",
"No public information is available to display.": "Ingen offentlige oplysninger er tilgængelige.",
"General Information & Personal Details": "Generelle oplysninger og personlige detaljer"
}

8
src/translations/locales/de.json

@ -779,5 +779,11 @@
"Clear": "Löschen",
"This field is required": "Dieses Feld ist erforderlich",
"Habib Coins": "Habib-Münzen",
"{} Habib Coins": "{} Habib-Münzen"
"{} Habib Coins": "{} Habib-Münzen",
"New Message": "Neue Nachricht",
"Text Message": "Textnachricht",
"Voice Call": "Sprachanruf",
"Video Call": "Videoanruf",
"No public information is available to display.": "Keine öffentlichen Informationen zur Anzeige verfügbar.",
"General Information & Personal Details": "Allgemeine Informationen und persönliche Details"
}

7
src/translations/locales/en.json

@ -828,5 +828,10 @@
"Clear": "Clear",
"This field is required": "This field is required",
"Habib Coins": "Habib Coins",
"{} Habib Coins": "{} Habib Coins"
"{} Habib Coins": "{} Habib Coins",
"New Message": "New Message",
"Text Message": "Text Message",
"Video Call": "Video Call",
"No public information is available to display.": "No public information is available to display.",
"General Information & Personal Details": "General Information & Personal Details"
}

8
src/translations/locales/es.json

@ -779,5 +779,11 @@
"Clear": "Borrar",
"This field is required": "Este campo es obligatorio",
"Habib Coins": "Monedas Habib",
"{} Habib Coins": "{} Monedas Habib"
"{} Habib Coins": "{} Monedas Habib",
"New Message": "Nuevos mensajes",
"Text Message": "Mensaje de Texto",
"Voice Call": "Llamada de Voz",
"Video Call": "Llamada de Video",
"No public information is available to display.": "No hay información pública disponible para mostrar.",
"General Information & Personal Details": "Información general y detalles personales"
}

7
src/translations/locales/fa.json

@ -828,5 +828,10 @@
"Clear": "پاک کردن",
"This field is required": "این فیلد ضروری است",
"Habib Coins": "حبیب کوین",
"{} Habib Coins": "{} حبیب کوین"
"{} Habib Coins": "{} حبیب کوین",
"New Message": "پیام متنی جدید",
"Text Message": "پیام متنی",
"Video Call": "تماس تصویری",
"No public information is available to display.": "اطلاعات عمومی قابل نمایشی ثبت نشده است.",
"General Information & Personal Details": "اطلاعات عمومی و مشخصات فردی"
}

8
src/translations/locales/fr.json

@ -779,5 +779,11 @@
"Clear": "Effacer",
"This field is required": "Ce champ est requis",
"Habib Coins": "Pièces Habib",
"{} Habib Coins": "{} Pièces Habib"
"{} Habib Coins": "{} Pièces Habib",
"New Message": "Nouveaux messages",
"Text Message": "Message texte",
"Voice Call": "Appel vocal",
"Video Call": "Appel vidéo",
"No public information is available to display.": "Aucune information publique disponible à afficher.",
"General Information & Personal Details": "Informations générales et détails personnels"
}

8
src/translations/locales/gu.json

@ -779,5 +779,11 @@
"Clear": "સાફ કરો",
"This field is required": "આ ક્ષેત્ર આવશ્યક છે",
"Habib Coins": "હબીબ સિક્કા",
"{} Habib Coins": "{} હબીબ કોઈન્સ"
"{} Habib Coins": "{} હબીબ કોઈન્સ",
"New Message": "New Message",
"Text Message": "ટેક્સ્ટ સંદેશ",
"Voice Call": "વૉઇસ કૉલ",
"Video Call": "વિડિયો કૉલ",
"No public information is available to display.": "પ્રદર્શિત કરવા માટે કોઈ જાહેર માહિતી ઉપલબ્ધ નથી.",
"General Information & Personal Details": "સામાન્ય માહિતી અને વ્યક્તિગત વિગતો"
}

8
src/translations/locales/ha.json

@ -779,5 +779,11 @@
"Clear": "Share",
"This field is required": "Ana buƙatar wannan filin",
"Habib Coins": "Kuɗin Habib",
"{} Habib Coins": "Tsabar Kudin Habib {}"
"{} Habib Coins": "Tsabar Kudin Habib {}",
"New Message": "Sabon Saƙo",
"Text Message": "Sakon Rubutu",
"Voice Call": "Kiran Murya",
"Video Call": "Kiran Bidiyo",
"No public information is available to display.": "Babu bayanan jama'a da za a iya nunawa.",
"General Information & Personal Details": "Bayanai na gama-gari da cikakkun bayanan sirri"
}

8
src/translations/locales/he.json

@ -316,5 +316,11 @@
"Clear": "Clear",
"This field is required": "This field is required",
"Habib Coins": "Habib Coins",
"{} Habib Coins": "{} Habib Coins"
"{} Habib Coins": "{} Habib Coins",
"New Message": "New Message",
"Text Message": "Text Message",
"Voice Call": "Voice Call",
"Video Call": "Video Call",
"No public information is available to display.": "אין מידע ציבורי זמין להצגה.",
"General Information & Personal Details": "מידע כללי ופרטים אישיים"
}

8
src/translations/locales/hi.json

@ -779,5 +779,11 @@
"Clear": "साफ़ करें",
"This field is required": "यह फ़ील्ड आवश्यक है",
"Habib Coins": "हबीब कॉइन्स",
"{} Habib Coins": "{} हबीब कॉइन्स"
"{} Habib Coins": "{} हबीब कॉइन्स",
"New Message": "नया संदेश",
"Text Message": "टेक्स्ट मैसेज",
"Voice Call": "वॉइस कॉल",
"Video Call": "वीडियो कॉल",
"No public information is available to display.": "प्रदर्शित करने के लिए कोई सार्वजनिक जानकारी उपलब्ध नहीं है।",
"General Information & Personal Details": "सामान्य जानकारी और व्यक्तिगत विवरण"
}

8
src/translations/locales/id.json

@ -316,5 +316,11 @@
"Clear": "Hapus",
"This field is required": "Bidang ini wajib diisi",
"Habib Coins": "Koin Habib",
"{} Habib Coins": "{} Koin Habib"
"{} Habib Coins": "{} Koin Habib",
"New Message": "Pesan Baru",
"Text Message": "Pesan Teks",
"Voice Call": "Panggilan Suara",
"Video Call": "Panggilan Video",
"No public information is available to display.": "Tidak ada informasi publik yang tersedia untuk ditampilkan.",
"General Information & Personal Details": "Informasi Umum & Rincian Pribadi"
}

8
src/translations/locales/ks.json

@ -316,5 +316,11 @@
"Clear": "صاف کٔرِو",
"This field is required": "یہ فیلڈ ضروری چھُ",
"Habib Coins": "حبیب کوینس",
"{} Habib Coins": "{} حبیب کوین"
"{} Habib Coins": "{} حبیب کوین",
"New Message": "New Message",
"Text Message": "ٹیکسٹ پیغام",
"Voice Call": "آوازی کال",
"Video Call": "ویڈیو کال",
"No public information is available to display.": "ڈسپلے کرنہٕ خٲطرٕ کانہہ عام معلومات دٔستیاب چُھنہٕ۔",
"General Information & Personal Details": "عام معلومات تہٕ ذٲتی تفصیٖل"
}

8
src/translations/locales/pt.json

@ -316,5 +316,11 @@
"Clear": "Limpar",
"This field is required": "Este campo é obrigatório",
"Habib Coins": "Moedas Habib",
"{} Habib Coins": "{} Moedas Habib"
"{} Habib Coins": "{} Moedas Habib",
"New Message": "Nova Mensagem",
"Text Message": "Mensagem de Texto",
"Voice Call": "Chamada de Voz",
"Video Call": "Chamada de Vídeo",
"No public information is available to display.": "Nenhuma informação pública disponível para exibição.",
"General Information & Personal Details": "Informações Gerais e Detalhes Pessoais"
}

8
src/translations/locales/ru.json

@ -783,5 +783,11 @@
"Clear": "Очистить",
"This field is required": "Это поле обязательно",
"Habib Coins": "Хабиб Коины",
"{} Habib Coins": "{} Хабиб Коины"
"{} Habib Coins": "{} Хабиб Коины",
"New Message": "Новые сообщения",
"Text Message": "Текстовое сообщение",
"Voice Call": "Голосовой звонок",
"Video Call": "Видеозвонок",
"No public information is available to display.": "Нет общедоступной информации для отображения.",
"General Information & Personal Details": "Общая информация и личные данные"
}

8
src/translations/locales/sw.json

@ -316,5 +316,11 @@
"Clear": "Futa",
"This field is required": "Sehemu hii inahitajika",
"Habib Coins": "Sarafu za Habib",
"{} Habib Coins": "{} Sarafu za Habib"
"{} Habib Coins": "{} Sarafu za Habib",
"New Message": "Ujumbe Mpya",
"Text Message": "Ujumbe wa Maandishi",
"Voice Call": "Simu ya Sauti",
"Video Call": "Simu ya Video",
"No public information is available to display.": "Hakuna taarifa za umma zinazoweza kuonyeshwa.",
"General Information & Personal Details": "Taarifa za Jumla na Maelezo Binafsi"
}

8
src/translations/locales/tg.json

@ -316,5 +316,11 @@
"Clear": "Покардан",
"This field is required": "Ин майдон ҳатмист",
"Habib Coins": "Тангаҳои Ҳабиб",
"{} Habib Coins": "{} Тангаҳои Ҳабиб"
"{} Habib Coins": "{} Тангаҳои Ҳабиб",
"New Message": "Паёми нав",
"Text Message": "Паёми матнӣ",
"Voice Call": "Зангҳои овозӣ",
"Video Call": "Зангҳои видеоӣ",
"No public information is available to display.": "Маълумоти умумии дастрас барои намоиш нест.",
"General Information & Personal Details": "Маълумоти умумӣ ва мушаххасоти инфиродӣ"
}

8
src/translations/locales/tr.json

@ -316,5 +316,11 @@
"Clear": "Temizle",
"This field is required": "Bu alan gereklidir",
"Habib Coins": "Habib Coin",
"{} Habib Coins": "{} Habib Coin"
"{} Habib Coins": "{} Habib Coin",
"New Message": "Yeni Mesaj",
"Text Message": "Yazılı Mesaj",
"Voice Call": "Sesli Arama",
"Video Call": "Görüntülü Arama",
"No public information is available to display.": "Görüntülenecek genel bilgi bulunmamaktadır.",
"General Information & Personal Details": "Genel Bilgiler ve Kişisel Detaylar"
}

8
src/translations/locales/ul.json

@ -316,5 +316,11 @@
"Clear": "Saaf karein",
"This field is required": "Ye Field Zaroori Hai",
"Habib Coins": "Habib Coins",
"{} Habib Coins": "{} Habib Coins"
"{} Habib Coins": "{} Habib Coins",
"New Message": "Naya paigham",
"Text Message": "Text Message",
"Voice Call": "Voice Call",
"Video Call": "Video Call",
"No public information is available to display.": "Görüntülenecek genel bilgi bulunmamaktadır.",
"General Information & Personal Details": "Genel Bilgiler ve Kişisel Detaylar"
}

8
src/translations/locales/ur.json

@ -316,5 +316,11 @@
"Clear": "صاف کریں",
"This field is required": "یہ فیلڈ ضروری ہے",
"Habib Coins": "حبیب کوائنز",
"{} Habib Coins": "{} حبیب کوائن"
"{} Habib Coins": "{} حبیب کوائن",
"New Message": "نئے پیغامات",
"Text Message": "پیغام",
"Voice Call": "وائس کال",
"Video Call": "ویڈیو کال",
"No public information is available to display.": "دکھانے کے لیے کوئی عوامی معلومات دستیاب نہیں ہے۔",
"General Information & Personal Details": "عام معلومات اور ذاتی تفصیلات"
}

8
src/translations/locales/uz.json

@ -316,5 +316,11 @@
"Clear": "Tozalash",
"This field is required": "Бу майдон тўлдирилиши шарт",
"Habib Coins": "Habib tangalari",
"{} Habib Coins": "{} Habib tangalari"
"{} Habib Coins": "{} Habib tangalari",
"New Message": "Янги хабар",
"Text Message": "Матнли хабар",
"Voice Call": "Овозли қўнғироқ",
"Video Call": "Видео қўнғироқ",
"No public information is available to display.": "Ko'rsatish uchun umumiy ma'lumot mavjud emas.",
"General Information & Personal Details": "Umumiy ma'lumotlar va shaxsiy tafsilotlar"
}

8
src/translations/locales/zh.json

@ -779,5 +779,11 @@
"Clear": "清除",
"This field is required": "此字段为必填项",
"Habib Coins": "哈比卜币",
"{} Habib Coins": "{} 哈比布金币"
"{} Habib Coins": "{} 哈比布金币",
"New Message": "新消息",
"Text Message": "短信",
"Voice Call": "语音通话",
"Video Call": "视频通话",
"No public information is available to display.": "没有可显示的公开信息。",
"General Information & Personal Details": "一般信息和个人资料"
}
Loading…
Cancel
Save