traxxx-web/pages/actors/@actorId/edit/+Page.vue

1161 lines
23 KiB
Vue

<template>
<div class="editor">
<p
v-if="submitted"
class="submitted"
>
<template v-if="apply">Your revision has been submitted. Thank you for your contribution!</template>
<template v-else>Your revision has been submitted for review. Thank you for your contribution!</template>
<ul>
<li>
<a
:href="`/actor/${actor.id}/${actor.slug}`"
class="link"
>Return to actor</a>
</li>
<li>
<a
:href="`/actor/edit/${actor.id}`"
class="link"
>Make another edit</a>
</li>
<li>
<a
:href="`/actor/revisions/${actor.id}/${actor.slug}`"
class="link"
>Go to actor revisions</a>
</li>
<li>
<a
:href="`/user/${user.username}/revisions`"
class="link"
>Go to user revisions</a>
</li>
<li v-if="user.role !== 'user'">
<a
href="/admin/revisions/actors"
class="link"
>Go to revisions admin</a>
</li>
</ul>
</p>
<form
v-else
@submit.prevent
>
<div class="editor-header">
<h2 class="heading ellipsis">Edit actor #{{ actor.id }} - {{ actor.name }}</h2>
<a
:href="`/actor/${actor.id}/${actor.slug}`"
target="_blank"
class="link noshrink"
>Go to actor</a>
</div>
<ul class="nolist">
<li
v-for="item in fields"
:key="`item-${item.key}`"
class="row"
>
<div class="item-header">
<div class="key">{{ item.label || item.key }}</div>
<div class="item-actions">
<Icon
v-if="!item.forced"
icon="pencil5"
:class="{ active: editing.has(item.key) }"
@click="toggleField(item)"
/>
</div>
</div>
<div
class="value"
:class="{ disabled: !editing.has(item.key) }"
>
<input
v-if="item.type === 'string'"
v-model="edits[item.key]"
class="string input"
:disabled="!editing.has(item.key)"
>
<textarea
v-if="item.type === 'text'"
v-model="edits[item.key]"
:placeholder="item.placeholder"
rows="3"
class="text input"
:disabled="!editing.has(item.key)"
/>
<template v-if="item.type === 'number'">
<input
v-model="edits[item.key]"
type="number"
:max="item.max"
:min="item.min"
class="number input"
:disabled="!editing.has(item.key)"
>{{ item.unit }}
</template>
<input
v-if="item.type === 'date'"
v-model="edits[item.key]"
type="date"
class="date input"
:disabled="!editing.has(item.key)"
>
<select
v-if="item.type === 'select'"
v-model="edits[item.key]"
class="select input"
:disabled="!editing.has(item.key)"
>
<option
v-for="option in item.options"
:key="`${item.key}-option-${option}`"
:value="typeof option?.value === 'undefined' ? option : option.value"
>{{ option?.label || option || 'unknown' }}</option>
</select>
<div
v-if="item.type === 'place'"
class="place"
>
<div class="value-section">
<span class="value-label">Country</span>
<select
v-model="edits[item.key].country"
class="select input"
placeholder="Country"
:disabled="!editing.has(item.key)"
>
<option
:key="`${item.key}-country-unknown`"
:value="null"
/>
<option
v-for="country in sortedCountries"
:key="`${item.key}-country-${country.alpha2}`"
:value="country.alpha2"
>{{ country.alias || country.name }}</option>
</select>
</div>
<div class="value-section">
<span class="value-label">State</span>
<input
v-model="edits[item.key].state"
class="string input"
:disabled="!editing.has(item.key)"
>
</div>
<div class="value-section">
<span class="value-label">City</span>
<input
v-model="edits[item.key].city"
class="string input"
:disabled="!editing.has(item.key)"
>
</div>
</div>
<div
v-if="item.type === 'figure'"
class="figure"
>
<div class="value-section">
<span class="value-label">Units</span>
<select
v-model="figureSystem"
class="input"
:disabled="!editing.has(item.key)"
>
<option value="us">USA</option>
<option value="eu">Europe/Asia</option>
<option value="jp">Japan</option>
<option value="aus">Australia</option>
</select>
</div>
<span class="figure-bust">
<div class="value-section">
<span class="value-label">Bust</span>
<input
v-model="edits[item.key].bust"
type="number"
class="input"
placeholder="Bust"
:disabled="!editing.has(item.key)"
>
</div>
<div class="value-section">
<span class="value-label">Cup</span>
<select
v-model="edits[item.key].cup"
class="select input"
placeholder="Cup"
:disabled="!editing.has(item.key)"
>
<option
:key="`${item.key}-cup-unknown`"
:value="null"
>Unknown</option>
<option
v-for="cup in cupSizes"
:key="`${item.key}-cup-${cup}`"
:value="cup"
>{{ cup }}</option>
</select>
</div>
</span>
<div class="value-section">
<span class="value-label">Waist</span>
<input
v-model="edits[item.key].waist"
type="number"
class="input"
placeholder="Waist"
:disabled="!editing.has(item.key)"
>
</div>
<div class="value-section">
<span class="value-label">Hip</span>
<input
v-model="edits[item.key].hip"
type="number"
class="input"
placeholder="Hip"
:disabled="!editing.has(item.key)"
>
</div>
</div>
<div
v-if="item.type === 'augmentation'"
class="figure augmentation"
>
<div class="value-section">
<span class="value-label">Natural boobs</span>
<select
v-model="edits[item.key].naturalBoobs"
class="input"
:disabled="!editing.has(item.key)"
>
<option :value="null">Unknown</option>
<option :value="true">Yes</option>
<option :value="false">No</option>
</select>
</div>
<div
v-if="edits[item.key].naturalBoobs === false"
class="value-section"
>
<span class="value-label">Implant CC</span>
<input
v-model="edits[item.key].boobsVolume"
type="number"
class="volume input"
placeholder="CC"
min="50"
max="10000"
:disabled="!editing.has(item.key)"
>
</div>
<div
v-if="edits[item.key].naturalBoobs === false"
class="value-section"
>
<span class="value-label">Implant type</span>
<select
v-model="edits[item.key].boobsImplant"
class="input"
:disabled="!editing.has(item.key)"
>
<option :value="null">Unknown</option>
<option value="saline">Saline</option>
<option value="silicone">Silicone</option>
<option value="gummy">Gummy</option>
<option value="fat">Fat transfer</option>
</select>
</div>
<div
v-if="edits[item.key].naturalBoobs === false"
class="value-section"
>
<span class="value-label">Implant placement</span>
<select
v-model="edits[item.key].boobsPlacement"
class="input"
:disabled="!editing.has(item.key)"
>
<option :value="null">Unknown</option>
<option value="over">Over muscle</option>
<option value="under">Under muscle</option>
</select>
</div>
<div
v-if="edits[item.key].naturalBoobs === false"
class="value-section value-divide"
>
<span class="value-label">Surgeon</span>
<input
v-model="edits[item.key].boobsSurgeon"
class="volume input"
placeholder="Surgeon"
list="surgeons"
:disabled="!editing.has(item.key)"
>
<datalist id="surgeons">
<option value="Dr. Revis" />
<option value="Dr. Dre" />
</datalist>
</div>
<div class="value-section">
<span class="value-label">Natural butt</span>
<select
v-model="edits[item.key].naturalButt"
class="input"
:disabled="!editing.has(item.key)"
>
<option :value="null">Unknown</option>
<option :value="true">Yes</option>
<option :value="false">No</option>
</select>
</div>
<div
v-if="edits[item.key].naturalButt === false"
class="value-section"
>
<span class="value-label">Implant CC</span>
<input
v-model="edits[item.key].buttVolume"
type="number"
class="volume input"
placeholder="CC"
min="50"
max="10000"
:disabled="!editing.has(item.key)"
>
</div>
<div
v-if="edits[item.key].naturalButt === false"
class="value-section"
>
<span class="value-label">Implant type</span>
<select
v-model="edits[item.key].buttImplant"
class="input"
:disabled="!editing.has(item.key)"
>
<option :value="null">Unknown</option>
<option value="bbl">BBL (fat transfer)</option>
<option value="lift">Direct lift</option>
<option value="filler">Filler (Sculptra)</option>
<option value="mms">MMS (CoolTone)</option>
<option value="lipo">Lipo without BBL</option>
</select>
</div>
</div>
<div
v-if="item.type === 'penis'"
class="figure penis"
>
<div class="value-section">
<span class="value-label">Units</span>
<select
v-model="penisSystem"
class="input"
:disabled="!editing.has(item.key)"
>
<option value="imperial">Imperial</option>
<option value="metric">Metric</option>
</select>
</div>
<div class="value-section">
<span class="value-label">Penis length</span>
<input
v-model="edits[item.key].penisLength"
type="number"
class="volume input"
min="1"
max="20"
:disabled="!editing.has(item.key)"
>
</div>
<div class="value-section">
<span class="value-label">Penis girth</span>
<input
v-model="edits[item.key].penisGirth"
type="number"
class="volume input"
min="1"
max="20"
:disabled="!editing.has(item.key)"
>
</div>
<div class="value-section">
<span class="value-label">Circumcised</span>
<select
v-model="edits[item.key].isCircumcised"
class="input"
:disabled="!editing.has(item.key)"
>
<option :value="null">Unknown</option>
<option :value="true">Yes</option>
<option :value="false">No</option>
</select>
</div>
</div>
<div
v-if="item.type === 'has'"
class="has"
>
<select
v-model="edits[item.key].has"
class="select input"
:disabled="!editing.has(item.key)"
>
<option :value="null">Unknown</option>
<option :value="true">Yes</option>
<option :value="false">No</option>
</select>
<input
v-model="edits[item.key].description"
class="description input"
placeholder="Description"
:disabled="!editing.has(item.key)"
>
</div>
<div
v-if="item.type === 'avatar'"
class="avatars"
:class="{ disabled: !editing.has(item.key) }"
>
<div
v-for="avatar in item.options"
:key="`avatar-${avatar.id}`"
class="avatar noshrink"
:class="{ selected: edits[item.key] === avatar.id }"
@click="setAvatar(avatar.id)"
>
<img
:src="getPath(avatar, 'thumbnail')"
class="avatar-image"
>
<span
class="avatar-credit"
title="Credit"
>{{ avatar.credit }}</span>
<span class="avatar-meta">
<span title="Dimensions">{{ avatar.width }}x{{ avatar.height }}</span>
<span title="Sharpness">{{ avatar.sharpness.toFixed(2) }}</span>
</span>
<a
:href="getPath(avatar)"
target="_blank"
class="avatar-zoom"
>
<Icon
icon="search"
/>
</a>
</div>
</div>
</div>
</li>
</ul>
<div class="editor-footer">
<div class="comment">
<textarea
v-model="comment"
rows="3"
placeholder="Please provide verifiable information and sources supporting your edits."
class="text input noshrink"
/>
</div>
<div class="editor-actions">
<Checkbox
v-if="user.role !== 'user'"
label="Approve and apply immediately"
:checked="apply"
:disabled="editing.size === 0"
@change="(checked) => apply = checked"
/>
<!-- we don't want the return key to submit the form -->
<button
class="button button-primary"
type="button"
:disabled="editing.size === 0"
@click="submit"
>
<template v-if="apply">Submit</template>
<template v-else>Submit for review</template>
</button>
</div>
</div>
</form>
</div>
</template>
<script setup>
import { ref, computed, inject } from 'vue';
import { format } from 'date-fns';
import getPath from '#/src/get-path.js';
import Checkbox from '#/components/form/checkbox.vue';
import {
// get,
post,
} from '#/src/api.js';
const pageContext = inject('pageContext');
const user = pageContext.user;
const countries = pageContext.pageProps.countries;
const actor = ref(pageContext.pageProps.actor);
console.log(actor.value);
const topCountries = [
'AU',
'BR',
'CZ',
'DE',
'JP',
'RU',
'GB',
'US',
];
const sortedCountries = countries.toSorted((countryA, countryB) => topCountries.indexOf(countryB.alpha2) - topCountries.indexOf(countryA.alpha2));
const cupSizes = [
'A', 'AA',
'B',
'C',
'D', 'DD', 'DDD',
'E',
'F', 'FF',
'G', 'GG',
'H', 'HH',
'J', 'JJ',
'K', 'KK',
'L',
'M',
'N',
'O',
'P',
];
const fields = computed(() => [
{
key: 'avatar',
type: 'avatar',
value: actor.value.avatar.id,
options: Object.values(Object.fromEntries(actor.value.profiles.filter((profile) => !!profile.avatar).map((profile) => [profile.avatar.id, profile.avatar]))),
},
...(user.role === 'admin'
? [{
key: 'name',
type: 'string',
value: actor.value.name,
}]
: []),
{
key: 'gender',
type: 'select',
value: actor.value.gender,
options: ['female', 'male', 'transsexual', 'other', null],
inline: true,
},
{
key: 'dateOfBirth',
label: 'date of birth',
type: 'date',
value: actor.value.dateOfBirth
? format(actor.value.dateOfBirth, 'yyyy-MM-dd')
: null,
inline: true,
},
{
key: 'origin',
type: 'place',
value: {
...actor.value.origin,
country: actor.value.origin?.country?.alpha2 || null,
},
},
{
key: 'residence',
type: 'place',
value: {
...actor.value.residence,
country: actor.value.residence?.country?.alpha2 || null,
},
},
{
key: 'height',
type: 'number',
value: actor.value.height.metric,
unit: 'cm',
inline: true,
},
{
key: 'weight',
type: 'number',
value: actor.value.weight.metric,
unit: 'kg',
inline: true,
},
{
key: 'figure',
type: 'figure',
value: {
bust: actor.value.bust,
cup: actor.value.cup,
waist: actor.value.waist,
hip: actor.value.hip,
},
},
{
key: 'augmentation',
type: 'augmentation',
value: {
naturalBoobs: actor.value.naturalBoobs,
boobsVolume: actor.value.boobsVolume,
boobsImplant: actor.value.boobsImplant,
boobsPlacement: actor.value.boobsPlacement,
boobsSurgeon: actor.value.boobsSurgeon,
naturalButt: actor.value.naturalButt,
buttVolume: actor.value.buttVolume,
buttImplant: actor.value.buttImplant,
},
},
{
key: 'hairColor',
label: 'hair color',
type: 'select',
value: actor.value.hairColor,
options: [
null,
'black',
'blonde',
'brown',
'red',
'gray',
'blue',
'green',
'pink',
'purple',
],
inline: true,
},
{
key: 'eyes',
label: 'eye color',
type: 'select',
value: actor.value.eyes,
options: [
null,
'blue',
'brown',
'gray',
'green',
'hazel',
],
inline: true,
},
{
key: 'tattoos',
type: 'has',
value: {
has: actor.value.hasTattoos,
description: actor.value.tattoos,
},
},
{
key: 'piercings',
type: 'has',
value: {
has: actor.value.hasPiercings,
description: actor.value.piercings,
},
},
{
key: 'penis',
type: 'penis',
value: {
penisLength: actor.value.penisLength,
penisGirth: actor.value.penisGirth,
isCircumcised: actor.value.isCircumcised,
},
},
{
key: 'dateOfDeath',
label: 'date of death',
type: 'date',
value: actor.value.dateOfDeath
? format(actor.value.dateOfDeath, 'yyyy-MM-dd')
: null,
},
]);
const editing = ref(new Set());
const edits = ref(Object.fromEntries(fields.value.map((field) => [field.key, field.value])));
const comment = ref(null);
const apply = ref(user.role !== 'user');
const submitted = ref(false);
const figureSystem = ref('us');
const penisSystem = ref('imperial');
function toggleField(item) {
if (editing.value.has(item.key)) {
editing.value.delete(item.key);
// delete edits.value[item.key];
return;
}
editing.value.add(item.key);
/*
if (Array.isArray(item.value)) {
edits.value[item.key] = item.value.map((value) => value.hash || value.id);
return;
}
edits.value[item.key] = item.value;
*/
}
/*
function setValue(item, event) {
console.log(item, event.target.value);
if (item.type === 'number') {
edits.value[item.key] = Number(event.target.value);
return;
}
edits.value[item.key] = event.target.value;
}
*/
function setAvatar(avatarId) {
edits.value.avatar = avatarId;
}
const keyMap = {
origin: {
country: 'originCountry',
state: 'originState',
city: 'originCity',
},
residence: {
country: 'residenceCountry',
state: 'residenceState',
city: 'residenceCity',
},
};
async function submit() {
console.log('SUBMIT', Array.from(editing.value), edits.value);
try {
await post('/revisions/actors', {
actorId: actor.value.id,
edits: Object.fromEntries(Array.from(editing.value).flatMap((key) => {
if (edits.value[key] && typeof edits.value[key] === 'object') {
return Object.entries(edits.value[key]).map(([valueKey, value]) => [keyMap[key]?.[valueKey] || valueKey, value]);
}
return [[key, edits.value[key]]];
})),
comment: comment.value,
apply: apply.value,
}, {
successFeedback: 'Your revision has been submitted for approval.',
appendErrorMessage: true,
});
editing.value = new Set();
edits.value = {};
comment.value = null;
submitted.value = true;
// actor.value = await get(`/actors/${actor.value.id}`);
} catch (error) {
console.error(error);
}
}
</script>
<style scoped>
.editor {
flex-grow: 1;
background: var(--background-dark-10);
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
}
.heading {
margin: 0;
}
.row {
display: flex;
align-items: center;
padding: .25rem 1rem;
margin-bottom: .25rem;
&.inline {
display: inline-flex;
& + .inline .key {
width: auto;
}
}
}
.key {
width: 10rem;
text-transform: capitalize;
font-weight: bold;
}
.input {
background: var(--background);
}
.row .input {
height: 2.5rem;
}
.select {
text-transform: capitalize;
}
.item-header {
display: flex;
align-items: center;
}
.value {
display: flex;
align-items: center;
flex-grow: 1;
.input {
flex-grow: 1;
&:disabled {
color: var(--glass-strong-10);
background: none;
border: solid 1px var(--glass-weak-30);
}
}
.duration {
.input {
width: 5rem;
margin-right: .25rem;
&:not(:first-child) {
margin-left: .75rem;
}
}
}
.number.input {
width: 6rem;
flex-grow: 0;
margin-right: .5rem;
}
&.disabled {
pointer-events: none;
}
}
.value-section {
max-width: 100%;
display: flex;
flex-direction: column;
}
.value-label {
padding: 0 .25rem;
margin-bottom: .25rem;
color: var(--shadow-strong-10);
font-size: .8rem;
font-weight: bold;
}
.value-divide {
padding-right: 1rem;
border-right: solid 1px var(--shadow-weak-30);
margin-right: .5rem;
}
.place,
.has,
.figure {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: .5rem;
overflow: hidden;
}
.figure {
.input[type="number"] {
width: 5rem;
}
.input[type="number"].volume {
width: 6rem;
}
}
.figure-bust {
display: flex;
align-items: center;
}
.has {
flex-grow: 1;
.select {
flex-grow: 0;
}
.description {
flex-grow: 1;
}
}
.avatars {
display: flex;
gap: .25rem;
padding-bottom: .5rem;
overflow-x: auto;
&.disabled {
opacity: .5;
}
}
.avatar {
display: inline-block;
position: relative;
border: solid 2px transparent;
border-radius: .35rem;
box-shadow: 0 0 3px var(--shadow-weak-10);
margin: 2px; /* clear outline */
font-size: 0;
overflow: hidden;
&.selected {
border: solid 2px var(--primary);
}
&:hover {
/*
.avatar-meta,
.avatar-credit {
display: none;
}
*/
.icon {
fill: var(--text-light);
}
}
}
.avatar-image {
height: 10rem;
}
.avatar-zoom {
position: absolute;
top: 0;
right: 0;
z-index: 10;
padding: .25rem;
.icon {
fill: var(--highlight);
}
}
.avatar-meta,
.avatar-credit {
position: absolute;
z-index: 10;
box-sizing: border-box;
padding: .15rem .25rem;
font-size: .7rem;
color: var(--text-light);
text-shadow: 1px 1px 0 var(--shadow-strong-30);
cursor: default;
}
.avatar-meta {
width: 100%;
display: flex;
justify-content: space-between;
bottom: 0;
left: 0;
}
.avatar-credit {
bottom: .75rem;
left: 0;
}
.item-actions {
.icon {
padding: .25rem 1rem;
fill: var(--glass);
overflow: hidden;
&:hover {
cursor: pointer;
fill: var(--text);
}
&.active {
fill: var(--primary);
}
}
}
.editor-footer {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 1rem 1rem 0 1rem;
border-top: solid 1px var(--primary-light-30);
margin: 1rem 0;
}
.comment {
width: 100%;
flex-shrink: 0;
.input {
width: 100%;
resize: vertical;
}
}
.editor-actions {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
margin: .5rem 0;
.button {
padding: .5rem 1rem;
font-size: 1.1rem;
}
}
.submitted {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
font-weight: bold;
line-height: 1.5;
}
@media(--small) {
.row {
flex-direction: column;
align-items: stretch;
margin-bottom: .25rem;
}
.item-header {
margin-bottom: .25rem;
}
.key {
flex-grow: 1;
}
}
</style>