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

10
pages/+config.js Normal file
View File

@@ -0,0 +1,10 @@
export default {
meta: {
data: {
env: {
server: true,
client: false,
},
},
},
};

5
pages/_error/+data.js Normal file
View File

@@ -0,0 +1,5 @@
export function data() {
return {
foo: 'bar',
};
}

View File

@@ -1,270 +1,9 @@
<template>
<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 value="name.asc">Name</option>
<option value="likes.desc">Likes</option>
<option value="scenes.desc">Scenes</option>
<option
value="relevance.desc"
:disabled="!q"
>Relevance</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>
<Actors />
</div>
</template>
<script setup>
import { ref, inject } from 'vue';
import { format, subYears } from 'date-fns';
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 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(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,
});
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,
};
navigate(`/actors/${currentPage.value}`, 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();
}
}
import Actors from '#/components/actors/actors.vue';
</script>
<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

@@ -12,7 +12,7 @@ export async function onBeforeRender(pageContext) {
page: Number(pageContext.routeParams.page) || 1,
limit: Number(pageContext.urlParsed.search.limit) || 120,
order: pageContext.urlParsed.search.order?.split('.') || ['likes', 'desc'],
});
}, pageContext.user);
return {
pageContext: {

View File

@@ -25,20 +25,19 @@
</span>
</li>
<!--
<Social
v-if="actor.social && actor.social.length > 0"
:actor="actor"
class="header-social"
<Icon
v-show="favorited"
icon="heart7"
class="heart favorited"
@click.native.stop="unstash"
/>
<StashButton
:stashed-by="stashedBy"
class="actor-stash light"
@stash="(stash) => stashActor(stash)"
@unstash="(stash) => unstashActor(stash)"
<Icon
v-show="!favorited && user"
icon="heart8"
class="heart"
@click.native.stop="stash"
/>
-->
</div>
<div class="content">
@@ -50,15 +49,61 @@
</template>
<script setup>
import { inject } from 'vue';
import { ref, inject } from 'vue';
import { post, del } from '#/src/api.js';
import events from '#/src/events.js';
import Bio from '#/components/actors/bio.vue';
import Gender from '#/components/actors/gender.vue';
import Scenes from '#/components/scenes/scenes.vue';
const pageContext = inject('pageContext');
const { pageProps } = pageContext;
const { pageProps, user } = pageContext;
const { actor } = pageProps;
const favorited = ref(actor.stashes?.some((sceneStash) => sceneStash.primary) || false);
async function stash() {
try {
favorited.value = true;
await post(`/stashes/${user.primaryStash.id}/actors`, { actorId: actor.id });
events.emit('feedback', {
type: 'success',
message: `${actor.name} stashed to ${user.primaryStash.name}`,
});
} catch (error) {
favorited.value = false;
events.emit('feedback', {
type: 'error',
message: `Failed to stash ${actor.name} to ${user.primaryStash.name}`,
});
}
}
async function unstash() {
try {
favorited.value = false;
await del(`/stashes/${user.primaryStash.id}/actors/${actor.id}`);
events.emit('feedback', {
type: 'remove',
message: `${actor.name} unstashed from ${user.primaryStash.name}`,
});
} catch (error) {
favorited.value = true;
console.error(error);
events.emit('feedback', {
type: 'error',
message: `Failed to unstash ${actor.name} from ${user.primaryStash.name}`,
});
}
}
</script>
<style scoped>
@@ -73,6 +118,7 @@ const { actor } = pageProps;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
color: var(--highlight-strong-30);
background: var(--grey-dark-40);
}
@@ -103,4 +149,24 @@ const { actor } = pageProps;
flex-grow: 1;
overflow-y: auto;
}
.icon.heart {
width: 2rem;
height: 1.5rem;
position: absolute;
top: 0;
right: 0;
padding: .5rem 1rem 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);
}
}
</style>

View File

