// 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 ( ) } export function FemaleGenitaliaIcon(props: IconProps) { return ( ) } export function FingeringIcon(props: IconProps) { return ( ) } export function BlowjobIcon(props: IconProps) { return ( ) } export function HandjobIcon(props: IconProps) { return ( ) } export function SixtyNineIcon(props: IconProps) { return ( ) } export function CowgirlIcon(props: IconProps) { return ( ) } export function CunnilingusIcon(props: IconProps) { return ( ) } export function FacesittingSexIcon(props: IconProps) { return ( {/* sitzende Person oben */} {/* Gesicht unten */} {/* Kontakt / Richtung */} ) } export function ProneBoneIcon(props: IconProps) { return ( ) } export function SittingSexIcon(props: IconProps) { return ( {/* sitzende Person */} {/* zweite Person unten / Schoß */} {/* Kontakt */} ) } export function SpooningSexIcon(props: IconProps) { return ( {/* hintere Person */} {/* vordere Person */} {/* umarmender Arm */} ) } 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 BraIcon(props: IconProps) { return ( ) } export function PantiesIcon(props: IconProps) { return ( ) } export function LingerieIcon(props: IconProps) { return ( ) } export function DressIcon(props: IconProps) { return ( ) } export function CropTopIcon(props: IconProps) { return ( ) } export function StockingsIcon(props: IconProps) { return ( ) } export function HighHeelsIcon(props: IconProps) { return ( ) } export function CollarIcon(props: IconProps) { return ( ) } export function SkirtIcon(props: IconProps) { return ( ) } export function ToyIcon(props: IconProps) { return ( ) } export function DildoIcon(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 HandcuffsIcon(props: IconProps) { return ( ) } export function HandIcon(props: IconProps) { return ( ) } export function MouthIcon(props: IconProps) { return ( ) } export function TongueIcon(props: IconProps) { return ( ) } export function ShowerIcon(props: IconProps) { return ( ) } export function BlindfoldIcon(props: IconProps) { return ( ) } export function ButtPlugIcon(props: IconProps) { return ( ) } export function StrapOnIcon(props: IconProps) { return ( ) } export function TowelIcon(props: IconProps) { return ( ) } export function BikiniIcon(props: IconProps) { return ( ) } export function HotpantsIcon(props: IconProps) { return ( ) } export function UnknownContentIcon(props: IconProps) { return ( ) } export function OtherContentIcon(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: FacesittingSexIcon, }, { match: ['prone_bone', 'prone-bone', 'prone bone'], text: 'Prone Bone', icon: ProneBoneIcon, }, { match: ['sitting', 'sitting_sex', 'sitting-sex', 'seated', 'seated_sex'], text: 'Sitting', icon: SittingSexIcon, }, { match: ['spooning', 'spoons', 'spoon'], text: 'Spooning', icon: SpooningSexIcon, }, { match: ['handjob', 'hand_job', 'hand-job', 'manual'], text: 'Handjob', icon: HandjobIcon, }, { match: ['fingering', 'fingered'], text: 'Fingering', icon: FingeringIcon, }, { match: ['oral'], text: 'Oral', icon: MouthIcon, }, { match: ['anal'], text: 'Anal', icon: AnusIcon, }, { 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, }, { match: ['other', 'misc', 'miscellaneous'], text: 'Andere', icon: OtherContentIcon, }, // 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: ['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: DildoIcon, }, { match: ['handcuffs', 'cuffs'], text: 'Handschellen', icon: HandcuffsIcon, }, { 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_position', 'toy-position', 'toy sex', 'toy_sex', 'toy_play', 'toy-play'], text: 'Toy', icon: ToyIcon, }, // Clothing { match: ['bikini'], text: 'Bikini', icon: BikiniIcon, }, { match: ['bra', 'brassiere'], text: 'BH', icon: BraIcon, }, { match: ['dress'], text: 'Kleid', icon: DressIcon, }, { match: ['heels', 'heel', 'high_heels', 'high-heels'], text: 'High Heels', icon: HighHeelsIcon, }, { match: ['hotpants', 'shorts'], text: 'Hotpants', icon: HotpantsIcon, }, { match: ['lingerie'], text: 'Lingerie', icon: LingerieIcon, }, { match: ['panties', 'underwear', 'briefs', 'boxers', 'thong'], text: 'Panties', icon: PantiesIcon, }, { match: ['skirt'], text: 'Rock', icon: SkirtIcon, }, { match: ['stockings', 'stocking'], text: 'Stockings', icon: StockingsIcon, }, { match: ['top', 'crop_top', 'crop-top', 'croptop'], text: 'Crop Top', icon: CropTopIcon, }, ] function normalizeSegmentLabel(label?: string): string { return String(label || '').trim().toLowerCase() } function normalizeLabelKey(value?: string): string { return String(value || '') .trim() .toLowerCase() .replace(/^(object|position|body|clothing|detector):/, '') .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 } } // Covered-Fallback vor Bodypart-Fallbacks. if (labelKey.includes('_covered') || labelKey.includes('covered_')) { return { match: [], text: prettifyUnknownLabel(normalized) .replace(/\berkannt\b/gi, '') .replace(/\bexposed\b/gi, '') .trim() || 'Bedeckt', icon: CoveredContentIcon, } } // Bodyparts // Spezialfall zuerst, weil "male_breast" sonst vom allgemeinen "breast" gefangen wird. if (labelKey.includes('male_breast')) { return { match: [], text: 'Männliche Brust', icon: MaleBreastIcon } } if ( labelKey.includes('ass') || labelKey.includes('butt') || labelKey.includes('bottom') ) { return { match: [], text: 'Hintern', icon: ButtocksIcon } } if ( labelKey.includes('pussy') || labelKey.includes('vagina') || labelKey.includes('vulva') || labelKey.includes('labia') ) { return { match: [], text: 'Vagina', icon: FemaleGenitaliaIcon } } if (labelKey.includes('penis')) { return { match: [], text: 'Penis', icon: MaleGenitaliaIcon } } if (labelKey.includes('breast')) { return { match: [], text: 'Brüste', icon: FemaleBreastIcon } } if (labelKey.includes('tongue')) { return { match: [], text: 'Zunge', icon: TongueIcon } } if (labelKey.includes('mouth') || labelKey.includes('lips')) { return { match: [], text: 'Mund', icon: MouthIcon } } if ( labelKey === 'face' || labelKey === 'face_female' || labelKey === 'face_male' || labelKey === 'female_face' || labelKey === 'male_face' || labelKey.includes('face_exposed') || labelKey.includes('face_covered') || labelKey.endsWith('_face') || labelKey.startsWith('face_') ) { return { match: [], text: 'Gesicht', icon: FaceIcon } } if ( labelKey.includes('belly') || labelKey.includes('stomach') || labelKey.includes('abdomen') || labelKey.includes('navel') ) { return { match: [], text: 'Bauch', icon: BellyIcon } } if (labelKey.includes('foot') || labelKey.includes('feet')) { return { match: [], text: 'Füße', icon: FeetIcon } } // Objects if (labelKey.includes('shower')) { return { match: [], text: 'Dusche', icon: ShowerIcon } } if (labelKey.includes('blindfold')) { return { match: [], text: 'Augenbinde', icon: BlindfoldIcon } } if (labelKey.includes('collar') || labelKey.includes('choker')) { return { match: [], text: 'Halsband / Choker', icon: CollarIcon } } if (labelKey.includes('buttplug') || labelKey.includes('butt_plug')) { return { match: [], text: 'Buttplug', icon: ButtPlugIcon } } if ( labelKey.includes('strapon') || labelKey.includes('strap_on') ) { return { match: [], text: 'Strap-on', icon: StrapOnIcon } } if (labelKey.includes('dildo')) { return { match: [], text: 'Dildo', icon: DildoIcon } } if (labelKey.includes('vibrator')) { return { match: [], text: 'Vibrator', icon: ToyIcon } } if (labelKey.includes('handcuff') || labelKey.includes('cuff')) { return { match: [], text: 'Handschellen', icon: HandcuffsIcon } } if (labelKey.includes('towel')) { return { match: [], text: 'Handtuch', icon: TowelIcon } } // Clothing if (labelKey.includes('bikini')) { return { match: [], text: 'Bikini', icon: BikiniIcon } } if (labelKey.includes('bra') || labelKey.includes('brassiere')) { return { match: [], text: 'BH', icon: BraIcon } } if (labelKey.includes('dress')) { return { match: [], text: 'Kleid', icon: DressIcon } } if (labelKey.includes('skirt')) { return { match: [], text: 'Rock', icon: SkirtIcon } } if (labelKey.includes('heel')) { return { match: [], text: 'High Heels', icon: HighHeelsIcon } } if (labelKey.includes('hotpants') || labelKey.includes('shorts')) { return { match: [], text: 'Hotpants', icon: HotpantsIcon } } if (labelKey.includes('lingerie')) { return { match: [], text: 'Lingerie', icon: LingerieIcon } } if ( labelKey.includes('panties') || labelKey.includes('underwear') || labelKey.includes('briefs') || labelKey.includes('boxers') || labelKey.includes('thong') ) { return { match: [], text: 'Panties', icon: PantiesIcon } } if (labelKey.includes('stocking')) { return { match: [], text: 'Stockings', icon: StockingsIcon } } if ( labelKey === 'top' || labelKey.includes('crop_top') || labelKey.includes('croptop') ) { return { match: [], text: 'Crop Top', icon: CropTopIcon } } if (labelKey.includes('other') || labelKey.includes('misc')) { return { match: [], text: 'Andere', icon: OtherContentIcon } } 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(/\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(/\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, 'Lingerie') .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(/\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(/\bprone bone\b/g, 'Prone Bone') .replace(/\bpronebone\b/g, 'Prone Bone') .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] }