Added actor creation page.

This commit is contained in:
2026-05-20 05:27:37 +02:00
parent 35ffc2b0f7
commit dc80e1e199
6 changed files with 220 additions and 64 deletions

View File

@@ -1 +0,0 @@
export default '/actor/edit/@actorId/*';

View File

@@ -7,12 +7,12 @@
<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>
<ul v-if="actor">
<li>
<a
:href="`/actor/${actor.id}/${actor.slug}`"
class="link"
>Return to actor</a>
>{{ creating ? 'Go' : 'Return' }} to actor</a>
</li>
<li>
@@ -22,21 +22,21 @@
>Make another edit</a>
</li>
<li>
<li v-if="!creating">
<a
:href="`/actor/revs/${actor.id}/${actor.slug}`"
class="link"
>Go to actor revisions</a>
</li>
<li>
<li v-if="!creating">
<a
:href="`/user/${user.username}/revisions/actors`"
class="link"
>Go to user revisions</a>
</li>
<li v-if="user.role !== 'user'">
<li v-if="user.role !== 'user' && !creating">
<a
href="/admin/revisions/actors"
class="link"
@@ -49,7 +49,10 @@
v-else
@submit.prevent
>
<div class="editor-header">
<div
v-if="actor"
class="editor-header"
>
<h2 class="heading ellipsis">Edit actor #{{ actor.id }} - {{ actor.name }}</h2>
<a
@@ -59,6 +62,13 @@
>Go to actor</a>
</div>
<div
v-else
class="editor-header"
>
<h2 class="heading ellipsis">Add actor</h2>
</div>
<ul class="nolist">
<li
v-for="item in fields"
@@ -281,6 +291,7 @@
</select>
<input
v-if="item.hasDescription !== false"
v-model="edits[item.key].description"
class="description input"
placeholder="Description"
@@ -317,7 +328,7 @@
<div class="editor-actions">
<Checkbox
v-if="user.role !== 'user'"
v-if="user.role !== 'user' && actor"
label="Approve and apply immediately"
:checked="apply"
:disabled="editing.size === 0"
@@ -346,6 +357,7 @@
<script setup>
import { ref, computed, inject } from 'vue';
import { format } from 'date-fns';
import omit from 'object.omit';
import EditSocials from '#/components/edit/socials.vue';
import EditPlace from '#/components/edit/place.vue';
@@ -361,13 +373,15 @@ import {
post,
} from '#/src/api.js';
import events from '#/src/events.js';
const pageContext = inject('pageContext');
const user = pageContext.user;
const actor = ref(pageContext.pageProps.actor);
const actor = ref(pageContext.pageProps.actor || null);
const fields = computed(() => [
...(actor.value.photos.length > 0 ? [{
...(actor.value?.photos.length > 0 ? [{
key: 'avatar',
type: 'avatar',
value: actor.value.avatar?.id,
@@ -377,13 +391,13 @@ const fields = computed(() => [
? [{
key: 'name',
type: 'string',
value: actor.value.name,
value: actor.value?.name,
}]
: []),
{
key: 'gender',
type: 'select',
value: actor.value.gender,
value: actor.value?.gender || null,
options: [null, 'female', 'male', 'transsexual', 'other'],
inline: true,
},
@@ -391,7 +405,7 @@ const fields = computed(() => [
key: 'dateOfBirth',
label: 'date of birth',
type: 'date',
value: actor.value.dateOfBirth
value: actor.value?.dateOfBirth
? format(actor.value.dateOfBirth, 'yyyy-MM-dd')
: null,
inline: true,
@@ -399,28 +413,28 @@ const fields = computed(() => [
{
key: 'socials',
type: 'socials',
value: actor.value.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(', '),
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(', '),
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,
value: actor.value?.ethnicity || null,
suggestions: [
'Asian',
'Black',
@@ -433,20 +447,20 @@ const fields = computed(() => [
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,
metricHeight: actor.value?.height?.metric || null,
metricWeight: actor.value?.weight?.metric || null,
imperialHeight: actor.value?.height?.imperial || [],
imperialWeight: actor.value?.weight?.imperial || null,
},
},
{
key: 'figure',
type: 'figure',
value: {
bust: actor.value.bust,
cup: actor.value.cup,
waist: actor.value.waist,
hip: actor.value.hip,
bust: actor.value?.bust || null,
cup: actor.value?.cup || null,
waist: actor.value?.waist || null,
hip: actor.value?.hip || null,
},
},
{
@@ -454,25 +468,25 @@ const fields = computed(() => [
type: 'augmentation',
note: 'Provide explicit evidence, such as social media posts, visible scarring, or before/after. Avoid "it\'s obvious".',
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,
naturalBoobs: actor.value?.naturalBoobs || null,
boobsVolume: actor.value?.boobsVolume || null,
boobsImplant: actor.value?.boobsImplant || null,
boobsPlacement: actor.value?.boobsPlacement || null,
boobsIncision: actor.value?.boobsIncision || null,
boobsSurgeon: actor.value?.boobsSurgeon || null,
naturalButt: actor.value?.naturalButt || null,
buttVolume: actor.value?.buttVolume || null,
buttImplant: actor.value?.buttImplant || null,
naturalLips: actor.value?.naturalLips || null,
lipsVolume: actor.value?.lipsVolume || null,
naturalLabia: actor.value?.naturalLabia || null,
},
},
{
key: 'hairColor',
label: 'hair color',
type: 'select',
value: actor.value.hairColor,
value: actor.value?.hairColor || null,
options: [
null,
'black',
@@ -491,7 +505,7 @@ const fields = computed(() => [
key: 'eyes',
label: 'eye color',
type: 'select',
value: actor.value.eyes,
value: actor.value?.eyes || null,
options: [
null,
'blue',
@@ -506,8 +520,8 @@ const fields = computed(() => [
key: 'tattoos',
type: 'has',
value: {
has: actor.value.hasTattoos,
description: actor.value.tattoos,
has: actor.value?.hasTattoos || null,
description: actor.value?.tattoos || null,
},
},
{
@@ -515,14 +529,14 @@ const fields = computed(() => [
type: 'has',
note: 'Excludes earrings',
value: {
has: actor.value.hasPiercings,
description: actor.value.piercings,
has: actor.value?.hasPiercings || null,
description: actor.value?.piercings || null,
},
},
{
key: 'agency',
type: 'string',
value: actor.value.agency,
value: actor.value?.agency || null,
suggestions: [
'101 Modeling',
'Adult Talent Managers (ATMLA)',
@@ -540,25 +554,41 @@ const fields = computed(() => [
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,
metricLength: actor.value?.penisLength?.metric || null,
metricGirth: actor.value?.penisGirth?.metric || null,
imperialLength: actor.value?.penisLength?.imperial || null,
imperialGirth: actor.value?.penisGirth?.imperial || null,
isCircumcised: actor.value?.isCircumcised || null,
},
},
{
key: 'dateOfDeath',
label: 'date of death',
type: 'date',
value: actor.value.dateOfDeath
value: actor.value?.dateOfDeath
? format(actor.value.dateOfDeath, 'yyyy-MM-dd')
: null,
},
]);
{
key: 'allowGlobalMatch',
label: 'global match',
type: 'has',
note: 'Allow this actor to be assigned to scenes automatically, overriding single-name protections.',
hasDescription: false,
value: {
has: actor.value?.isGlobal || null,
},
},
]
.filter((field) => (actor.value ? true : ['name', 'gender'].includes(field.key)))
.map((field) => ({
...field,
forced: true,
})));
const editing = ref(new Set());
const editing = ref(new Set(actor.value ? [] : ['name', 'gender']));
const edits = ref(Object.fromEntries(fields.value.map((field) => [field.key, field.value])));
const creating = !actor.value;
const comment = ref(null);
const apply = ref(user.role !== 'user');
const submitting = ref(false);
@@ -609,13 +639,54 @@ const groupMap = {
weight: 'size',
};
async function submitCreate() {
submitting.value = true;
try {
const newActor = {
actor: Object.fromEntries(Array.from(['name', 'gender']).flatMap((key) => {
if (!edits.value[key]) {
throw new Error(`Missing ${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]]];
})),
comment: comment.value,
};
const { actor: createdActor } = await post('/actors', newActor, {
successFeedback: 'Actor has been added.',
appendErrorMessage: true,
});
actor.value = createdActor;
} catch (error) {
events.emit('feedback', {
type: 'error',
message: error.message,
});
}
submitting.value = false;
submitted.value = true;
}
async function submit() {
if (!actor.value) {
submitCreate();
return;
}
try {
submitting.value = true;
await post('/revisions/actors', {
actorId: actor.value.id,
edits: {
edits: omit({
...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]);
@@ -629,15 +700,16 @@ async function submit() {
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,
},
}, [
'metricHeight',
'metricWeight',
'imperialHeight',
'imperialWeight',
'metricLength',
'metricGirth',
'imperialLength',
'imperialGirth',
]),
sizeUnits: sizeUnits.value,
figureUnits: figureUnits.value,
penisUnits: penisUnits.value,

View File

@@ -0,0 +1,34 @@
import { redirect } from 'vike/abort'; /* eslint-disable-line import/extensions */
import { fetchActorsById } from '#/src/actors.js';
import { fetchCountries } from '#/src/countries.js';
export async function onBeforeRender(pageContext) {
if (!pageContext.user) {
throw redirect(`/login?r=${encodeURIComponent(pageContext.urlOriginal)}`);
}
const [actor] = pageContext.routeParams.actorId
? await fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user)
: [];
const countries = await fetchCountries();
/*
if (!actor) {
throw render(404, `Cannot find actor '${pageContext.routeParams.actorId}'.`);
}
*/
return {
pageContext: {
title: actor
? `Editing '${actor.name}'`
: 'Adding actor',
pageProps: {
actor,
countries,
},
},
};
}

View File

@@ -0,0 +1,20 @@
// export default '/actor/edit/@actorId/*';
// import { redirect } from 'vike/abort'; /* eslint-disable-line import/extensions */
import { match } from 'path-to-regexp';
const path = '/actor/(edit|new)/:actorId?/:actorSlug?';
const urlMatch = match(path, { decode: decodeURIComponent });
export default (pageContext) => {
const matched = urlMatch(pageContext.urlPathname);
if (matched) {
return {
routeParams: matched.params.actorId ? {
actorId: matched.params.actorId,
} : {},
};
}
return false;
};

View File

@@ -3,6 +3,7 @@ import { differenceInYears } from 'date-fns';
import { unit } from 'mathjs';
import { MerkleJson } from 'merkle-json';
import moment from 'moment';
import { nanoid } from 'nanoid';
import omit from 'object.omit';
import convert from 'convert';
import unprint from 'unprint';
@@ -521,6 +522,27 @@ export async function fetchActors(filters, rawOptions, reqUser) {
};
}
function curateActorEntry(actor, context) {
return {
name: actor.name,
slug: slugify(actor.name),
entry_id: nanoid(), // allows for manual creation of multiple actors with the same name
gender: actor.gender,
comment: context?.comment,
};
}
export async function createActor(newActor, context, reqUser) {
if (!reqUser || reqUser.role === 'user') {
throw new HttpError('You are not permitted to create actors', 403);
}
const curatedActorEntry = curateActorEntry(newActor, context);
const [actorEntry] = await knex('actors').insert(curatedActorEntry).returning('*');
return curateActor(actorEntry);
}
export async function fetchActorRevisions(revisionId, filters = {}, reqUser) {
const limit = filters.limit || 50;
const page = filters.page || 1;

View File

@@ -1,8 +1,10 @@
import Router from 'express-promise-router';
import omit from 'object.omit';
import {
fetchActors,
fetchActorsById,
createActor,
fetchActorRevisions,
createActorRevision,
reviewActorRevision,
@@ -169,6 +171,12 @@ export async function fetchActorsByIdGraphql(query, _req, _info) {
return curatedActors[0];
}
export async function createActorApi(req, res) {
const actor = await createActor(req.body.actor, omit(req.body, ['actor']), req.user);
res.send({ actor });
}
async function fetchActorRevisionsApi(req, res) {
const revisions = await fetchActorRevisions(Number(req.params.revisionId) || null, req.query, req.user);
@@ -190,6 +198,7 @@ async function reviewActorRevisionApi(req, res) {
export const actorsRouter = Router();
actorsRouter.get('/api/actors', fetchActorsApi);
actorsRouter.post('/api/actors', createActorApi);
actorsRouter.get('/api/revisions/actors', fetchActorRevisionsApi);
actorsRouter.get('/api/revisions/actors/:revisionId', fetchActorRevisionsApi);