<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/revs/${actor.id}/${actor.slug}`" class="link" >Go to actor revisions</a> </li> <li> <a :href="`/user/${user.username}/revisions/actors`" 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 noselect"> <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" :list="item.suggestions && `suggestions-${item.key}`" :disabled="!editing.has(item.key)" > <datalist v-if="item.suggestions" :id="`suggestions-${item.key}`" > <option v-for="(suggestion, index) in item.suggestions" :key="`suggestion-${item.key}-${index}`" >{{ suggestion }}</option> </datalist> <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 }}</option> </select> <div v-if="item.type === 'size'" class="figure size" > <div class="value-section"> <span class="value-label">Units</span> <select v-model="sizeUnits" class="input" :disabled="!editing.has(item.key)" > <option value="metric">Metric</option> <option value="imperial">Imperial</option> </select> </div> <span class="figure-height"> <div class="value-section"> <span class="value-label">Height</span> <span v-if="sizeUnits === 'metric'"> <input v-model="edits[item.key].metricHeight" type="number" class="input" :disabled="!editing.has(item.key)" > cm </span> <span v-if="sizeUnits === 'imperial'"> <input v-model="edits[item.key].imperialHeight[0]" type="number" class="input" :disabled="!editing.has(item.key)" > ft <input v-model="edits[item.key].imperialHeight[1]" type="number" class="input" :disabled="!editing.has(item.key)" > in </span> </div> </span> <span class="figure-weight"> <div class="value-section"> <span class="value-label">Weight</span> <span v-if="sizeUnits === 'metric'"> <input v-model="edits[item.key].metricWeight" type="number" class="input" :disabled="!editing.has(item.key)" > kg </span> <span v-if="sizeUnits === 'imperial'"> <input v-model="edits[item.key].imperialWeight" type="number" class="input" :disabled="!editing.has(item.key)" > <template v-if="sizeUnits === 'imperial'"> lbs</template> <template v-else> kg</template> </span> </div> </span> </div> <EditSocials v-if="item.type === 'socials'" :edits="edits" :editing="editing" @socials="(socials) => edits.socials = socials" /> <EditPlace v-if="item.type === 'place'" :item="item" :edits="edits" :editing="editing" @place="(place) => edits[item.key] = place" /> <EditFigure v-if="item.type === 'figure'" :edits="edits" :editing="editing" :units="figureUnits" @figure="(figure) => edits.figure = figure" @units="(units) => figureUnits = units" /> <EditAugmentation v-if="item.type === 'augmentation'" :edits="edits" :editing="editing" @augmentation="(augmentation) => edits.augmentation = augmentation" /> <EditPenis v-if="item.type === 'penis'" :edits="edits" :editing="editing" :units="penisUnits" @penis="(penis) => edits.penis = penis" @units="(units) => penisUnits = units" /> <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" /> <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) }" > <Avatar v-for="avatar in item.options" :key="`avatar-${avatar.id}`" :avatar="avatar" :class="{ selected: edits[item.key] === avatar.id }" @click="setAvatar(avatar.id)" /> </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" /> <Ellipsis v-if="submitting" /> <!-- we don't want the return key to submit the form --> <button v-else 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 EditSocials from '#/components/edit/socials.vue'; import EditPlace from '#/components/edit/place.vue'; import EditFigure from '#/components/edit/figure.vue'; import EditAugmentation from '#/components/edit/augmentation.vue'; import EditPenis from '#/components/edit/penis.vue'; import Avatar from '#/components/edit/avatar.vue'; import Checkbox from '#/components/form/checkbox.vue'; import Ellipsis from '#/components/loading/ellipsis.vue'; import { // get, post, } from '#/src/api.js'; const pageContext = inject('pageContext'); const user = pageContext.user; const actor = ref(pageContext.pageProps.actor); // console.log(actor.value); const fields = computed(() => [ ...(actor.value.photos.length > 0 ? [{ key: 'avatar', type: 'avatar', value: actor.value.avatar?.id, options: actor.value.photos, }] : []), ...(user.role === 'admin' ? [{ key: 'name', type: 'string', value: actor.value.name, }] : []), { key: 'gender', type: 'select', value: actor.value.gender, options: [null, 'female', 'male', 'transsexual', 'other'], 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: 'socials', type: 'socials', value: actor.value.socials, }, { key: 'origin', type: 'place', value: { country: actor.value.origin?.country?.alpha2 || null, place: [actor.value.origin?.city, actor.value.origin?.state].filter(Boolean).join(', '), }, }, { key: 'residence', type: 'place', value: { country: actor.value.residence?.country?.alpha2 || null, place: [actor.value.residence?.city, actor.value.residence?.state].filter(Boolean).join(', '), }, }, { key: 'ethnicity', type: 'string', value: actor.value.ethnicity, suggestions: [ 'Asian', 'Black', 'Indian', 'Latina', 'White', ], }, { key: 'size', type: 'size', value: { metricHeight: actor.value.height?.metric, metricWeight: actor.value.weight?.metric, imperialHeight: actor.value.height?.imperial || [], imperialWeight: actor.value.weight?.imperial, }, }, { 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, boobsIncision: actor.value.boobsIncision, boobsSurgeon: actor.value.boobsSurgeon, naturalButt: actor.value.naturalButt, buttVolume: actor.value.buttVolume, buttImplant: actor.value.buttImplant, naturalLips: actor.value.naturalLips, lipsVolume: actor.value.lipsVolume, naturalLabia: actor.value.naturalLabia, }, }, { 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: 'agency', type: 'string', value: actor.value.agency, suggestions: [ '101 Modeling', 'Adult Talent Managers (ATMLA)', 'AMA Modeling', 'The Bakery Talent', 'Coxxx Models', 'East Coast Talent (ECT)', 'Hussie Models', 'Invision Models', 'OC Modeling', 'Spiegler Girls', ], }, { key: 'penis', type: 'penis', value: { metricLength: actor.value.penisLength?.metric, metricGirth: actor.value.penisGirth?.metric, imperialLength: actor.value.penisLength?.imperial, imperialGirth: actor.value.penisGirth?.imperial, 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 submitting = ref(false); const submitted = ref(false); const sizeUnits = ref('metric'); const figureUnits = ref('us'); const penisUnits = 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); } function setAvatar(avatarId) { edits.value.avatar = avatarId; } const keyMap = { origin: { country: 'originCountry', place: 'originPlace', }, residence: { country: 'residenceCountry', place: 'residencePlace', }, tattoos: { has: 'hasTattoos', description: 'tattoos', }, piercings: { has: 'hasPiercings', description: 'piercings', }, }; const groupMap = { penisLength: 'penis', penisGirth: 'penis', height: 'size', weight: 'size', }; async function submit() { try { submitting.value = true; 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' && !Array.isArray(edits.value[key])) { return Object.entries(edits.value[key]).map(([valueKey, value]) => [keyMap[key]?.[valueKey] || valueKey, value]); } return [[key, edits.value[key]]]; })), ...Object.fromEntries(Object.entries({ height: sizeUnits.value === 'imperial' ? edits.value.size.imperialHeight : edits.value.size.metricHeight, weight: sizeUnits.value === 'imperial' ? edits.value.size.imperialWeight : edits.value.size.metricWeight, penisLength: penisUnits.value === 'imperial' ? edits.value.penis.imperialLength : edits.value.penis.metricLength, penisGirth: penisUnits.value === 'imperial' ? edits.value.penis.imperialGirth : edits.value.penis.metricGirth, }).filter(([key]) => editing.value.has(groupMap[key] || key))), metricHeight: undefined, metricWeight: undefined, imperialHeight: undefined, imperialWeight: undefined, metricLength: undefined, metricGirth: undefined, imperialLength: undefined, imperialGirth: undefined, }, sizeUnits: sizeUnits.value, figureUnits: figureUnits.value, penisUnits: penisUnits.value, comment: comment.value, apply: apply.value, }, { successFeedback: 'Your revision has been submitted for approval.', appendErrorMessage: true, }); submitting.value = false; 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> .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; overflow: hidden; &.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; flex-shrink: 0; min-width: 7rem; } .select { text-transform: capitalize; } .item-header { display: flex; align-items: center; } .value { display: flex; align-items: center; flex-grow: 1; overflow: hidden; .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 { width: 100%; display: flex; gap: .25rem; padding-bottom: .5rem; overflow-x: auto; &.disabled { opacity: .5; } } .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>