<template> <div class="profile" :class="{ expanded, 'with-avatar': !!actor.avatar }" > <div v-if="actor.avatar" class="avatar-container" > <img :src="getPath(actor.avatar, 'thumbnail')" :title="actor.avatar.credit && `© ${actor.avatar.credit}`" class="avatar" > <span v-if="actor.avatar?.credit" class="avatar-credit" >{{ actor.avatar.credit }}</span> </div> <ul class="bio nolist"> <li v-if="actor.dateOfBirth" class="bio-item" > <dfn class="bio-label"><Icon icon="cake" />Date of birth</dfn> <span class="birthdate"> <span class="birthdate-long">{{ formatDate(actor.dateOfBirth, actor.dateOfBirth.getFullYear() > 1 ? 'MMMM d, yyyy' : 'MMMM d') }}</span> <span class="birthdate-short">{{ formatDate(actor.dateOfBirth, actor.dateOfBirth.getFullYear() > 1 ? 'MMM d, yyyy' : 'MMM d') }}</span> <span v-if="!actor.dateOfDeath && actor.dateOfBirth.getFullYear() > 1" class="age" >{{ actor.ageFromBirth }}</span> </span> </li> <li v-else-if="actor.age && !actor.dateOfDeath" class="bio-item" > <dfn class="bio-label"><Icon icon="cake" />Age</dfn> <span :title="'Exact date of birth or age unknown'" class="birthdate" >> {{ actor.age }}</span> </li> <li v-if="actor.dateOfDeath" class="bio-item" > <dfn class="bio-label"><Icon icon="tombstone" />Date of death</dfn> <span class="birthdate">{{ formatDate(actor.dateOfDeath, 'MMMM d, yyyy') }}<span v-if="actor.ageAtDeath" class="age" >{{ actor.ageAtDeath }}</span></span> </li> <li v-if="actor.orientation" class="bio-item" > <dfn class="bio-label"><Icon icon="heart7" />Orientation</dfn> <span class="orientation">{{ actor.orientation }}</span> </li> <li v-if="actor.origin" class="bio-item birth" > <dfn class="bio-label"><Icon icon="home2" />Born in</dfn> <span> <span v-if="actor.origin.city" class="city" >{{ actor.origin.city }}</span><span v-if="actor.origin.state && (!actor.origin.city || (actor.origin.country && actor.origin.country.alpha2 === 'US'))" class="state" >{{ actor.origin.city ? `, ${actor.origin.state}` : actor.origin.state }}</span> <span v-if="actor.origin.country" class="country birthcountry" > <img class="flag" :src="`/img/flags/${actor.origin.country.alpha2.toLowerCase()}.svg`" > <span class="country-name">{{ actor.origin.country.alias || actor.origin.country.name }}</span> <span class="country-alpha2">{{ actor.origin.country.alpha2 }}</span> </span> </span> </li> <li v-if="actor.residence" class="bio-item residence hideable" :class="{ hideable: !!actor.origin }" > <dfn class="bio-label"><Icon icon="location" />Lives in</dfn> <span> <span v-if="actor.residence.city" class="city" >{{ actor.residence.city }}</span><span v-if="actor.residence.state && actor.residence.country && actor.residence.country.alpha2 === 'US'" class="state" >{{ actor.residence.city ? `, ${actor.residence.state}` : actor.residence.state }}</span> <span v-if="actor.residence.country" class="country" > <img class="flag" :src="`/img/flags/${actor.residence.country.alpha2.toLowerCase()}.svg`" >{{ actor.residence.country.alias || actor.residence.country.name }} </span> </span> </li> <li v-if="actor.ethnicity" class="bio-item ethnicity hideable" > <dfn class="bio-label"><Icon icon="earth2" />Ethnicity</dfn> <span>{{ actor.ethnicity }}</span> </li> <li v-if="actor.bust || actor.waist || actor.hip" title="bust-waist-hip" class="bio-item figure" > <dfn class="bio-label"><Icon icon="ruler" />Figure</dfn> <span class="bio-value">{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}</span> </li> <li v-if="actor.naturalBoobs === false || actor.naturalButt === false" class="bio-item augmentations hideable" > <dfn class="bio-label"><Icon icon="magic-wand2" />Enhanced</dfn> <span class="bio-value"> <div v-if="actor.naturalBoobs === false" :title="[ actor.boobsVolume && `${actor.boobsVolume}cc`, augmentationMap[actor.boobsPlacement] || actor.boobsPlacement, augmentationMap[actor.boobsIncision] || actor.boobsIncision, augmentationMap[actor.boobsImplant] || actor.boobsImplant ].filter(Boolean).join(' ')" class="augmentations-section" >Boobs<template v-if="actor.boobsVolume || actor.boobsImplant">: </template> <template v-if="actor.boobsVolume">{{ actor.boobsVolume }}cc</template> <template v-if="actor.boobsImplant"> {{ augmentationMap[actor.boobsImplant] || actor.boobsImplant }}</template> </div> <div v-if="actor.naturalButt === false" class="augmentations-section" >Butt<template v-if="actor.buttVolume || actor.buttImplant">: </template> <template v-if="actor.buttVolume">{{ actor.buttVolume }}cc</template> <template v-if="actor.buttImplant"> {{ augmentationMap[actor.buttImplant] || actor.buttImplant }}</template> </div> <div v-if="actor.naturalLips === false" class="augmentations-section" >Lip filler<template v-if="actor.lipsVolume || actor.lipsImplant">: </template> <template v-if="actor.lipsVolume">{{ actor.lipsVolume }}cc</template> <template v-if="actor.lipsImplant"> {{ augmentationMap[actor.lipsImplant] || actor.lipsImplant }}</template> </div> <div v-if="actor.naturalLabia === false" class="augmentations-section" >Labiaplasty</div> </span> </li> <li v-if="actor.penisLength || actor.penisGirth || actor.circumcised" class="bio-item penis" > <dfn class="bio-label"><Icon icon="pencil-ruler" />Dick</dfn> <span> <Icon v-if="actor.isCircumcised" :title="'Circumcised'" icon="pagebreak" class="circumcised" /> <template v-if="actor.penisLength && actor.penisGirth"> <span>{{ actor.penisLength.imperial }}" × {{ actor.penisLength.imperial }}"</span> <span class="bio-segment">{{ actor.penisLength.metric }} × {{ actor.penisGirth.metric }} cm</span> </template> <template v-else-if="actor.penisLength"> <span>{{ actor.penisLength.imperial }}"</span> <span class="bio-segment">{{ actor.penisLength.metric }} cm</span> </template> </span> </li> <li v-if="actor.height" class="bio-item height" > <dfn class="bio-label"><Icon icon="height" />Height</dfn> <span> <span class="height-metric">{{ actor.height.metric }} cm</span> <span class="height-imperial">{{ actor.height.imperial[0] }}' {{ actor.height.imperial[1] }}"</span> </span> </li> <li v-if="actor.weight" class="bio-item weight hideable" > <dfn class="bio-label"><Icon icon="scale" />Weight</dfn> <span> <span class="weight-metric">{{ actor.weight.metric }} kg</span> <span class="weight-imperial">{{ actor.weight.imperial }} lbs</span> </span> </li> <li v-if="actor.eyes" class="bio-item eyes hideable" > <dfn class="bio-label"><Icon icon="eye" />Eyes</dfn> <span>{{ actor.eyes }}</span> </li> <li v-if="actor.hairColor" class="bio-item hair hideable" > <dfn class="bio-label"><Icon icon="haircut" />Hair</dfn> <span><span v-if="actor.hairLength">{{ actor.hairLength }}, </span>{{ actor.hairColor }}</span> </li> <li v-if="actor.hasTattoos" class="bio-item tattoos hideable" > <dfn class="bio-label"><Icon icon="lotus" />Tattoos</dfn> <span v-if="actor.tattoos" :title="actor.tattoos" class="bio-value" >{{ actor.tattoos }}</span> <span v-else>Yes</span> </li> <li v-if="actor.hasPiercings" class="bio-item piercings hideable" > <dfn class="bio-label"><Icon icon="trophy4" />Piercings</dfn> <span v-if="actor.piercings" :title="actor.piercings" class="bio-value" >{{ actor.piercings }}</span> <span v-else>Yes</span> </li> <li v-if="actor.agency" class="bio-item" > <dfn class="bio-label"><Icon icon="user-tie" />Agency</dfn> <span :title="actor.agency" class="bio-value" >{{ actor.agency }}</span> </li> <div class="bio-item bio-socials hideable"> <ul class="socials"> <a v-for="social in socials" :key="`social-${social.id}`" :href="getSocialUrl(social)" target="_blank" rel="noopener" :title="social.platform || social.url" class="social ellipsis" > <Icon v-if="social.platform && env.socials.urls[social.platform]" :icon="iconMap[social.platform] || social.platform" :title="social.platform" :class="`icon-social icon-${social.platform}`" /> <Icon v-else-if="social.platform" icon="bubbles10" :title="social.platform" :class="`icon-social icon-${social.platform} icon-generic`" /> <Icon v-else-if="social.url" icon="sphere" :title="social.platform" :class="`icon-social icon-${social.platform} icon-generic`" /> <template v-if="social.platform">{{ env.socials.prefix[social.platform] || env.socials.prefix.default }}</template>{{ social.handle }} </a> </ul> </div> <li class="bio-item updated"> <span class="ellipsis" :title="formatDate(actor.updatedAt, 'yyyy-MM-dd hh:mm')" >{{ formatDate(actor.updatedAt, 'yyyy-MM-dd') }}</span> <div class="actor-actions"> <a v-if="user && user.role !== 'user'" :href="`/actor/edit/${actor.id}/${actor.slug}`" target="_blank" class="link" >Edit bio</a> <a :href="`/actor/revs/${actor.id}/${actor.slug}`" target="_blank" class="link" >Revisions</a> </div> </li> </ul> <div class="descriptions-container"> <div v-if="descriptions.length > 0" class="descriptions" > <p v-for="description in descriptions" :key="`description-${description.entity.id}`" class="description" > {{ description.text }} <a :href="`/${description.entity.type}/${description.entity.slug}`"> <img v-if="description.entity.type === 'network' || !description.entity.parent || description.entity.isIndependent" :src="`/logos/${description.entity.slug}/thumbs/network.png`" class="description-logo" loading="lazy" > <img v-else :src="`/logos/${description.entity.parent.slug}/thumbs/${description.entity.slug}.png`" class="description-logo" loading="lazy" > </a> </p> </div> </div> <div v-if="showExpand" class="expand-container" > <button type="button" class="expand" @click="expanded = !expanded" > <Icon v-show="expanded" icon="arrow-up3" /> <Icon v-show="!expanded" icon="arrow-down3" /> </button> </div> </div> </template> <script setup> import { ref, inject } from 'vue'; import formatTemplate from 'template-format'; import getPath from '#/src/get-path.js'; import { formatDate } from '#/utils/format.js'; const expanded = ref(false); const pageContext = inject('pageContext'); const { user, env } = pageContext; const props = defineProps({ actor: { type: Object, default: null, }, }); const iconMap = { twitter: 'twitter-x', }; // if the profile is empty, the expand button overlaps the header const showExpand = [ 'age', 'bust', 'cup', 'eyes', 'hairColor', 'hasPiercings', 'hasTattoos', 'height', 'hip', 'naturalBoobs', 'origin', 'residence', 'waist', 'weight', ].some((attribute) => !!props.actor[attribute]); const augmentationMap = { bbl: 'BBL', fat: 'fat transfer', lift: 'direct lift', lipo: 'lipo without BBL', filler: 'filler', mms: 'MMS', over: 'over-muscle', under: 'under-muscle', mammary: 'under breast', areolar: 'areolar', crescent: 'crescent areolar', lollipop: 'lollipop', axillary: 'armpit', umbilical: 'navel', }; const descriptions = Object.values(Object.fromEntries(props.actor.profiles .filter((profile) => !!profile.description) .map((profile) => [profile.descriptionHash, { text: profile.description, entity: profile.entity, }]))); function getSocialUrl(social) { if (social.url) { return social.url; } if (pageContext.env.socials.urls[social.platform]) { return formatTemplate(pageContext.env.socials.urls[social.platform], { handle: social.handle }); } return null; } const socials = props.actor.socials.map((social) => ({ ...social, handle: social.url ? new URL(social.url).hostname : social.handle, })); </script> <style> .header-gender { display: inline-flex; align-items: center; margin: 0 0 0 .5rem; transform: translate(0, .125rem); font-size: 0; .icon { width: 1.25rem; height: auto; } } </style> <style scoped> .profile { background: var(--grey-dark-40); color: var(--highlight-strong-30); width: 100%; max-height: 18rem; display: flex; flex-direction: row; flex-shrink: 0; position: relative; &.with-avatar { height: 18rem; /* profile overlaps avatar in chrome */ } .avatar-container { position: relative; margin: 0 .5rem 1rem 1rem; flex-shrink: 0; font-size: 0; } .avatar { height: 100%; flex-shrink: 0; border: solid 3px var(--highlight-weak-30); border-radius: .5rem; } .avatar-credit { width: 100%; position: absolute; left: 0; bottom: 0; z-index: 1; box-sizing: border-box; padding: 0 .5rem; color: var(--text-light); font-size: .75rem; text-shadow: 1px 1px 0 var(--shadow-strong-20); } &.expanded { padding-bottom: 1.5rem; margin-bottom: .75rem; } } .bio { flex-grow: 1; height: 100%; display: flex; flex-direction: column; flex-wrap: wrap; box-sizing: border-box; overflow: hidden; } .bio-header { width: calc(50% - 2rem); display: flex; justify-content: space-between; align-items: center; padding: 0 .5rem .5rem 0; margin: 0 0 0 1rem; } .bio-item { display: flex; justify-content: space-between; box-sizing: border-box; padding: .25rem 0 ; margin: 0 0 .25rem 1rem; line-height: 1.75; text-align: right; font-size: .9rem; font-weight: 600; overflow: hidden; &:not(:last-of-type) { border-bottom: solid 1px var(--highlight-weak-40); } .icon { height: auto; /* prevents jumping */ } } .bio-label, .bio-value { display: flex; align-items: center; } .bio-label { color: var(--highlight-strong-20); margin: 0 1rem 0 0; flex-shrink: 0; font-style: normal; font-weight: 400; .icon { fill: var(--highlight); margin: -.25rem .5rem 0 0; } } .bio-value { margin: 0 0 0 2rem; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; .icon { margin: -.25rem 0 0 0; } } .flag { height: 1rem; margin: .25rem .5rem 0 0; border-radius: 3px; } .bio-name { display: inline-block; padding: 0; margin: 0; } .birthdate { display: block; } .birthdate-short { display: none; } .age { font-weight: bold; padding: 0 0 0 .5rem; border-left: solid 1px var(--highlight-weak-20); margin: 0 0 0 .5rem; } .country { display: flex; justify-content: flex-end; } .country-alpha2 { display: none; } .figure .bio-label .icon { margin: -.5rem .5rem 0 0; } .height-imperial, .weight-imperial, .penis-girth-imperial, .penis-length-imperial, .bio-segment { padding: 0 0 0 .5rem; border-left: solid 1px var(--highlight-weak-20); margin: 0 0 0 .5rem; } .enhanced.icon, .circumcised.icon { fill: var(--primary); padding: 0 .5rem; margin-right: .25rem; transform: translateY(2px); } .enhanced.icon { transform: scaleX(-1); } .ethnicity, .hair, .eyes, .orientation { text-transform: capitalize; } .alias:not(:last-child)::after { content: ',\00a0'; } .augmentations .bio-value { flex-direction: column; align-items: flex-end; } .updated { color: var(--highlight-weak-10); font-size: .8rem; text-align: left; .ellipsis { width: 0; flex-grow: 1; } } .bio-socials { display: block; } .socials { display: grid; grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); grid-gap: 0 0; overflow: hidden; gap: .25rem; padding: 0; } .social { display: flex; height: 2rem; align-items: center; padding: .1rem .5rem; border-radius: .25rem; color: inherit; text-decoration: none; font-size: .9rem; font-weight: normal; background: var(--highlight-weak-40); .icon { margin-right: .5rem; } .icon-generic { fill: var(--highlight); } &:hover { color: var(--primary); cursor: pointer; .icon { fill: var(--primary); } } } .actor-actions { display: flex; flex-shrink: 0; justify-content: flex-start; gap: 1rem; margin-right: .5rem; .link { color: inherit; flex-shrink: 0; } } .descriptions-container { max-width: 30rem; max-height: 100%; position: relative; display: block; flex-grow: 1; box-sizing: border-box; overflow: hidden; &::after { content: ''; width: 100%; height: 1.5rem; position: absolute; bottom: 0; background: linear-gradient(transparent, 25%, var(--profile) 75%); pointer-events: none; } } .descriptions { height: 100%; overflow: auto; scrollbar-width: none; &::-webkit-scrollbar { display: none; } } .description { margin: 0; padding: 0 1rem; border-left: solid 3px var(--highlight-hint); line-height: 1.5; font-size: .9rem; } .description-logo { display: block; width: 12rem; max-height: 1.5rem; margin: .5rem 0 1.5rem 0; object-fit: contain; object-position: 0 50%; } .actor-content { display: flex; flex-grow: 1; flex-direction: column; background: var(--background-soft); } .heading { padding: 0; margin: 0 0 1rem 0; } .profile-social { display: none; } .expand-container { width: 100%; display: none; justify-content: center; position: absolute; z-index: 1; bottom: -.25rem; } .expand { width: 4rem; height: 2rem; display: inline-flex; justify-content: center; align-items: center; border: none; border-radius: .5rem; background: var(--grey-dark-50); box-shadow: 0 0 3px var(--shadow); .icon { fill: var(--text-light); } &:hover { cursor: pointer; background: var(--primary); } } .scroll { background: var(--background-dim); border-bottom: solid 1px var(--shadow-hint); } .stash.icon { width: 1.5rem; height: 1.5rem; padding: 0 1rem; fill: var(--highlight); &.stashed { fill: var(--primary); } &:hover { fill: var(--primary); cursor: pointer; } } @media(--big) { .descriptions-container { display: none; } .bio { margin-right: 1rem; } } @media(--compact) { .profile { .avatar-container { display: none; } &.with-avatar { height: auto; max-height: 18rem; } } .actor-content { flex-direction: column; } } @media(--small) { .profile { height: auto; max-height: none; flex-direction: column; &.with-avatar { height: auto; max-height: none; } &:not(.expanded) .hideable { display: none; } /* only hide update/actions line if other bio lines and thus the expand button are present */ &:not(.expanded) .bio-item + .updated { display: none; } } .bio { width: 100%; height: auto; padding: 0 1rem; margin: 0; } .bio-item { width: 100%; margin: 0; } .expanded .bio-value { white-space: normal; } .expand-container { display: flex; } .actor-stash { margin: 0 .5rem 0 0; } } @media(--small-30) { .header-social { display: none; } .expanded .profile-social { display: block; margin: 1rem 0 0 0; } .header-name { flex-grow: 1; font-size: 1.3rem; padding: .5rem .5rem .5rem 1rem; } .stash.icon { width: 1.25rem; height: 1.25rem; padding: 0 1rem 0 .25rem; transform: translate(0, -.1rem); } } @media(--small-60) { .birthdate-long, .age { display: none; } .birthdate-short { display: inline-block; } .country-name { display: none; } .country-alpha2 { display: inline-block; } } </style>