diff --git a/.eslintrc b/.eslintrc index e3adf429..92eb27f0 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 967e6e61..43b7360a 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 858a6704..727149dc 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 491fae14..c2007344 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 8c3bbc44..5c36e5d9 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 d4ccac2e..12bbaf9c 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 00000000..b4027ee6 --- /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 c1da8aa5..1f892816 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 c7c78501..3837e93a 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 eaf33c84..936eba23 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 134f6529..13c9f5f1 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 b970e54d..eaadf9cd 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 43dd905d..e0b0ccb9 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);