588 lines
14 KiB
Vue
588 lines
14 KiB
Vue
<template>
|
|
<div class="page">
|
|
<form
|
|
v-show="showFilters"
|
|
class="filters"
|
|
@submit.prevent
|
|
>
|
|
<div class="filter">
|
|
<input
|
|
v-model="q"
|
|
type="search"
|
|
placeholder="Search actors"
|
|
class="input search"
|
|
@search="search"
|
|
>
|
|
</div>
|
|
|
|
<ul class="filter genders nolist">
|
|
<li>
|
|
<button
|
|
:class="{ selected: filters.gender === undefined }"
|
|
class="gender-button all"
|
|
@click="updateFilter('gender', undefined, true)"
|
|
>all</button>
|
|
</li>
|
|
|
|
<li>
|
|
<button
|
|
:class="{ selected: filters.gender === 'female' }"
|
|
class="gender-button female"
|
|
@click="updateFilter('gender', 'female', true)"
|
|
><Gender gender="female" /></button>
|
|
</li>
|
|
|
|
<li>
|
|
<button
|
|
:class="{ selected: filters.gender === 'male' }"
|
|
class="gender-button male"
|
|
@click="updateFilter('gender', 'male', true)"
|
|
><Gender gender="male" /></button>
|
|
</li>
|
|
|
|
<li>
|
|
<button
|
|
:class="{ selected: filters.gender === 'transsexual' }"
|
|
class="gender-button transsexual"
|
|
@click="updateFilter('gender', 'transsexual', true)"
|
|
><Gender gender="transsexual" /></button>
|
|
</li>
|
|
|
|
<li>
|
|
<button
|
|
:class="{ selected: filters.gender === 'other' }"
|
|
class="gender-button other"
|
|
@click="updateFilter('gender', 'other', true)"
|
|
><Icon icon="question5" /></button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="filter">
|
|
<div class="filter-section">
|
|
<RangeFilter
|
|
label="age"
|
|
:min="18"
|
|
:max="100"
|
|
:value="filters.age"
|
|
:disabled="!filters.ageRequired"
|
|
@enable="(checked) => updateFilter('ageRequired', checked, filters.ageRequired !== checked)"
|
|
@input="(range) => updateFilter('age', range, false)"
|
|
@change="(range) => updateFilter('age', range, true)"
|
|
>
|
|
<template #start><Icon icon="leaf" /></template>
|
|
<template #end><Icon icon="tree3" /></template>
|
|
</RangeFilter>
|
|
</div>
|
|
|
|
<div class="filter-section">
|
|
<label class="filter-label">
|
|
<span class="label">
|
|
<Checkbox
|
|
:checked="filters.dobRequired"
|
|
class="checkbox"
|
|
@change="(checked) => updateFilter('dobRequired', checked, true)"
|
|
/>
|
|
|
|
<select
|
|
v-model="filters.dobType"
|
|
class="input select"
|
|
@change="() => updateFilter('dobRequired', true, true)"
|
|
>
|
|
<option value="birthday">Birthday</option>
|
|
<option value="dob">Date of birth</option>
|
|
</select>
|
|
</span>
|
|
</label>
|
|
|
|
<div
|
|
class="input-container"
|
|
@click="() => updateFilter('dobRequired', true, true)"
|
|
>
|
|
<input
|
|
:value="filters.dob"
|
|
:disabled="!filters.dobRequired"
|
|
:max="maxDob"
|
|
type="date"
|
|
class="input"
|
|
@change="(event) => updateFilter('dob', event.target.value, true)"
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="filter">
|
|
<RangeFilter
|
|
label="bra size"
|
|
:min="0"
|
|
:max="braSizes.length - 1"
|
|
:value="filters.braSize"
|
|
:values="braSizes"
|
|
:disabled="!filters.braSizeRequired"
|
|
@enable="(checked) => updateFilter('braSizeRequired', checked, true)"
|
|
@input="(range) => updateFilter('braSize', range, false)"
|
|
@change="(range) => updateFilter('braSize', range, true)"
|
|
>
|
|
<template #start><Icon icon="boobs-small" /></template>
|
|
<template #end><Icon icon="boobs-big" /></template>
|
|
</RangeFilter>
|
|
|
|
<span class="filter-label">Enhanced Boobs</span>
|
|
|
|
<span
|
|
:class="{ [['off', 'default', 'on'][naturalBoobsValues.indexOf(filters.naturalBoobs)]]: true }"
|
|
class="toggle-container noclick"
|
|
>
|
|
<span
|
|
class="toggle-label off"
|
|
@click="updateFilter('naturalBoobs', true, true)"
|
|
><Icon icon="leaf" /></span>
|
|
|
|
<input
|
|
:value="naturalBoobsValues.indexOf(filters.naturalBoobs)"
|
|
class="toggle"
|
|
type="range"
|
|
min="0"
|
|
max="2"
|
|
@change="updateFilter('naturalBoobs', naturalBoobsValues[$event.target.value], true)"
|
|
>
|
|
|
|
<span
|
|
class="toggle-label on"
|
|
@click="updateFilter('naturalBoobs', false, true)"
|
|
><Icon icon="magic-wand2" /></span>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="filter">
|
|
<RangeFilter
|
|
label="height"
|
|
:min="50"
|
|
:max="220"
|
|
:value="filters.height"
|
|
:disabled="!filters.heightRequired"
|
|
unit="cm"
|
|
@enable="(checked) => updateFilter('heightRequired', checked, filters.heightRequired !== checked)"
|
|
@input="(range) => updateFilter('height', range, false)"
|
|
@change="(range) => updateFilter('height', range, true)"
|
|
>
|
|
<template #start><Icon icon="height-short" /></template>
|
|
<template #end><Icon icon="height" /></template>
|
|
</RangeFilter>
|
|
|
|
<RangeFilter
|
|
label="weight"
|
|
:min="30"
|
|
:max="200"
|
|
:value="filters.weight"
|
|
:disabled="!filters.weightRequired"
|
|
unit="kg"
|
|
@enable="(checked) => updateFilter('weightRequired', checked, filters.weightRequired !== checked)"
|
|
@input="(range) => updateFilter('weight', range, false)"
|
|
@change="(range) => updateFilter('weight', range, true)"
|
|
>
|
|
<template #start><Icon icon="meter-slow" /></template>
|
|
<template #end><Icon icon="meter-fast" /></template>
|
|
</RangeFilter>
|
|
</div>
|
|
|
|
<div
|
|
v-if="filteredCountries.length > 0"
|
|
class="countries-container"
|
|
>
|
|
<input
|
|
v-if="!filters.country"
|
|
v-model="countryQuery"
|
|
type="search"
|
|
placeholder="Filter country"
|
|
class="input input-inline countries-search"
|
|
>
|
|
|
|
<div class="countries-list">
|
|
<Countries
|
|
v-if="!countryQuery && !filters.country && topCountries.length < filteredCountries.length"
|
|
:countries="topCountries"
|
|
:selected-country="filters.country"
|
|
:update-value="updateFilter"
|
|
/>
|
|
|
|
<Countries
|
|
:countries="filteredCountries"
|
|
:selected-country="filters.country"
|
|
:update-value="updateFilter"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="filter">
|
|
<Checkbox
|
|
:checked="filters.avatarRequired"
|
|
label="Require photo"
|
|
@change="(checked) => updateFilter('avatarRequired', checked, true)"
|
|
/>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="actors-anchor">
|
|
<div
|
|
class="sidebar-toggle"
|
|
@click="toggleFilters"
|
|
>
|
|
<Icon
|
|
v-show="showFilters"
|
|
icon="arrow-left3"
|
|
/>
|
|
|
|
<Icon
|
|
v-show="!showFilters"
|
|
icon="arrow-right3"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
ref="container"
|
|
class="actors-container"
|
|
>
|
|
<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>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, inject } from 'vue';
|
|
import { format, subYears } from 'date-fns';
|
|
|
|
import navigate from '#/src/navigate.js';
|
|
import { get } from '#/src/api.js';
|
|
|
|
import ActorTile from '#/components/actors/tile.vue';
|
|
import Pagination from '#/components/pagination/pagination.vue';
|
|
import Gender from '#/components/actors/gender.vue';
|
|
import Checkbox from '#/components/form/checkbox.vue';
|
|
import RangeFilter from '#/components/filters/range.vue';
|
|
import Countries from '#/components/filters/countries.vue';
|
|
|
|
const pageContext = inject('pageContext');
|
|
const { pageProps, urlParsed, routeParams } = pageContext;
|
|
|
|
const q = ref(urlParsed.search.q);
|
|
const actors = ref([]);
|
|
|
|
const container = ref(null);
|
|
const showFilters = ref(true);
|
|
|
|
const countries = ref(pageProps.countries);
|
|
const countryQuery = ref('');
|
|
|
|
const topCountryAlpha2s = ['AU', 'BR', 'CZ', 'DE', 'JP', 'RU', 'GB', 'US'];
|
|
const topCountries = computed(() => topCountryAlpha2s.map((alpha2) => countries.value.find((country) => country.alpha2 === alpha2)).filter(Boolean));
|
|
const filteredCountries = computed(() => countries.value.filter((country) => new RegExp(countryQuery.value, 'i').test(country.name)));
|
|
|
|
const maxDob = format(subYears(new Date(), 18), 'yyyy-MM-dd');
|
|
|
|
actors.value = pageProps.actors;
|
|
|
|
const braSizes = 'ABCDEFGHIJKZ'.split('');
|
|
const naturalBoobsValues = [true, undefined, false];
|
|
|
|
const currentPage = ref(Number(routeParams.page));
|
|
const total = ref(Number(pageProps.total));
|
|
|
|
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(resetPage = true) {
|
|
if (resetPage) {
|
|
currentPage.value = 1;
|
|
}
|
|
|
|
const query = {
|
|
q: q.value || undefined,
|
|
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,
|
|
};
|
|
|
|
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;
|
|
|
|
container.value.scrollTop = 0;
|
|
|
|
navigate(`/actors/${currentPage.value}`, query, { redirect: false });
|
|
}
|
|
|
|
function paginate({ page }) {
|
|
currentPage.value = page;
|
|
search(false);
|
|
}
|
|
|
|
function updateFilter(prop, value, reload = true) {
|
|
filters.value[prop] = value;
|
|
|
|
if (reload) {
|
|
search();
|
|
}
|
|
}
|
|
|
|
function toggleFilters() {
|
|
showFilters.value = !showFilters.value;
|
|
}
|
|
</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 {
|
|
height: 100%;
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.filters {
|
|
width: 17rem;
|
|
position: relative;
|
|
flex-shrink: 0;
|
|
border-right: solid 1px var(--shadow-weak-30);
|
|
overflow-y: auto;
|
|
|
|
.input {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
.filter {
|
|
padding: .5rem;
|
|
|
|
&:not(:last-child) {
|
|
border-bottom: solid 1px var(--shadow-weak-30);
|
|
}
|
|
}
|
|
|
|
.actors-anchor {
|
|
flex-grow: 1;
|
|
position: relative;
|
|
}
|
|
|
|
.sidebar-toggle {
|
|
width: 1.5rem;
|
|
height: 2rem;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
position: absolute;
|
|
bottom: .5rem;
|
|
left: 0;
|
|
z-index: 10;
|
|
border-radius: 0 .25rem .25rem 0;
|
|
background: var(--background);
|
|
box-shadow: 0 0 3px var(--shadow-weak-30);
|
|
|
|
.icon {
|
|
fill: var(--shadow);
|
|
}
|
|
|
|
&:hover {
|
|
cursor: pointer;
|
|
|
|
.icon {
|
|
fill: var(--primary);
|
|
}
|
|
}
|
|
}
|
|
|
|
.actors-container {
|
|
height: 100%;
|
|
box-sizing: border-box;
|
|
padding: 1rem;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.actors {
|
|
display: grid;
|
|
flex-grow: 1;
|
|
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
|
gap: .25rem;
|
|
}
|
|
|
|
.genders {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: .5rem;
|
|
}
|
|
|
|
.gender-button {
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-sizing: border-box;
|
|
border: none;
|
|
color: var(--shadow);
|
|
background: var(--background);
|
|
font-weight: bold;
|
|
text-decoration: none;
|
|
font-size: .9rem;
|
|
box-shadow: 0 0 3px var(--shadow-weak-20);
|
|
|
|
.male,
|
|
.female,
|
|
.transsexual {
|
|
padding: .2rem 0 0 0;
|
|
}
|
|
|
|
.icon {
|
|
fill: var(--shadow);
|
|
}
|
|
|
|
&:hover {
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
|
|
.icon {
|
|
fill: var(--text);
|
|
}
|
|
}
|
|
|
|
&.selected {
|
|
background: var(--primary);
|
|
color: var(--text-light);
|
|
|
|
&.other .icon {
|
|
fill: var(--text-light);
|
|
}
|
|
}
|
|
}
|
|
|
|
.select {
|
|
flex-grow: 1;
|
|
color: var(--shadow-strong-10);
|
|
|
|
option {
|
|
color: var(--text);
|
|
}
|
|
}
|
|
|
|
.countries-container {
|
|
border-bottom: solid 1px var(--shadow-weak-30);
|
|
padding: .25rem 0;
|
|
margin-bottom: .5rem;
|
|
}
|
|
|
|
.countries-search {
|
|
width: 100%;
|
|
margin-bottom: .25rem;
|
|
border-bottom: solid 1px var(--shadow-weak-40);
|
|
}
|
|
|
|
.countries-list {
|
|
max-height: 13rem;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
:deep(.country.selected) .country-name {
|
|
padding: .5rem;
|
|
}
|
|
|
|
.filter-trigger {
|
|
display: flex;
|
|
align-items: center;
|
|
color: var(--shadow);
|
|
font-size: .9rem;
|
|
font-weight: bold;
|
|
|
|
.icon,
|
|
.flag {
|
|
fill: var(--shadow);
|
|
width: 1rem;
|
|
height: 1rem;
|
|
margin: -.1rem .75rem 0 0;
|
|
}
|
|
|
|
&:hover {
|
|
color: var(--shadow-strong);
|
|
cursor: pointer;
|
|
|
|
.icon {
|
|
fill: var(--shadow-strong);
|
|
}
|
|
}
|
|
|
|
&.enabled {
|
|
color: var(--primary);
|
|
|
|
.icon {
|
|
fill: var(--primary);
|
|
}
|
|
}
|
|
}
|
|
</style>
|