<template> <div ref="content" class="actors" > <nav ref="filters" class="filters" > <div class="filters-row"> <ul class="genders nolist"> <li class="gender"> <router-link :to="{ name: 'actors', params: { gender: 'all', pageNumber: 1 }, query: $route.query }" :class="{ selected: gender === 'all' }" class="gender-link all" >all</router-link> </li> <li class="gender"> <router-link :to="{ name: 'actors', params: { gender: 'female', pageNumber: 1 }, query: $route.query }" :class="{ selected: gender === 'female' }" class="gender-link female" ><Gender gender="female" /></router-link> </li> <li class="gender"> <router-link :to="{ name: 'actors', params: { gender: 'male', pageNumber: 1 }, query: $route.query }" :class="{ selected: gender === 'male' }" class="gender-link male" replace ><Gender gender="male" /></router-link> </li> <li class="gender"> <router-link :to="{ name: 'actors', params: { gender: 'trans', pageNumber: 1 }, query: $route.query }" :class="{ selected: gender === 'trans' }" class="gender-link transsexual" replace ><Gender gender="transsexual" /></router-link> </li> <li class="gender"> <router-link :to="{ name: 'actors', params: { gender: 'other', pageNumber: 1 }, query: $route.query }" :class="{ selected: gender === 'other' }" class="gender-link other" replace ><Icon icon="question5" /></router-link> </li> </ul> <ul class="filters-attributes nolist"> <li> <Tooltip class="filter boobs"> <span class="filter-trigger" :class="{ enabled: ageRequired }" ><Icon icon="vcard" />Age</span> <template v-slot:tooltip> <RangeFilter label="age" :min="18" :max="100" :value="age" :disabled="!ageRequired" @enable="(checked) => updateValue('ageRequired', checked, true)" @input="(range) => updateValue('age', range, false)" @change="(range) => updateValue('age', range, true)" > <template v-slot:start><Icon icon="leaf" /></template> <template v-slot:end><Icon icon="tree3" /></template> </RangeFilter> <div class="filter-section"> <label class="filter-label"> <span class="label"> <Checkbox :checked="dobRequired" class="checkbox" @change="(checked) => updateValue('dobRequired', checked, true)" />Date of birth </span> </label> <div class="input-container" @click="() => updateValue('dobRequired', true, true)" > <input v-model="dob" :disabled="!dobRequired" type="date" class="input" @change="updateFilters" > </div> </div> </template> </Tooltip> </li> <li> <Tooltip class="filter"> <span class="filter-trigger boobs" :class="{ enabled: boobSizeRequired || naturalBoobs !== 1 }" ><Icon icon="boobs" />Boobs</span> <template v-slot:tooltip> <RangeFilter label="size" :min="0" :max="boobSizes.length - 1" :value="boobSize" :values="boobSizes" :disabled="!boobSizeRequired" @enable="(checked) => updateValue('boobSizeRequired', checked, true)" @input="(range) => updateValue('boobSize', range, false)" @change="(range) => updateValue('boobSize', range, true)" > <template v-slot:start><Icon icon="boobs-small" /></template> <template v-slot:end><Icon icon="boobs-big" /></template> </RangeFilter> <div class="filter-section"> <span class="filter-label">Enhanced</span> <span :class="{ [['off', 'default', 'on'][naturalBoobs]]: true }" class="toggle-container noclick" > <span class="toggle-label off" @click="updateValue('naturalBoobs', 0)" ><Icon icon="leaf" /></span> <input v-model.number="naturalBoobs" class="toggle" type="range" min="0" max="2" @change="updateFilters" > <span class="toggle-label on" @click="updateValue('naturalBoobs', 2)" ><Icon icon="magic-wand2" /></span> </span> </div> </template> </Tooltip> </li> <li> <Tooltip class="filter boobs"> <span class="filter-trigger" :class="{ enabled: heightRequired || weightRequired }" ><Icon icon="rulers" />Physique</span> <template v-slot:tooltip> <RangeFilter label="height" :min="50" :max="220" :value="height" :disabled="!heightRequired" unit="cm" @enable="(checked) => updateValue('heightRequired', checked, true)" @input="(range) => updateValue('height', range, false)" @change="(range) => updateValue('height', range, true)" > <template v-slot:start><Icon icon="height-short" /></template> <template v-slot:end><Icon icon="height" /></template> </RangeFilter> <RangeFilter label="weight" :min="30" :max="200" :value="weight" :disabled="!weightRequired" unit="kg" @enable="(checked) => updateValue('weightRequired', checked, true)" @input="(range) => updateValue('weight', range, false)" @change="(range) => updateValue('weight', range, true)" > <template v-slot:start><Icon icon="meter-slow" /></template> <template v-slot:end><Icon icon="meter-fast" /></template> </RangeFilter> </template> </Tooltip> </li> <li> <Tooltip class="filter"> <span :class="{ enabled: country }" class="filter-trigger" ><img v-if="$route.query.c" :src="`/img/flags/${$route.query.c.toLowerCase()}.svg`" class="flag" ><Icon v-else icon="earth2" />Country</span> <template v-slot:tooltip> <input v-model="countryQuery" placeholder="Search" class="input" > <Countries v-if="!countryQuery" :countries="topCountries" :selected-country="country" :update-value="updateValue" /> <Countries :countries="filteredCountries" :selected-country="country" :update-value="updateValue" /> </template> </Tooltip> </li> </ul> </div> <SearchBar :placeholder="`Search ${totalCount} actors`" /> </nav> <div ref="tiles" class="tiles" > <Actor v-for="actor in actors" :key="`actor-${actor.id}`" :actor="actor" /> </div> <Pagination v-if="totalCount > 0" :items-total="totalCount" :items-per-page="limit" class="pagination-bottom" /> <Footer /> </div> </template> <script> import dayjs from 'dayjs'; import Actor from './tile.vue'; import Gender from './gender.vue'; import Checkbox from '../form/checkbox.vue'; import Countries from './countries.vue'; import RangeFilter from './filter-range.vue'; import SearchBar from '../search/bar.vue'; import Pagination from '../pagination/pagination.vue'; const toggleValues = [true, null, false]; const boobSizes = 'ABCDEFGHIJKZ'.split(''); const topCountries = ['AU', 'BR', 'CZ', 'DE', 'JP', 'RU', 'GB', 'US']; function updateFilters() { this.$router.push({ name: 'actors', params: { pageNumber: 1, gender: this.gender, }, query: { nb: this.naturalBoobs !== 1 ? this.naturalBoobs : undefined, bs: this.boobSizeRequired ? this.boobSize.join(',') : undefined, h: this.heightRequired ? this.height.join(',') : undefined, w: this.weightRequired ? this.weight.join(',') : undefined, c: this.country ? this.country : undefined, age: this.ageRequired ? this.age.join(',') : undefined, dob: this.dobRequired ? this.dob : undefined, query: this.$route.query.query, }, }); } function updateValue(prop, value, load = true) { this[prop] = value; if (load) { this.updateFilters(); } } async function fetchActors(scroll) { const curatedGender = this.gender.replace('trans', 'transsexual'); const { actors, countries, totalCount } = await this.$store.dispatch('fetchActors', { limit: this.limit, pageNumber: Number(this.$route.params.pageNumber) || 1, query: this.$route.query.query, gender: curatedGender === 'other' ? null : curatedGender, age: this.ageRequired && this.age, dob: this.dobRequired && this.dob, boobSize: this.boobSizeRequired && this.boobSize, country: this.country, naturalBoobs: toggleValues[this.naturalBoobs] ?? null, height: this.heightRequired && this.height, weight: this.weightRequired && this.weight, }); const countriesByAlpha2 = countries.reduce((acc, country) => ({ ...acc, [country.alpha2]: country }), {}); this.actors = actors; this.totalCount = totalCount; this.countries = countries; this.topCountries = [...(this.country && !topCountries.includes(this.country) ? [this.country] : []), ...topCountries].map(alpha2 => countriesByAlpha2[alpha2]); if (scroll) { this.$refs.tiles?.scrollIntoView(); } } function filteredCountries() { const countryQueryExpression = new RegExp(this.countryQuery, 'i'); return this.countryQuery?.length > 0 ? this.countries.filter(country => countryQueryExpression.test(country.name) || countryQueryExpression.test(country.alpha2)) : this.countries; } function gender() { return this.$route.params.gender || 'all'; } async function route(to, from) { const scroll = to.params.pageNumber !== from.params.pageNumber || to.params.gender !== from.params.gender; await this.fetchActors(scroll); } async function mounted() { this.pageTitle = 'Actors'; await this.fetchActors(); } export default { components: { Actor, Checkbox, Countries, Gender, RangeFilter, SearchBar, Pagination, }, data() { return { actors: [], countries: [], topCountries: [], countryQuery: null, pageTitle: null, totalCount: 0, limit: 50, age: this.$route.query.age?.split(',') || [18, 100], ageRequired: !!this.$route.query.age, dob: this.$route.query.dob || dayjs().subtract(21, 'years').format('YYYY-MM-DD'), dobRequired: !!this.$route.query.dob, boobSizes, boobSize: this.$route.query.bs?.split(',') || ['A', 'Z'], boobSizeRequired: !!this.$route.query.bs, country: this.$route.query.c || null, naturalBoobs: Number(this.$route.query.nb) || 1, height: this.$route.query.h?.split(',').map(Number) || [50, 220], heightRequired: !!this.$route.query.h, weight: this.$route.query.w?.split(',').map(Number) || [30, 200], weightRequired: !!this.$route.query.w, }; }, computed: { gender, filteredCountries, }, watch: { $route: route, }, mounted, methods: { fetchActors, updateFilters, updateValue, }, }; </script> <style lang="scss"> .gender-link { &.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 lang="scss" scoped> @import 'breakpoints'; .actors { display: flex; flex-direction: column; flex-grow: 1; overflow-y: auto; } .tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); grid-template-rows: min-content; grid-gap: .5rem; padding: 1rem; flex-grow: 1; } .search { width: 0; justify-content: flex-end; flex-grow: 1; box-sizing: border-box; padding: 0 1rem; } .filters, .filters-row { display: flex; justify-content: flex-end; align-items: center; } .filters { margin: 1rem 0 .5rem 0; } .filters-row, .filter { padding: 0 1rem; } .genders { display: flex; flex-shrink: 0; padding: 0 .5rem 0 0; } .gender { display: inline-block; } .gender-link { width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; box-sizing: border-box; margin: .25rem .5rem .25rem 0; color: var(--shadow); background: var(--background); font-weight: bold; text-decoration: none; box-shadow: 0 0 3px var(--darken-weak); .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); } } } .filter-trigger { display: inline-flex; align-items: center; color: var(--shadow); 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); } } } .label-values { font-weight: normal; } .filter-split { display: flex; align-items: center; } .filter-label { display: flex; justify-content: space-between; padding: .75rem .5rem .5rem .5rem; color: var(--shadow); font-weight: bold; font-size: .9rem; .checkbox { margin: 0 .75rem 0 0; } .label { display: inline-flex; align-items: center; text-transform: capitalize; } } .input-container { box-sizing: border-box; padding: 0 .5rem .5rem .5rem; .input { width: 100%; } } .toggle-container, .range-container { display: flex; flex-grow: 1; align-items: center; padding: .5rem 0; &.on { .toggle-label.on { color: var(--enabled); .icon { fill: var(--enabled); } } .toggle { background-color: var(--enabled-background); &::-webkit-slider-thumb { background: var(--enabled); } &::-moz-range-thumb { background: var(--enabled); } } } &.off { .toggle-label.off { color: var(--disabled); .icon { fill: var(--disabled); } } .toggle { background-color: var(--disabled-background); &::-webkit-slider-thumb { background: var(--disabled); } &::-moz-range-thumb { background: var(--disabled); } } } } .toggle-label { display: inline-flex; justify-content: center; min-width: 1.5rem; flex-shrink: 0; padding: 0 .5rem; color: var(--shadow); font-weight: bold; font-size: .9rem; &.on { text-align: right; } .icon { fill: var(--shadow); } &:hover { cursor: pointer; &.on { color: var(--enabled); .icon { fill: var(--enabled); } } &.off { color: var(--disabled); .icon { fill: var(--disabled); } } } } .toggle { width: 0; flex-grow: 1; height: 1.25rem; appearance: none; border-radius: 1rem; background-color: var(--shadow-hint); background-image: radial-gradient(circle, var(--shadow-weak) .3rem, transparent calc(.3rem + 1px)); cursor: pointer; &::-webkit-slider-thumb { appearance: none; background: var(--disabled-handle); width: 1.25rem; height: 1.25rem; border-radius: .625rem; box-shadow: 0 0 3px var(--darken-weak); } &::-moz-range-thumb { appearance: none; background: var(--disabled-handle); width: 1.25rem; height: 1.25rem; border: none; border-radius: .625rem; box-shadow: 0 0 3px var(--darken-weak); } } @media(max-width: $breakpoint-mega) { .filters { flex-direction: column-reverse; } ::v-deep(.search) { width: 100%; justify-content: center; margin: 0 0 1rem 0; } } @media(max-width: $breakpoint-kilo) { .filters { margin: 1rem 0 0 0; } .filters-row { flex-direction: column; .filter { padding: 0 1rem 1rem 1rem; } } .filters-attributes { display: flex; flex-wrap: wrap; justify-content: center; } .genders { padding: 0; margin: 0 0 1.5rem 0; } .tiles { padding: .5rem 1rem 1rem 1rem; } } @media(max-width: $breakpoint-micro) { .tiles { grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); } } </style>