traxxx-web/components/actors/bio.vue

889 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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, 'MMMM d, yyyy') }}</span>
<span class="birthdate-short">{{ formatDate(actor.dateOfBirth, 'MMM d, yyyy') }}</span>
<span
v-if="!actor.dateOfDeath"
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"
>&gt; {{ 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">:&nbsp;</template>
<template v-if="actor.boobsVolume">{{ actor.boobsVolume }}cc</template>
<template v-if="actor.boobsImplant">&nbsp;{{ augmentationMap[actor.boobsImplant] || actor.boobsImplant }}</template>
</div>
<div
v-if="actor.naturalButt === false"
class="augmentations-section"
>Butt<template v-if="actor.buttVolume || actor.buttImplant">:&nbsp;</template>
<template v-if="actor.buttVolume">{{ actor.buttVolume }}cc</template>
<template v-if="actor.buttImplant">&nbsp;{{ 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">:&nbsp;</template>
<template v-if="actor.lipsVolume">{{ actor.lipsVolume }}cc</template>
<template v-if="actor.lipsImplant">&nbsp;{{ 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>
<li class="bio-item updated">
<span
class="ellipsis"
:title="`#${actor.id} Updated ${formatDate(actor.updatedAt, 'yyyy-MM-dd hh:mm')}`"
>#{{ actor.id }} Updated {{ 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 getPath from '#/src/get-path.js';
import { formatDate } from '#/utils/format.js';
const expanded = ref(false);
const pageContext = inject('pageContext');
const user = pageContext.user;
const props = defineProps({
actor: {
type: Object,
default: null,
},
});
// 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,
}])));
</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 {
width: calc(50% - 4rem);
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;
}
.actor-actions {
display: flex;
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;
}
}
@media(--compact) {
.profile {
.avatar-container {
display: none;
}
&.with-avatar {
height: auto;
max-height: 18rem;
}
}
.actor-content {
flex-direction: column;
}
}
@media(--small-15) {
.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>