Upgraded dependencies, bumped to Node 24.
This commit is contained in:
@@ -1,3 +1,132 @@
|
||||
<script setup>
|
||||
import { format, subYears } from 'date-fns';
|
||||
import { parse } from 'path-to-regexp';
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import ActorTile from '#/components/actors/tile.vue';
|
||||
import BirthdateFilter from '#/components/filters/birthdate.vue';
|
||||
import BoobsFilter from '#/components/filters/boobs.vue';
|
||||
|
||||
import CountryFilter from '#/components/filters/country.vue';
|
||||
import Filters from '#/components/filters/filters.vue';
|
||||
import GenderFilter from '#/components/filters/gender.vue';
|
||||
import PhysiqueFilter from '#/components/filters/physique.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
import Pagination from '#/components/pagination/pagination.vue';
|
||||
import { get } from '#/src/api.js';
|
||||
import events from '#/src/events.js';
|
||||
import navigate from '#/src/navigate.js';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { pageProps, urlParsed, routeParams } = pageContext;
|
||||
|
||||
const q = ref(urlParsed.search.q);
|
||||
const actors = ref([]);
|
||||
const pageStash = pageProps.stash;
|
||||
const countries = ref(pageProps.countries);
|
||||
const cupRange = ref(pageProps.cupRange);
|
||||
|
||||
actors.value = pageProps.actors;
|
||||
|
||||
const currentPage = ref(Number(routeParams.page));
|
||||
const total = ref(Number(pageProps.actorTotal || pageProps.total));
|
||||
const order = ref(routeParams.order || urlParsed.search.order || 'likes.desc');
|
||||
|
||||
const filters = ref({
|
||||
gender: urlParsed.search.gender,
|
||||
ageRequired: !!urlParsed.search.age,
|
||||
age: urlParsed.search.age?.split(',').map((age) => Number(age)) || [18, 100],
|
||||
dobRequired: !!urlParsed.search.dob,
|
||||
dobType: urlParsed.search.dobt ? ({ bd: 'birthday', dob: 'dob' })[urlParsed.search.dobt] : 'birthday',
|
||||
dob: urlParsed.search.dob || format(subYears(new Date(), 21), 'yyyy-MM-dd'),
|
||||
country: urlParsed.search.c,
|
||||
braSizeRequired: !!urlParsed.search.cup,
|
||||
braSize: urlParsed.search.cup?.split(',') || ['A', 'Z'],
|
||||
naturalBoobs: urlParsed.search.nb ? urlParsed.search.nb === 'true' : undefined,
|
||||
heightRequired: !!urlParsed.search.height,
|
||||
height: urlParsed.search.height?.split(',').map((height) => Number(height)) || [50, 220],
|
||||
weightRequired: !!urlParsed.search.weight,
|
||||
weight: urlParsed.search.weight?.split(',').map((weight) => Number(weight)) || [30, 200],
|
||||
avatarRequired: !!urlParsed.search.avatar,
|
||||
});
|
||||
|
||||
function getPath(preserveQuery) {
|
||||
const path = parse(routeParams.path).map((segment) => {
|
||||
if (segment.name === 'page') {
|
||||
return `${segment.prefix}${currentPage.value || 1}`;
|
||||
}
|
||||
|
||||
return `${segment.prefix || ''}${routeParams[segment.name] || segment}`;
|
||||
}).join('');
|
||||
|
||||
if (preserveQuery && urlParsed.searchOriginal) {
|
||||
return `${path}${urlParsed.searchOriginal}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
async function search(options = {}) {
|
||||
if (options.resetPage !== false) {
|
||||
currentPage.value = 1;
|
||||
}
|
||||
|
||||
if (options.autoScope !== false) {
|
||||
if (q.value) {
|
||||
order.value = 'results.desc';
|
||||
}
|
||||
|
||||
if (!q.value && order.value.includes('results')) {
|
||||
order.value = 'likes.desc';
|
||||
}
|
||||
}
|
||||
|
||||
const query = {
|
||||
q: q.value || undefined,
|
||||
order: order.value,
|
||||
gender: filters.value.gender || undefined,
|
||||
age: filters.value.ageRequired ? filters.value.age.join(',') : undefined,
|
||||
dob: filters.value.dobRequired ? filters.value.dob : undefined,
|
||||
dobt: filters.value.dobRequired ? ({ birthday: 'bd', dob: 'dob' })[filters.value.dobType] : undefined,
|
||||
cup: filters.value.braSizeRequired ? filters.value.braSize.join(',') : undefined,
|
||||
c: filters.value.country || undefined,
|
||||
nb: filters.value.naturalBoobs,
|
||||
height: filters.value.heightRequired ? filters.value.height.join(',') : undefined,
|
||||
weight: filters.value.weightRequired ? filters.value.weight.join(',') : undefined,
|
||||
avatar: filters.value.avatarRequired || undefined,
|
||||
stashId: pageStash?.id,
|
||||
};
|
||||
|
||||
navigate(getPath(false), query, { redirect: false });
|
||||
|
||||
const res = await get('/actors', {
|
||||
...query,
|
||||
page: currentPage.value, // client uses param rather than query pagination
|
||||
});
|
||||
|
||||
actors.value = res.actors;
|
||||
total.value = res.total;
|
||||
|
||||
countries.value = res.countries;
|
||||
|
||||
events.emit('scrollUp');
|
||||
}
|
||||
|
||||
function paginate({ page }) {
|
||||
currentPage.value = page;
|
||||
|
||||
search({ resetPage: false });
|
||||
}
|
||||
|
||||
function updateFilter(prop, value, reload = true) {
|
||||
filters.value[prop] = value;
|
||||
|
||||
if (reload) {
|
||||
search();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<Filters :results="total">
|
||||
@@ -111,135 +240,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import { format, subYears } from 'date-fns';
|
||||
import { parse } from 'path-to-regexp';
|
||||
|
||||
import navigate from '#/src/navigate.js';
|
||||
import { get } from '#/src/api.js';
|
||||
import events from '#/src/events.js';
|
||||
|
||||
import ActorTile from '#/components/actors/tile.vue';
|
||||
import Pagination from '#/components/pagination/pagination.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
import Filters from '#/components/filters/filters.vue';
|
||||
import GenderFilter from '#/components/filters/gender.vue';
|
||||
import BirthdateFilter from '#/components/filters/birthdate.vue';
|
||||
import BoobsFilter from '#/components/filters/boobs.vue';
|
||||
import PhysiqueFilter from '#/components/filters/physique.vue';
|
||||
import CountryFilter from '#/components/filters/country.vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { pageProps, urlParsed, routeParams } = pageContext;
|
||||
|
||||
const q = ref(urlParsed.search.q);
|
||||
const actors = ref([]);
|
||||
const pageStash = pageProps.stash;
|
||||
const countries = ref(pageProps.countries);
|
||||
const cupRange = ref(pageProps.cupRange);
|
||||
|
||||
actors.value = pageProps.actors;
|
||||
|
||||
const currentPage = ref(Number(routeParams.page));
|
||||
const total = ref(Number(pageProps.actorTotal || pageProps.total));
|
||||
const order = ref(routeParams.order || urlParsed.search.order || 'likes.desc');
|
||||
|
||||
const filters = ref({
|
||||
gender: urlParsed.search.gender,
|
||||
ageRequired: !!urlParsed.search.age,
|
||||
age: urlParsed.search.age?.split(',').map((age) => Number(age)) || [18, 100],
|
||||
dobRequired: !!urlParsed.search.dob,
|
||||
dobType: urlParsed.search.dobt ? ({ bd: 'birthday', dob: 'dob' })[urlParsed.search.dobt] : 'birthday',
|
||||
dob: urlParsed.search.dob || format(subYears(new Date(), 21), 'yyyy-MM-dd'),
|
||||
country: urlParsed.search.c,
|
||||
braSizeRequired: !!urlParsed.search.cup,
|
||||
braSize: urlParsed.search.cup?.split(',') || ['A', 'Z'],
|
||||
naturalBoobs: urlParsed.search.nb ? urlParsed.search.nb === 'true' : undefined,
|
||||
heightRequired: !!urlParsed.search.height,
|
||||
height: urlParsed.search.height?.split(',').map((height) => Number(height)) || [50, 220],
|
||||
weightRequired: !!urlParsed.search.weight,
|
||||
weight: urlParsed.search.weight?.split(',').map((weight) => Number(weight)) || [30, 200],
|
||||
avatarRequired: !!urlParsed.search.avatar,
|
||||
});
|
||||
|
||||
function getPath(preserveQuery) {
|
||||
const path = parse(routeParams.path).map((segment) => {
|
||||
if (segment.name === 'page') {
|
||||
return `${segment.prefix}${currentPage.value || 1}`;
|
||||
}
|
||||
|
||||
return `${segment.prefix || ''}${routeParams[segment.name] || segment}`;
|
||||
}).join('');
|
||||
|
||||
if (preserveQuery && urlParsed.searchOriginal) {
|
||||
return `${path}${urlParsed.searchOriginal}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
async function search(options = {}) {
|
||||
if (options.resetPage !== false) {
|
||||
currentPage.value = 1;
|
||||
}
|
||||
|
||||
if (options.autoScope !== false) {
|
||||
if (q.value) {
|
||||
order.value = 'results.desc';
|
||||
}
|
||||
|
||||
if (!q.value && order.value.includes('results')) {
|
||||
order.value = 'likes.desc';
|
||||
}
|
||||
}
|
||||
|
||||
const query = {
|
||||
q: q.value || undefined,
|
||||
order: order.value,
|
||||
gender: filters.value.gender || undefined,
|
||||
age: filters.value.ageRequired ? filters.value.age.join(',') : undefined,
|
||||
dob: filters.value.dobRequired ? filters.value.dob : undefined,
|
||||
dobt: filters.value.dobRequired ? ({ birthday: 'bd', dob: 'dob' })[filters.value.dobType] : undefined,
|
||||
cup: filters.value.braSizeRequired ? filters.value.braSize.join(',') : undefined,
|
||||
c: filters.value.country || undefined,
|
||||
nb: filters.value.naturalBoobs,
|
||||
height: filters.value.heightRequired ? filters.value.height.join(',') : undefined,
|
||||
weight: filters.value.weightRequired ? filters.value.weight.join(',') : undefined,
|
||||
avatar: filters.value.avatarRequired || undefined,
|
||||
stashId: pageStash?.id,
|
||||
};
|
||||
|
||||
navigate(getPath(false), query, { redirect: false });
|
||||
|
||||
const res = await get('/actors', {
|
||||
...query,
|
||||
page: currentPage.value, // client uses param rather than query pagination
|
||||
});
|
||||
|
||||
actors.value = res.actors;
|
||||
total.value = res.total;
|
||||
|
||||
countries.value = res.countries;
|
||||
|
||||
events.emit('scrollUp');
|
||||
}
|
||||
|
||||
function paginate({ page }) {
|
||||
currentPage.value = page;
|
||||
|
||||
search({ resetPage: false });
|
||||
}
|
||||
|
||||
function updateFilter(prop, value, reload = true) {
|
||||
filters.value[prop] = value;
|
||||
|
||||
if (reload) {
|
||||
search();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.gender-button {
|
||||
&.selected .gender .icon {
|
||||
|
||||
@@ -1,3 +1,62 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||
|
||||
import { post } from '#/src/api.js';
|
||||
import events from '#/src/events.js';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const nameInput = ref(null);
|
||||
const isSubmitting = ref(false);
|
||||
const isSubmitted = ref(false);
|
||||
|
||||
const name = ref(null);
|
||||
const gender = ref('female');
|
||||
const allowGlobalMatch = ref(true);
|
||||
|
||||
const newActor = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
nameInput.value.focus();
|
||||
});
|
||||
|
||||
async function addActor() {
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
const { actor } = await post('/actors', {
|
||||
actor: {
|
||||
name: name.value,
|
||||
gender: gender.value,
|
||||
allowGlobalMatch: allowGlobalMatch.value,
|
||||
},
|
||||
}, {
|
||||
successFeedback: 'Actor has been added.',
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
name.value = null;
|
||||
gender.value = null;
|
||||
allowGlobalMatch.value = true;
|
||||
|
||||
newActor.value = actor;
|
||||
}
|
||||
catch (error) {
|
||||
events.emit('feedback', {
|
||||
type: 'error',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
isSubmitting.value = false;
|
||||
isSubmitted.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
title="Add actor"
|
||||
@@ -80,64 +139,6 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
import { post } from '#/src/api.js';
|
||||
import events from '#/src/events.js';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const nameInput = ref(null);
|
||||
const isSubmitting = ref(false);
|
||||
const isSubmitted = ref(false);
|
||||
|
||||
const name = ref(null);
|
||||
const gender = ref('female');
|
||||
const allowGlobalMatch = ref(true);
|
||||
|
||||
const newActor = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
nameInput.value.focus();
|
||||
});
|
||||
|
||||
async function addActor() {
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
const { actor } = await post('/actors', {
|
||||
actor: {
|
||||
name: name.value,
|
||||
gender: gender.value,
|
||||
allowGlobalMatch: allowGlobalMatch.value,
|
||||
},
|
||||
}, {
|
||||
successFeedback: 'Actor has been added.',
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
name.value = null;
|
||||
gender.value = null;
|
||||
allowGlobalMatch.value = true;
|
||||
|
||||
newActor.value = actor;
|
||||
} catch (error) {
|
||||
events.emit('feedback', {
|
||||
type: 'error',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
isSubmitting.value = false;
|
||||
isSubmitted.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
width: 20rem;
|
||||
|
||||
@@ -1,3 +1,99 @@
|
||||
<script setup>
|
||||
import formatTemplate from 'template-format';
|
||||
import { inject, ref, useId } from 'vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
|
||||
const props = defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const expanded = ref(false);
|
||||
const enhancedTooltipId = useId();
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { user, env } = pageContext;
|
||||
|
||||
const iconMap = {
|
||||
twitter: 'twitter-x',
|
||||
};
|
||||
|
||||
// if the profile is empty, the expand button overlaps the header
|
||||
const showExpand = [
|
||||
'age',
|
||||
'bust',
|
||||
'cup',
|
||||
'eyes',
|
||||
'ethnicity',
|
||||
'hairColor',
|
||||
'hasPiercings',
|
||||
'hasTattoos',
|
||||
'height',
|
||||
'hip',
|
||||
'naturalBoobs',
|
||||
'origin',
|
||||
'residence',
|
||||
'waist',
|
||||
'weight',
|
||||
].some((attribute) => !!props.actor[attribute]);
|
||||
|
||||
const augmentationMap = {
|
||||
bbl: 'BBL',
|
||||
fat: 'fat transfer',
|
||||
lift: 'direct lift',
|
||||
lipo: 'lipo without BBL',
|
||||
filler: 'filler',
|
||||
mms: 'MMS',
|
||||
over: 'over-muscle',
|
||||
under: 'under-muscle',
|
||||
mammary: 'under breast',
|
||||
areolar: 'areolar',
|
||||
crescent: 'crescent areolar',
|
||||
lollipop: 'lollipop',
|
||||
axillary: 'armpit',
|
||||
umbilical: 'navel',
|
||||
};
|
||||
|
||||
const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
||||
.filter((profile) => !!profile.description)
|
||||
.map((profile) => [profile.descriptionHash, {
|
||||
text: profile.description,
|
||||
entity: profile.entity,
|
||||
}])));
|
||||
|
||||
function getSocialUrl(social) {
|
||||
if (social.url) {
|
||||
return social.url;
|
||||
}
|
||||
|
||||
if (pageContext.env.socials.urls[social.platform]) {
|
||||
return formatTemplate(pageContext.env.socials.urls[social.platform], { handle: social.handle });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const socials = props.actor.socials.slice(0, 10).map((social) => ({
|
||||
...social,
|
||||
handle: social.url
|
||||
? new URL(social.url).hostname
|
||||
: social.handle,
|
||||
}));
|
||||
|
||||
const aliases = Object
|
||||
.entries(props.actor.aliases.reduce((acc, alias) => {
|
||||
acc[alias.name] = (props.actor[alias.name] || 0) + 1;
|
||||
return acc;
|
||||
}, {}))
|
||||
.toSorted(([, countA], [, countB]) => countB - countA)
|
||||
.map(([alias]) => alias)
|
||||
.filter((alias) => alias !== props.actor.name);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="profile"
|
||||
@@ -53,7 +149,7 @@
|
||||
<dfn class="bio-label"><Icon icon="cake" />Age</dfn>
|
||||
|
||||
<span
|
||||
:title="'Exact date of birth or age unknown'"
|
||||
title="Exact date of birth or age unknown"
|
||||
class="birthdate"
|
||||
>> {{ actor.age }}</span>
|
||||
</li>
|
||||
@@ -162,7 +258,7 @@
|
||||
{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}
|
||||
<Icon
|
||||
v-if="actor.naturalBoobs === false || actor.naturalButt === false"
|
||||
v-tooltip="[actor.naturalBoobs === false ? 'Enhanced boobs' : null, actor.naturalButt === false ? 'Enhanced butt' : null].filter(Boolean).join(', ')"
|
||||
v-tooltip="{ content: [actor.naturalBoobs === false ? 'Enhanced boobs' : null, actor.naturalButt === false ? 'Enhanced butt' : null].filter(Boolean).join(', '), ariaId: enhancedTooltipId }"
|
||||
icon="magic-wand2"
|
||||
class="enhanced"
|
||||
/>
|
||||
@@ -184,7 +280,7 @@
|
||||
actor.boobsVolume && `${actor.boobsVolume}cc`,
|
||||
augmentationMap[actor.boobsPlacement] || actor.boobsPlacement,
|
||||
augmentationMap[actor.boobsIncision] || actor.boobsIncision,
|
||||
augmentationMap[actor.boobsImplant] || actor.boobsImplant
|
||||
augmentationMap[actor.boobsImplant] || actor.boobsImplant,
|
||||
].filter(Boolean).join(' ')"
|
||||
class="augmentations-section"
|
||||
>Boobs<template v-if="actor.boobsVolume || actor.boobsImplant">: </template>
|
||||
@@ -224,7 +320,7 @@
|
||||
<span>
|
||||
<Icon
|
||||
v-if="actor.isCircumcised"
|
||||
:title="'Circumcised'"
|
||||
title="Circumcised"
|
||||
icon="pagebreak"
|
||||
class="circumcised"
|
||||
/>
|
||||
@@ -328,7 +424,7 @@
|
||||
<a
|
||||
v-for="social in socials"
|
||||
:key="`social-${social.id}`"
|
||||
v-tooltip="social.platform ? `${social.platform} ${env.socials.prefix[social.platform] || env.socials.prefix.default}${social.handle}` : social.url"
|
||||
v-tooltip="{ content: social.platform ? `${social.platform} ${env.socials.prefix[social.platform] || env.socials.prefix.default}${social.handle}` : social.url, ariaId: `social-${social.id}` }"
|
||||
:href="getSocialUrl(social)"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
@@ -438,101 +534,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import formatTemplate from 'template-format';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
|
||||
const expanded = ref(false);
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { user, env } = pageContext;
|
||||
|
||||
const props = defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const iconMap = {
|
||||
twitter: 'twitter-x',
|
||||
};
|
||||
|
||||
// if the profile is empty, the expand button overlaps the header
|
||||
const showExpand = [
|
||||
'age',
|
||||
'bust',
|
||||
'cup',
|
||||
'eyes',
|
||||
'ethnicity',
|
||||
'hairColor',
|
||||
'hasPiercings',
|
||||
'hasTattoos',
|
||||
'height',
|
||||
'hip',
|
||||
'naturalBoobs',
|
||||
'origin',
|
||||
'residence',
|
||||
'waist',
|
||||
'weight',
|
||||
].some((attribute) => !!props.actor[attribute]);
|
||||
|
||||
const augmentationMap = {
|
||||
bbl: 'BBL',
|
||||
fat: 'fat transfer',
|
||||
lift: 'direct lift',
|
||||
lipo: 'lipo without BBL',
|
||||
filler: 'filler',
|
||||
mms: 'MMS',
|
||||
over: 'over-muscle',
|
||||
under: 'under-muscle',
|
||||
mammary: 'under breast',
|
||||
areolar: 'areolar',
|
||||
crescent: 'crescent areolar',
|
||||
lollipop: 'lollipop',
|
||||
axillary: 'armpit',
|
||||
umbilical: 'navel',
|
||||
};
|
||||
|
||||
const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
||||
.filter((profile) => !!profile.description)
|
||||
.map((profile) => [profile.descriptionHash, {
|
||||
text: profile.description,
|
||||
entity: profile.entity,
|
||||
}])));
|
||||
|
||||
function getSocialUrl(social) {
|
||||
if (social.url) {
|
||||
return social.url;
|
||||
}
|
||||
|
||||
if (pageContext.env.socials.urls[social.platform]) {
|
||||
return formatTemplate(pageContext.env.socials.urls[social.platform], { handle: social.handle });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const socials = props.actor.socials.slice(0, 10).map((social) => ({
|
||||
...social,
|
||||
handle: social.url
|
||||
? new URL(social.url).hostname
|
||||
: social.handle,
|
||||
}));
|
||||
|
||||
const aliases = Object
|
||||
.entries(props.actor.aliases.reduce((acc, alias) => {
|
||||
acc[alias.name] = (props.actor[alias.name] || 0) + 1;
|
||||
return acc;
|
||||
}, {}))
|
||||
.toSorted(([, countA], [, countB]) => countB - countA)
|
||||
.map(([alias]) => alias)
|
||||
.filter((alias) => alias !== props.actor.name);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.header-gender {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
gender: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
v-if="gender"
|
||||
@@ -8,15 +17,6 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
gender: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gender {
|
||||
&.female .icon {
|
||||
|
||||
@@ -1,3 +1,81 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, useId } from 'vue';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||
|
||||
import { get, post } from '#/src/api.js';
|
||||
|
||||
const props = defineProps({
|
||||
actors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'merged']);
|
||||
|
||||
const targetActor = ref(null);
|
||||
const actorInput = ref(null);
|
||||
const actorQuery = ref('');
|
||||
const actorResults = ref([]);
|
||||
const submitted = ref(false);
|
||||
const merged = ref(false);
|
||||
const searchDropdownId = useId();
|
||||
const conflictTooltipId = useId();
|
||||
|
||||
const sourceTargetConflict = computed(() => props.actors.some((actor) => actor.id === targetActor.value?.id));
|
||||
|
||||
async function searchActors() {
|
||||
const res = await get('/actors', {
|
||||
q: actorQuery.value, // return partial matches
|
||||
limit: 10,
|
||||
global: true,
|
||||
});
|
||||
|
||||
actorResults.value = res.actors;
|
||||
}
|
||||
|
||||
function getActorNames() {
|
||||
if (props.actors.length > 1) {
|
||||
return `${props.actors.length} actors`;
|
||||
}
|
||||
|
||||
if (props.actors[0].entity) {
|
||||
return `${props.actors[0].name} (${props.actors[0].entity.name})`;
|
||||
}
|
||||
|
||||
return props.actors[0].name;
|
||||
}
|
||||
|
||||
async function merge() {
|
||||
submitted.value = true;
|
||||
|
||||
await post(`/actors/${targetActor.value.id}/merge/${props.actors.map((actor) => actor.id).join(',')}`, null, {
|
||||
successFeedback: `Merged ${getActorNames()} into ${targetActor.value.name}`,
|
||||
errorFeedback: `Failed to merge ${getActorNames()} into ${targetActor.value.name}`,
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
submitted.value = false;
|
||||
merged.value = true;
|
||||
|
||||
emit('merged');
|
||||
|
||||
// emit('close');
|
||||
}
|
||||
|
||||
function selectActor(actor) {
|
||||
targetActor.value = actor;
|
||||
actorQuery.value = '';
|
||||
actorResults.value = [];
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
actorInput.value.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:title="`Merge ${actors.length === 1 ? `'${actors[0].name}'` : `${actors.length} actors`}`"
|
||||
@@ -62,6 +140,7 @@
|
||||
|
||||
<template v-else>
|
||||
<VDropdown
|
||||
:aria-id="searchDropdownId"
|
||||
:triggers="[]"
|
||||
:shown="actorResults.length > 0"
|
||||
:auto-hide="false"
|
||||
@@ -99,7 +178,7 @@
|
||||
|
||||
<button
|
||||
v-else
|
||||
v-tooltip="sourceTargetConflict && 'Cannot merge actor profile into itself'"
|
||||
v-tooltip="sourceTargetConflict && { content: 'Cannot merge actor profile into itself', ariaId: conflictTooltipId }"
|
||||
type="submit"
|
||||
class="button button-primary"
|
||||
:disabled="!targetActor || sourceTargetConflict"
|
||||
@@ -110,82 +189,6 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||
|
||||
import { get, post } from '#/src/api.js';
|
||||
|
||||
const props = defineProps({
|
||||
actors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'merged']);
|
||||
|
||||
const targetActor = ref(null);
|
||||
const actorInput = ref(null);
|
||||
const actorQuery = ref('');
|
||||
const actorResults = ref([]);
|
||||
const submitted = ref(false);
|
||||
const merged = ref(false);
|
||||
|
||||
const sourceTargetConflict = computed(() => props.actors.some((actor) => actor.id === targetActor.value?.id));
|
||||
|
||||
async function searchActors() {
|
||||
const res = await get('/actors', {
|
||||
q: actorQuery.value, // return partial matches
|
||||
limit: 10,
|
||||
global: true,
|
||||
});
|
||||
|
||||
actorResults.value = res.actors;
|
||||
}
|
||||
|
||||
function getActorNames() {
|
||||
if (props.actors.length > 1) {
|
||||
return `${props.actors.length} actors`;
|
||||
}
|
||||
|
||||
if (props.actors[0].entity) {
|
||||
return `${props.actors[0].name} (${props.actors[0].entity.name})`;
|
||||
}
|
||||
|
||||
return props.actors[0].name;
|
||||
}
|
||||
|
||||
async function merge() {
|
||||
submitted.value = true;
|
||||
|
||||
await post(`/actors/${targetActor.value.id}/merge/${props.actors.map((actor) => actor.id).join(',')}`, null, {
|
||||
successFeedback: `Merged ${getActorNames()} into ${targetActor.value.name}`,
|
||||
errorFeedback: `Failed to merge ${getActorNames()} into ${targetActor.value.name}`,
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
submitted.value = false;
|
||||
merged.value = true;
|
||||
|
||||
emit('merged');
|
||||
|
||||
// emit('close');
|
||||
}
|
||||
|
||||
function selectActor(actor) {
|
||||
targetActor.value = actor;
|
||||
actorQuery.value = '';
|
||||
actorResults.value = [];
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
actorInput.value.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
width: 20rem;
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
<script setup>
|
||||
import { ref, useId } from 'vue';
|
||||
|
||||
import { get } from '#/src/api.js';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['actor']);
|
||||
const dropdownId = useId();
|
||||
const actors = ref([]);
|
||||
const query = ref(null);
|
||||
const queryInput = ref(null);
|
||||
|
||||
async function search() {
|
||||
const data = await get('/actors', { q: query.value });
|
||||
|
||||
actors.value = data.actors;
|
||||
}
|
||||
|
||||
function focus() {
|
||||
setTimeout(() => {
|
||||
queryInput.value?.focus();
|
||||
}, 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDropdown
|
||||
:aria-id="dropdownId"
|
||||
:disabled="disabled"
|
||||
class="trigger"
|
||||
@show="focus"
|
||||
@@ -36,38 +68,6 @@
|
||||
</VDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { get } from '#/src/api.js';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
const actors = ref([]);
|
||||
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%;
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
<script setup>
|
||||
import { inject, ref, useId } from 'vue';
|
||||
|
||||
import Heart from '#/components/stashes/heart.vue';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
import Gender from './gender.vue';
|
||||
|
||||
const props = defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { user, restriction } = pageContext;
|
||||
const pageStash = pageContext.pageProps.stash;
|
||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||
|
||||
const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id === currentStash?.id));
|
||||
const entityTooltipId = useId();
|
||||
const aliasTooltipId = useId();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="tile"
|
||||
@@ -81,14 +107,14 @@
|
||||
|
||||
<img
|
||||
v-if="actor.entity"
|
||||
v-tooltip="actor.entity.name"
|
||||
v-tooltip="{ content: actor.entity.name, ariaId: entityTooltipId }"
|
||||
:src="`/logos/${actor.entity.slug}/favicon_dark.png`"
|
||||
class="favicon"
|
||||
>
|
||||
|
||||
<Icon
|
||||
v-if="actor.alias && actor.alias.name !== actor.name"
|
||||
v-tooltip="`Credited as '${actor.alias.name}'`"
|
||||
v-tooltip="{ content: `Credited as '${actor.alias.name}'`, ariaId: aliasTooltipId }"
|
||||
icon="at-sign"
|
||||
class="alias"
|
||||
/>
|
||||
@@ -96,30 +122,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
|
||||
import Heart from '#/components/stashes/heart.vue';
|
||||
import Gender from './gender.vue';
|
||||
|
||||
const props = defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { user, restriction } = pageContext;
|
||||
const pageStash = pageContext.pageProps.stash;
|
||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||
|
||||
const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id === currentStash?.id));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tile {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user