IGDV v3 migration (#125)

* Updated endpoints to use IGDB v3

* Updated computed props to match v3 changes

* Moved ageRatings to getter + updated for v3

* Updated getters to support v3 changes

* Moved all ratings images under same folder

* Use new endpoints in actions

* Compatibility updates

* Null check

* formatting

* Load games in chunks of 10 to meet new API requirements

* More compatibility changes

* Use ids for New and standard 3DS, great job naming consoles nintendo!

* Drop v2 suffix
This commit is contained in:
Roman Cervantes 2019-06-08 19:04:07 -07:00 committed by GitHub
parent 3589bfe62d
commit c2823c2a59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 177 additions and 196 deletions

View file

@ -1,22 +1,25 @@
const functions = require('firebase-functions');
const axios = require('axios');
axios.defaults.headers.common['user-key'] = functions.config().igdb.key;
const igdbUrl = 'https://api-endpoint.igdb.com';
const igdbV3Url = 'https://api-v3.igdb.com/';
const igdbFields = 'id,name,slug,created_at,updated_at,summary,rating,category,player_perspectives,release_dates,name,cover,platforms,screenshots,videos,websites,esrb,pegi,themes.name,game.name&expand=game,themes,developers,publishers,game_engines,game_modes,genres,platforms,player_perspectives';
const igdbV3Fields = 'fields alternative_name,character,collection,company,description,game,name,person,platform,popularity,published_at,test_dummy,theme;';
const igdbFieldsMinimal = 'id,name,slug,rating,name,cover,release_dates';
axios.defaults.headers.common['user-key'] = functions.config().igdbv3.key;
exports.search = functions.https.onRequest((req, res) => {
res.set('Access-Control-Allow-Origin', "*")
const { searchText, platformId } = req.query;
const { search, platform } = req.query;
axios.get(`${igdbUrl}/games/?search=${searchText}&fields=${igdbFields}&filter[platforms][eq]=${platformId}&limit=20&order=popularity:desc`)
const data = `search "${search}"; fields id,name,slug,rating,name,cover.image_id; where platforms = (${platform});`
axios({
url: 'https://api-v3.igdb.com/games',
method: 'POST',
headers: {
'Accept': 'application/json',
},
data,
})
.then(({ data }) => { res.status(200).send(data) })
.catch(() => { res.send(400) });
.catch((error) => { res.send(error) });
});
exports.games = functions.https.onRequest((req, res) => {
@ -24,9 +27,22 @@ exports.games = functions.https.onRequest((req, res) => {
const { games } = req.query;
axios.get(`${igdbUrl}/games/${games}/?fields=${igdbFieldsMinimal}`)
if (!games) {
res.status(400).send('missing param games');
}
const data = `fields id,name,slug,rating,name,cover.image_id; where id = (${ games });`;
axios({
url: 'https://api-v3.igdb.com/games',
method: 'POST',
headers: {
'Accept': 'application/json',
},
data,
})
.then(({ data }) => { res.status(200).send(data) })
.catch(() => { res.send(400) });
.catch((error) => { res.send(error) });
});
exports.game = functions.https.onRequest((req, res) => {
@ -34,9 +50,43 @@ exports.game = functions.https.onRequest((req, res) => {
const { gameId } = req.query;
axios.get(`${igdbUrl}/games/${gameId}/?fields=${igdbFields}`)
if (!gameId) {
res.status(400).send('missing param gameId');
}
const data = `fields
name,
summary,
cover.image_id,
screenshots.image_id,
player_perspectives.name,
involved_companies.company.name,
involved_companies.developer,
involved_companies.publisher,
release_dates.platform,
release_dates.date,
websites.category,
websites.url,
age_ratings.*,
videos.video_id,
rating,
genres.name,
platforms.name,
game_modes.name,
time_to_beat;
where id = ${ gameId };`;
axios({
url: 'https://api-v3.igdb.com/games',
method: 'POST',
headers: {
'Accept': 'application/json',
},
data,
})
.then(({ data }) => { res.status(200).send(data) })
.catch(() => { res.send(400) });
.catch((error) => { res.send(error) });
});
exports.email = functions.https.onRequest((req, res) => {
@ -66,76 +116,5 @@ exports.email = functions.https.onRequest((req, res) => {
data,
})
.then(({ data }) => { res.status(200).send(data) })
.catch(() => { res.send(400) });
.catch((error) => { res.send(error) });
});
exports.searchV2 = functions.https.onRequest((req, res) => {
res.set('Access-Control-Allow-Origin', "*")
const { search, platform } = req.query;
const data = `search "${search}"; fields id,name,slug,rating,name,cover.image_id; where platforms = (${platform});`
axios({
url: 'https://api-v3.igdb.com/games',
method: 'POST',
headers: {
'Accept': 'application/json',
'user-key': '3b516a46c3af209bb6e287e9090d720c'
},
data,
})
.then(({ data }) => { res.status(200).send(data) })
.catch(() => { res.send(400) });
});
exports.gameV2 = functions.https.onRequest((req, res) => {
res.set('Access-Control-Allow-Origin', "*")
const { gameId } = req.query;
if (!gameId) {
res.status(400).send('missing param gameId');
}
const data = `fields *; where id = ${ gameId };`;
axios({
url: 'https://api-v3.igdb.com/games',
method: 'POST',
headers: {
'Accept': 'application/json',
'user-key': '3b516a46c3af209bb6e287e9090d720c'
},
data,
})
.then(({ data }) => { res.status(200).send(data) })
.catch(() => { res.send(400) });
});
exports.gamesV2 = functions.https.onRequest((req, res) => {
res.set('Access-Control-Allow-Origin', "*")
const { gameId } = req.query;
if (!gameId) {
res.status(400).send('missing param gameId');
}
const data = `fields id,name,slug,rating,name,cover.url; where id = (${ gameId });`;
axios({
url: 'https://api-v3.igdb.com/games',
method: 'POST',
headers: {
'Accept': 'application/json',
'user-key': '3b516a46c3af209bb6e287e9090d720c'
},
data,
})
.then(({ data }) => { res.status(200).send(data) })
.catch(() => { res.send(400) });
});
// TODO: merge search and game, swap out or request fields in FE

View file

@ -59,10 +59,10 @@ export default {
},
coverUrl() {
const url = 'https://images.igdb.com/igdb/image/upload/t_cover_small_2x/';
const game = this.games[this.gameId];
return this.games && this.games[this.gameId].cover
? `${url}${this.games[this.gameId].cover.cloudinary_id}.jpg`
return game.cover && game.cover.image_id
? `https://images.igdb.com/igdb/image/upload/t_cover_small_2x/${game.cover.image_id}.jpg`
: '/static/no-image.jpg';
},

View file

@ -41,10 +41,10 @@ export default {
},
coverUrl() {
const url = 'https://images.igdb.com/igdb/image/upload/t_cover_small_2x/';
const game = this.games[this.id];
return this.games && this.games[this.id].cover
? `${url}${this.games[this.id].cover.cloudinary_id}.jpg`
return game.cover && game.cover.image_id
? `https://images.igdb.com/igdb/image/upload/t_cover_small_2x/${game.cover.image_id}.jpg`
: '/static/no-image.jpg';
},
},

View file

@ -2,46 +2,32 @@
<div :class="['game-header', { dark: darkModeEnabled}]">
<img :src="coverUrl" :alt="game.name" class="game-cover" />
<div class="game-rating" v-if="hasRatings">
<div class="game-rating" v-if="game.age_ratings">
<img
v-if="game.esrb"
:src='`/static/img/esrb/${igdb.esrb[game.esrb.rating]}.png`'
:alt="game.esrb.synopsis"
>
<img
v-if="game.pegi"
:src='`/static/img/pegi/${igdb.pegi[game.pegi.rating]}.png`'
:alt="game.pegi.synopsis"
>
v-for="{ rating, synopsis, id } in game.age_ratings"
:key="id"
:src='`/static/img/ageRatings/${ageRatings[rating]}.png`'
:alt="synopsis"
/>
</div>
</div>
</template>
<script>
import igdb from '@/shared/igdb';
import { mapState, mapGetters } from 'vuex';
export default {
props: {
gameId: [Number, String],
},
data() {
return {
igdb,
};
},
computed: {
...mapState(['game', 'platform']),
...mapGetters(['darkModeEnabled']),
...mapGetters(['darkModeEnabled', 'ageRatings']),
coverUrl() {
const url = 'https://images.igdb.com/igdb/image/upload/t_cover_big/';
return this.game && this.game.cover
? `${url}${this.game.cover.cloudinary_id}.jpg`
? `https://images.igdb.com/igdb/image/upload/t_cover_small_2x/${this.game.cover.image_id}.jpg`
: '/static/no-image.jpg';
},
@ -50,10 +36,6 @@ export default {
? `background: url(${this.getImageUrl(this.game.screenshots[0].cloudinary_id)}) center center no-repeat; background-size: cover;`
: '';
},
hasRatings() {
return this.game.esrb || this.game.pegi;
},
},
methods: {
@ -96,6 +78,8 @@ export default {
.game-rating {
margin-top: $gp;
display: grid;
grid-template-columns: auto auto;
img {
height: 60px;

View file

@ -1,6 +1,7 @@
<template lang="html">
<div v-if="game" class="review-box">
<div class="info">
<!-- TODO: get icons for everything -->
<section v-if="playerPerspectives">
<strong>Perspective</strong> {{ playerPerspectives }}
</section>
@ -14,6 +15,8 @@
</section>
<section v-if="gamePlatforms">
<!-- TODO: show current platform icon,
and also show "available on these other platforms" -->
<strong>{{ $t('gameDetail.gamePlatforms') }}</strong> {{ gamePlatforms }}
</section>
@ -25,15 +28,17 @@
<strong>{{ $t('gameDetail.publishers') }}</strong> {{ publishers }}
</section>
<!-- <section v-if="releaseDate">
<strong>Release date</strong> {{ releaseDate }}
</section> -->
<!-- <game-links /> -->
<section v-if="releaseDate">
<strong>Release date</strong> {{ moment.unix(releaseDate).format('ll') }}
</section>
<game-links />
</div>
</div>
</template>
<script>
import moment from 'moment';
import GameLinks from '@/components/GameDetail/GameLinks';
import { mapState, mapGetters } from 'vuex';
@ -42,6 +47,12 @@ export default {
GameLinks,
},
data() {
return {
moment,
};
},
computed: {
...mapState([
'game',
@ -53,6 +64,7 @@ export default {
'developers',
'gameModes',
'gamePlatforms',
'releaseDate',
'genres',
'publishers',
]),

View file

@ -43,20 +43,26 @@ export default {
// eslint-disable-next-line
return this.game.screenshots
? this.game.screenshots.map((image, index) => {
const url = 'https://images.igdb.com/igdb/image/upload/t_screenshot_huge/';
const href = `https://images.igdb.com/igdb/image/upload/t_screenshot_huge/${image.image_id}.jpg`;
return {
href: `${url}${image.cloudinary_id}.jpg`,
href,
title: `${this.game.name} (${index + 1} of ${this.game.screenshots.length})`,
};
})
: null;
},
coverUrl() {
return this.game && this.game.cover
? `https://images.igdb.com/igdb/image/upload/t_cover_small_2x/${this.game.cover.image_id}.jpg`
: '/static/no-image.jpg';
},
thumbnails() {
// eslint-disable-next-line
return this.game.screenshots ? this.game.screenshots.map((image) => {
return `https://images.igdb.com/igdb/image/upload/t_thumb/${image.cloudinary_id}.jpg`;
return `https://images.igdb.com/igdb/image/upload/t_thumb/${image.image_id}.jpg`;
}) : null;
},
},

View file

@ -34,7 +34,10 @@
@update="updateLists"
/>
<div :class="`game-grid game-grid-${listIndex}`" v-if="view === 'grid' && !editing && !searching">
<div
:class="`game-grid game-grid-${listIndex}`"
v-if="view === 'grid' && !editing && !searching"
>
<component
v-for="game in games"
:is="gameCardComponent"
@ -158,7 +161,7 @@ export default {
},
gameCardComponent() {
return this.view
return this.view && Object.keys(this.gameCardComponents).includes(this.view)
? this.gameCardComponents[this.view]
: 'GameCardDefault';
},

View file

@ -82,6 +82,7 @@ import Panel from '@/components/Panel/Panel';
import GameDetail from '@/pages/GameDetail';
import Modal from '@/components/Modal/Modal';
// import DevDebug from '@/components/DevDebug/DevDebug';
import { chunk } from 'lodash';
import List from '@/components/Lists/List';
import draggable from 'vuedraggable';
import { mapState, mapGetters } from 'vuex';
@ -214,6 +215,7 @@ export default {
if (this.list) {
const gameList = [];
// TODO: refactor this to use reduce or map
this.list.forEach((list) => {
if (list && list.games.length) {
list.games.forEach((id) => {
@ -225,15 +227,19 @@ export default {
});
if (gameList.length > 0) {
this.loading = true;
const chunkedGameList = chunk(gameList, 10);
this.$store.dispatch('LOAD_GAMES', gameList)
.then(() => {
this.loading = false;
})
.catch(() => {
this.$bus.$emit('TOAST', { message: 'Error loading game', type: 'error' });
});
chunkedGameList.forEach((partialGameList) => {
this.loading = true;
this.$store.dispatch('LOAD_GAMES', partialGameList.toString())
.then(() => {
this.loading = false;
})
.catch(() => {
this.$bus.$emit('TOAST', { message: 'Error loading game', type: 'error' });
});
});
}
}
},

View file

@ -1,8 +1,7 @@
<template lang="html">
<div class="game-detail-wrapper">
<game-detail-placeholder v-if="!game" :id="id" />
<div class="game-detail" v-else :class="{ dark: darkModeEnabled }">
<div class="game-detail" v-if="game" :class="{ dark: darkModeEnabled }">
<div class="game-hero" :style="style" />
<div class="game-detail-container">
@ -48,10 +47,12 @@
<p class="game-description" v-html="game.summary" />
<game-notes />
<!-- Time to beat
{{ new Date(game.time_to_beat * 1000)
.toISOString().substr(11, 8) }} -->
<game-review-box />
</div>
</div>
<game-screenshots />
@ -59,6 +60,8 @@
<igdb-credit gray />
</div>
</div>
<game-detail-placeholder v-else :id="id" />
</div>
</template>
@ -105,9 +108,15 @@ export default {
},
style() {
return this.game && this.game.screenshots
? `background: url(${this.getImageUrl(this.game.screenshots[0].cloudinary_id)}) center center no-repeat; background-size: cover;`
const screenshot = this.game.screenshots && this.game.screenshots.length > 0
? this.game.screenshots[0]
: null;
const screenshotUrl = screenshot && screenshot.image_id
? `https://images.igdb.com/igdb/image/upload/t_screenshot_big/${screenshot.image_id}.jpg`
: '';
return `background: url('${screenshotUrl}') center center no-repeat; background-size: cover;`;
},
activePlatform() {
@ -119,10 +128,8 @@ export default {
},
coverUrl() {
const url = 'https://images.igdb.com/igdb/image/upload/t_cover_small_2x/';
return this.game && this.game.cover
? `${url}${this.game.cover.cloudinary_id}.jpg`
return this.game.cover && this.game.cover.image_id
? `https://images.igdb.com/igdb/image/upload/t_cover_small_2x/${this.game.cover.image_id}.jpg`
: '/static/no-image.jpg';
},
},
@ -132,12 +139,6 @@ export default {
},
methods: {
getImageUrl(cloudinaryId) {
return cloudinaryId
? `https://images.igdb.com/igdb/image/upload/t_screenshot_huge/${cloudinaryId}.jpg`
: null;
},
removeTag(tagName) {
this.$store.commit('REMOVE_GAME_TAG', { tagName, gameId: this.game.id });
this.$bus.$emit('SAVE_TAGS', this.tags);

View file

@ -1,20 +1,4 @@
export default {
pegi: {
1: '3',
2: '7',
3: '12',
4: '16',
5: '18',
},
esrb: {
1: 'RP',
2: 'EC',
3: 'E',
4: 'E10+',
5: 'T',
6: 'M',
7: 'AO',
},
linkTypes: {
1: 'official',
2: 'wikia',

View file

@ -192,7 +192,7 @@ export default [
{
name: 'Nintendo 3DS',
code: 'n3ds',
id: 37,
id: '37,137',
generation: 8,
},
{

View file

@ -45,32 +45,11 @@ export default {
SEARCH({ commit, state }, searchText) {
return new Promise((resolve, reject) => {
axios.get(`${FIREBASE_URL}/search?searchText=${searchText}&platformId=${state.platform.id}`)
axios.get(`${FIREBASE_URL}/search?search=${searchText}&platform=${state.platform.id}`)
.then(({ data }) => {
const originalData = data.slice();
if (state.platform.id === 37) {
// TODO: specify platform ids in platforms file,
// let endpoint handle multiple ids
axios.get(`${FIREBASE_URL}/search?searchText=${searchText}&platformId=137`)
.then((response) => {
const joinedData = [
...originalData,
...response.data,
];
const ids = [...new Set(joinedData.map(({ id }) => id))];
const unique = ids.map(e => joinedData.find(({ id }) => id === e));
commit('SET_SEARCH_RESULTS', unique);
commit('CACHE_GAME_DATA', unique);
resolve();
}).catch(reject);
} else {
commit('SET_SEARCH_RESULTS', data);
commit('CACHE_GAME_DATA', data);
resolve();
}
commit('SET_SEARCH_RESULTS', data);
commit('CACHE_GAME_DATA', data);
resolve();
}).catch(reject);
});
},

View file

@ -1,17 +1,44 @@
export default {
// eslint-disable-next-line
ageRatings: () => {
return {
1: '3',
2: '7',
3: '12',
4: '16',
5: '18',
6: 'RP',
7: 'EC',
8: 'E',
9: 'E10',
10: 'T',
11: 'M',
12: 'AO',
};
},
releaseDate: (state) => {
// eslint-disable-next-line
const releaseDate = state.game.release_dates.filter(({ platform }) => state.platform.id === platform);
return releaseDate && releaseDate.length
? releaseDate[0].date
: null;
},
developers: (state) => {
const developers = state.game.developers;
const developers = state.game.involved_companies.filter(({ developer }) => developer);
return developers
? developers.map(developer => developer.name).join(', ')
? developers.map(publisher => publisher.company.name).join(', ')
: null;
},
publishers: (state) => {
const publishers = state.game.publishers;
const publishers = state.game.involved_companies.filter(({ publisher }) => publisher);
return publishers
? publishers.map(publisher => publisher.name).join(', ')
? publishers.map(publisher => publisher.company.name).join(', ')
: null;
},

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB