traxxx-web/components/actors/bio.vue

1003 lines
20 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>
<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>