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-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> <template v-else>Your revision has been submitted for review. Thank you for your contribution!</template>
<ul> <ul v-if="actor">
<li> <li>
<a <a
:href="`/actor/${actor.id}/${actor.slug}`" :href="`/actor/${actor.id}/${actor.slug}`"
class="link" class="link"
>Return to actor</a> >{{ creating ? 'Go' : 'Return' }} to actor</a>
</li> </li>
<li> <li>
@@ -22,21 +22,21 @@
>Make another edit</a> >Make another edit</a>
</li> </li>
<li> <li v-if="!creating">
<a <a
:href="`/actor/revs/${actor.id}/${actor.slug}`" :href="`/actor/revs/${actor.id}/${actor.slug}`"
class="link" class="link"
>Go to actor revisions</a> >Go to actor revisions</a>
</li> </li>
<li> <li v-if="!creating">
<a <a
:href="`/user/${user.username}/revisions/actors`" :href="`/user/${user.username}/revisions/actors`"
class="link" class="link"
>Go to user revisions</a> >Go to user revisions</a>
</li> </li>
<li v-if="user.role !== 'user'"> <li v-if="user.role !== 'user' && !creating">
<a <a
href="/admin/revisions/actors" href="/admin/revisions/actors"
class="link" class="link"
@@ -49,7 +49,10 @@
v-else v-else
@submit.prevent @submit.prevent
> >
<div class="editor-header"> <div
v-if="actor"
class="editor-header"
>
<h2 class="heading ellipsis">Edit actor #{{ actor.id }} - {{ actor.name }}</h2> <h2 class="heading ellipsis">Edit actor #{{ actor.id }} - {{ actor.name }}</h2>
<a <a
@@ -59,6 +62,13 @@
>Go to actor</a> >Go to actor</a>
</div> </div>
<div
v-else
class="editor-header"
>
<h2 class="heading ellipsis">Add actor</h2>
</div>
<ul class="nolist"> <ul class="nolist">
<li <li
v-for="item in fields" v-for="item in fields"
@@ -281,6 +291,7 @@
</select> </select>
<input <input
v-if="item.hasDescription !== false"
v-model="edits[item.key].description" v-model="edits[item.key].description"
class="description input" class="description input"
placeholder="Description" placeholder="Description"
@@ -317,7 +328,7 @@
<div class="editor-actions"> <div class="editor-actions">
<Checkbox <Checkbox
v-if="user.role !== 'user'" v-if="user.role !== 'user' && actor"
label="Approve and apply immediately" label="Approve and apply immediately"
:checked="apply" :checked="apply"
:disabled="editing.size === 0" :disabled="editing.size === 0"
@@ -346,6 +357,7 @@
<script setup> <script setup>
import { ref, computed, inject } from 'vue'; import { ref, computed, inject } from 'vue';
import { format } from 'date-fns'; import { format } from 'date-fns';
import omit from 'object.omit';
import EditSocials from '#/components/edit/socials.vue'; import EditSocials from '#/components/edit/socials.vue';
import EditPlace from '#/components/edit/place.vue'; import EditPlace from '#/components/edit/place.vue';
@@ -361,13 +373,15 @@ import {
post, post,
} from '#/src/api.js'; } from '#/src/api.js';
import events from '#/src/events.js';
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
const user = pageContext.user; const user = pageContext.user;
const actor = ref(pageContext.pageProps.actor); const actor = ref(pageContext.pageProps.actor || null);
const fields = computed(() => [ const fields = computed(() => [
...(actor.value.photos.length > 0 ? [{ ...(actor.value?.photos.length > 0 ? [{
key: 'avatar', key: 'avatar',
type: 'avatar', type: 'avatar',
value: actor.value.avatar?.id, value: actor.value.avatar?.id,
@@ -377,13 +391,13 @@ const fields = computed(() => [
? [{ ? [{
key: 'name', key: 'name',
type: 'string', type: 'string',
value: actor.value.name, value: actor.value?.name,
}] }]
: []), : []),
{ {
key: 'gender', key: 'gender',
type: 'select', type: 'select',
value: actor.value.gender, value: actor.value?.gender || null,
options: [null, 'female', 'male', 'transsexual', 'other'], options: [null, 'female', 'male', 'transsexual', 'other'],
inline: true, inline: true,
}, },
@@ -391,7 +405,7 @@ const fields = computed(() => [
key: 'dateOfBirth', key: 'dateOfBirth',
label: 'date of birth', label: 'date of birth',
type: 'date', type: 'date',
value: actor.value.dateOfBirth value: actor.value?.dateOfBirth
? format(actor.value.dateOfBirth, 'yyyy-MM-dd') ? format(actor.value.dateOfBirth, 'yyyy-MM-dd')
: null, : null,
inline: true, inline: true,
@@ -399,28 +413,28 @@ const fields = computed(() => [
{ {
key: 'socials', key: 'socials',
type: 'socials', type: 'socials',
value: actor.value.socials, value: actor.value?.socials || [],
}, },
{ {
key: 'origin', key: 'origin',
type: 'place', type: 'place',
value: { value: {
country: actor.value.origin?.country?.alpha2 || null, country: actor.value?.origin?.country?.alpha2 || null,
place: [actor.value.origin?.city, actor.value.origin?.state].filter(Boolean).join(', '), place: [actor.value?.origin?.city, actor.value?.origin?.state].filter(Boolean).join(', '),
}, },
}, },
{ {
key: 'residence', key: 'residence',
type: 'place', type: 'place',
value: { value: {
country: actor.value.residence?.country?.alpha2 || null, country: actor.value?.residence?.country?.alpha2 || null,
place: [actor.value.residence?.city, actor.value.residence?.state].filter(Boolean).join(', '), place: [actor.value?.residence?.city, actor.value?.residence?.state].filter(Boolean).join(', '),
}, },
}, },
{ {
key: 'ethnicity', key: 'ethnicity',
type: 'string', type: 'string',
value: actor.value.ethnicity, value: actor.value?.ethnicity || null,
suggestions: [ suggestions: [
'Asian', 'Asian',
'Black', 'Black',
@@ -433,20 +447,20 @@ const fields = computed(() => [
key: 'size', key: 'size',
type: 'size', type: 'size',
value: { value: {
metricHeight: actor.value.height?.metric, metricHeight: actor.value?.height?.metric || null,
metricWeight: actor.value.weight?.metric, metricWeight: actor.value?.weight?.metric || null,
imperialHeight: actor.value.height?.imperial || [], imperialHeight: actor.value?.height?.imperial || [],
imperialWeight: actor.value.weight?.imperial, imperialWeight: actor.value?.weight?.imperial || null,
}, },
}, },
{ {
key: 'figure', key: 'figure',
type: 'figure', type: 'figure',
value: { value: {
bust: actor.value.bust, bust: actor.value?.bust || null,
cup: actor.value.cup, cup: actor.value?.cup || null,
waist: actor.value.waist, waist: actor.value?.waist || null,
hip: actor.value.hip, hip: actor.value?.hip || null,
}, },
}, },
{ {
@@ -454,25 +468,25 @@ const fields = computed(() => [
type: 'augmentation', type: 'augmentation',
note: 'Provide explicit evidence, such as social media posts, visible scarring, or before/after. Avoid "it\'s obvious".', note: 'Provide explicit evidence, such as social media posts, visible scarring, or before/after. Avoid "it\'s obvious".',
value: { value: {
naturalBoobs: actor.value.naturalBoobs, naturalBoobs: actor.value?.naturalBoobs || null,
boobsVolume: actor.value.boobsVolume, boobsVolume: actor.value?.boobsVolume || null,
boobsImplant: actor.value.boobsImplant, boobsImplant: actor.value?.boobsImplant || null,
boobsPlacement: actor.value.boobsPlacement, boobsPlacement: actor.value?.boobsPlacement || null,
boobsIncision: actor.value.boobsIncision, boobsIncision: actor.value?.boobsIncision || null,
boobsSurgeon: actor.value.boobsSurgeon, boobsSurgeon: actor.value?.boobsSurgeon || null,
naturalButt: actor.value.naturalButt, naturalButt: actor.value?.naturalButt || null,
buttVolume: actor.value.buttVolume, buttVolume: actor.value?.buttVolume || null,
buttImplant: actor.value.buttImplant, buttImplant: actor.value?.buttImplant || null,
naturalLips: actor.value.naturalLips, naturalLips: actor.value?.naturalLips || null,
lipsVolume: actor.value.lipsVolume, lipsVolume: actor.value?.lipsVolume || null,
naturalLabia: actor.value.naturalLabia, naturalLabia: actor.value?.naturalLabia || null,
}, },
}, },
{ {
key: 'hairColor', key: 'hairColor',
label: 'hair color', label: 'hair color',
type: 'select', type: 'select',
value: actor.value.hairColor, value: actor.value?.hairColor || null,
options: [ options: [
null, null,
'black', 'black',
@@ -491,7 +505,7 @@ const fields = computed(() => [
key: 'eyes', key: 'eyes',
label: 'eye color', label: 'eye color',
type: 'select', type: 'select',
value: actor.value.eyes, value: actor.value?.eyes || null,
options: [ options: [
null, null,
'blue', 'blue',
@@ -506,8 +520,8 @@ const fields = computed(() => [
key: 'tattoos', key: 'tattoos',
type: 'has', type: 'has',
value: { value: {
has: actor.value.hasTattoos, has: actor.value?.hasTattoos || null,
description: actor.value.tattoos, description: actor.value?.tattoos || null,
}, },
}, },
{ {
@@ -515,14 +529,14 @@ const fields = computed(() => [
type: 'has', type: 'has',
note: 'Excludes earrings', note: 'Excludes earrings',
value: { value: {
has: actor.value.hasPiercings, has: actor.value?.hasPiercings || null,
description: actor.value.piercings, description: actor.value?.piercings || null,
}, },
}, },
{ {
key: 'agency', key: 'agency',
type: 'string', type: 'string',
value: actor.value.agency, value: actor.value?.agency || null,
suggestions: [ suggestions: [
'101 Modeling', '101 Modeling',
'Adult Talent Managers (ATMLA)', 'Adult Talent Managers (ATMLA)',
@@ -540,25 +554,41 @@ const fields = computed(() => [
key: 'penis', key: 'penis',
type: 'penis', type: 'penis',
value: { value: {
metricLength: actor.value.penisLength?.metric, metricLength: actor.value?.penisLength?.metric || null,
metricGirth: actor.value.penisGirth?.metric, metricGirth: actor.value?.penisGirth?.metric || null,
imperialLength: actor.value.penisLength?.imperial, imperialLength: actor.value?.penisLength?.imperial || null,
imperialGirth: actor.value.penisGirth?.imperial, imperialGirth: actor.value?.penisGirth?.imperial || null,
isCircumcised: actor.value.isCircumcised, isCircumcised: actor.value?.isCircumcised || null,
}, },
}, },
{ {
key: 'dateOfDeath', key: 'dateOfDeath',
label: 'date of death', label: 'date of death',
type: 'date', type: 'date',
value: actor.value.dateOfDeath value: actor.value?.dateOfDeath
? format(actor.value.dateOfDeath, 'yyyy-MM-dd') ? format(actor.value.dateOfDeath, 'yyyy-MM-dd')
: null, : 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 edits = ref(Object.fromEntries(fields.value.map((field) => [field.key, field.value])));
const creating = !actor.value;
const comment = ref(null); const comment = ref(null);
const apply = ref(user.role !== 'user'); const apply = ref(user.role !== 'user');
const submitting = ref(false); const submitting = ref(false);
@@ -609,13 +639,54 @@ const groupMap = {
weight: 'size', 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() { async function submit() {
if (!actor.value) {
submitCreate();
return;
}
try { try {
submitting.value = true; submitting.value = true;
await post('/revisions/actors', { await post('/revisions/actors', {
actorId: actor.value.id, actorId: actor.value.id,
edits: { edits: omit({
...Object.fromEntries(Array.from(editing.value).flatMap((key) => { ...Object.fromEntries(Array.from(editing.value).flatMap((key) => {
if (edits.value[key] && typeof edits.value[key] === 'object' && !Array.isArray(edits.value[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 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, penisLength: penisUnits.value === 'imperial' ? edits.value.penis.imperialLength : edits.value.penis.metricLength,
penisGirth: penisUnits.value === 'imperial' ? edits.value.penis.imperialGirth : edits.value.penis.metricGirth, penisGirth: penisUnits.value === 'imperial' ? edits.value.penis.imperialGirth : edits.value.penis.metricGirth,
}).filter(([key]) => editing.value.has(groupMap[key] || key))), }).filter(([key]) => editing.value.has(groupMap[key] || key))),
metricHeight: undefined, }, [
metricWeight: undefined, 'metricHeight',
imperialHeight: undefined, 'metricWeight',
imperialWeight: undefined, 'imperialHeight',
metricLength: undefined, 'imperialWeight',
metricGirth: undefined, 'metricLength',
imperialLength: undefined, 'metricGirth',
imperialGirth: undefined, 'imperialLength',
}, 'imperialGirth',
]),
sizeUnits: sizeUnits.value, sizeUnits: sizeUnits.value,
figureUnits: figureUnits.value, figureUnits: figureUnits.value,
penisUnits: penisUnits.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 { unit } from 'mathjs';
import { MerkleJson } from 'merkle-json'; import { MerkleJson } from 'merkle-json';
import moment from 'moment'; import moment from 'moment';
import { nanoid } from 'nanoid';
import omit from 'object.omit'; import omit from 'object.omit';
import convert from 'convert'; import convert from 'convert';
import unprint from 'unprint'; 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) { export async function fetchActorRevisions(revisionId, filters = {}, reqUser) {
const limit = filters.limit || 50; const limit = filters.limit || 50;
const page = filters.page || 1; const page = filters.page || 1;

View File

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