<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> </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(); } } </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; } </style>