Added actor stash.

This commit is contained in:
2024-03-21 02:54:05 +01:00
parent 9b50b53df6
commit a8aab600c7
37 changed files with 1292 additions and 490 deletions

View File

@@ -1,30 +1,297 @@
<template>
<ul class="actors nolist">
<li
v-for="actor in actors"
:key="`actor-${actor.id}`"
class="actor"
>
<ActorTile :actor="actor" />
</li>
</ul>
<div class="page">
<Filters :results="total">
<div class="filter">
<input
v-model="q"
type="search"
placeholder="Search actors"
class="input search"
@search="search"
>
</div>
<GenderFilter
:filters="filters"
@update="updateFilter"
/>
<BirthdateFilter
:filters="filters"
@update="updateFilter"
/>
<BoobsFilter
:filters="filters"
:cup-range="cupRange"
@update="updateFilter"
/>
<PhysiqueFilter
:filters="filters"
@update="updateFilter"
/>
<CountryFilter
:filters="filters"
:countries="countries"
@update="updateFilter"
/>
<div class="filter">
<Checkbox
:checked="filters.avatarRequired"
label="Require photo"
@change="(checked) => updateFilter('avatarRequired', checked, true)"
/>
</div>
</Filters>
<div class="actors-container">
<div class="actors-header">
<div class="meta">
<span class="count">{{ total }} results</span>
<select
v-model="order"
class="input"
@change="search({ autoScope: false })"
>
<option
v-if="pageStash"
:selected="order === 'stashed.desc'"
value="stashed.desc"
>Added</option>
<option
v-if="q"
:selected="order === 'relevance.desc'"
value="relevance.desc"
>Relevance</option>
<option value="name.asc">Name</option>
<option value="likes.desc">Likes</option>
<option value="scenes.desc">Scenes</option>
</select>
</div>
</div>
<ul class="actors nolist">
<li
v-for="actor in actors"
:key="`actor-${actor.id}`"
>
<ActorTile
:actor="actor"
/>
</li>
</ul>
<Pagination
:page="currentPage"
:total="total"
:redirect="false"
@navigation="paginate"
/>
</div>
</div>
</template>
<script setup>
import ActorTile from './tile.vue';
import { ref, inject } from 'vue';
import { format, subYears } from 'date-fns';
import { parse } from 'path-to-regexp';
defineProps({
actors: {
type: Array,
default: () => [],
},
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.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}${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 = 'relevance.desc';
}
if (!q.value && order.value.includes('relevance')) {
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 scoped>
.actors {
display: grid;
grid-template-columns: repeat(auto-fill, 10rem);
gap: .5rem;
<style>
.gender-button {
&.selected .gender .icon {
fill: var(--text-light);
filter: none;
}
&:hover:not(.selected) {
.gender .icon {
fill: var(--text-light);
}
.male .icon {
filter: drop-shadow(0 0 1px var(--male));
}
.female .icon {
filter: drop-shadow(0 0 1px var(--female));
}
}
&:hover:not(.selected) .transsexual .icon {
fill: var(--female);
filter: drop-shadow(1px 0 0 var(--text-light)) drop-shadow(-1px 0 0 var(--text-light)) drop-shadow(0 1px 0 var(--text-light)) drop-shadow(0 -1px 0 var(--text-light)) drop-shadow(1px 0 0 var(--male)) drop-shadow(-1px 0 0 var(--male)) drop-shadow(0 1px 0 var(--male)) drop-shadow(0 -1px 0 var(--male)) drop-shadow(0 0 1px rgba(0, 0, 0, 0.5));
}
}
</style>
<style scoped>
.page {
min-height: 100%;
display: flex;
align-items: stretch;
}
.actors-header {
display: flex;
align-items: center;
padding: .5rem 0 .25rem 2rem;
}
.meta {
display: flex;
flex-grow: 1;
justify-content: space-between;
align-items: center;
}
.actors-container {
display: flex;
flex-grow: 1;
flex-direction: column;
box-sizing: border-box;
padding: 0 1rem 1rem 1rem;
}
.actors {
display: grid;
flex-grow: 1;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
gap: .25rem;
}
@media(--small-40) {
.actors {
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
}
}
</style>

View File

@@ -1,19 +1,40 @@
<template>
<div class="actor">
<div
class="tile"
:class="{
unstashed: !favorited && pageStash && user && pageStash.id === user.primaryStash?.id
}"
>
<span class="name">{{ actor.name }}</span>
<Link
:href="`/actor/${actor.id}/${actor.slug}`"
class="avatar-link no-link"
>
<img
v-if="actor.avatar"
:src="actor.avatar.isS3 ? `https://cdndev.traxxx.me/${actor.avatar.thumbnail}` : `/media/${actor.avatar.thumbnail}`"
:style="{ 'background-image': actor.avatar.isS3 ? `url(https://cdndev.traxxx.me/${actor.avatar.lazy})` : `url(/media/${actor.avatar.lazy})` }"
loading="lazy"
class="avatar"
<div class="avatar-container">
<Link
:href="`/actor/${actor.id}/${actor.slug}`"
class="avatar-link no-link"
>
</Link>
<img
v-if="actor.avatar"
:src="actor.avatar.isS3 ? `https://cdndev.traxxx.me/${actor.avatar.thumbnail}` : `/media/${actor.avatar.thumbnail}`"
:style="{ 'background-image': actor.avatar.isS3 ? `url(https://cdndev.traxxx.me/${actor.avatar.lazy})` : `url(/media/${actor.avatar.lazy})` }"
loading="lazy"
class="avatar"
>
</Link>
<Icon
v-show="favorited"
icon="heart7"
class="heart favorited"
@click.native.stop="unstash"
/>
<Icon
v-show="!favorited && user"
icon="heart8"
class="heart"
@click.native.stop="stash"
/>
</div>
<div class="details">
<span class="birth">
@@ -48,20 +69,72 @@
</template>
<script setup>
import { ref, inject } from 'vue';
import { formatDate } from '#/utils/format.js';
import { post, del } from '#/src/api.js';
import events from '#/src/events.js';
import Gender from './gender.vue';
defineProps({
const props = defineProps({
actor: {
type: Object,
default: null,
},
});
const pageContext = inject('pageContext');
const { user } = pageContext;
const pageStash = pageContext.pageProps.stash;
// console.log(props.actor);
const favorited = ref(props.actor.stashes?.some((sceneStash) => sceneStash.primary) || false);
async function stash() {
try {
favorited.value = true;
await post(`/stashes/${user.primaryStash.id}/actors`, { actorId: props.actor.id });
events.emit('feedback', {
type: 'success',
message: `${props.actor.name} stashed to ${user.primaryStash.name}`,
});
} catch (error) {
favorited.value = false;
events.emit('feedback', {
type: 'error',
message: `Failed to stash ${props.actor.name} to ${user.primaryStash.name}`,
});
}
}
async function unstash() {
try {
favorited.value = false;
await del(`/stashes/${user.primaryStash.id}/actors/${props.actor.id}`);
events.emit('feedback', {
type: 'remove',
message: `${props.actor.name} unstashed from ${user.primaryStash.name}`,
});
} catch (error) {
favorited.value = true;
console.error(error);
events.emit('feedback', {
type: 'error',
message: `Failed to unstash ${props.actor.name} from ${user.primaryStash.name}`,
});
}
}
</script>
<style scoped>
.actor {
.tile {
display: flex;
flex-direction: column;
width: 100%;
@@ -79,6 +152,10 @@ defineProps({
color: var(--primary);
}
}
&.unstashed {
opacity: .5;
}
}
.name {
@@ -92,9 +169,14 @@ defineProps({
user-select: all;
}
.avatar-link {
display: block;
.avatar-container {
position: relative;
flex-grow: 1;
}
.avatar-link {
height: 100%;
display: block;
overflow: hidden;
}
@@ -107,6 +189,26 @@ defineProps({
background-position: center 0;
}
.icon.heart {
width: 2rem;
height: 1.5rem;
position: absolute;
top: 0;
right: 0;
padding: .5rem .5rem 1rem 1rem;
fill: var(--highlight-strong-10);
filter: drop-shadow(0 0 3px var(--shadow));
&:hover {
cursor: pointer;
fill: var(--primary);
}
&.favorited {
fill: var(--primary);
}
}
.details {
width: 100%;
height: 1.5rem;