nsfwapp/frontend/src/components/ui/ModelGenderIcon.tsx
2026-04-27 14:40:16 +02:00

176 lines
4.2 KiB
TypeScript

// frontend\src\components\ui\ModelGenderIcon.tsx
'use client'
import { type ComponentType, type ReactNode } from 'react'
function cn(...parts: Array<string | false | null | undefined>) {
return parts.filter(Boolean).join(' ')
}
type GenderKind = 'female' | 'male' | 'trans' | 'couple' | null
function normalizeGender(
gender?: string | null,
): GenderKind {
const raw = `${gender || ''}`.trim().toLowerCase()
if (!raw) return null
if (raw.includes('couple') || raw.includes('pair') || raw === 'c') {
return 'couple'
}
if (
raw.includes('trans') ||
raw.includes('transgender') ||
raw === 't'
) {
return 'trans'
}
if (
raw.includes('female') ||
raw.includes('girl') ||
raw.includes('woman') ||
raw === 'f' ||
raw === 'w'
) {
return 'female'
}
if (
raw.includes('male') ||
raw.includes('boy') ||
raw.includes('man') ||
raw === 'm'
) {
return 'male'
}
return null
}
type IconProps = {
className?: string
}
function BaseIcon({
children,
className,
}: {
children: ReactNode
className?: string
}) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={cn('size-3.5', className)}
>
{children}
</svg>
)
}
function FemaleIcon({ className }: IconProps) {
return (
<BaseIcon className={className}>
<circle cx="12" cy="8" r="4.25" />
<path d="M12 12.25V20" />
<path d="M9 17H15" />
</BaseIcon>
)
}
function MaleIcon({ className }: IconProps) {
return (
<BaseIcon className={className}>
<circle cx="9" cy="15" r="4.25" />
<path d="M12 12L19 5" />
<path d="M14.75 5H19V9.25" />
</BaseIcon>
)
}
function TransIcon({ className }: IconProps) {
return (
<BaseIcon className={className}>
<circle cx="10.5" cy="10.5" r="3.75" />
<path d="M13.25 7.75L18.5 2.5" />
<path d="M15.75 2.5H18.5V5.25" />
<path d="M10.5 14.25V21" />
<path d="M7.75 18.25H13.25" />
<path d="M6.5 10.5H2.75" />
<path d="M4.5 8.75L2.75 10.5L4.5 12.25" />
</BaseIcon>
)
}
function CoupleIcon({ className }: IconProps) {
return (
<BaseIcon className={className}>
<circle cx="8" cy="9" r="3" />
<circle cx="16" cy="9" r="3" />
<path d="M3.75 18.5C4.5 15.75 6 14.5 8 14.5C10 14.5 11.5 15.75 12.25 18.5" />
<path d="M11.75 18.5C12.5 15.75 14 14.5 16 14.5C18 14.5 19.5 15.75 20.25 18.5" />
</BaseIcon>
)
}
type Props = {
gender?: string | null
variant?: 'hero' | 'default'
className?: string
iconClassName?: string
}
export default function ModelGenderIcon({
gender,
variant = 'default',
className,
iconClassName,
}: Props) {
const kind = normalizeGender(gender)
if (!kind) return null
const map: Record<Exclude<GenderKind, null>, { label: string; Icon: ComponentType<IconProps> }> = {
female: { label: 'Female', Icon: FemaleIcon },
male: { label: 'Male', Icon: MaleIcon },
trans: { label: 'Trans', Icon: TransIcon },
couple: { label: 'Couple', Icon: CoupleIcon },
}
const { label, Icon } = map[kind]
const toneClass =
kind === 'female'
? 'bg-pink-100 text-pink-700 ring-pink-200 dark:bg-pink-500/15 dark:text-pink-200 dark:ring-pink-400/25'
: kind === 'male'
? 'bg-sky-100 text-sky-700 ring-sky-200 dark:bg-sky-500/15 dark:text-sky-200 dark:ring-sky-400/25'
: kind === 'trans'
? 'bg-violet-100 text-violet-700 ring-violet-200 dark:bg-violet-500/15 dark:text-violet-200 dark:ring-violet-400/25'
: 'bg-gradient-to-r from-sky-100 to-pink-100 text-fuchsia-700 ring-fuchsia-200 dark:from-sky-500/15 dark:to-pink-500/15 dark:text-pink-200 dark:ring-pink-400/25'
return (
<span
className={cn(
'inline-flex shrink-0 items-center justify-center rounded-full ring-1 ring-inset',
variant === 'hero'
? cn('size-5 backdrop-blur shadow-sm', toneClass)
: cn('size-5', toneClass),
className
)}
title={label}
aria-label={label}
>
<Icon className={iconClassName} />
</span>
)
}