Added actor revision overviews to actor and user pages.

This commit is contained in:
DebaucheryLibrarian 2024-10-23 01:28:54 +02:00
parent d0cf9bf5d0
commit 05bd7b703d
21 changed files with 424 additions and 219 deletions

2
common

@ -1 +1 @@
Subproject commit 40011a62dae9da8deda71e9f8daf39665a8b7958 Subproject commit e55818ab448d463c4765c3394a6049280799ec33

View File

@ -12,6 +12,11 @@
:title="actor.avatar.credit && `© ${actor.avatar.credit}`" :title="actor.avatar.credit && `© ${actor.avatar.credit}`"
class="avatar" class="avatar"
> >
<span
v-if="actor.avatar?.credit"
class="avatar-credit"
>{{ actor.avatar.credit }}</span>
</div> </div>
<ul class="bio nolist"> <ul class="bio nolist">
@ -140,14 +145,14 @@
</li> </li>
<li <li
v-if="!actor.naturalBoobs || !actor.naturalButt" v-if="actor.naturalBoobs === false || actor.naturalButt === false"
class="bio-item augmentations" class="bio-item augmentations"
> >
<dfn class="bio-label"><Icon icon="magic-wand2" />Augmentations</dfn> <dfn class="bio-label"><Icon icon="magic-wand2" />Enhanced</dfn>
<span class="bio-value"> <span class="bio-value">
<div <div
v-if="!actor.naturalBoobs" v-if="actor.naturalBoobs === false"
:title="[actor.boobsVolume, augmentationMap[actor.boobsPlacement] || actor.boobsPlacement, augmentationMap[actor.boobsImplant] || actor.boobsImplant].filter(Boolean).join(' ')" :title="[actor.boobsVolume, augmentationMap[actor.boobsPlacement] || actor.boobsPlacement, augmentationMap[actor.boobsImplant] || actor.boobsImplant].filter(Boolean).join(' ')"
class="augmentations-section" class="augmentations-section"
>Boobs<template v-if="actor.boobsVolume || actor.boobsImplant">:&nbsp;</template> >Boobs<template v-if="actor.boobsVolume || actor.boobsImplant">:&nbsp;</template>
@ -156,7 +161,7 @@
</div> </div>
<div <div
v-if="!actor.naturalButt" v-if="actor.naturalButt === false"
class="augmentations-section" class="augmentations-section"
>Butt<template v-if="actor.buttVolume || actor.buttImplant">:&nbsp;</template> >Butt<template v-if="actor.buttVolume || actor.buttImplant">:&nbsp;</template>
<template v-if="actor.buttVolume">{{ actor.buttVolume }}cc</template> <template v-if="actor.buttVolume">{{ actor.buttVolume }}cc</template>
@ -258,21 +263,38 @@
<span v-else>Yes</span> <span v-else>Yes</span>
</li> </li>
<li class="bio-item updated hideable">Updated {{ formatDate(actor.updatedAt, 'yyyy-MM-dd hh:mm') }}, ID: {{ actor.id }}</li> <li
v-if="actor.agency"
class="bio-item"
>
<dfn class="bio-label"><Icon icon="user-tie" />Agency</dfn>
<li class="bio-item actor-actions"> <span
<a :title="actor.agency"
v-if="user && user.role !== 'user'" class="bio-value"
:href="`/actor/edit/${actor.id}/${actor.slug}`" >{{ actor.agency }}</span>
target="_blank" </li>
class="link"
>Edit bio</a>
<a <li class="bio-item updated hideable">
:href="`/actor/revisions/${actor.id}/${actor.slug}`" <span
target="_blank" class="ellipsis"
class="link" :title="`#${actor.id} Updated ${formatDate(actor.updatedAt, 'yyyy-MM-dd hh:mm')}`"
>Revisions</a> >#{{ actor.id }} Updated {{ 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> </li>
</ul> </ul>
@ -413,17 +435,32 @@ const descriptions = Object.values(Object.fromEntries(props.actor.profiles
} }
.avatar-container { .avatar-container {
padding: 0 0 1rem 1rem; position: relative;
margin: 0 .5rem 1rem 1rem;
flex-shrink: 0; flex-shrink: 0;
font-size: 0;
} }
.avatar { .avatar {
height: 100%; height: 100%;
flex-shrink: 0; flex-shrink: 0;
border: solid 3px var(--highlight-hint); border: solid 3px var(--highlight-weak-30);
margin: 0 .5rem 0 0; 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 { &.expanded {
padding-bottom: 1.5rem; padding-bottom: 1.5rem;
margin-bottom: .75rem; margin-bottom: .75rem;
@ -573,10 +610,22 @@ const descriptions = Object.values(Object.fromEntries(props.actor.profiles
} }
.updated { .updated {
color: var(--highlight-weak-20); color: var(--highlight-weak-10);
font-size: .8rem; font-size: .8rem;
} }
.actor-actions {
display: flex;
justify-content: flex-start;
gap: 1rem;
margin-right: .5rem;
.link {
color: inherit;
flex-shrink: 0;
}
}
.descriptions-container { .descriptions-container {
max-width: 30rem; max-width: 30rem;
max-height: 100%; max-height: 100%;
@ -691,16 +740,6 @@ const descriptions = Object.values(Object.fromEntries(props.actor.profiles
} }
} }
.actor-actions {
justify-content: flex-start;
gap: 1rem;
font-weight: normal;
.link {
color: var(--highlight-strong-20);
}
}
@media(--big) { @media(--big) {
.descriptions-container { .descriptions-container {
display: none; display: none;

View File

@ -45,6 +45,7 @@ const pageContext = inject('pageContext');
padding: 1rem 1rem .75rem 1rem; padding: 1rem 1rem .75rem 1rem;
border-bottom: solid 1px var(--shadow-weak-30); border-bottom: solid 1px var(--shadow-weak-30);
margin-bottom: .25rem; margin-bottom: .25rem;
overflow-x: auto;
} }
.nav-items { .nav-items {
@ -54,6 +55,7 @@ const pageContext = inject('pageContext');
.nav-item { .nav-item {
display: block; display: block;
flex-shrink: 0;
background: var(--background-dark-20); background: var(--background-dark-20);
border-radius: 1rem; border-radius: 1rem;
color: var(--glass-strong-20); color: var(--glass-strong-20);

110
components/edit/avatar.vue Normal file
View File

@ -0,0 +1,110 @@
<template>
<div class="avatar noshrink">
<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>
</template>
<script setup>
import getPath from '#/src/get-path.js';
defineProps({
avatar: {
type: Object,
default: null,
},
});
</script>
<style scoped>
.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;
}
</style>

View File

@ -116,6 +116,12 @@
>{{ item.name || item.id || item }}</li>&nbsp;] >{{ item.name || item.id || item }}</li>&nbsp;]
</ul> </ul>
<Avatar
v-else-if="delta.key === 'avatar'"
:avatar="avatarsById[rev.base[delta.key]]"
class="delta-avatar"
/>
<template v-else-if="rev.base[delta.key] instanceof Date">{{ format(rev.base[delta.key], 'yyyy-MM-dd hh:mm') }}</template> <template v-else-if="rev.base[delta.key] instanceof Date">{{ format(rev.base[delta.key], 'yyyy-MM-dd hh:mm') }}</template>
<template v-else>{{ rev.base[delta.key] }}</template> <template v-else>{{ rev.base[delta.key] }}</template>
</span> </span>
@ -135,6 +141,12 @@
>{{ item.name || item.id || item }}</li>&nbsp;] >{{ item.name || item.id || item }}</li>&nbsp;]
</ul> </ul>
<Avatar
v-else-if="delta.key === 'avatar'"
:avatar="avatarsById[delta.value]"
class="delta-avatar"
/>
<template v-else-if="delta.value instanceof Date">{{ format(delta.value, 'yyyy-MM-dd hh:mm') }}</template> <template v-else-if="delta.value instanceof Date">{{ format(delta.value, 'yyyy-MM-dd hh:mm') }}</template>
<template v-else>{{ delta.value }}</template> <template v-else>{{ delta.value }}</template>
</span> </span>
@ -156,7 +168,7 @@
@click="expanded.add(rev.id)" @click="expanded.add(rev.id)"
> >
<span class="rev-scene nolink noshrink"><template v-if="context !== 'scene'">{{ rev.sceneId }}</template>@{{ rev.hash.slice(0, 6) }}</span> <span class="rev-scene nolink noshrink"><template v-if="context !== 'scene' && context !== 'actor'">{{ rev.sceneId || rev.actorId }}</template>@{{ rev.hash.slice(0, 6) }}</span>
<span <span
v-if="context !== 'scene'" v-if="context !== 'scene'"
@ -205,6 +217,7 @@
import { ref, computed, inject } from 'vue'; import { ref, computed, inject } from 'vue';
import { format } from 'date-fns'; import { format } from 'date-fns';
import Avatar from '#/components/edit/avatar.vue';
import Checkbox from '#/components/form/checkbox.vue'; import Checkbox from '#/components/form/checkbox.vue';
import { get, post } from '#/src/api.js'; import { get, post } from '#/src/api.js';
@ -224,10 +237,12 @@ const domain = pageContext.routeParams.domain;
const actors = ref(pageContext.pageProps.actors); const actors = ref(pageContext.pageProps.actors);
const tags = ref(pageContext.pageProps.tags); const tags = ref(pageContext.pageProps.tags);
const movies = ref(pageContext.pageProps.movies); const movies = ref(pageContext.pageProps.movies);
const avatars = ref(pageContext.pageProps.avatars);
const actorsById = computed(() => Object.fromEntries(actors.value.map((actor) => [actor.id, actor]))); const actorsById = computed(() => Object.fromEntries(actors.value.map((actor) => [actor.id, actor])));
const tagsById = computed(() => Object.fromEntries(tags.value.map((tag) => [tag.id, tag]))); const tagsById = computed(() => Object.fromEntries(tags.value.map((tag) => [tag.id, tag])));
const moviesById = computed(() => Object.fromEntries(movies.value.map((movie) => [movie.id, movie]))); const moviesById = computed(() => Object.fromEntries(movies.value.map((movie) => [movie.id, movie])));
const avatarsById = computed(() => Object.fromEntries(avatars.value.map((avatar) => [avatar.id, avatar])));
const feedbacks = ref({}); const feedbacks = ref({});
const showReviewed = ref(false); const showReviewed = ref(false);
@ -318,6 +333,7 @@ async function reloadRevisions() {
actors.value = updatedRevisions.actors; actors.value = updatedRevisions.actors;
tags.value = updatedRevisions.tags; tags.value = updatedRevisions.tags;
movies.value = updatedRevisions.movies; movies.value = updatedRevisions.movies;
avatars.value = updatedRevisions.avatars;
revisions.value = updatedRevisions.revisions; revisions.value = updatedRevisions.revisions;
} }

View File

@ -39,18 +39,27 @@
class="photos nobar" class="photos nobar"
:class="{ 'has-avatar': actor.avatar, 'has-photos': actor.avatar ? photos.length > 1 : photos.length > 0 }" :class="{ 'has-avatar': actor.avatar, 'has-photos': actor.avatar ? photos.length > 1 : photos.length > 0 }"
> >
<img <div
v-for="photo in photos" v-for="photo in photos"
:key="`photo-${photo.id}`" :key="`photo-${photo.id}`"
:src="getPath(photo, 'thumbnail')" class="photo-container"
:width="photo.width"
:height="photo.height"
:style="{ 'background-image': `url('${getPath(photo, 'lazy')}')` }"
:title="photo.credit && `© ${photo.credit}`"
loading="lazy"
class="photo"
:class="{ avatar: photo.isAvatar }" :class="{ avatar: photo.isAvatar }"
> >
<img
:src="getPath(photo, 'thumbnail')"
:width="photo.width"
:height="photo.height"
:style="{ 'background-image': `url('${getPath(photo, 'lazy')}')` }"
:title="photo.credit && `© ${photo.credit}`"
loading="lazy"
class="photo"
>
<span
v-if="photo.credit"
class="photo-credit"
>{{ photo.credit }}</span>
</div>
</div> </div>
<Domains <Domains
@ -150,18 +159,37 @@ const photos = Object.values(Object.fromEntries(actor.profiles
} }
} }
.photo-container {
position: relative;
font-size: 0;
&.avatar {
display: none;
}
}
.photo { .photo {
height: 14rem; height: 14rem;
width: auto; width: auto;
object-fit: cover; object-fit: cover;
object-position: 50% 0; object-position: 50% 0;
border-radius: .25rem;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
box-shadow: 0 0px 3px var(--shadow-weak-30); box-shadow: 0 0px 3px var(--shadow-weak-30);
}
&.avatar { .photo-credit {
display: none; width: 100%;
} position: absolute;
left: 0;
bottom: 0;
z-index: 1;
box-sizing: border-box;
padding: .15rem .2rem;
color: var(--text-light);
font-size: .75rem;
text-shadow: 1px 1px 0 var(--shadow-strong-20);
} }
.domains-bar { .domains-bar {
@ -174,7 +202,7 @@ const photos = Object.values(Object.fromEntries(actor.profiles
display: flex; display: flex;
} }
.photo.avatar { .photo-container.avatar {
display: inline-block; display: inline-block;
} }
} }

View File

@ -24,7 +24,7 @@
<li> <li>
<a <a
:href="`/actor/revisions/${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>
@ -86,9 +86,20 @@
v-if="item.type === 'string'" v-if="item.type === 'string'"
v-model="edits[item.key]" v-model="edits[item.key]"
class="string input" class="string input"
:list="item.suggestions && `suggestions-${item.key}`"
:disabled="!editing.has(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 <textarea
v-if="item.type === 'text'" v-if="item.type === 'text'"
v-model="edits[item.key]" v-model="edits[item.key]"
@ -485,38 +496,13 @@
class="avatars" class="avatars"
:class="{ disabled: !editing.has(item.key) }" :class="{ disabled: !editing.has(item.key) }"
> >
<div <Avatar
v-for="avatar in item.options" v-for="avatar in item.options"
:key="`avatar-${avatar.id}`" :key="`avatar-${avatar.id}`"
class="avatar noshrink" :avatar="avatar"
:class="{ selected: edits[item.key] === avatar.id }" :class="{ selected: edits[item.key] === avatar.id }"
@click="setAvatar(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>
</div> </div>
</li> </li>
@ -561,8 +547,7 @@
import { ref, computed, inject } from 'vue'; import { ref, computed, inject } from 'vue';
import { format } from 'date-fns'; import { format } from 'date-fns';
import getPath from '#/src/get-path.js'; import Avatar from '#/components/edit/avatar.vue';
import Checkbox from '#/components/form/checkbox.vue'; import Checkbox from '#/components/form/checkbox.vue';
import { import {
@ -576,8 +561,6 @@ const user = pageContext.user;
const countries = pageContext.pageProps.countries; const countries = pageContext.pageProps.countries;
const actor = ref(pageContext.pageProps.actor); const actor = ref(pageContext.pageProps.actor);
console.log(actor.value);
const topCountries = [ const topCountries = [
'AU', 'AU',
'BR', 'BR',
@ -658,14 +641,14 @@ const fields = computed(() => [
{ {
key: 'height', key: 'height',
type: 'number', type: 'number',
value: actor.value.height.metric, value: actor.value.height?.metric,
unit: 'cm', unit: 'cm',
inline: true, inline: true,
}, },
{ {
key: 'weight', key: 'weight',
type: 'number', type: 'number',
value: actor.value.weight.metric, value: actor.value.weight?.metric,
unit: 'kg', unit: 'kg',
inline: true, inline: true,
}, },
@ -743,6 +726,22 @@ const fields = computed(() => [
description: actor.value.piercings, description: actor.value.piercings,
}, },
}, },
{
key: 'agency',
type: 'string',
value: actor.value.agency,
suggestions: [
'101 Modeling',
'Adult Talent Managers (ATMLA)',
'The Bakery Talent',
'Coxxx Models',
'East Coast Talent (ECT)',
'Hussie Models',
'Invision Models',
'OC Modeling',
'Spiegler Girls',
],
},
{ {
key: 'penis', key: 'penis',
type: 'penis', type: 'penis',
@ -780,30 +779,8 @@ function toggleField(item) {
} }
editing.value.add(item.key); 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) { function setAvatar(avatarId) {
edits.value.avatar = avatarId; edits.value.avatar = avatarId;
} }
@ -822,8 +799,6 @@ const keyMap = {
}; };
async function submit() { async function submit() {
console.log('SUBMIT', Array.from(editing.value), edits.value);
try { try {
await post('/revisions/actors', { await post('/revisions/actors', {
actorId: actor.value.id, actorId: actor.value.id,
@ -1014,75 +989,6 @@ async function submit() {
} }
} }
.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 { .item-actions {
.icon { .icon {
padding: .25rem 1rem; padding: .25rem 1rem;

View File

@ -1,23 +1,23 @@
<template> <template>
<div class="content"> <div class="content">
<div class="revs-header"> <div class="revs-header">
<h2 class="heading">Revisions for "{{ scene.title }}"</h2> <h2 class="heading">Revisions for "{{ actor.name }}"</h2>
<div class="revs-actions"> <div class="revs-actions">
<a <a
:href="`/scene/edit/${scene.id}/${scene.slug}`" :href="`/actor/edit/${actor.id}/${actor.slug}`"
class="link" class="link"
>Edit scene</a> >Edit actor</a>
<a <a
:href="`/scene/${scene.id}/${scene.slug}`" :href="`/actor/${actor.id}/${actor.slug}`"
target="_blank" target="_blank"
class="link" class="link"
>Go to scene</a> >Go to actor</a>
</div> </div>
</div> </div>
<Revisions context="scene" /> <Revisions context="actor" />
</div> </div>
</template> </template>
@ -27,7 +27,7 @@ import { inject } from 'vue';
import Revisions from '#/components/edit/revisions.vue'; import Revisions from '#/components/edit/revisions.vue';
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
const scene = pageContext.pageProps.scene; const actor = pageContext.pageProps.actor;
</script> </script>
<style scoped> <style scoped>

View File

@ -1,23 +1,24 @@
import { fetchActorsById } from '#/src/actors.js'; import { fetchActorsById, fetchActorRevisions } from '#/src/actors.js';
import { fetchSceneRevisions } from '#/src/scenes.js';
export async function onBeforeRender(pageContext) { export async function onBeforeRender(pageContext) {
const [actor] = await fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user); const [actor] = await fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user);
const { const {
revisions, revisions,
} = await fetchSceneRevisions(null, { avatars,
sceneId: actor.id, } = await fetchActorRevisions(null, {
actorId: actor.id,
isFinalized: true, isFinalized: true,
limit: 100, limit: 100,
}, pageContext.user); }, pageContext.user);
return { return {
pageContext: { pageContext: {
title: `Revisions for '${actor.name}'`, title: `Revs for '${actor.name}'`,
pageProps: { pageProps: {
actor, actor,
revisions, revisions,
avatars,
}, },
}, },
}; };

View File

@ -1 +1,19 @@
export default '/actor/revisions/@actorId/*'; import { match } from 'path-to-regexp';
const path = '/actor/revs/:actorId/:slug?';
const urlMatch = match(path, { decode: decodeURIComponent });
export default (pageContext) => {
const matched = urlMatch(pageContext.urlPathname);
if (matched) {
return {
routeParams: {
actorId: matched.params.actorId,
domain: 'actors',
},
};
}
return false;
};

View File

@ -1,9 +1,36 @@
<template> <template>
<Admin> <Admin>
<h2 class="heading">Admin Panel</h2> <h2 class="heading">Admin Panel</h2>
<ul class="menu">
<li>
<a
href="/admin/revisions/scenes"
class="link"
>Scene Revisions</a>
</li>
<li>
<a
href="/admin/revisions/actors"
class="link"
>Actor Revisions</a>
</li>
</ul>
</Admin> </Admin>
</template> </template>
<script setup> <script setup>
import Admin from '#/components/admin/admin.vue'; import Admin from '#/components/admin/admin.vue';
</script> </script>
<style scoped>
.menu {
margin: 0;
.link {
display: block;
padding: .25rem;
}
}
</style>

View File

@ -8,6 +8,7 @@ export async function onBeforeRender(pageContext) {
const { const {
revisions, revisions,
avatars,
} = await fetchActorRevisions(null, { } = await fetchActorRevisions(null, {
isFinalized: false, isFinalized: false,
limit: 50, limit: 50,
@ -18,6 +19,7 @@ export async function onBeforeRender(pageContext) {
title: pageContext.routeParams.section, title: pageContext.routeParams.section,
pageProps: { pageProps: {
revisions, revisions,
avatars,
}, },
}, },
}; };

View File

@ -345,7 +345,7 @@
>Edit scene</a> >Edit scene</a>
<a <a
:href="`/scene/revisions/${scene.id}/${scene.slug}`" :href="`/scene/revs/${scene.id}/${scene.slug}`"
target="_blank" target="_blank"
class="link" class="link"
>Revisions</a> >Revisions</a>

View File

@ -24,7 +24,7 @@
<li> <li>
<a <a
:href="`/scene/revisions/${scene.id}/${scene.slug}`" :href="`/scene/revs/${scene.id}/${scene.slug}`"
class="link" class="link"
>Go to scene revisions</a> >Go to scene revisions</a>
</li> </li>
@ -320,7 +320,7 @@ function setDuration(unit, event) {
async function submit() { async function submit() {
try { try {
await post('/revisions', { await post('/revisions/scenes', {
sceneId: scene.value.id, sceneId: scene.value.id,
edits: { edits: {
...edits.value, ...edits.value,

View File

@ -21,7 +21,7 @@ export async function onBeforeRender(pageContext) {
return { return {
pageContext: { pageContext: {
title: `Revisions for '${scene.title}'`, title: `Revs for '${scene.title}'`,
pageProps: { pageProps: {
scene, scene,
revisions, revisions,

View File

@ -1 +1,19 @@
export default '/scene/revisions/@sceneId/*'; import { match } from 'path-to-regexp';
const path = '/scene/revs/:sceneId/:slug?';
const urlMatch = match(path, { decode: decodeURIComponent });
export default (pageContext) => {
const matched = urlMatch(pageContext.urlPathname);
if (matched) {
return {
routeParams: {
sceneId: matched.params.sceneId,
domain: 'scenes',
},
};
}
return false;
};

View File

@ -22,42 +22,48 @@
<a <a
:href="`/user/${profile.username}/stashes`" :href="`/user/${profile.username}/stashes`"
class="domain nolink" class="domain nolink"
:class="{ active: domain === 'stashes' }" :class="{ active: section === 'stashes' }"
>Stashes</a> >Stashes</a>
<a <a
:href="`/user/${profile.username}/alerts`" :href="`/user/${profile.username}/alerts`"
class="domain nolink" class="domain nolink"
:class="{ active: domain === 'alerts' }" :class="{ active: section === 'alerts' }"
>Alerts</a> >Alerts</a>
<a <a
:href="`/user/${profile.username}/templates`" :href="`/user/${profile.username}/templates`"
class="domain nolink" class="domain nolink"
:class="{ active: domain === 'templates' }" :class="{ active: section === 'templates' }"
>Templates</a> >Templates</a>
<a <a
:href="`/user/${profile.username}/revisions`" :href="`/user/${profile.username}/revisions/scenes`"
class="domain nolink" class="domain nolink"
:class="{ active: domain === 'revisions' }" :class="{ active: section === 'revisions' && domain === 'scenes' }"
>Revisions</a> >Scene Revisions</a>
<a
:href="`/user/${profile.username}/revisions/actors`"
class="domain nolink"
:class="{ active: section === 'revisions' && domain === 'actors' }"
>Actor Revisions</a>
</nav> </nav>
<Stashes v-if="domain === 'stashes'" /> <Stashes v-if="section === 'stashes'" />
<Alerts v-if="domain === 'alerts' && profile.id === user?.id" /> <Alerts v-if="section === 'alerts' && profile.id === user?.id" />
<Summaries <Summaries
v-if="domain === 'templates' && profile.id === user?.id" v-if="section === 'templates' && profile.id === user?.id"
:release="mockupRelease" :release="mockupRelease"
/> />
</div> </div>
<div <div
v-if="domain === 'revisions' && profile.id === user?.id" v-if="section === 'revisions' && profile.id === user?.id"
class="profile-section revisions" class="profile-section revisions"
> >
<h3 class="section-header heading">Revisions</h3> <h3 class="section-header heading">{{ domain.slice(0, -1) }} Revisions</h3>
<Revisions context="user" /> <Revisions context="user" />
</div> </div>
</div> </div>
@ -73,7 +79,10 @@ import Summaries from '#/components/scenes/summaries.vue';
import Revisions from '#/components/edit/revisions.vue'; import Revisions from '#/components/edit/revisions.vue';
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
const section = pageContext.routeParams.section;
const domain = pageContext.routeParams.domain; const domain = pageContext.routeParams.domain;
const user = pageContext.user; const user = pageContext.user;
const profile = ref(pageContext.pageProps.profile); const profile = ref(pageContext.pageProps.profile);
@ -117,6 +126,7 @@ const mockupRelease = {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: .5rem 1rem; padding: .5rem 1rem;
text-transform: capitalize;
.button { .button {
margin-left: 1rem; margin-left: 1rem;
@ -222,6 +232,11 @@ const mockupRelease = {
padding: 0 1rem; padding: 0 1rem;
} }
.revisions-nav {
display: flex;
gap: 1rem;
}
@media(--compact) { @media(--compact) {
.domains { .domains {
padding: .5rem 1rem; padding: .5rem 1rem;

View File

@ -4,19 +4,37 @@ import { fetchUser } from '#/src/users.js';
import { fetchUserStashes } from '#/src/stashes.js'; import { fetchUserStashes } from '#/src/stashes.js';
import { fetchAlerts } from '#/src/alerts.js'; import { fetchAlerts } from '#/src/alerts.js';
import { fetchSceneRevisions } from '#/src/scenes.js'; import { fetchSceneRevisions } from '#/src/scenes.js';
import { fetchActorRevisions } from '#/src/actors.js';
async function fetchRevisions(pageContext) {
if (pageContext.routeParams.username !== pageContext.user?.username) {
return {};
}
if (pageContext.routeParams.section === 'revisions' && pageContext.routeParams.domain === 'scenes') {
return fetchSceneRevisions(null, {
userId: pageContext.user.id,
limit: 100,
}, pageContext.user);
}
if (pageContext.routeParams.section === 'revisions' && pageContext.routeParams.domain === 'actors') {
return fetchActorRevisions(null, {
userId: pageContext.user.id,
limit: 100,
}, pageContext.user);
}
return {};
}
export async function onBeforeRender(pageContext) { export async function onBeforeRender(pageContext) {
const [profile, alerts, userRevisions] = await Promise.all([ const [profile, alerts, userRevisions] = await Promise.all([
fetchUser(pageContext.routeParams.username, {}, pageContext.user), fetchUser(pageContext.routeParams.username, {}, pageContext.user),
pageContext.routeParams.domain === 'stashes' && pageContext.routeParams.username === pageContext.user?.username pageContext.routeParams.section === 'alerts' && pageContext.routeParams.username === pageContext.user?.username
? fetchAlerts(pageContext.user) ? fetchAlerts(pageContext.user)
: [], : [],
pageContext.routeParams.domain === 'revisions' && pageContext.routeParams.username === pageContext.user?.username fetchRevisions(pageContext),
? fetchSceneRevisions(null, {
userId: pageContext.user.id,
limit: 100,
}, pageContext.user)
: {},
]); ]);
if (!profile) { if (!profile) {
@ -28,8 +46,11 @@ export async function onBeforeRender(pageContext) {
actors, actors,
tags, tags,
movies, movies,
avatars,
} = userRevisions; } = userRevisions;
console.log(userRevisions);
const stashes = await fetchUserStashes(profile.id, pageContext.user); const stashes = await fetchUserStashes(profile.id, pageContext.user);
return { return {
@ -43,6 +64,7 @@ export async function onBeforeRender(pageContext) {
actors, actors,
tags, tags,
movies, movies,
avatars,
}, },
}, },
}; };

View File

@ -1,7 +1,7 @@
import { redirect } from 'vike/abort'; /* eslint-disable-line import/extensions */ import { redirect } from 'vike/abort'; /* eslint-disable-line import/extensions */
import { match } from 'path-to-regexp'; import { match } from 'path-to-regexp';
const path = '/user/:username/:domain?'; const path = '/user/:username/:section?/:domain?';
const urlMatch = match(path, { decode: decodeURIComponent }); const urlMatch = match(path, { decode: decodeURIComponent });
export default (pageContext) => { export default (pageContext) => {
@ -15,7 +15,8 @@ export default (pageContext) => {
return { return {
routeParams: { routeParams: {
username: matched.params.username, username: matched.params.username,
domain: matched.params.domain || 'stashes', section: matched.params.section || 'stashes',
domain: matched.params.domain || 'scenes',
}, },
}; };
} }

View File

@ -1,6 +1,7 @@
{ {
"name": "traxxx", "name": "traxxx",
"short_name": "traxxx", "short_name": "traxxx",
"scope": "/",
"icons": [ "icons": [
{ {
"src": "/img/favicon/android-chrome-192x192.png", "src": "/img/favicon/android-chrome-192x192.png",

View File

@ -79,6 +79,7 @@ export function curateActor(actor, context = {}) {
city: actor.residence_city, city: actor.residence_city,
state: actor.residence_state, state: actor.residence_state,
}, },
agency: actor.agency,
avatar: curateMedia(actor.avatar), avatar: curateMedia(actor.avatar),
profiles: context.profiles?.map((profile) => ({ profiles: context.profiles?.map((profile) => ({
id: profile.id, id: profile.id,
@ -159,8 +160,6 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
: [], : [],
]); ]);
console.log(actors);
if (options.order) { if (options.order) {
return actors.map((actorEntry) => curateActor(actorEntry, { return actors.map((actorEntry) => curateActor(actorEntry, {
stashes: stashes.filter((stash) => stash.actor_id === actorEntry.id), stashes: stashes.filter((stash) => stash.actor_id === actorEntry.id),
@ -183,8 +182,6 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
}); });
}).filter(Boolean); }).filter(Boolean);
console.log(curatedActors);
return curatedActors; return curatedActors;
} }
@ -448,11 +445,16 @@ export async function fetchActorRevisions(revisionId, filters = {}, reqUser) {
.limit(limit) .limit(limit)
.offset((page - 1) * limit); .offset((page - 1) * limit);
const avatarIds = Array.from(new Set(revisions.flatMap((revision) => [revision.base.avatar, revision.deltas.find((delta) => delta.key === 'avatar')?.value]))).filter(Boolean);
const avatarEntries = await knex('media').whereIn('id', avatarIds);
const avatars = avatarEntries.map((avatar) => curateMedia(avatar));
const curatedRevisions = revisions.map((revision) => curateRevision(revision)); const curatedRevisions = revisions.map((revision) => curateRevision(revision));
return { return {
revisions: curatedRevisions, revisions: curatedRevisions,
revision: revisionId && curatedRevisions.find((revision) => revision.id === revisionId), revision: revisionId && curatedRevisions.find((revision) => revision.id === revisionId),
avatars,
}; };
} }
@ -483,8 +485,6 @@ const keyMap = {
}; };
async function applyActorValueDelta(profileId, delta, trx) { async function applyActorValueDelta(profileId, delta, trx) {
console.log('value delta', profileId, delta, keyMap[delta.key], delta.value);
return knex('actors_profiles') return knex('actors_profiles')
.where('id', profileId) .where('id', profileId)
.update(keyMap[delta.key] || delta.key, delta.value) .update(keyMap[delta.key] || delta.key, delta.value)
@ -492,8 +492,6 @@ async function applyActorValueDelta(profileId, delta, trx) {
} }
async function applyActorDirectDelta(actorId, delta, trx) { async function applyActorDirectDelta(actorId, delta, trx) {
console.log('value delta', delta);
return knex('actors') return knex('actors')
.where('id', actorId) .where('id', actorId)
.update(keyMap[delta.key] || delta.key, delta.value) .update(keyMap[delta.key] || delta.key, delta.value)
@ -601,6 +599,7 @@ async function applyActorRevision(revisionIds, reqUser) {
'tattoos', 'tattoos',
'hasPiercings', 'hasPiercings',
'piercings', 'piercings',
'agency',
].includes(delta.key)) { ].includes(delta.key)) {
return applyActorValueDelta(mainProfile.id, delta, trx); return applyActorValueDelta(mainProfile.id, delta, trx);
} }