From 1c3f17ec824d7fa350eaa39dd6f232df83564360 Mon Sep 17 00:00:00 2001 From: Niels Simenon Date: Fri, 15 Nov 2019 01:27:58 +0100 Subject: [PATCH] Added timerange filters. Refactored releases module for more efficient queries. --- .eslintrc | 2 +- assets/components/home/filter-bar.vue | 109 ++++++++++++++++++++++---- assets/components/home/filters.vue | 36 --------- assets/components/home/home.vue | 15 ++++ assets/components/release/release.vue | 56 +++++++------ assets/css/style.scss | 2 +- assets/js/range-dates.js | 18 +++++ assets/js/releases/actions.js | 18 ++++- public/css/style.css | 33 +++++++- seeds/01_sites.js | 1 + src/releases.js | 105 +++++++++---------------- src/web/releases.js | 37 +++++++-- src/web/server.js | 3 +- 13 files changed, 280 insertions(+), 155 deletions(-) create mode 100644 assets/js/range-dates.js diff --git a/.eslintrc b/.eslintrc index e3adf429e..92eb27f0c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,7 +2,7 @@ "root": true, "extends": ["airbnb-base", "plugin:vue/recommended"], "parserOptions": { - "ecmaVersion": 2017, + "ecmaVersion": 2019, "sourceType": "module" }, "rules": { diff --git a/assets/components/home/filter-bar.vue b/assets/components/home/filter-bar.vue index 967e6e61c..43b7360a9 100644 --- a/assets/components/home/filter-bar.vue +++ b/assets/components/home/filter-bar.vue @@ -1,24 +1,70 @@ @@ -34,6 +80,10 @@ export default { type: Array, default: () => [], }, + range: { + type: String, + default: null, + }, }, }; @@ -43,7 +93,8 @@ export default { .filter-bar { background: $background; - display: block; + display: flex; + justify-content: space-between; padding: .5rem 1rem; font-size: 0; box-shadow: 0 0 3px $shadow; @@ -64,6 +115,30 @@ export default { margin: 0 0 0 .5rem; } +.range-button { + color: $shadow; + background: $background; + display: inline-block; + padding: .5rem 1rem; + border: none; + box-shadow: 0 0 2px $shadow-weak; + font-size: .8rem; + font-weight: bold; + + &:hover { + color: $text; + cursor: pointer; + } +} + +.range-input { + display: none; + + &:checked + .range-button { + color: $primary; + } +} + @media(max-width: $breakpoint) { .filters-container { display: none; diff --git a/assets/components/home/filters.vue b/assets/components/home/filters.vue index 858a67047..727149dc3 100644 --- a/assets/components/home/filters.vue +++ b/assets/components/home/filters.vue @@ -1,25 +1,6 @@ diff --git a/assets/components/home/home.vue b/assets/components/home/home.vue index 491fae14a..c20073448 100644 --- a/assets/components/home/home.vue +++ b/assets/components/home/home.vue @@ -2,7 +2,9 @@
@@ -23,9 +25,12 @@ import FilterBar from './filter-bar.vue'; import ReleaseTile from '../tile/release.vue'; +import rangeDates from '../../js/range-dates'; + async function fetchReleases() { this.releases = await this.$store.dispatch('fetchReleases', { filter: this.filter, + ...rangeDates(this.range), }); } @@ -36,6 +41,13 @@ async function setFilter(filter) { await this.fetchReleases(); } +async function setRange(range) { + this.range = range; + localStorage.setItem('range', this.range); + + await this.fetchReleases(); +} + async function mounted() { this.pageTitle = ''; @@ -49,9 +61,11 @@ export default { }, data() { const storedFilter = localStorage.getItem('filter'); + const storedRange = localStorage.getItem('range'); return { filter: storedFilter ? storedFilter.split(',') : ['gay', 'transsexual'], + range: storedRange || 'new', releases: [], networks: [], pageTitle: null, @@ -61,6 +75,7 @@ export default { methods: { fetchReleases, setFilter, + setRange, }, }; diff --git a/assets/components/release/release.vue b/assets/components/release/release.vue index 8c3bbc440..5c36e5d9b 100644 --- a/assets/components/release/release.vue +++ b/assets/components/release/release.vue @@ -9,6 +9,7 @@ +
+ + + {{ Math.floor(release.duration / 3600) }}: + {{ Math.floor((release.duration % 3600) / 60).toString().padStart(2, '0') }}: + {{ (release.duration % 60).toString().padStart(2, '0') }} +
+

{{ release.studio.name }}

- - {{ release.shootId }} - -
- + - {{ Math.floor(release.duration / 3600) }}: - {{ Math.floor((release.duration % 3600) / 60).toString().padStart(2, '0') }}: - {{ (release.duration % 60).toString().padStart(2, '0') }} + {{ release.shootId }}
@@ -199,7 +207,7 @@ function pageTitle() { } async function mounted() { - [this.release] = await this.$store.dispatch('fetchReleases', { id: this.$route.params.releaseId }); + this.release = await this.$store.dispatch('fetchReleases', { id: this.$route.params.releaseId }); } export default { @@ -271,6 +279,7 @@ export default { &.date, &.duration, &.shoot { + flex-shrink: 0; padding: 1.25rem 1rem 1.25rem 0; margin: 0 1rem 0 0; } @@ -293,12 +302,14 @@ export default { height: 3rem; max-width: 15rem; object-fit: contain; + object-position: 100% 50%; } .logo-network { height: 1.5rem; max-width: 10rem; object-fit: contain; + object-position: 100% 50%; } .chain { @@ -373,7 +384,8 @@ export default { } .logo-site { - max-width: 10rem; + width: 15rem; + max-width: 100%; } } diff --git a/assets/css/style.scss b/assets/css/style.scss index d4ccac2e0..12bbaf9cc 100644 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -46,6 +46,6 @@ body { @media(max-width: $breakpoint) { .scenes { - grid-template-columns: repeat(auto-fit, minmax(22.5rem, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); } } diff --git a/assets/js/range-dates.js b/assets/js/range-dates.js new file mode 100644 index 000000000..b4027ee67 --- /dev/null +++ b/assets/js/range-dates.js @@ -0,0 +1,18 @@ +function rangeDates(range) { + return ({ + new: () => ({ + after: new Date(0), + before: new Date(), + }), + upcoming: () => ({ + after: new Date(), + before: new Date(2 ** 42), + }), + all: () => ({ + after: new Date(0), + before: new Date(2 ** 42), + }), + })[range](); +} + +export default rangeDates; diff --git a/assets/js/releases/actions.js b/assets/js/releases/actions.js index c1da8aa53..1f892816f 100644 --- a/assets/js/releases/actions.js +++ b/assets/js/releases/actions.js @@ -1,8 +1,22 @@ +import dayjs from 'dayjs'; + import { get } from '../api'; function initReleasesActions(_store, _router) { - async function fetchReleases({ _commit }, { id, filter }) { - const releases = await get(`/releases/${id || ''}`, { filter }); + async function fetchReleases({ _commit }, { + id, + filter, + after, + before, + }) { + const afterString = dayjs(after).format('YYYY-MM-DD'); + const beforeString = dayjs(before).format('YYYY-MM-DD'); + + const releases = await get(`/releases/${id || ''}`, { + filter, + after: afterString, + before: beforeString, + }); return releases; } diff --git a/public/css/style.css b/public/css/style.css index c7c785017..3837e93a0 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -42,7 +42,8 @@ /* $primary: #ff886c; */ .filter-bar[data-v-a678373a] { background: #fff; - display: block; + display: flex; + justify-content: space-between; padding: .5rem 1rem; font-size: 0; box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); @@ -59,6 +60,26 @@ display: none; margin: 0 0 0 .5rem; } +.range-button[data-v-a678373a] { + color: rgba(0, 0, 0, 0.5); + background: #fff; + display: inline-block; + padding: .5rem 1rem; + border: none; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + font-size: .8rem; + font-weight: bold; +} +.range-button[data-v-a678373a]:hover { + color: #222; + cursor: pointer; +} +.range-input[data-v-a678373a] { + display: none; +} +.range-input:checked + .range-button[data-v-a678373a] { + color: #ff6c88; +} @media (max-width: 720px) { .filters-container[data-v-a678373a] { display: none; @@ -271,6 +292,7 @@ margin: 0 .25rem 0 0; } .tidbit.date[data-v-2bc41e74], .tidbit.duration[data-v-2bc41e74], .tidbit.shoot[data-v-2bc41e74] { + flex-shrink: 0; padding: 1.25rem 1rem 1.25rem 0; margin: 0 1rem 0 0; } @@ -290,12 +312,16 @@ max-width: 15rem; -o-object-fit: contain; object-fit: contain; + -o-object-position: 100% 50%; + object-position: 100% 50%; } .logo-network[data-v-2bc41e74] { height: 1.5rem; max-width: 10rem; -o-object-fit: contain; object-fit: contain; + -o-object-position: 100% 50%; + object-position: 100% 50%; } .chain[data-v-2bc41e74] { color: rgba(0, 0, 0, 0.5); @@ -355,7 +381,8 @@ display: block; } .logo-site[data-v-2bc41e74] { - max-width: 10rem; + width: 15rem; + max-width: 100%; } } @@ -686,7 +713,7 @@ body { @media (max-width: 720px) { .scenes { - grid-template-columns: repeat(auto-fit, minmax(22.5rem, 1fr)); } } + grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); } } /* $primary: #ff886c; */ .header[data-v-10b7ec04] { diff --git a/seeds/01_sites.js b/seeds/01_sites.js index eaf33c84f..936eba239 100644 --- a/seeds/01_sites.js +++ b/seeds/01_sites.js @@ -992,6 +992,7 @@ function getSites(networksMap) { name: 'Evil Angel', url: 'https://evilangel.com', description: 'Welcome to the award winning Evil Angel website, home to the most popular pornstars of today, yesterday and tomorrow in their most extreme and hardcore porn scenes to date. We feature almost 30 years of rough sex videos and hardcore anal porn like you\'ve never seen before, and have won countless AVN and XBiz awards including \'Best Site\' and \'Best Studio\'.', + parameters: JSON.stringify({ independent: true }), network_id: networksMap['evilangel'], }, // JULES JORDAN diff --git a/src/releases.js b/src/releases.js index 134f65293..13c9f5f14 100644 --- a/src/releases.js +++ b/src/releases.js @@ -1,6 +1,7 @@ 'use strict'; const knex = require('./knex'); +const whereOr = require('./utils/where-or'); async function curateRelease(release) { const [actors, tags, media] = await Promise.all([ @@ -70,10 +71,13 @@ function curateReleases(releases) { return Promise.all(releases.map(async release => curateRelease(release))); } -async function fetchReleases(releaseId, filter = []) { - // const straightFilter = filter.includes('straight') ? ['gay', 'lesbian'] : []; - - const releases = await knex('releases') +function commonQuery(queryBuilder, { + filter = [], + after = new Date(0), // January 1970 + before = new Date(2 ** 44), // May 2109 + limit = 100, +}) { + queryBuilder .leftJoin('sites', 'releases.site_id', 'sites.id') .leftJoin('studios', 'releases.studio_id', 'studios.id') .leftJoin('networks', 'sites.network_id', 'networks.id') @@ -84,7 +88,7 @@ async function fetchReleases(releaseId, filter = []) { 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', ) .whereNotExists((builder) => { - // apply filters + // apply tag filters builder .select('*') .from('tags_associated') @@ -92,91 +96,58 @@ async function fetchReleases(releaseId, filter = []) { .whereIn('tags.slug', filter) .andWhereRaw('tags_associated.release_id = releases.id'); }) - .andWhere(releaseId ? { 'releases.id': releaseId } : {}) + .andWhere('date', '>', after) + .andWhere('date', '<=', before) .orderBy([{ column: 'date', order: 'desc' }, { column: 'created_at', order: 'desc' }]) - .limit(100); - - return curateReleases(releases); + .limit(limit); } -async function fetchSiteReleases(siteId, siteSlug) { +async function fetchReleases(queryObject = {}, options = {}) { const releases = await knex('releases') - .where({ 'sites.id': siteId }) - .orWhere({ 'sites.slug': siteSlug }) - .select( - 'releases.*', - 'sites.name as site_name', 'sites.slug as site_slug', 'sites.url as site_url', 'sites.network_id', 'sites.parameters as site_parameters', - 'studios.name as studio_name', 'sites.slug as site_slug', 'studios.url as studio_url', - 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', - ) - .leftJoin('sites', 'releases.site_id', 'sites.id') - .leftJoin('studios', 'releases.studio_id', 'studios.id') - .leftJoin('networks', 'sites.network_id', 'networks.id') - .orderBy([{ column: 'date', order: 'desc' }, { column: 'created_at', order: 'desc' }]) - .limit(100); + .modify(commonQuery, options) + .andWhere(builder => whereOr(queryObject, 'releases', builder)); return curateReleases(releases); } -async function fetchNetworkReleases(networkId, networkSlug) { +async function fetchSiteReleases(queryObject, options = {}) { const releases = await knex('releases') - .where({ 'networks.id': networkId }) - .orWhere({ 'networks.slug': networkSlug }) - .select( - 'releases.*', - 'sites.name as site_name', 'sites.slug as site_slug', 'sites.url as site_url', 'sites.network_id', 'sites.parameters as site_parameters', - 'studios.name as studio_name', 'sites.slug as site_slug', 'studios.url as studio_url', - 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', - ) - .leftJoin('sites', 'releases.site_id', 'sites.id') - .leftJoin('studios', 'releases.studio_id', 'studios.id') - .leftJoin('networks', 'sites.network_id', 'networks.id') - .orderBy([{ column: 'date', order: 'desc' }, { column: 'created_at', order: 'desc' }]) - .limit(100); + .modify(commonQuery, options) + .where(builder => whereOr(queryObject, 'sites', builder)); return curateReleases(releases); } -async function fetchActorReleases(actorId, actorSlug) { +async function fetchNetworkReleases(queryObject, options = {}) { + const releases = await knex('releases') + .modify(commonQuery, options) + .where(builder => whereOr(queryObject, 'networks', builder)); + + return curateReleases(releases); +} + +async function fetchActorReleases(queryObject, options = {}) { const releases = await knex('actors_associated') - .where({ 'actors.id': actorId }) - .orWhere({ 'actors.slug': actorSlug }) - .select( - 'releases.*', - 'actors.name as actor_name', - 'sites.name as site_name', 'sites.slug as site_slug', 'sites.url as site_url', 'sites.network_id', 'sites.parameters as site_parameters', - 'studios.name as studio_name', 'sites.slug as site_slug', 'studios.url as studio_url', - 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', - ) .leftJoin('releases', 'actors_associated.release_id', 'releases.id') .leftJoin('actors', 'actors_associated.actor_id', 'actors.id') - .leftJoin('sites', 'releases.site_id', 'sites.id') - .leftJoin('studios', 'releases.studio_id', 'studios.id') - .leftJoin('networks', 'sites.network_id', 'networks.id') - .orderBy([{ column: 'releases.date', order: 'desc' }, { column: 'releases.created_at', order: 'desc' }]) - .limit(100); + .select( + 'actors.name as actor_name', + ) + .modify(commonQuery, options) + .where(builder => whereOr(queryObject, 'actors', builder)); return curateReleases(releases); } -async function fetchTagReleases(tagId, tagSlug) { +async function fetchTagReleases(queryObject, options = {}) { const releases = await knex('tags_associated') - .where({ 'tags.id': tagId }) - .orWhere({ 'tags.slug': tagSlug }) - .select( - 'releases.*', - 'tags.name as tag_name', - 'sites.name as site_name', 'sites.slug as site_slug', 'sites.url as site_url', 'sites.network_id', - 'studios.name as studio_name', 'sites.slug as site_slug', 'studios.url as studio_url', - 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', - ) .leftJoin('releases', 'tags_associated.release_id', 'releases.id') .leftJoin('tags', 'tags_associated.tag_id', 'tags.id') - .leftJoin('sites', 'releases.site_id', 'sites.id') - .leftJoin('studios', 'releases.studio_id', 'studios.id') - .leftJoin('networks', 'sites.network_id', 'networks.id') - .orderBy([{ column: 'releases.date', order: 'desc' }, { column: 'releases.created_at', order: 'desc' }]) - .limit(100); + .select( + 'tags.name as tag_name', + ) + .modify(commonQuery, options) + .where(builder => whereOr(queryObject, 'tags', builder)); return curateReleases(releases); } diff --git a/src/web/releases.js b/src/web/releases.js index b970e54de..eaadf9cd9 100644 --- a/src/web/releases.js +++ b/src/web/releases.js @@ -9,16 +9,33 @@ const { } = require('../releases'); async function fetchReleasesApi(req, res) { - const releases = await fetchReleases(req.params.releaseId, req.query.filter ? [].concat(req.query.filter) : []); + const filter = req.query.filter ? [].concat(req.query.filter) : []; // don't filter for 'undefined' + + const releases = await fetchReleases({}, { + filter, + after: req.query.after, + before: req.query.before, + }); res.send(releases); } +async function fetchReleaseByIdApi(req, res) { + const [release] = await fetchReleases({ + id: req.params.releaseId, + }); + + res.send(release); +} + async function fetchActorReleasesApi(req, res) { const actorId = Number.isInteger(Number(req.params.actorId)) ? Number(req.params.actorId) : null; const actorSlug = typeof req.params.actorId === 'string' ? req.params.actorId : null; - const releases = await fetchActorReleases(actorId, actorSlug); + const releases = await fetchActorReleases({ + id: actorId, + slug: actorSlug, + }); res.send(releases); } @@ -27,7 +44,10 @@ async function fetchNetworkReleasesApi(req, res) { const networkId = typeof req.params.networkId === 'number' ? req.params.networkId : null; const networkSlug = typeof req.params.networkId === 'string' ? req.params.networkId : null; - const releases = await fetchNetworkReleases(networkId, networkSlug); + const releases = await fetchNetworkReleases({ + id: networkId, + slug: networkSlug, + }); res.send(releases); } @@ -36,7 +56,10 @@ async function fetchSiteReleasesApi(req, res) { const siteId = typeof req.params.siteId === 'number' ? req.params.siteId : null; const siteSlug = typeof req.params.siteId === 'string' ? req.params.siteId : null; - const releases = await fetchSiteReleases(siteId, siteSlug); + const releases = await fetchSiteReleases({ + id: siteId, + slug: siteSlug, + }); res.send(releases); } @@ -45,13 +68,17 @@ async function fetchTagReleasesApi(req, res) { const tagId = typeof req.params.tagId === 'number' ? req.params.tagId : null; const tagSlug = typeof req.params.tagId === 'string' ? req.params.tagId : null; - const releases = await fetchTagReleases(tagId, tagSlug); + const releases = await fetchTagReleases({ + id: tagId, + slug: tagSlug, + }); res.send(releases); } module.exports = { fetchReleases: fetchReleasesApi, + fetchReleaseById: fetchReleaseByIdApi, fetchActorReleases: fetchActorReleasesApi, fetchNetworkReleases: fetchNetworkReleasesApi, fetchSiteReleases: fetchSiteReleasesApi, diff --git a/src/web/server.js b/src/web/server.js index 43dd905df..e0b0ccb9d 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -8,6 +8,7 @@ const bodyParser = require('body-parser'); const { fetchReleases, + fetchReleaseById, fetchActorReleases, fetchNetworkReleases, fetchSiteReleases, @@ -37,7 +38,7 @@ function initServer() { router.use(bodyParser.json({ strict: false })); router.get('/api/releases', fetchReleases); - router.get('/api/releases/:releaseId', fetchReleases); + router.get('/api/releases/:releaseId', fetchReleaseById); router.get('/api/releases/networks', fetchNetworksFromReleases); router.get('/api/actors', fetchActors);