Added experimental edit page and revision history.

This commit is contained in:
2024-09-10 02:47:03 +02:00
parent 4b8dfba289
commit 8bf9e22b39
20 changed files with 1177 additions and 14 deletions

View File

@@ -0,0 +1,115 @@
<template>
<VDropdown
:disabled="disabled"
class="trigger"
@show="focus"
>
<slot />
<template #popper>
<div>
<input
ref="queryInput"
v-model="query"
placeholder="Search actor"
class="input"
@input="search"
>
<ul class="actors nolist">
<li
v-for="actor in actors"
:key="`actor-${actor.slug}`"
v-close-popper
class="actor"
@click="emit('actor', actor)"
>{{ actor.name }} ({{ [actor.ageFromBirth, actor.origin?.country?.alpha2].join(', ') }})
<img
v-if="actor.avatar"
:src="getPath(actor.avatar, 'thumbnail')"
class="avatar"
>
</li>
</ul>
</div>
</template>
</VDropdown>
</template>
<script setup>
import { ref, inject } from 'vue';
import { get } from '#/src/api.js';
import getPath from '#/src/get-path.js';
const pageContext = inject('pageContext');
const actorNames = {
dp: 'double penetration',
};
const defaultActors = pageContext.pageProps.actorIds
? Object.entries(pageContext.pageProps.actorIds).map(([slug, id]) => ({
id,
slug,
name: actorNames[slug] || slug,
}))
: [];
const actors = ref(defaultActors);
const query = ref(null);
const queryInput = ref(null);
defineProps({
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['actor']);
async function search() {
const data = await get('/actors', { q: query.value });
actors.value = data.actors;
}
function focus() {
setTimeout(() => {
queryInput.value?.focus();
}, 100);
}
</script>
<style scoped>
.trigger {
height: 100%;
overflow: hidden;
}
.actor {
display: block;
padding: .25rem .5rem;
&:hover {
background: var(--glass-weak-50);
color: var(--primary);
cursor: pointer;
.avatar {
display: block;
}
}
}
.avatar {
position: fixed;
display: none;
left: 7rem;
width: 8rem;
border-radius: .25rem;
box-shadow: 0 0 3px var(--shadow-weak-10);
pointer-events: none;
}
</style>

153
components/edit/actors.vue Normal file
View File

@@ -0,0 +1,153 @@
<template>
<ul
class="actors nolist"
:class="{ disabled: !editing.has(item.key) }"
>
<li
v-for="actor in [...item.value, ...newActors]"
:key="`actor-${actor.id}`"
class="actor"
:class="{ deleted: edits.actors && !edits.actors.some((actorId) => actorId === actor.id) }"
>
<span class="actor-name">{{ actor.name }}</span>
<Icon
v-if="edits.actors && !edits.actors.some((actorId) => actorId === actor.id)"
icon="checkmark"
class="add"
@click="emit('actors', edits.actors.concat(actor.id))"
/>
<Icon
v-else
icon="cross2"
class="remove"
@click="emit('actors', edits.actors.filter((actorId) => actorId !== actor.id))"
/>
</li>
<li class="new">
<ActorSearch
:disabled="!editing.has(item.key)"
@actor="addActor"
>
<Icon
icon="plus3"
class="add"
/>
</ActorSearch>
</li>
</ul>
</template>
<script setup>
import { ref, watch } from 'vue';
import ActorSearch from '#/components/actors/search.vue';
const newActors = ref([]);
const props = defineProps({
item: {
type: Object,
default: null,
},
scene: {
type: Object,
default: null,
},
edits: {
type: Object,
default: () => {},
},
editing: {
type: Set,
default: null,
},
});
const emit = defineEmits(['actors']);
function addActor(actor) {
newActors.value = newActors.value.concat(actor);
emit('actors', props.edits.actors.concat(actor.id));
}
watch(() => props.scene, () => { newActors.value = []; });
</script>
<style scoped>
.actors {
display: flex;
flex-wrap: wrap;
gap: .25rem;
&.disabled {
.actor {
color: var(--shadow);
.remove,
.add {
background: var(--shadow-weak-40);
}
}
.new .icon {
background: var(--shadow-weak-40);
}
}
.new {
display: flex;
align-items: center;
margin-left: .25rem;
.icon {
height: 100%;
padding: 0 .5rem;
fill: var(--text-light);
}
}
}
.actor {
display: flex;
align-items: stretch;
background: var(--glass-weak-30);
border-radius: .25rem;
&.deleted {
color: var(--glass);
text-decoration: line-through;
}
}
.actor,
.new {
.remove,
.add {
height: auto;
padding: .25rem .3rem;
fill: var(--highlight-strong-10);
border-radius: .25rem;
&:hover {
fill: var(--text-light);
cursor: pointer;
}
}
.remove {
background: var(--error);
}
.add {
background: var(--success);
}
}
.actor-name {
padding: .25rem .5rem;
}
</style>

153
components/edit/tags.vue Normal file
View File

@@ -0,0 +1,153 @@
<template>
<ul
class="tags nolist"
:class="{ disabled: !editing.has(item.key) }"
>
<li
v-for="tag in [...item.value, ...newTags]"
:key="`tag-${tag.id}`"
class="tag"
:class="{ deleted: edits.tags && !edits.tags.some((tagId) => tagId === tag.id) }"
>
<span class="tag-name">{{ tag.name }}</span>
<Icon
v-if="edits.tags && !edits.tags.some((tagId) => tagId === tag.id)"
icon="checkmark"
class="add"
@click="emit('tags', edits.tags.concat(tag.id))"
/>
<Icon
v-else
icon="cross2"
class="remove"
@click="emit('tags', edits.tags.filter((tagId) => tagId !== tag.id))"
/>
</li>
<li class="new">
<TagSearch
:disabled="!editing.has(item.key)"
@tag="addTag"
>
<Icon
icon="plus3"
class="add"
/>
</TagSearch>
</li>
</ul>
</template>
<script setup>
import { ref, watch } from 'vue';
import TagSearch from '#/components/tags/search.vue';
const newTags = ref([]);
const props = defineProps({
item: {
type: Object,
default: null,
},
scene: {
type: Object,
default: null,
},
edits: {
type: Object,
default: () => {},
},
editing: {
type: Set,
default: null,
},
});
const emit = defineEmits(['tags']);
function addTag(tag) {
newTags.value = newTags.value.concat(tag);
emit('tags', props.edits.tags.concat(tag.id));
}
watch(() => props.scene, () => { newTags.value = []; });
</script>
<style scoped>
.tags {
display: flex;
flex-wrap: wrap;
gap: .25rem;
&.disabled {
.tag {
color: var(--shadow);
.remove,
.add {
background: var(--shadow-weak-40);
}
}
.new .icon {
background: var(--shadow-weak-40);
}
}
.new {
display: flex;
align-items: center;
margin-left: .25rem;
.icon {
height: 100%;
padding: 0 .5rem;
fill: var(--text-light);
}
}
}
.tag {
display: flex;
align-items: stretch;
background: var(--glass-weak-30);
border-radius: .25rem;
&.deleted {
color: var(--glass);
text-decoration: line-through;
}
}
.tag,
.new {
.remove,
.add {
height: auto;
padding: .25rem .3rem;
fill: var(--highlight-strong-10);
border-radius: .25rem;
&:hover {
fill: var(--text-light);
cursor: pointer;
}
}
.remove {
background: var(--error);
}
.add {
background: var(--success);
}
}
.tag-name {
padding: .25rem .5rem;
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<VDropdown
:disabled="disabled"
class="trigger"
@show="focus"
>
<slot />
<template #popper>
<div>
<input
ref="queryInput"
v-model="query"
placeholder="Search tag"
class="input"
@input="search"
>
<ul class="tags nolist">
<li
v-for="tag in tags"
:key="`tag-${tag.slug}`"
v-close-popper
class="tag"
@click="emit('tag', tag)"
>{{ tag.name }}</li>
</ul>
</div>
</template>
</VDropdown>
</template>
<script setup>
import { ref, inject } from 'vue';
import { get } from '#/src/api.js';
const pageContext = inject('pageContext');
const tagNames = {
dp: 'double penetration',
};
const defaultTags = pageContext.pageProps.tagIds
? Object.entries(pageContext.pageProps.tagIds).map(([slug, id]) => ({
id,
slug,
name: tagNames[slug] || slug,
}))
: [];
const tags = ref(defaultTags);
const query = ref(null);
const queryInput = ref(null);
defineProps({
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['tag']);
async function search() {
tags.value = await get('/tags', { query: query.value });
}
function focus() {
setTimeout(() => {
queryInput.value?.focus();
}, 100);
}
</script>
<style scoped>
.trigger {
height: 100%;
overflow: hidden;
}
.tag {
display: block;
padding: .25rem .5rem;
&:hover {
background: var(--glass-weak-50);
color: var(--primary);
cursor: pointer;
}
}
</style>