// frontend\src\components\ui\Icons.tsx 'use client' import type { JSX, ReactNode, SVGProps } from 'react' export type IconProps = SVGProps & { title?: string } type IconComponent = (props: IconProps) => JSX.Element function iconClassName(className: string | undefined, extra: string) { return [className, extra].filter(Boolean).join(' ') } function IconBase({ title, children, viewBox = '0 0 24 24', className, ...props }: IconProps & { children: ReactNode }) { return ( {title ? {title} : null} {children} ) } /* ---------------------------------- */ /* Einzelne Icons */ /* ---------------------------------- */ export function FemaleBreastIcon(props: IconProps) { return ( ) } export function ButtocksIcon(props: IconProps) { return ( {/* obere Kante */} {/* linke Außenkontur */} {/* rechte Außenkontur */} {/* mittlere Falte */} {/* innere Po-Linien */} ) } export function FemaleGenitaliaIcon(props: IconProps) { return ( {/* äußere Form */} {/* mittlere Form */} {/* obere kleine Form */} {/* innere längliche Form */} ) } export function FingeringIcon(props: IconProps) { return ( {/* stilisierte Vulva weiter rechts */} {/* Zeige- und Mittelfinger seitlich von links */} {/* Handfläche / Handkante - bewusst weiter weg von der Vulva */} {/* angedeutete eingeklappte Finger / Knöchel */} {/* Daumen */} {/* kleine Bewegungslinien */} ) } export function BlowjobIcon(props: IconProps) { return ( ) } export function HandjobIcon(props: IconProps) { return ( {/* Penis vertikal */} {/* Hoden */} {/* Hand umfasst den Schaft */} {/* Finger über dem Schaft */} {/* Daumen */} {/* Griff-/Bewegungslinien */} ) } export function SixtyNineIcon(props: IconProps) { return ( ) } export function CowgirlIcon(props: IconProps) { return ( ) } export function CunnilingusIcon(props: IconProps) { return ( ) } export function DoggyIcon(props: IconProps) { return ( ) } export function MissionaryIcon(props: IconProps) { return ( ) } export function ReverseCowgirlIcon(props: IconProps) { return ( ) } export function StandingSexIcon(props: IconProps) { return ( ) } export function StandingDoggySexIcon(props: IconProps) { return ( ) } export function MaleGenitaliaIcon(props: IconProps) { return ( ) } export function AnusIcon(props: IconProps) { return ( ) } export function CoveredContentIcon(props: IconProps) { return ( ) } export function MaleBreastIcon(props: IconProps) { return ( ) } export function BellyIcon(props: IconProps) { return ( ) } export function FeetIcon(props: IconProps) { return ( ) } export function FaceIcon(props: IconProps) { return ( ) } export function FemalePersonIcon(props: IconProps) { return ( ) } export function MalePersonIcon(props: IconProps) { return ( ) } export function ClothingIcon(props: IconProps) { return ( ) } export function CropTopIcon(props: IconProps) { return ( {/* Träger */} {/* kurze Top-Form */} {/* Crop-Kante */} {/* leichte Stofffalten */} ) } export function BraIcon(props: IconProps) { return ( ) } export function UnderwearIcon(props: IconProps) { return ( ) } export function PantsIcon(props: IconProps) { return ( ) } export function DressIcon(props: IconProps) { return ( ) } export function ShoesIcon(props: IconProps) { return ( ) } export function SockIcon(props: IconProps) { return ( ) } export function CondomIcon(props: IconProps) { return ( ) } export function ToyIcon(props: IconProps) { return ( ) } export function BottleIcon(props: IconProps) { return ( ) } export function BedIcon(props: IconProps) { return ( ) } export function ChairIcon(props: IconProps) { return ( ) } export function PhoneIcon(props: IconProps) { return ( ) } export function CameraIcon(props: IconProps) { return ( ) } export function RopeIcon(props: IconProps) { return ( ) } export function HandIcon(props: IconProps) { return ( ) } export function MouthIcon(props: IconProps) { return ( ) } export function TongueIcon(props: IconProps) { return ( {/* äußere Lippenkontur */} {/* innere Lippen / Mundöffnung */} {/* Zunge */} ) } export function BathIcon(props: IconProps) { return ( ) } export function ShowerIcon(props: IconProps) { return ( ) } export function BlindfoldIcon(props: IconProps) { return ( ) } export function CollarIcon(props: IconProps) { return ( ) } export function ButtPlugIcon(props: IconProps) { return ( ) } export function StrapOnIcon(props: IconProps) { return ( ) } export function TowelIcon(props: IconProps) { return ( {/* äußere Handtuchform */} {/* obere innere Faltkante */} {/* gefaltete Lagen unten */} {/* kleiner Aufhänger / untere Lasche */} ) } export function BikiniIcon(props: IconProps) { return ( ) } export function FishnetIcon(props: IconProps) { return ( ) } export function HotpantsIcon(props: IconProps) { return ( ) } export function UnknownContentIcon(props: IconProps) { return ( ) } /* ---------------------------------- */ /* Label-Mapping */ /* ---------------------------------- */ type SegmentLabelMeta = { match: string[] text: string icon: IconComponent } const SEGMENT_LABEL_META: SegmentLabelMeta[] = [ // Sex positions { match: ['69', 'sixty_nine', 'sixty-nine', 'sixtynine'], text: '69', icon: SixtyNineIcon, }, { match: ['missionary'], text: 'Missionary', icon: MissionaryIcon, }, { match: ['reverse_cowgirl', 'reverse-cowgirl', 'reverse cowgirl'], text: 'Reverse Cowgirl', icon: ReverseCowgirlIcon, }, { match: ['cowgirl', 'riding', 'ride'], text: 'Cowgirl', icon: CowgirlIcon, }, { match: ['blowjob', 'fellatio'], text: 'Blowjob', icon: BlowjobIcon, }, { match: ['cunnilingus'], text: 'Cunnilingus', icon: CunnilingusIcon, }, { match: ['facesitting', 'face_sitting', 'face-sitting', 'face sitting'], text: 'Facesitting', icon: FemalePersonIcon, }, { match: ['oral'], text: 'Oral', icon: MouthIcon, }, { match: ['anal'], text: 'Anal', icon: AnusIcon, }, { match: ['handjob', 'hand_job', 'hand-job', 'manual'], text: 'Handjob', icon: HandjobIcon, }, { match: ['fingering', 'fingered'], text: 'Fingering', icon: FingeringIcon, }, { match: ['solo', 'masturbation'], text: 'Solo', icon: HandIcon, }, { match: ['standing_doggy', 'standing-doggy', 'standing doggy'], text: 'Standing Doggy', icon: StandingDoggySexIcon, }, { match: ['standing', 'standing_sex', 'standing-sex', 'standing sex'], text: 'Stehend', icon: StandingSexIcon, }, { match: ['doggy', 'doggystyle', 'doggy_style'], text: 'Doggy', icon: DoggyIcon, }, { match: ['unknown'], text: 'Unbekannt', icon: UnknownContentIcon, }, // People { match: ['person_female', 'female_person'], text: 'Weibliche Person', icon: FemalePersonIcon, }, { match: ['person_male', 'male_person'], text: 'Männliche Person', icon: MalePersonIcon, }, { match: ['person_unknown', 'person'], text: 'Person', icon: UnknownContentIcon, }, // Bodyparts { match: ['anus_exposed', 'anus'], text: 'Anus', icon: AnusIcon, }, { match: ['ass', 'buttocks_exposed', 'buttocks', 'bottom'], text: 'Hintern', icon: ButtocksIcon, }, { match: ['breasts', 'female_breast_exposed', 'breast_exposed', 'breasts_exposed', 'female_breast'], text: 'Brüste', icon: FemaleBreastIcon, }, { match: ['penis', 'male_genitalia_exposed', 'penis_exposed'], text: 'Penis', icon: MaleGenitaliaIcon, }, { match: ['tongue'], text: 'Zunge', icon: TongueIcon, }, { match: ['mouth', 'lips'], text: 'Mund', icon: MouthIcon, }, { match: ['pussy', 'female_genitalia_exposed', 'vulva_exposed', 'labia_exposed', 'vagina'], text: 'Vagina', icon: FemaleGenitaliaIcon, }, { match: ['belly_exposed', 'stomach_exposed', 'abdomen_exposed', 'navel_exposed', 'belly', 'stomach', 'abdomen', 'navel'], text: 'Bauch', icon: BellyIcon, }, { match: ['feet_exposed', 'foot_exposed', 'feet', 'foot'], text: 'Füße', icon: FeetIcon, }, { match: ['face_female', 'face_male', 'face', 'female_face', 'male_face'], text: 'Gesicht', icon: FaceIcon, }, { match: ['hand', 'hands', 'finger', 'fingers'], text: 'Hand', icon: HandIcon, }, // Objects { match: ['bath', 'bathtub', 'tub'], text: 'Badewanne', icon: BathIcon, }, { match: ['blindfold'], text: 'Augenbinde', icon: BlindfoldIcon, }, { match: ['buttplug', 'butt_plug'], text: 'Buttplug', icon: ButtPlugIcon, }, { match: ['collar', 'choker'], text: 'Halsband / Choker', icon: CollarIcon, }, { match: ['dildo'], text: 'Dildo', icon: ToyIcon, }, { match: ['handcuffs', 'cuffs'], text: 'Handschellen', icon: RopeIcon, }, { match: ['rope', 'tie', 'restraint', 'restraints'], text: 'Seil', icon: RopeIcon, }, { match: ['shower'], text: 'Dusche', icon: ShowerIcon, }, { match: ['strapon', 'strap_on', 'strap-on'], text: 'Strap-on', icon: StrapOnIcon, }, { match: ['towel'], text: 'Handtuch', icon: TowelIcon, }, { match: ['vibrator'], text: 'Vibrator', icon: ToyIcon, }, { match: ['toy', 'plug'], text: 'Toy', icon: ToyIcon, }, { match: ['condom'], text: 'Kondom', icon: CondomIcon, }, { match: ['bottle', 'lotion', 'lubricant', 'lube'], text: 'Flasche', icon: BottleIcon, }, { match: ['bed', 'mattress', 'pillow', 'blanket', 'sheet', 'sheets'], text: 'Bett', icon: BedIcon, }, { match: ['chair', 'sofa', 'couch', 'bench', 'stool'], text: 'Möbel', icon: ChairIcon, }, { match: ['phone', 'smartphone', 'mobile'], text: 'Handy', icon: PhoneIcon, }, { match: ['camera', 'webcam'], text: 'Kamera', icon: CameraIcon, }, // Clothing { match: ['bikini'], text: 'Bikini', icon: BikiniIcon, }, { match: ['bra', 'brassiere'], text: 'BH', icon: BraIcon, }, { match: ['dress'], text: 'Kleid', icon: DressIcon, }, { match: ['fishnet', 'fishnets'], text: 'Fishnet', icon: FishnetIcon, }, { match: ['heels', 'heel', 'high_heels', 'high-heels'], text: 'High Heels', icon: ShoesIcon, }, { match: ['hotpants', 'shorts'], text: 'Hotpants', icon: HotpantsIcon, }, { match: ['lingerie'], text: 'Lingerie', icon: UnderwearIcon, }, { match: ['panties', 'underwear', 'briefs', 'boxers', 'thong'], text: 'Panties', icon: UnderwearIcon, }, { match: ['shirt', 'tshirt', 't-shirt', 'blouse', 'hoodie', 'sweater', 'jacket', 'coat'], text: 'Shirt', icon: ClothingIcon, }, { match: ['skirt'], text: 'Rock', icon: DressIcon, }, { match: ['stockings', 'stocking', 'socks', 'sock'], text: 'Stockings', icon: SockIcon, }, { match: ['top', 'crop_top', 'crop-top', 'croptop'], text: 'Crop Top', icon: CropTopIcon, }, { match: ['pants', 'jeans', 'trousers', 'leggings'], text: 'Hose', icon: PantsIcon, }, { match: ['shoe', 'shoes', 'boot', 'boots', 'sneaker', 'sneakers'], text: 'Schuhe', icon: ShoesIcon, }, { match: ['clothing', 'clothes', 'garment', 'outfit'], text: 'Kleidung', icon: ClothingIcon, }, ] function normalizeSegmentLabel(label?: string): string { return String(label || '').trim().toLowerCase() } function normalizeLabelKey(value?: string): string { return String(value || '') .trim() .toLowerCase() .replaceAll('-', '_') .replaceAll(' ', '_') } function labelMatches(normalizedLabel: string, key: string): boolean { const label = normalizeLabelKey(normalizedLabel) const matchKey = normalizeLabelKey(key) if (!label || !matchKey) return false return ( label === matchKey || label.startsWith(`${matchKey}_`) || label.endsWith(`_${matchKey}`) || label.includes(`_${matchKey}_`) ) } function findSegmentLabelMeta(label?: string): SegmentLabelMeta | null { const normalized = normalizeSegmentLabel(label) if (!normalized) return null const labelKey = normalizeLabelKey(normalized) // Wichtig: // Personen-Labels exakt vor jedem Fuzzy-Matching behandeln. // Sonst kann "person" auch "person_male" / "person_female" matchen. if (labelKey === 'person_male' || labelKey === 'male_person') { return { match: ['person_male', 'male_person'], text: 'Männliche Person', icon: MalePersonIcon, } } if (labelKey === 'person_female' || labelKey === 'female_person') { return { match: ['person_female', 'female_person'], text: 'Weibliche Person', icon: FemalePersonIcon, } } if (labelKey === 'person' || labelKey === 'person_unknown') { return { match: ['person_unknown', 'person'], text: 'Person', icon: UnknownContentIcon, } } // Erst exakte Matches prüfen. for (const item of SEGMENT_LABEL_META) { if (item.match.some((key) => labelKey === normalizeLabelKey(key))) { return item } } // Danach erst weiche Matches prüfen. for (const item of SEGMENT_LABEL_META) { if (item.match.some((key) => labelMatches(normalized, key))) { return item } } if (normalized.includes('_covered') || normalized.includes('covered_')) { return { match: [], text: prettifyUnknownLabel(normalized).replace(/\bExposed\b/g, '').trim() || 'Bedeckt', icon: CoveredContentIcon, } } if (normalized.includes('ass') || normalized.includes('butt') || normalized.includes('bottom')) { return { match: [], text: 'Hintern', icon: ButtocksIcon } } if ( normalized.includes('pussy') || normalized.includes('vagina') || normalized.includes('vulva') || normalized.includes('labia') ) { return { match: [], text: 'Vagina', icon: FemaleGenitaliaIcon } } if (normalized.includes('penis')) { return { match: [], text: 'Penis', icon: MaleGenitaliaIcon } } if (normalized.includes('breast')) { return { match: [], text: 'Brüste', icon: FemaleBreastIcon } } if (normalized.includes('tongue')) { return { match: [], text: 'Zunge', icon: TongueIcon } } if (normalized.includes('mouth') || normalized.includes('lips')) { return { match: [], text: 'Mund', icon: MouthIcon } } if (normalized.includes('bath') || normalized.includes('tub')) { return { match: [], text: 'Badewanne', icon: BathIcon } } if (normalized.includes('shower')) { return { match: [], text: 'Dusche', icon: ShowerIcon } } if (normalized.includes('blindfold')) { return { match: [], text: 'Augenbinde', icon: BlindfoldIcon } } if (normalized.includes('collar')) { return { match: [], text: 'Halsband', icon: CollarIcon } } if (normalized.includes('buttplug') || normalized.includes('butt_plug')) { return { match: [], text: 'Buttplug', icon: ButtPlugIcon } } if ( normalized.includes('strapon') || normalized.includes('strap_on') || normalized.includes('strap-on') ) { return { match: [], text: 'Strap-on', icon: StrapOnIcon } } if (normalized.includes('dildo')) { return { match: [], text: 'Dildo', icon: ToyIcon } } if (normalized.includes('vibrator')) { return { match: [], text: 'Vibrator', icon: ToyIcon } } if (normalized.includes('handcuff') || normalized.includes('cuff')) { return { match: [], text: 'Handschellen', icon: RopeIcon } } if (normalized.includes('rope') || normalized.includes('restraint') || normalized.includes('tie')) { return { match: [], text: 'Seil', icon: RopeIcon } } if (normalized.includes('towel')) { return { match: [], text: 'Handtuch', icon: TowelIcon } } if (normalized.includes('bikini')) { return { match: [], text: 'Bikini', icon: BikiniIcon } } if (normalized.includes('bra')) { return { match: [], text: 'BH', icon: BraIcon } } if (normalized.includes('dress')) { return { match: [], text: 'Kleid', icon: DressIcon } } if (normalized.includes('skirt')) { return { match: [], text: 'Rock', icon: DressIcon } } if (normalized.includes('fishnet')) { return { match: [], text: 'Fishnet', icon: FishnetIcon } } if (normalized.includes('heel')) { return { match: [], text: 'High Heels', icon: ShoesIcon } } if (normalized.includes('hotpants') || normalized.includes('shorts')) { return { match: [], text: 'Hotpants', icon: HotpantsIcon } } if ( normalized.includes('lingerie') || normalized.includes('panties') || normalized.includes('underwear') || normalized.includes('briefs') || normalized.includes('thong') ) { return { match: [], text: 'Unterwäsche', icon: UnderwearIcon } } if (normalized.includes('stocking') || normalized.includes('sock')) { return { match: [], text: 'Stockings', icon: SockIcon } } if ( normalized === 'top' || normalized.includes('crop_top') || normalized.includes('crop-top') || normalized.includes('croptop') ) { return { match: [], text: 'Crop Top', icon: CropTopIcon } } if ( normalized.includes('shirt') || normalized.includes('tshirt') || normalized.includes('t-shirt') || normalized.includes('blouse') || normalized.includes('hoodie') || normalized.includes('sweater') || normalized.includes('jacket') || normalized.includes('coat') || normalized.includes('clothing') ) { return { match: [], text: 'Oberteil', icon: ClothingIcon } } if ( normalized.includes('pants') || normalized.includes('jeans') || normalized.includes('trousers') || normalized.includes('leggings') ) { return { match: [], text: 'Hose', icon: PantsIcon } } if ( normalized.includes('shoe') || normalized.includes('boot') || normalized.includes('sneaker') ) { return { match: [], text: 'Schuhe', icon: ShoesIcon } } if ( normalized === 'face' || normalized === 'face_female' || normalized === 'face_male' || normalized === 'female_face' || normalized === 'male_face' || normalized.includes('face_exposed') || normalized.includes('face_covered') || normalized.endsWith('_face') || normalized.startsWith('face_') ) { return { match: [], text: 'Gesicht', icon: FaceIcon } } if ( normalized.includes('belly') || normalized.includes('stomach') || normalized.includes('abdomen') || normalized.includes('navel') ) { return { match: [], text: 'Bauch', icon: BellyIcon } } if (normalized.includes('foot') || normalized.includes('feet')) { return { match: [], text: 'Füße', icon: FeetIcon } } if (normalized.includes('male_breast')) { return { match: [], text: 'Männliche Brust', icon: MaleBreastIcon } } return null } function prettifyUnknownLabel(label?: string): string { const normalized = normalizeSegmentLabel(label) if (!normalized) return 'Unbekannt' return normalized .replaceAll('_', ' ') .replace(/\bcrop top\b/g, 'Crop Top') .replace(/\bcroptop\b/g, 'Crop Top') .replace(/\bmale\b/g, 'männlich') .replace(/\bfemale\b/g, 'weiblich') .replace(/\bgenitalia\b/g, 'Genitalien') .replace(/\bbreasts\b/g, 'Brüste') .replace(/\bbreast\b/g, 'Brust') .replace(/\bbuttocks\b/g, 'Gesäß') .replace(/\bass\b/g, 'Hintern') .replace(/\bback\b/g, 'Rücken') .replace(/\banus\b/g, 'Anus') .replace(/\bpussy\b/g, 'Vagina') .replace(/\bpenis\b/g, 'Penis') .replace(/\btongue\b/g, 'Zunge') .replace(/\bbath\b/g, 'Badewanne') .replace(/\bblindfold\b/g, 'Augenbinde') .replace(/\bbuttplug\b/g, 'Buttplug') .replace(/\bbutt plug\b/g, 'Buttplug') .replace(/\bcollar\b/g, 'Halsband') .replace(/\bdildo\b/g, 'Dildo') .replace(/\bhandcuffs\b/g, 'Handschellen') .replace(/\brope\b/g, 'Seil') .replace(/\bshower\b/g, 'Dusche') .replace(/\bstrapon\b/g, 'Strap-on') .replace(/\bstrap on\b/g, 'Strap-on') .replace(/\btowel\b/g, 'Handtuch') .replace(/\bvibrator\b/g, 'Vibrator') .replace(/\bbikini\b/g, 'Bikini') .replace(/\bbra\b/g, 'BH') .replace(/\bdress\b/g, 'Kleid') .replace(/\bfishnet\b/g, 'Fishnet') .replace(/\bheels\b/g, 'High Heels') .replace(/\bheel\b/g, 'High Heel') .replace(/\bhotpants\b/g, 'Hotpants') .replace(/\blingerie\b/g, 'Lingerie') .replace(/\bpanties\b/g, 'Panties') .replace(/\bshirt\b/g, 'Shirt') .replace(/\bskirt\b/g, 'Rock') .replace(/\bstockings\b/g, 'Stockings') .replace(/\bstocking\b/g, 'Stocking') .replace(/\btop\b/g, 'Crop Top') .replace(/\bbelly\b/g, 'Bauch') .replace(/\bstomach\b/g, 'Bauch') .replace(/\babdomen\b/g, 'Bauch') .replace(/\bnavel\b/g, 'Bauchnabel') .replace(/\bfeet\b/g, 'Füße') .replace(/\bfoot\b/g, 'Fuß') .replace(/\bface\b/g, 'Gesicht') .replace(/\bfacesitting\b/g, 'Facesitting') .replace(/\bface sitting\b/g, 'Facesitting') .replace(/\bfingering\b/g, 'Fingering') .replace(/\bfingered\b/g, 'Fingering') .replace(/\bblowjob\b/g, 'Blowjob') .replace(/\bfellatio\b/g, 'Blowjob') .replace(/\bhandjob\b/g, 'Handjob') .replace(/\bhand job\b/g, 'Handjob') .replace(/\bcovered\b/g, 'bedeckt') .replace(/\bexposed\b/g, 'erkannt') .replace(/\s+/g, ' ') .trim() .replace(/^./, (char) => char.toUpperCase()) } export function getSegmentIcon(label?: string): IconComponent { return findSegmentLabelMeta(label)?.icon ?? UnknownContentIcon } export function getSegmentLabelText(label?: string): string { return findSegmentLabelMeta(label)?.text ?? prettifyUnknownLabel(label) } export function SegmentLabelIcon({ label, ...props }: IconProps & { label?: string }) { const Icon = getSegmentIcon(label) return } export type SegmentLabelItem = { key: string text: string icon: IconComponent } export function getSegmentLabelItem(label?: string): SegmentLabelItem { const meta = findSegmentLabelMeta(label) if (meta) { return { key: normalizeSegmentLabel(label) || meta.text, text: meta.text, icon: meta.icon, } } return { key: normalizeSegmentLabel(label) || 'unknown', text: prettifyUnknownLabel(label), icon: UnknownContentIcon, } } export function getSegmentLabelItems(labels: Array): SegmentLabelItem[] { const out: SegmentLabelItem[] = [] const seen = new Set() for (const raw of labels) { const item = getSegmentLabelItem(raw || undefined) const dedupeKey = `${item.text}::${item.icon.name}` if (seen.has(dedupeKey)) continue seen.add(dedupeKey) out.push(item) } return out } export function getHighestPrioritySegmentLabelItem( labels: Array ): SegmentLabelItem { const items = getSegmentLabelItems(labels) if (items.length === 0) { return getSegmentLabelItem(undefined) } const priority = (item: SegmentLabelItem): number => { switch (item.text) { case 'Vagina': return 0 case 'Brüste': return 1 case 'Gesäß': return 2 case 'Penis': return 3 case 'Anus': return 4 case 'Männliche Brust': return 5 case 'Bauch': return 6 case 'Füße': return 7 case 'Gesicht': return 8 default: return 999 } } return [...items].sort((a, b) => { const pa = priority(a) const pb = priority(b) if (pa !== pb) return pa - pb return a.text.localeCompare(b.text, 'de') })[0] }