@@ -4,7 +4,7 @@ import { curateScenesQuery } from '#/src/web/scenes.js';
export async function onBeforeRender(pageContext) {
const [[actor], actorScenes] = await Promise.all([
fetchActorsById([Number(pageContext.routeParams.actorId)]),
fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user),
fetchScenes(await curateScenesQuery({
...pageContext.urlQuery,
scope: pageContext.routeParams.scope || 'latest',
@@ -13,7 +13,7 @@ export async function onBeforeRender(pageContext) {
page: Number(pageContext.routeParams.page) || 1,
limit: Number(pageContext.urlParsed.search.limit) || 30,
aggregate: true,
}),
}, pageContext.user),
]);
const {

View File

@@ -26,6 +26,7 @@
<div
v-if="scene.photos.length > 0"
class="album"
:class="{ single: scene.photos.length === 1 }"
>
<div
v-for="photo in scene.photos"
@@ -92,7 +93,10 @@
>{{ scene.title }}</h2>
<div class="actions">
<div class="bookmarks">
<div
v-if="user"
class="bookmarks"
>
<Icon icon="folder-heart" />
<Icon
@@ -103,7 +107,7 @@
/>
<Icon
v-show="!favorited && user"
v-show="!favorited"
icon="heart8"
class="heart"
@click.native.stop="stash"
@@ -179,7 +183,11 @@
<div class="section details">
<div class="detail">
<h3 class="heading">Added</h3>
<span class="added-date">{{ formatDate(scene.createdAt, 'yyyy-MM-dd hh:mm') }}</span> <span class="added-batch">batch #{{ scene.createdBatchId }}</span>
<span class="added-date">{{ formatDate(scene.createdAt, 'yyyy-MM-dd') }}</span>
<span
:title="`Batch ${scene.createdBatchId}`"
class="added-batch"
>#{{ scene.createdBatchId }}</span>
</div>
<div
@@ -246,28 +254,29 @@ async function unstash() {
.banner-container {
background-position: center;
background-size: cover;
border-radius: .25rem .5rem 0 0;
border-radius: .5rem .5rem 0 0;
margin-top: .5rem;
box-shadow: 0 0 3px var(--shadow-weak-30);
}
.banner {
max-height: 21rem;
border-radius: .5rem .5rem 0 0;
border-radius: .5rem 0 0 0;
display: flex;
font-size: 0;
backdrop-filter: blur(1rem);
backdrop-filter: brightness(150%) blur(1rem);
overflow: hidden;
}
.poster-container {
flex-shrink: 0;
padding: .5rem;
margin-right: .5rem;
}
.poster {
height: 100%;
width: 100%;
border-radius: .25rem;
border-radius: .25rem 0 0 0;
}
.poster,
@@ -282,19 +291,33 @@ async function unstash() {
height: auto;
flex-grow: 1;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
gap: .5rem;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
gap: .25rem;
box-sizing: border-box;
padding: .5rem .5rem .5rem 0;
overflow-y: auto;
scrollbar-width: 0;
&::-webkit-scrollbar {
display: none;
}
&.single .photo {
max-height: calc(100% - 1.5rem);
}
}
.photo-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.photo-link {
width: 100%;
height: 100%;
}
.photo {
width: 100%;
height: 100%;
@@ -341,7 +364,7 @@ async function unstash() {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1rem 1.5rem 1rem;
padding: 1rem 1rem 0.5rem 1rem;
}
.title {
@@ -447,7 +470,8 @@ async function unstash() {
}
.added-batch {
color: var(--shadow);
color: var(--shadow-weak-10);
margin-left: .25rem;
}
@media(--small-10) {
@@ -473,6 +497,14 @@ async function unstash() {
.album {
display: none;
}
.header {
padding: 1rem .5rem 1.5rem .5rem;
}
.info {
padding: 0 .5rem;
}
}
@media (--small) {

View File

@@ -1,7 +1,10 @@
import { fetchScenesById } from '#/src/scenes.js';
export async function onBeforeRender(pageContext) {
const [scene] = await fetchScenesById([Number(pageContext.routeParams.sceneId)], pageContext.user);
const [scene] = await fetchScenesById([Number(pageContext.routeParams.sceneId)], {
reqUser: pageContext.user,
actorStashes: true,
});
return {
pageContext: {

View File

@@ -1,108 +0,0 @@
<template>
<div class="stash">
<div class="header">
<h2 class="title">
<Icon
v-if="stash.primary"
icon="heart7"
/>
<Icon
v-else
icon="box"
/>
{{ stash.name }}
</h2>
<a
:href="`/user/${stash.user.username}`"
class="user nolink"
>
<img
:src="stash.user.avatar"
class="avatar"
><span class="userame ellipsis">{{ stash.user.username }}</span>
</a>
</div>
<div class="scenes-container">
<Scenes />
</div>
</div>
</template>
<script setup>
import { inject } from 'vue';
import Scenes from '#/components/scenes/scenes.vue';
const pageContext = inject('pageContext');
const stash = pageContext.pageProps.stash;
</script>
<style scoped>
.stash {
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
display: flex;
justify-content: space-between;
padding: .5rem 1rem;
color: var(--text-light);
background: var(--grey-dark-40);
flex-shrink: 0;
}
.title {
margin: 0;
text-transform: capitalize;
display: flex;
align-items: center;
font-size: 1.2rem;
margin-right: 1rem;
.icon {
width: 1.25rem;
height: 1.25rem;
margin-right: .75rem;
fill: var(--text-light);
}
}
.user {
display: flex;
align-items: center;
font-weight: bold;
overflow: hidden;
}
.avatar {
width: 1.5rem;
height: 1.5rem;
margin-right: .75rem;
border-radius: .25rem;
}
.scenes-container {
overflow-y: auto;
}
@media(--small-50) {
.title {
font-size: 1rem;
.icon {
width: 1rem;
height: 1rem;
}
}
.avatar {
display: none;
}
}
</style>

View File

@@ -0,0 +1,10 @@
<template>
<Stash>
<Actors />
</Stash>
</template>
<script setup>
import Stash from '#/components/stashes/stash.vue';
import Actors from '#/components/actors/actors.vue';
</script>

View File

@@ -0,0 +1,49 @@
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
import { fetchStashByUsernameAndSlug } from '#/src/stashes.js';
import { fetchActors } from '#/src/actors.js';
import { curateActorsQuery } from '#/src/web/actors.js';
import { HttpError } from '#/src/errors.js';
export async function onBeforeRender(pageContext) {
try {
const stash = await fetchStashByUsernameAndSlug(pageContext.routeParams.username, pageContext.routeParams.stashSlug, pageContext.user);
const stashActors = await fetchActors(curateActorsQuery({
...pageContext.urlQuery,
stashId: stash.id,
}), {
page: Number(pageContext.routeParams.page) || 1,
limit: Number(pageContext.urlParsed.search.limit) || 120,
order: pageContext.urlParsed.search.order?.split('.') || ['stashed', 'desc'],
}, pageContext.user);
const {
actors,
countries,
cupRange,
limit,
total,
} = stashActors;
return {
pageContext: {
title: `${stash.name} by ${stash.user.username}`,
pageProps: {
stash,
actors,
countries,
cupRange,
limit,
total,
},
},
};
} catch (error) {
if (error instanceof HttpError) {
throw render(error.httpCode, error.message);
}
throw error;
}
}

View File

@@ -0,0 +1,24 @@
import { match } from 'path-to-regexp';
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
const path = '/stash/:username/:stashSlug/:domain(actors)/:page?';
const urlMatch = match(path, { decode: decodeURIComponent });
export default (pageContext) => {
const matched = urlMatch(pageContext.urlPathname);
if (matched) {
return {
routeParams: {
username: matched.params.username,
stashSlug: matched.params.stashSlug,
domain: matched.params.domain,
order: 'stashed.desc',
page: matched.params.page || '1',
path,
},
};
}
return false;
};

View File

@@ -0,0 +1,10 @@
<template>
<Stash>
<h2>Movies</h2>
</Stash>
</template>
<script setup>
import Stash from '#/components/stashes/stash.vue';
// import Actors from '#/components/actors/actors.vue';
</script>

View File

@@ -0,0 +1,25 @@
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
import { fetchStashByUsernameAndSlug } from '#/src/stashes.js';
import { HttpError } from '#/src/errors.js';
export async function onBeforeRender(pageContext) {
try {
const stash = await fetchStashByUsernameAndSlug(pageContext.routeParams.username, pageContext.routeParams.stashSlug, pageContext.user);
return {
pageContext: {
title: `${stash.name} by ${stash.user.username}`,
pageProps: {
stash,
},
},
};
} catch (error) {
if (error instanceof HttpError) {
throw render(error.httpCode, error.message);
}
throw error;
}
}

View File

@@ -1,7 +1,7 @@
import { match } from 'path-to-regexp';
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
const path = '/stash/:username/:stashSlug/:scope?/:page?';
const path = '/stash/:username/:stashSlug/:domain(movies)/:scope?/:page?';
const urlMatch = match(path, { decode: decodeURIComponent });
export default (pageContext) => {
@@ -12,6 +12,7 @@ export default (pageContext) => {
routeParams: {
username: matched.params.username,
stashSlug: matched.params.stashSlug,
domain: matched.params.domain,
scope: matched.params.scope || 'stashed',
page: matched.params.page || '1',
path,

View File

@@ -0,0 +1,10 @@
<template>
<Stash>
<Scenes />
</Stash>
</template>
<script setup>
import Stash from '#/components/stashes/stash.vue';
import Scenes from '#/components/scenes/scenes.vue';
</script>

View File

@@ -0,0 +1,24 @@
import { match } from 'path-to-regexp';
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
const path = '/stash/:username/:stashSlug/:domain?/:scope?/:page?';
const urlMatch = match(path, { decode: decodeURIComponent });
export default (pageContext) => {
const matched = urlMatch(pageContext.urlPathname);
if (matched) {
return {
routeParams: {
username: matched.params.username,
stashSlug: matched.params.stashSlug,
domain: matched.params.domain || 'scenes',
scope: matched.params.scope || 'stashed',
page: matched.params.page || '1',
path,
},
};
}
return false;
};