Convert site to use Vuex to manage its state

This puts all the SplatNet data and other parameters in a centralized store (rather than passing down everything from a top-level component via props).
This commit is contained in:
Matt Isenhower
2018-08-25 14:18:08 -07:00
parent 0088442e7a
commit d2c1d2b527
53 changed files with 924 additions and 699 deletions

View File

@@ -53,39 +53,39 @@ async function captureScreenshot(options) {
return result;
}
function captureScheduleScreenshot(startTime) {
let hash = `/schedules/${startTime}`;
function captureScheduleScreenshot(now) {
let hash = `/schedules/${now}`;
return captureScreenshot({ hash });
}
function captureGearScreenshot(startTime, endTime) {
let hash = `/splatNetGear/${startTime}/${endTime}`;
function captureGearScreenshot(now) {
let hash = `/splatNetGear/${now}`;
return captureScreenshot({ hash, viewport: { height: 700 } });
}
function captureSalmonRunScreenshot(startTime) {
let hash = `/salmonRun/${startTime}`;
function captureSalmonRunScreenshot(now) {
let hash = `/salmonRun/${now}`;
return captureScreenshot({ hash });
}
function captureSalmonRunGearScreenshot(startTime) {
let hash = `/salmonRunGear/${startTime}`;
function captureSalmonRunGearScreenshot(now) {
let hash = `/salmonRunGear/${now}`;
return captureScreenshot({ hash });
}
function captureNewWeaponScreenshot(releaseTime) {
let hash = `/newWeapon/${releaseTime}`;
function captureNewWeaponScreenshot(now) {
let hash = `/newWeapon/${now}`;
return captureScreenshot({ hash });
}
function captureSplatfestScreenshot(region, startTime, regions) {
function captureSplatfestScreenshot(region, now, regions) {
regions = regions.join(',');
let hash = `/splatfest/${region}/${startTime}?regions=${regions}`;
let hash = `/splatfest/${region}/${now}?regions=${regions}`;
return captureScreenshot({ hash });
}

View File

@@ -27,9 +27,8 @@ class GearTweet extends TwitterPostBase {
}
getImage(data) {
let startTime = getTopOfCurrentHour();
let endTime = this.getDataTime();
return captureGearScreenshot(startTime, endTime);
let now = getTopOfCurrentHour();
return captureGearScreenshot(now);
}
getText(data) {

View File

@@ -36,7 +36,7 @@
<!-- Empty spacer to add some room when festivals are active -->
</div>
<div class="level-item" v-if="merchandises && merchandises.length">
<div class="level-item" v-if="hasMerchandises">
<router-link to="/splatnet" class="button is-translucent-dark is-rounded">
<span class="font-splatoon2">{{ $t('splatnet_gear.button') }}</span>
</router-link>
@@ -58,73 +58,73 @@
<div v-else>
<!-- Active Splatfest: Show the Splatfest box and the Splatfest Battle box -->
<div v-if="isSelectedFestivalActive" class="columns is-desktop limited-width">
<div v-if="selectedRegionHasActiveSplatfest" class="columns is-desktop limited-width">
<div class="column">
<div class="splatfest tilt-left">
<div class="hook-box">
<SplatfestBox :festival="selectedFestival" :now="now"></SplatfestBox>
<SplatfestBox :festival="selectedRegionCurrentSplatfest" />
</div>
</div>
</div>
<div class="column">
<ScheduleBox class="main-schedule-box splatfest-schedule-box tilt-right" cssClass="regular" :schedules="regular" :festival="selectedFestival" :now="now"></ScheduleBox>
<ScheduleBox class="main-schedule-box splatfest-schedule-box tilt-right" mode="regular" />
</div>
</div>
<!-- No active Splatfest: Show the Regular, Ranked, and League Battle boxes -->
<div v-else class="columns is-desktop">
<div class="column">
<ScheduleBox class="main-schedule-box tilt-left" cssClass="regular" :schedules="regular" :now="now"></ScheduleBox>
<ScheduleBox class="main-schedule-box tilt-left" mode="regular" />
</div>
<div class="column">
<ScheduleBox class="main-schedule-box tilt-right" cssClass="ranked" :schedules="ranked" :now="now"></ScheduleBox>
<ScheduleBox class="main-schedule-box tilt-right" mode="ranked" />
</div>
<div class="column">
<ScheduleBox class="main-schedule-box tilt-left" cssClass="league" :schedules="league" :now="now"></ScheduleBox>
<ScheduleBox class="main-schedule-box tilt-left" mode="league" />
</div>
</div>
<!-- Upcoming Splatfest and Salmon Run boxes -->
<div class="columns is-desktop" :class="{'limited-width': !bottomRowHasThreeColumns}">
<div class="column" v-if="selectedFestival && !isSelectedFestivalActive">
<div class="column" v-if="selectedRegionCurrentSplatfest && !selectedRegionHasActiveSplatfest">
<div class="splatfest tilt-right" style="margin-top: 40px">
<div class="hook-box">
<SplatfestBox :festival="selectedFestival" :results="selectedFestivalResults" :now="now"></SplatfestBox>
<SplatfestBox :festival="selectedRegionCurrentSplatfest" />
</div>
</div>
</div>
<div class="column" v-if="coopSchedules">
<div class="column" v-if="hasSalmonRun">
<div class="salmon-run tilt-left">
<div class="hook-box">
<SalmonRunBox :coop="coop" :coopSchedules="coopSchedules" :now="now"></SalmonRunBox>
<SalmonRunBox />
</div>
</div>
</div>
<div class="column is-narrow-desktop-only" :class="{'is-hidden-touch is-hidden-desktop-only is-hidden-widescreen-only': bottomRowHasThreeColumns }" style="margin-top: 40px" v-if="newWeapons">
<div class="column is-narrow-desktop-only" :class="{'is-hidden-touch is-hidden-desktop-only is-hidden-widescreen-only': bottomRowHasThreeColumns }" style="margin-top: 40px" v-if="hasWeapons">
<div class="new-weapon">
<div class="columns">
<div class="column" v-for="(weapon, index) in newWeapons">
<div class="column" v-for="(weapon, index) in weapons" :key="weapon.id">
<NewWeaponBox
:weapon="weapon"
:class="(index % 2 == 0) ? 'tilt-right' : 'tilt-left'"
></NewWeaponBox>
/>
</div>
</div>
</div>
</div>
</div>
<div class="is-hidden-fullhd" style="margin-top: 20px" v-if="newWeapons && bottomRowHasThreeColumns">
<div class="is-hidden-fullhd" style="margin-top: 20px" v-if="hasWeapons && bottomRowHasThreeColumns">
<div class="new-weapon">
<div style="display: flex; align-items: center; justify-content: center;">
<div v-for="(weapon, index) in newWeapons" style="margin: 0 20px">
<div v-for="(weapon, index) in weapons" style="margin: 0 20px" :key="weapon.id">
<NewWeaponBox
:weapon="weapon"
:class="(index % 2 == 0) ? 'tilt-right' : 'tilt-left'"
style="min-width: 250px"
></NewWeaponBox>
/>
</div>
</div>
</div>
@@ -143,7 +143,7 @@
<router-link to="/calendars">Calendar Feeds</router-link>
&ndash;
<a href="https://twitter.com/Splatoon2inkbot" target="_blank">Twitter</a>
<template v-if="splatnet.festivals">
<template v-if="hasSplatfests">
&ndash;
<router-link to="/splatfests">{{ $t('splatfest.previous') }}</router-link>
</template>
@@ -153,20 +153,6 @@
</div>
<router-view @close="$router.push('/')" />
<SplatNetGearDialog
v-if="splatNetGearOpen"
:merchandises="merchandises"
:now="now"
@close="$router.push('/')"
></SplatNetGearDialog>
<SplatfestHistoryDialog
v-if="splatfestHistoryOpen"
:allFestivals="splatnet.festivals"
:now="now"
:initialRegion="selectedRegionKey"
@close="$router.push('/')"
/>
</div>
</template>
@@ -179,62 +165,60 @@
</style>
<script>
import axios from 'axios';
import { mapActions } from 'vuex';
import * as regions from '@/common/regions.esm';
import { mapGetters, mapActions } from 'vuex';
import Dropdown from './Dropdown.vue';
import ScheduleBox from './splatoon/ScheduleBox.vue';
import SalmonRunBox from './splatoon/SalmonRunBox.vue';
import SplatfestBox from './splatoon/SplatfestBox.vue';
import NewWeaponBox from './splatoon/NewWeaponBox.vue';
import AboutDialog from './AboutDialog.vue';
import SplatNetGearDialog from './splatoon/SplatNetGearDialog.vue';
import SplatfestHistoryDialog from './splatoon/SplatfestHistoryDialog.vue';
const localStorage = window.localStorage;
export default {
components: { Dropdown, ScheduleBox, SalmonRunBox, SplatfestBox, NewWeaponBox, AboutDialog, SplatNetGearDialog, SplatfestHistoryDialog },
data() {
return {
actualSelectedRegionKey: null,
actualSelectedLanguageKey: null,
now: null,
splatnet: {
schedules: null,
coopSchedules: null,
timeline: null,
merchandises: null,
festivals: null,
},
language: null,
};
},
watch: {
language(value) {
if (value)
this.loadLocale(value);
this.$i18n.set(value || 'en');
},
},
components: { Dropdown, ScheduleBox, SalmonRunBox, SplatfestBox, NewWeaponBox },
computed: {
loading() { return !this.splatnet.schedules; },
...mapGetters('splatoon/regions', ['selectedRegion']),
...mapGetters('splatoon/languages', ['selectedLanguage']),
...mapGetters('splatoon/splatfests', [
'anyRegionHasCurrentSplatfest',
'selectedRegionCurrentSplatfest',
'selectedRegionHasActiveSplatfest',
]),
...mapGetters('splatoon/newWeapons', ['weapons']),
loading() { return !this.$store.state.splatoon.data.schedules },
hasMerchandises() {
let merchandises = this.$store.getters['splatoon/splatNetStore/merchandises'];
return merchandises && merchandises.length;
},
hasSplatfests() {
return this.$store.state.splatoon.data.festivals;
},
hasSalmonRun() {
let schedules = this.$store.getters['splatoon/salmonRun/currentSchedules'];
return schedules && schedules.length;
},
hasWeapons() {
return this.weapons && this.weapons.length;
},
// Selected region
regions() {
return regions.splatoonRegions.map(({ key }) => {
return this.$store.state.splatoon.regions.all.map(({ key }) => {
let name = (key) ? this.$t(`regions.${key}.name`) : this.$t('regions.global.name');
return { key, name };
});
},
selectedRegionKey: {
get() { return this.actualSelectedRegionKey; },
set(value) { this.setRegion(value); },
get() { return this.selectedRegion && this.selectedRegion.key; },
set(value) { this.setRegion({ region: { key: value } }); },
},
// Selected langauge
languages() {
return regions.languages
return this.$store.state.splatoon.languages.all
.reduce((languages, languageInfo) => {
// Remove duplicates
if (!languages.find(li => li.language == languageInfo.language))
@@ -244,205 +228,38 @@ export default {
.map(({ language, name }) => ({ key: language, name }));
},
selectedLanguageKey: {
get() { return this.language; },
set(value) { this.setLanguage(value); },
get() { return this.selectedLanguage && this.selectedLanguage.language; },
set(value) { this.setLanguage({ language: { language: value } }); },
},
// Routes (temporary)
splatNetGearOpen() {
return this.$route.path === '/splatnet';
},
splatfestHistoryOpen() {
return this.$route.path === '/splatfests';
},
// Normal battles
regular() { return !this.loading && this.splatnet.schedules.regular.filter(this.filterEndTime) },
ranked() { return !this.loading && this.splatnet.schedules.gachi.filter(this.filterEndTime) },
league() { return !this.loading && this.splatnet.schedules.league.filter(this.filterEndTime) },
// Festivals
festivals() {
if (this.splatnet.festivals) {
return {
na: (this.splatnet.festivals.na.festivals || []).filter(this.filterFestivals),
eu: (this.splatnet.festivals.eu.festivals || []).filter(this.filterFestivals),
jp: (this.splatnet.festivals.jp.festivals || []).filter(this.filterFestivals),
};
}
},
showFestivalRegionDropdown() {
if (this.festivals)
return Object.values(this.festivals).some(arr => arr.length > 0);
},
selectedFestival() {
if (this.festivals && this.selectedRegionKey)
return this.festivals[this.selectedRegionKey][0];
},
selectedFestivalResults() {
if (this.selectedFestival)
return this.splatnet.festivals[this.selectedRegionKey].results.find(r => r.festival_id == this.selectedFestival.festival_id);
},
isSelectedFestivalActive() {
return this.selectedFestival && this.selectedFestival.times.start <= this.now && this.selectedFestival.times.end > this.now;
},
// Salmon Run
coop() {
if (this.splatnet.timeline && this.splatnet.timeline.coop && this.splatnet.timeline.coop.schedule.end_time > this.now)
return this.splatnet.timeline.coop;
},
coopSchedules() {
if (this.splatnet.coopSchedules) {
let details = this.splatnet.coopSchedules.details.filter(this.filterEndTime);
let schedules = this.splatnet.coopSchedules.schedules.filter(this.filterEndTime);
return { details, schedules };
}
},
// New weapons
newWeapons() {
if (this.splatnet.timeline && this.splatnet.timeline.weapon_availability && this.splatnet.timeline.weapon_availability.availabilities) {
let weapons = this.splatnet.timeline.weapon_availability.availabilities
.filter(a => a.release_time <= this.now)
.map(a => a.weapon);
if (weapons.length > 0)
return weapons;
}
},
// SplatNet Merchandise
merchandises() {
if (this.splatnet.merchandises && this.splatnet.merchandises.merchandises)
return this.splatnet.merchandises.merchandises.filter(this.filterEndTime);
return this.anyRegionHasCurrentSplatfest;
},
// Other
bottomRowHasThreeColumns() {
return this.selectedFestival && !this.isSelectedFestivalActive && (this.coop || this.coopSchedules) && this.newWeapons;
return this.selectedRegionCurrentSplatfest
&& !this.selectedRegionHasActiveSplatfest
&& this.hasSalmonRun
&& this.hasWeapons;
},
},
created() {
this.loadLanguage();
this.loadRegion(true);
window.addEventListener('storage', this.loadLanguage);
window.addEventListener('storage', this.loadRegion);
this.updateNow();
this.updateNowTimer = setInterval(() => {
this.updateNow();
}, 200);
// Periodically retrieve updated data
this.scheduleUpdateData();
this.updateData();
this.initialize();
this.startUpdatingNow();
this.startUpdatingData();
},
beforeDestroy() {
clearInterval(this.updateNowTimer);
clearInterval(this.updateDataTimer);
window.removeEventListener('storage', this.loadLanguage);
window.removeEventListener('storage', this.loadRegion);
this.stopUpdatingNow();
this.stopUpdatingData();
this.shutdown();
},
methods: {
...mapActions(['loadLocale']),
loadLanguage() {
let language = localStorage.getItem('selected-language');
let languageInfo = regions.languages.find(li => li.language === language);
if (!languageInfo)
language = regions.detectSplatoonLanguage();
this.language = language;
},
setLanguage(key) {
this.language = key;
localStorage.setItem('selected-language', key);
this.updateLanguageData();
},
loadRegion(autoDetect = false) {
// Get the previously-selected region from local storage
let key = localStorage.getItem('selected-region');
if (key !== null) {
let region = regions.getRegionByKey(key);
this.actualSelectedRegionKey = (region) ? region.key : null;
return;
}
// If no region was previously selected, attempt to detect the region by the browser's language
if (autoDetect)
this.actualSelectedRegionKey = regions.detectSplatoonRegion();
},
setRegion(key) {
let region = regions.getRegionByKey(key);
key = (region) ? region.key : null;
this.actualSelectedRegionKey = key;
localStorage.setItem('selected-region', key);
},
scheduleUpdateData() {
let date = new Date;
// If we're more than 20 seconds past the current hour, schedule the update for the next hour
if (date.getMinutes() !== 0 || date.getSeconds() >= 20)
date.setHours(date.getHours() + 1);
date.setMinutes(0);
// Random number of seconds past the hour (so all open browsers don't hit the server at the same time)
let minSec = 25;
let maxSec = 60;
date.setSeconds(Math.floor(Math.random() * (maxSec - minSec + 1)) + minSec);
// Set the timeout
this.updateDataTimer = setTimeout(() => {
this.updateData();
this.scheduleUpdateData();
}, (date - new Date));
},
updateLanguageData() {
if (this.language) {
axios.get(`/data/locale/${this.language}.json`)
.then(response => this.$i18n.add(this.language, { splatnet: response.data }))
.catch(e => console.error(e));
}
},
updateData() {
this.updateLanguageData();
// Main map schedules
axios.get('/data/schedules.json')
.then(response => this.splatnet.schedules = response.data)
.catch(e => console.error(e));
// Co-op schedules
axios.get('/data/coop-schedules.json')
.then(response => this.splatnet.coopSchedules = response.data)
.catch(e => console.error(e));
// Timeline data (Salmon Run weapon of the month and new weapon availability)
axios.get('/data/timeline.json')
.then(response => this.splatnet.timeline = response.data)
.catch(e => console.error(e));
// Splatfest data
axios.get('/data/festivals.json')
.then(response => this.splatnet.festivals = response.data)
.catch(e => console.error(e));
// Splatnet merchandise data
axios.get('/data/merchandises.json')
.then(response => this.splatnet.merchandises = response.data)
.catch(e => console.error(e));
},
updateNow() {
this.now = Math.trunc((new Date).getTime() / 1000);
},
filterEndTime(item) {
return item.end_time > this.now;
},
filterFestivals(item) {
// We want to show future/current festivals and festivals that ended recently (so we can show their results)
// Filter based on the result time and show festival results for 2 days
let cutoff = item.times.result + 86400 * 3; // 3 days
return cutoff > this.now;
},
...mapActions('splatoon', ['startUpdatingNow', 'stopUpdatingNow']),
...mapActions('splatoon/settings', ['initialize', 'shutdown']),
...mapActions('splatoon/data', ['startUpdatingData', 'stopUpdatingData']),
...mapActions('splatoon/regions', ['setRegion']),
...mapActions('splatoon/languages', ['setLanguage']),
},
}
</script>

View File

@@ -3,3 +3,24 @@
<router-view></router-view>
</div>
</template>
<script>
import { mapActions } from 'vuex';
export default {
watch: {
'$route': {
handler: 'loadData',
immediate: true,
},
},
methods: {
...mapActions('splatoon', ['setNow']),
...mapActions('splatoon/data', ['updateAll']),
loadData() {
this.setNow({ now: this.$route.params.now || 0 });
this.updateAll();
},
},
};
</script>

View File

@@ -12,7 +12,7 @@
<div v-if="latestGear">
<strong>
<router-link :to="`/splatNetGear/${topOfHour}/${latestGear.end_time}`">
<router-link :to="`/splatNetGear/${latestGearTime}`">
SplatNet Gear
</router-link>
</strong>
@@ -48,7 +48,7 @@
<router-link
v-for="splatfest in splatfests"
:key="splatfest.region"
:to="`/splatfest/${splatfest.region}/${splatfest.festival.times.start}`">
:to="`/splatfest/${splatfest.region}/${splatfest.time}`">
{{ splatfest.region }}
</router-link>
</strong>
@@ -59,72 +59,40 @@
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
schedules: null,
coopSchedules: null,
merchandises: null,
timeline: null,
festivals: null,
};
},
computed: {
topOfHour() {
let date = new Date;
date.setUTCMinutes(0);
date.setUTCSeconds(0);
return Math.floor(date.getTime() / 1000);
},
latestSchedule() {
if (this.schedules)
return this.schedules.regular[0];
return this.$store.state.splatoon.data.schedules
&& this.$store.state.splatoon.data.schedules.regular[0];
},
latestGear() {
if (this.merchandises)
return this.merchandises[this.merchandises.length - 1];
return this.$store.state.splatoon.data.merchandises
&& this.$store.state.splatoon.data.merchandises.merchandises[0];
},
latestGearTime() {
return this.latestGear && this.latestGear.end_time - (2 * 60 * 60); // 2 hours
},
latestSalmonRun() {
if (this.coopSchedules)
return this.coopSchedules.details[0];
return this.$store.state.splatoon.data.coop_schedules
&& this.$store.state.splatoon.data.coop_schedules.schedules[0];
},
salmonRunGear() {
if (this.timeline)
return this.timeline.coop.reward_gear;
return this.$store.state.splatoon.data.timeline
&& this.$store.state.splatoon.data.timeline.coop.reward_gear;
},
newWeaponAvailability() {
if (this.timeline && this.timeline.weapon_availability && this.timeline.weapon_availability.availabilities) {
return this.timeline.weapon_availability.availabilities[0];
}
return this.$store.state.splatoon.data.timeline
&& this.$store.state.splatoon.data.timeline.weapon_availability
&& this.$store.state.splatoon.data.timeline.weapon_availability.availabilities[0];
},
splatfests() {
if (this.festivals) {
let splatfests = [];
for (let region in this.festivals) {
let festival = this.festivals[region].festivals[0];
if (festival)
splatfests.push({ region, festival });
}
if (splatfests.length)
return splatfests;
if (this.$store.state.splatoon.data.festivals) {
return ['na', 'eu', 'jp'].map(region => ({
region,
time: this.$store.state.splatoon.data.festivals[region].festivals[0].times.start,
}));
}
},
},
created() {
axios.get('data/schedules.json')
.then(response => this.schedules = response.data);
axios.get('data/coop-schedules.json')
.then(response => this.coopSchedules = response.data);
axios.get('data/merchandises.json')
.then(response => this.merchandises = response.data.merchandises);
axios.get('data/timeline.json')
.then(response => this.timeline = response.data);
axios.get('data/festivals.json')
.then(response => this.festivals = response.data);
},
}
};
</script>

View File

@@ -30,15 +30,15 @@
<div class="font-splatoon2 is-size-7 has-text-grey-light time-table">
<table>
<tr>
<td colspan="3" class="is-size-6 has-text-white">{{ time | dateTime('en', 'UTC', 'MMMM D, YYYY HH:mm z') }}</td>
<td colspan="3" class="is-size-6 has-text-white">{{ now | dateTime('en', 'UTC', 'MMMM D, YYYY HH:mm z') }}</td>
</tr>
<tr>
<td>{{ time | dateTime('en-us', 'America/Los_Angeles') }}</td>
<td>{{ time | dateTime('en-gb', 'Europe/London') }}</td>
<td>{{ now | dateTime('en-us', 'America/Los_Angeles') }}</td>
<td>{{ now | dateTime('en-gb', 'Europe/London') }}</td>
</tr>
<tr>
<td>{{ time | dateTime('en-us', 'America/New_York') }}</td>
<td>{{ time | dateTime('ja-jp', 'Asia/Tokyo') }}</td>
<td>{{ now | dateTime('en-us', 'America/New_York') }}</td>
<td>{{ now | dateTime('ja-jp', 'Asia/Tokyo') }}</td>
</tr>
</table>
</div>
@@ -51,10 +51,11 @@
</template>
<script>
import { mapGetters } from 'vuex';
import moment from 'moment-timezone';
export default {
props: ['title', 'time'],
props: ['title'],
filters: {
dateTime(value, locale, timeZone, format = 'LLL z') {
let date = moment.unix(value).tz(timeZone);
@@ -62,5 +63,8 @@ export default {
return date.format(format);
},
},
}
computed: {
...mapGetters('splatoon', ['now']),
},
};
</script>

View File

@@ -1,13 +1,13 @@
<template>
<Wrapper :title="title" :time="releaseTime">
<div :class="{ 'new-weapons-large': newWeapons.length <= 3 }" v-if="newWeapons">
<Wrapper :title="title">
<div :class="{ 'new-weapons-large': weapons.length <= 3 }" v-if="weapons">
<div style="display: flex; align-items: center; justify-content: center;">
<div v-for="(weapon, index) in newWeapons" style="margin: 0 20px">
<div v-for="(weapon, index) in weapons" style="margin: 0 20px" :key="index">
<NewWeaponBox
:weapon="weapon"
:class="(index % 2 == 0) ? 'tilt-left' : 'tilt-right'"
title="Now Available"
></NewWeaponBox>
/>
</div>
</div>
</div>
@@ -15,38 +15,19 @@
</template>
<script>
import axios from 'axios';
import { mapGetters } from 'vuex';
import Wrapper from '@/web/components/screenshots/Wrapper.vue';
import NewWeaponBox from '@/web/components/splatoon/NewWeaponBox.vue';
export default {
components: { Wrapper, NewWeaponBox },
props: ['releaseTime'],
data() {
return {
timeline: null,
};
},
computed: {
newWeapons() {
if (this.timeline && this.timeline.weapon_availability && this.timeline.weapon_availability.availabilities) {
let weapons = this.timeline.weapon_availability.availabilities
.filter(a => a.release_time == this.releaseTime)
.map(a => a.weapon);
if (weapons.length > 0)
return weapons;
}
},
...mapGetters('splatoon/newWeapons', ['weapons']),
title() {
if (this.newWeapons && this.newWeapons.length == 1)
if (this.weapons && this.weapons.length == 1)
return 'New Weapon';
return 'New Weapons';
},
},
created() {
axios.get('data/timeline.json')
.then(response => this.timeline = response.data)
},
}
};
</script>

View File

@@ -1,54 +1,18 @@
<template>
<Wrapper title="Salmon Run" :time="now">
<Wrapper title="Salmon Run">
<div class="salmon-run tilt-left">
<div class="hook-box">
<SalmonRunBox
v-if="coopSchedules"
:coop="coop"
:coopSchedules="filteredCoopSchedules"
:now="now"
:screenshotMode="true"
></SalmonRunBox>
<SalmonRunBox screenshot-mode />
</div>
</div>
</Wrapper>
</template>
<script>
import axios from 'axios';
import Wrapper from '@/web/components/screenshots/Wrapper.vue';
import SalmonRunBox from '@/web/components/splatoon/SalmonRunBox.vue';
export default {
components: { Wrapper, SalmonRunBox },
props: ['startTime'],
data() {
return {
coopSchedules: null,
timeline: null,
};
},
computed: {
now() {
return this.startTime;
},
coop() {
if (this.timeline && this.timeline.coop && this.timeline.coop.schedule.end_time > this.now)
return this.timeline.coop;
},
filteredCoopSchedules() {
if (this.coopSchedules) {
let details = this.coopSchedules.details.filter(s => s.start_time == this.now);
let schedules = this.coopSchedules.schedules.filter(s => s.start_time == this.now);
return { details, schedules };
}
},
},
created() {
axios.get('data/coop-schedules.json')
.then(response => this.coopSchedules = response.data)
axios.get('data/timeline.json')
.then(response => this.timeline = response.data)
},
}
};
</script>

View File

@@ -1,33 +1,23 @@
<template>
<Wrapper title="Salmon Run Gear" :time="now">
<div class="tilt-left salmon-run-gear" style="display: flex; align-items: center; justify-content: center;" v-if="merchandise">
<Wrapper title="Salmon Run Gear">
<div class="tilt-left salmon-run-gear" style="display: flex; align-items: center; justify-content: center;" v-if="rewardGear">
<MerchandiseBox :merchandise="merchandise" class="salmon-run-gear" />
</div>
</Wrapper>
</template>
<script>
import axios from 'axios';
import { mapGetters } from 'vuex';
import Wrapper from '@/web/components/screenshots/Wrapper.vue';
import MerchandiseBox from '@/web/components/splatoon/MerchandiseBox.vue';
export default {
components: { Wrapper, MerchandiseBox },
props: ['now'],
data() {
return {
timeline: null,
};
},
computed: {
...mapGetters('splatoon/salmonRun', ['rewardGear']),
merchandise() {
if (this.timeline)
return { gear: this.timeline.coop.reward_gear.gear };
return { gear: this.rewardGear };
},
},
created() {
axios.get('data/timeline.json')
.then(response => this.timeline = response.data)
},
}
};
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="schedule">
<div v-if="schedule" :class="mode">
<h3 class="title is-3 font-splatoon1 has-text-centered">
{{ schedule.game_mode.name }}
</h3>
@@ -24,6 +24,11 @@ import Stage from '@/web/components/splatoon/Stage.vue';
export default {
components: { Stage },
props: ['schedule'],
props: ['mode'],
computed: {
schedule() {
return this.$store.getters[`splatoon/schedules/${this.mode}/activeSchedule`];
},
},
}
</script>

View File

@@ -1,51 +1,26 @@
<template>
<Wrapper title="Map Schedules" :time="startTime">
<Wrapper title="Map Schedules">
<div class="columns font-splatoon2">
<div class="column">
<ScheduleBox class="main-schedule-box tilt-left regular" :schedule="regular"></ScheduleBox>
<ScheduleBox class="main-schedule-box tilt-left" mode="regular" />
</div>
<div class="column">
<ScheduleBox class="main-schedule-box tilt-right ranked" :schedule="ranked"></ScheduleBox>
<ScheduleBox class="main-schedule-box tilt-right" mode="ranked" />
</div>
<div class="column">
<ScheduleBox class="main-schedule-box tilt-left league" :schedule="league"></ScheduleBox>
<ScheduleBox class="main-schedule-box tilt-left" mode="league" />
</div>
</div>
</Wrapper>
</template>
<script>
import axios from 'axios';
import Wrapper from '@/web/components/screenshots/Wrapper.vue';
import ScheduleBox from './ScheduleBox.vue';
export default {
components: { Wrapper, ScheduleBox },
props: ['startTime'],
data() {
return {
schedules: null,
};
},
computed: {
regular() {
if (this.schedules)
return this.schedules.regular.find(s => s.start_time == this.startTime);
},
ranked() {
if (this.schedules)
return this.schedules.gachi.find(s => s.start_time == this.startTime);
},
league() {
if (this.schedules)
return this.schedules.league.find(s => s.start_time == this.startTime);
},
},
created() {
axios.get('data/schedules.json')
.then(response => this.schedules = response.data)
},
}
};
</script>

View File

@@ -1,23 +1,22 @@
<template>
<Wrapper :title="title" :time="now">
<Wrapper :title="title" v-if="festival">
<div class="level">
<div class="level-item">
<div class="splatfest tilt-left" v-if="festival">
<div class="splatfest tilt-left">
<div class="hook-box">
<SplatfestBox :festival="festival" :now="now" :results="results" :screenshotMode="true" />
<SplatfestBox :festival="festival" screenshot-mode />
</div>
</div>
</div>
<div class="level-item" v-if="results">
<SplatfestResultsBox :festival="festival" :results="results" />
<div class="level-item" v-if="festival.results">
<SplatfestResultsBox :festival="festival" />
</div>
</div>
</Wrapper>
</template>
<script>
import axios from 'axios';
import Wrapper from '@/web/components/screenshots/Wrapper.vue';
import SplatfestBox from '@/web/components/splatoon/SplatfestBox.vue';
import SplatfestResultsBox from '@/web/components/splatoon/SplatfestResultsBox.vue';
@@ -25,15 +24,10 @@ import { splatoonRegions } from '@/common/regions.esm';
export default {
components: { Wrapper, SplatfestBox, SplatfestResultsBox },
props: ['region', 'startTime'],
data() {
return {
festivals: null,
};
},
props: ['region'],
computed: {
now() {
return this.startTime;
festival() {
return this.$store.getters[`splatoon/splatfests/${this.region}/currentSplatfest`];
},
regions() {
let regions = this.$route.query.regions;
@@ -52,23 +46,12 @@ export default {
title() {
return `${this.displayRegions} Splatfest`;
},
festival() {
if (this.festivals)
return this.festivals[this.region].festivals.find(f => f.times.announce <= this.now && f.times.result >= this.now);
},
results() {
if (this.festival)
return this.festivals[this.region].results.find(r => r.festival_id == this.festival.festival_id);
},
},
created() {
if (this.region == 'jp') {
if (this.region === 'jp') {
this.$i18n.add('ja', require('@/web/locale/ja'));
this.$i18n.set('ja');
}
axios.get('data/festivals.json')
.then(response => this.festivals = response.data)
},
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<Wrapper title="SplatNet Gear" :time="startTime" v-if="merchandises">
<Wrapper title="SplatNet Gear" v-if="merchandises">
<div class="columns">
<div class="column is-5">
<h1 class="title is-3 font-splatoon2 has-text-centered">New Gear</h1>
@@ -13,19 +13,17 @@
<div class="column is-5" style="min-height: 560px; display: flex; align-items: center; justify-content: center;">
<MerchandiseBox
:merchandise="featuredMerchandise"
:now="startTime"
class="featured-merchandise"
></MerchandiseBox>
/>
</div>
<div class="column" style="display: flex; align-items: center; justify-content: center;">
<div class="columns is-multiline is-centered">
<div class="column is-one-third" v-for="(merchandise, index) in otherMerchandises">
<div class="column is-one-third" v-for="(merchandise, index) in otherMerchandises" :key="index">
<MerchandiseBox
:merchandise="merchandise"
:now="startTime"
:class="(index % 2 == 0) ? 'tilt-right' : 'tilt-left'"
></MerchandiseBox>
/>
</div>
</div>
</div>
@@ -34,34 +32,22 @@
</template>
<script>
import axios from 'axios';
import Wrapper from '@/web/components/screenshots/Wrapper.vue';
import MerchandiseBox from '@/web/components/splatoon/MerchandiseBox.vue';
export default {
components: { Wrapper, MerchandiseBox },
props: ['startTime', 'endTime'],
data() {
return {
merchandises: null,
};
},
computed: {
merchandises() {
return this.$store.getters['splatoon/splatNetStore/merchandises']
&& this.$store.getters['splatoon/splatNetStore/merchandises'].slice().reverse();
},
featuredMerchandise() {
if (this.merchandises)
return this.merchandises.find(m => m.end_time == this.endTime);
return this.merchandises && this.merchandises[0];
},
otherMerchandises() {
if (this.merchandises) {
return this.merchandises
.filter(m => m != this.featuredMerchandise && m.end_time > this.startTime)
.sort((a, b) => b.end_time - a.end_time);
}
return this.merchandises && this.merchandises.slice(1);
},
},
created() {
axios.get('data/merchandises.json')
.then(response => this.merchandises = response.data.merchandises)
},
}
};
</script>

View File

@@ -3,9 +3,9 @@
<div class="level-left">
<div class="level-item">
<div class="image is-48x48" style="margin-bottom: -6px">
<img v-if="gameMode.key == 'regular'" src="~@/web/assets/img/battle-regular.png" />
<img v-if="gameMode.key == 'gachi'" src="~@/web/assets/img/battle-ranked.png" />
<img v-if="gameMode.key == 'league'" src="~@/web/assets/img/battle-league.png" />
<img v-if="mode === 'regular'" src="~@/web/assets/img/battle-regular.png" />
<img v-if="mode === 'ranked'" src="~@/web/assets/img/battle-ranked.png" />
<img v-if="mode === 'league'" src="~@/web/assets/img/battle-league.png" />
</div>
</div>
<div class="level-item">
@@ -18,16 +18,17 @@
</template>
<script>
import { mapGetters } from 'vuex';
export default {
props: ['gameMode', 'festival', 'now'],
props: ['mode'],
computed: {
isFestival() {
return this.festival && this.festival.times.start <= this.now && this.festival.times.end > this.now;
},
...mapGetters('splatoon/splatfests', ['selectedRegionHasActiveSplatfest']),
name() {
if (this.isFestival)
if (this.selectedRegionHasActiveSplatfest)
return this.$t('splatfest.battle');
return this.$t(`splatnet.game_modes.${this.gameMode.key}.name`, this.gameMode.name);
return this.$t(`game_mode.${this.mode}`);
},
},
}

View File

@@ -10,7 +10,7 @@
<div class="main skill-img-bg" v-if="merchandise.skill">
<img :src="merchandise.skill.image | localSplatNetImageUrl" :title="skillName" />
</div>
<div class="sub" v-for="i in merchandise.gear.rarity + 1">
<div class="sub" v-for="i in merchandise.gear.rarity + 1" :key="i">
<img src="~@/web/assets/img/blank-skill-slot.png" />
</div>
</div>
@@ -38,7 +38,7 @@
<div class="skill-img-bg strikethrough">
<img :src="merchandise.original_gear.skill.image | localSplatNetImageUrl" :title="originalSkillName" />
</div>
<div class="sub" v-for="i in merchandise.original_gear.rarity + 1">
<div class="sub" v-for="i in merchandise.original_gear.rarity + 1" :key="i">
<img src="~@/web/assets/img/blank-skill-slot.png" />
</div>
</div>
@@ -74,9 +74,12 @@
</template>
<script>
import { mapGetters } from 'vuex';
export default {
props: ['merchandise', 'now'],
props: ['merchandise'],
computed: {
...mapGetters('splatoon', ['now']),
brandName() {
let brand = this.merchandise.gear.brand;
return this.$t(`splatnet.brands.${brand.id}.name`, brand.name);

View File

@@ -21,43 +21,43 @@
<div class="column is-4 salmon-run-ad-img">
<div class="image is-16by9 is-hidden-desktop"></div>
<template v-if="!screenshotMode && coop && coop.reward_gear">
<template v-if="!screenshotMode && rewardGear">
<div class="salmon-run-gear-text">
{{ $t('coop.this_months_gear') }}
</div>
<div class="salmon-run-gear-image hand" @click="gearDialogOpen = true" :title="rewardGearName">
<div class="image is-32x32">
<img :src="coop.reward_gear.gear.image | localSplatNetImageUrl" />
<img :src="rewardGear.image | localSplatNetImageUrl" />
</div>
</div>
</template>
</div>
<div class="column">
<div class="salmon-run-content">
<div class="salmon-run-open" v-if="currentSchedule">
<div class="salmon-run-open" v-if="activeSchedule">
<div class="is-size-5 title-squid font-splatoon1">
{{ $t('times.open') }}
</div>
<template v-if="screenshotMode">
<div class="title-color is-size-5">
{{ currentSchedule.end_time - now | durationHours | time.remaining }}
{{ activeSchedule.end_time - now | durationHours | time.remaining }}
</div>
</template>
<template v-else>
<div class="title-color is-size-5">
{{ currentSchedule.start_time | date }}
{{ currentSchedule.start_time | time }}
{{ activeSchedule.start_time | date }}
{{ activeSchedule.start_time | time }}
&ndash;
{{ currentSchedule.end_time | date }}
{{ currentSchedule.end_time | time }}
{{ activeSchedule.end_time | date }}
{{ activeSchedule.end_time | time }}
</div>
<div>
{{ currentSchedule.end_time - now | duration | time.remaining }}
{{ activeSchedule.end_time - now | duration | time.remaining }}
</div>
</template>
<div class="salmon-run-details">
<SalmonRunDetailsBar :schedule="currentSchedule"></SalmonRunDetailsBar>
<SalmonRunDetailsBar :schedule="activeSchedule" />
</div>
</div>
@@ -66,7 +66,7 @@
<div class="is-size-6 title-squid font-splatoon1">
{{ $t('times.soon') }}
</div>
<div v-for="event in upcomingSchedules" class="event">
<div v-for="event in upcomingSchedules" :key="event.key" class="event">
<div class="columns is-marginless is-gapless is-mobile">
<div class="column is-narrow" style="margin-right: 5px">
<div class="image is-4by3" style="width: 23px">
@@ -91,7 +91,7 @@
</div>
</div>
<SalmonRunDetailsBar :schedule="event" :mini="true"></SalmonRunDetailsBar>
<SalmonRunDetailsBar :schedule="event" mini />
</div>
</div>
</div>
@@ -100,51 +100,33 @@
</div>
<SalmonRunGearDialog
v-if="gearDialogOpen && coop && coop.reward_gear"
:gear="coop.reward_gear.gear"
v-if="gearDialogOpen && rewardGear"
@close="gearDialogOpen = false"
/>
</div>
</template>
<script>
import Vue from 'vue';
import { mapGetters } from 'vuex';
import SalmonRunDetailsBar from './SalmonRunDetailsBar.vue';
import SalmonRunGearDialog from './SalmonRunGearDialog.vue';
export default {
components: { SalmonRunDetailsBar, SalmonRunGearDialog },
props: ['coop', 'coopSchedules', 'now', 'screenshotMode'],
props: {
screenshotMode: Boolean,
},
data() {
return {
gearDialogOpen: false,
};
},
computed: {
allSchedules() {
// Combine the "schedules" and "details" sections into one list of schedules
let results = {};
// Add the less-detailed schedules in first
for (let schedule of this.coopSchedules.schedules)
results[schedule.start_time] = schedule;
// Add/replace the schedules that have map/weapon details
for (let schedule of this.coopSchedules.details)
results[schedule.start_time] = schedule;
return Object.values(results).sort((a, b) => { return a.start_time - b.start_time });
},
upcomingSchedules() { return this.allSchedules.filter(schedule => schedule.start_time > this.now) },
currentSchedule() {
if (this.allSchedules.length > 0 && this.allSchedules[0].start_time <= this.now)
return this.allSchedules[0];
},
...mapGetters('splatoon', ['now']),
...mapGetters('splatoon/salmonRun', ['activeSchedule', 'upcomingSchedules', 'rewardGear']),
rewardGearName() {
if (this.coop.reward_gear) {
let gear = this.coop.reward_gear.gear;
return this.$t(`splatnet.gear.${gear.id}.name`, gear.name);
}
if (this.rewardGear)
return this.$t(`splatnet.gear.${this.rewardGear.kind}.${this.rewardGear.id}.name`, this.rewardGear.name);
},
},
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="columns is-mobile is-slimmer" style="padding: 0.5rem 0" v-if="schedule.stage || schedule.weapons">
<div class="column" :class="mini ? 'is-3' : 'is-5'" v-if="schedule.stage">
<Stage :stage="schedule.stage" :isSalmonRun="true" :showTitle="!mini"></Stage>
<Stage :stage="schedule.stage" is-salmon-run :showTitle="!mini"></Stage>
</div>
<div class="column" style="display: flex; align-items: center;" v-if="mini && schedule.stage">
@@ -45,7 +45,10 @@ import Stage from './Stage.vue';
export default {
components: { Stage },
props: ['schedule', 'mini'],
props: {
schedule: null,
mini: Boolean,
},
computed: {
stageName() {
let stage = this.schedule.stage;

View File

@@ -9,14 +9,17 @@
</template>
<script>
import { mapGetters } from 'vuex';
import Modal from '@/web/components/Modal.vue';
import MerchandiseBox from './MerchandiseBox.vue';
export default {
components: { Modal, MerchandiseBox },
props: ['gear'],
computed: {
merchandise() { return { gear: this.gear }; },
...mapGetters('splatoon/salmonRun', ['rewardGear']),
merchandise() {
return { gear: this.rewardGear };
},
},
}
</script>

View File

@@ -1,25 +1,25 @@
<template>
<div class="font-splatoon2" :class="cssClass" v-if="gameMode">
<GameModeHeader :gameMode="gameMode" :festival="festival" :now="now"></GameModeHeader>
<div class="font-splatoon2" :class="mode">
<GameModeHeader :mode="mode" />
<div class="main-schedule" v-if="currentSchedule">
<div class="main-schedule" v-if="activeSchedule">
<div class="level is-mobile top-bar">
<div class="level-left">
<div class="level-item title-color is-size-5">{{ currentRuleName }}</div>
<div class="level-item title-color is-size-5">{{ activeRuleName }}</div>
</div>
<div class="level-right">
<div class="level-item">
{{ currentSchedule.start_time | time }} &ndash;
{{ currentSchedule.end_time | time }}
{{ activeSchedule.start_time | time }} &ndash;
{{ activeSchedule.end_time | time }}
</div>
</div>
</div>
<div class="is-clearfix"></div>
<div class="columns is-mobile is-slim">
<div class="column"><Stage :stage="currentSchedule.stage_a"></Stage></div>
<div class="column"><Stage :stage="currentSchedule.stage_b"></Stage></div>
<template v-if="festival">
<div class="column"><Stage :stage="festival.special_stage"></Stage></div>
<div class="column"><Stage :stage="activeSchedule.stage_a"></Stage></div>
<div class="column"><Stage :stage="activeSchedule.stage_b"></Stage></div>
<template v-if="selectedRegionHasActiveSplatfest">
<div class="column"><Stage :stage="selectedRegionCurrentSplatfest.special_stage"></Stage></div>
</template>
</div>
</div>
@@ -29,11 +29,7 @@
{{ $t('times.next') }}
</div>
<ScheduleRow
:schedule="upcomingSchedules[0]"
:now="now"
:smallSize="true"
></ScheduleRow>
<ScheduleRow :schedule="upcomingSchedules[0]" is-small />
</template>
<p class="has-text-centered" style="margin-top: 10px">
@@ -47,16 +43,14 @@
<ScheduleDialog
v-if="dialogOpen"
:schedules="schedules"
:gameMode="gameMode"
:cssClass="cssClass"
:now="now"
:mode="mode"
@close="dialogOpen = false"
></ScheduleDialog>
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import GameModeHeader from './GameModeHeader.vue';
import Stage from './Stage.vue';
import ScheduleRow from './ScheduleRow.vue';
@@ -64,32 +58,31 @@ import ScheduleDialog from './ScheduleDialog.vue';
export default {
components: { GameModeHeader, Stage, ScheduleRow, ScheduleDialog },
props: ['schedules', 'cssClass', 'festival', 'now'],
props: ['mode'],
data() {
return {
dialogOpen: false,
};
},
computed: {
gameMode() {
if (this.schedules && this.schedules[0])
return this.schedules[0].game_mode;
},
currentSchedule() {
if (this.schedules && this.schedules[0] && this.schedules[0].start_time <= this.now)
return this.schedules[0];
},
currentRuleName() {
let rule = this.currentSchedule.rule;
return this.$t(`splatnet.rules.${rule.key}.name`, rule.name);
...mapGetters('splatoon', ['now']),
...mapGetters('splatoon/splatfests', [
'selectedRegionCurrentSplatfest',
'selectedRegionHasActiveSplatfest',
]),
activeSchedule() {
return this.$store.getters[`splatoon/schedules/${this.mode}/activeSchedule`];
},
upcomingSchedules() {
if (this.schedules) {
if (this.currentSchedule)
return this.schedules.slice(1);
return this.schedules;
return this.$store.getters[`splatoon/schedules/${this.mode}/upcomingSchedules`];
},
activeRuleName() {
if (this.activeSchedule) {
let rule = this.activeSchedule.rule;
return this.$t(`splatnet.rules.${rule.key}.name`, rule.name);
}
}
},
},
}
</script>

View File

@@ -1,11 +1,11 @@
<template>
<Modal @close="$emit('close')" class="font-splatoon2">
<div class="modal-card schedule-box tilt-left-slight" :class="cssClass">
<div class="modal-card schedule-box tilt-left-slight" :class="mode">
<header class="modal-card-head">
<GameModeHeader :gameMode="gameMode"></GameModeHeader>
<GameModeHeader :mode="mode" />
</header>
<section class="modal-card-body">
<ScheduleList :schedules="schedules" :now="now"></ScheduleList>
<ScheduleList :mode="mode" />
</section>
</div>
</Modal>
@@ -19,9 +19,9 @@ import ScheduleList from './ScheduleList.vue';
export default {
components: { Modal, GameModeHeader, ScheduleList },
props: ['schedules', 'gameMode', 'cssClass', 'now'],
props: ['mode'],
mounted() {
analytics.event('Schedule', 'Open', this.gameMode.name);
analytics.event('Schedule', 'Open', this.mode);
},
}
</script>

View File

@@ -2,11 +2,11 @@
<div>
<div v-if="first">
<div class="is-size-5 title-squid font-splatoon1">
<span v-if="first.start_time <= this.now">{{ $t('times.now') }}</span>
<span v-if="first.start_time <= now">{{ $t('times.now') }}</span>
<span v-else>{{ $t('times.soon') }}</span>
</div>
<ScheduleRow :schedule="first" :now="now"></ScheduleRow>
<ScheduleRow :schedule="first" />
</div>
<template>
<div v-if="second">
@@ -14,7 +14,7 @@
{{ $t('times.next') }}
</div>
<ScheduleRow :schedule="second" :now="now"></ScheduleRow>
<ScheduleRow :schedule="second" />
</div>
<div v-if="others && others.length">
@@ -24,23 +24,26 @@
<ScheduleRow
v-for="schedule in others"
:key="schedule.id"
:key="schedule.start_time"
:schedule="schedule"
:now="now"
></ScheduleRow>
/>
</div>
</template>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import ScheduleRow from './ScheduleRow.vue';
export default {
components: { ScheduleRow },
props: ['schedules', 'now'],
props: ['mode'],
computed: {
...mapGetters('splatoon', ['now']),
schedules() {
return this.$store.getters[`splatoon/schedules/${this.mode}/currentSchedules`];
},
first() { return this.schedules && this.schedules[0]; },
second() { return this.schedules && this.schedules[1]; },
others() { return this.schedules && this.schedules.slice(2); },

View File

@@ -37,8 +37,8 @@
</div>
<div class="column is-8">
<div class="columns is-mobile is-slim">
<div class="column"><Stage :stage="schedule.stage_a"></Stage></div>
<div class="column"><Stage :stage="schedule.stage_b"></Stage></div>
<div class="column"><Stage :stage="schedule.stage_a" /></div>
<div class="column"><Stage :stage="schedule.stage_b" /></div>
</div>
</div>
</div>
@@ -47,12 +47,17 @@
<script>
import Vue from 'vue';
import { mapGetters } from 'vuex';
import Stage from './Stage.vue';
export default {
components: { Stage },
props: ['schedule', 'now', 'smallSize'],
props: {
schedule: null,
isSmall: Boolean,
},
computed: {
...mapGetters('splatoon', ['now']),
isToday() {
let now = new Date(this.now * 1000);
let start = new Date(this.schedule.start_time * 1000);
@@ -62,8 +67,8 @@ export default {
let rule = this.schedule.rule;
return this.$t(`splatnet.rules.${rule.key}.name`, rule.name);
},
ruleNameClass() { return this.smallSize ? 'is-6' : 'is-5' },
scheduleClass() { return this.smallSize ? 'is-7' : 'is-6' },
ruleNameClass() { return this.isSmall ? 'is-6' : 'is-5' },
scheduleClass() { return this.isSmall ? 'is-7' : 'is-6' },
timeFromNow() {
let time = Vue.filter('duration')(this.schedule.start_time - this.now);
return this.$t('time.in', { time });

View File

@@ -4,12 +4,11 @@
<div class="splatnet-gear-dialog">
<h3 class="title is-3 has-text-centered font-splatoon1 text-shadow">{{ $t('splatnet_gear.title') }}</h3>
<div class="columns is-marginless is-multiline is-mobile is-centered">
<div class="column is-half-mobile is-one-third-tablet" v-for="(merchandise, index) in merchandises">
<div class="column is-half-mobile is-one-third-tablet" v-for="(merchandise, index) in merchandises" :key="merchandise.end_time">
<MerchandiseBox
:merchandise="merchandise"
:now="now"
:class="(index % 2 == 0) ? 'tilt-left' : 'tilt-right'"
></MerchandiseBox>
:class="(index % 2 === 0) ? 'tilt-left' : 'tilt-right'"
/>
</div>
</div>
</div>
@@ -18,13 +17,16 @@
</template>
<script>
import { mapGetters } from 'vuex';
import analytics from '@/web/support/analytics';
import Modal from '@/web/components/Modal.vue';
import MerchandiseBox from './MerchandiseBox.vue';
export default {
components: { Modal, MerchandiseBox },
props: ['merchandises', 'now'],
computed: {
...mapGetters('splatoon/splatNetStore', ['merchandises']),
},
mounted() {
analytics.event('SplatNet Gear', 'Open');
},

View File

@@ -1,12 +1,12 @@
<template>
<div class="font-splatoon2" :class="{ 'has-results': results && !screenshotMode }">
<div class="font-splatoon2" :class="{ 'has-results': showingResults }">
<div class="splatfest-header">
<h2 class="title is-3 is-size-2-fullhd font-splatoon1">
{{ title }}
</h2>
</div>
<div class="panel-container" :class="{ 'is-hidden-mobile' : results }">
<div class="panel-container" :class="{ 'is-hidden-mobile' : festival.results }">
<div class="image panel-image">
<img :src="image" />
</div>
@@ -21,15 +21,15 @@
<div class="column has-text-right" v-text="teamNames.short.bravo"></div>
</div>
<SplatfestResultsBox :festival="festival" :results="results" v-if="results && !screenshotMode" />
<SplatfestResultsBox :festival="festival" v-if="showingResults" />
</div>
<div class="mobile-results is-hidden-tablet" v-if="results">
<SplatfestResultsBox :festival="festival" :results="results" v-if="results && !screenshotMode" />
<div class="mobile-results is-hidden-tablet" v-if="festival.results">
<SplatfestResultsBox :festival="festival" v-if="showingResults" />
</div>
<div class="has-text-centered is-size-5 title-color festival-period-container">
<div v-if="!results" class="festival-period" :style="{ 'background-color': festival.colors.middle.css_rgb }">
<div v-if="!festival.results" class="festival-period" :style="{ 'background-color': festival.colors.middle.css_rgb }">
<template v-if="!screenshotMode">
<span class="nowrap">
{{ festival.times.start | date(dateOptions) }}
@@ -42,10 +42,10 @@
</span>
</template>
<template v-else>
<template v-if="state == 'upcoming'">
<template v-if="festival.state == 'upcoming'">
{{ festival.times.start - now | shortDuration | time.in }}
</template>
<template v-else-if="state == 'past' && !results && festival.times.result > now">
<template v-else-if="festival.state == 'past' && !results && festival.times.result > now">
{{ festival.times.result - now | durationHours | resultsIn }}
</template>
<template v-else>
@@ -54,23 +54,19 @@
</template>
</div>
<SplatfestWinnerBar
v-else
:festival="festival"
:results="results"
/>
<SplatfestWinnerBar :festival="festival" v-else />
</div>
<div class="splatfest-content has-text-centered" v-if="!screenshotMode">
<template v-if="state == 'upcoming'">
<template v-if="festival.state == 'upcoming'">
{{ festival.times.start - now | duration | time.in }}
</template>
<template v-else-if="state == 'active'">
<template v-else-if="festival.state == 'active'">
{{ festival.times.end - now | duration | time.remaining }}
</template>
<template v-else-if="state == 'past' && !results && festival.times.result > now">
<template v-else-if="festival.state == 'past' && !festival.results && festival.times.result > now">
{{ festival.times.result - now | duration | resultsIn }}
</template>
</div>
@@ -79,24 +75,26 @@
<script>
import Vue from 'vue';
import { mapGetters } from 'vuex';
import SplatfestResultsBox from './SplatfestResultsBox.vue';
import SplatfestWinnerBar from './SplatfestWinnerBar.vue';
export default {
components: { SplatfestResultsBox, SplatfestWinnerBar },
props: ['festival', 'results', 'now', 'screenshotMode', 'historyMode'],
props: {
festival: null,
screenshotMode: Boolean,
historyMode: Boolean,
},
filters: {
resultsIn(time) {
return Vue.i18n.translate('splatfest.results_in', { time });
},
},
computed: {
state() {
if (this.festival.times.start > this.now)
return 'upcoming';
if (this.festival.times.end > this.now)
return 'active';
return 'past';
...mapGetters('splatoon', ['now']),
showingResults() {
return this.festival.results && !this.screenshotMode && !this.historyMode;
},
dateOptions() {
if (this.historyMode)
@@ -104,9 +102,9 @@ export default {
return { weekday: 'short' };
},
title() {
if (this.state == 'upcoming')
if (this.festival.state == 'upcoming')
return this.$t('splatfest.upcoming');
if (this.state == 'past' && !this.screenshotMode)
if (this.festival.state == 'past' && !this.screenshotMode)
return this.$t('splatfest.recent');
return this.$t('splatfest.title');
},

View File

@@ -1,5 +1,5 @@
<template>
<Modal @close="$emit('close')" class="is-wide splatfest-history-dialog modal-rounded-box" v-if="allFestivals">
<Modal @close="$emit('close')" class="is-wide splatfest-history-dialog modal-rounded-box" v-if="allSplatfests">
<div class="modal-card">
<div class="modal-card-head">
<div class="level">
@@ -18,16 +18,12 @@
</div>
<div class="modal-card-body" ref="body">
<div v-for="(info, index) in festivals" class="splatfest-history-row" :key="info.festival.festival_id">
<div v-for="(festival, index) in festivals" class="splatfest-history-row" :key="festival.festival_id">
<div class="level">
<div class="level-item">
<div class="splatfest" :class="(index % 2 == 0) ? 'tilt-left' : 'tilt-right'">
<div class="hook-box">
<SplatfestBox
:festival="info.festival"
:now="now"
:historyMode="true"
/>
<SplatfestBox :festival="festival" history-mode />
</div>
</div>
</div>
@@ -35,14 +31,14 @@
<div class="level-item">
<div class="splatfest-results-container">
<SplatfestResultsBox
v-if="info.results"
:festival="info.festival"
:results="info.results"
:historyMode="true"
v-if="festival.results"
:festival="festival"
:results="festival.results"
history-mode
/>
<div v-else-if="info.festival.times.result > now" class="has-text-centered font-splatoon2 is-size-5">
{{ info.festival.times.result - now | duration | resultsIn }}
<div v-else-if="festival.times.result > now" class="has-text-centered font-splatoon2 is-size-5">
{{ festival.times.result - now | duration | resultsIn }}
</div>
</div>
</div>
@@ -55,8 +51,8 @@
<script>
import Vue from 'vue';
import { mapGetters } from 'vuex';
import analytics from '@/web/support/analytics';
import * as regions from '@/common/regions.esm';
import Modal from '@/web/components/Modal.vue';
import Dropdown from '@/web/components/Dropdown.vue';
import SplatfestBox from './SplatfestBox.vue';
@@ -64,7 +60,6 @@ import SplatfestResultsBox from './SplatfestResultsBox.vue';
export default {
components: { Modal, Dropdown, SplatfestBox, SplatfestResultsBox },
props: ['allFestivals', 'now', 'initialRegion'],
filters: {
resultsIn(time) {
return Vue.i18n.translate('splatfest.results_in', { time });
@@ -76,19 +71,23 @@ export default {
};
},
computed: {
...mapGetters('splatoon', ['now']),
...mapGetters('splatoon/regions', { initialRegion: 'selectedKey' }),
allSplatfests() {
return {
na: this.$store.getters['splatoon/splatfests/na/allSplatfests'],
eu: this.$store.getters['splatoon/splatfests/eu/allSplatfests'],
jp: this.$store.getters['splatoon/splatfests/jp/allSplatfests'],
};
},
regions() {
return regions.splatoonRegions.filter(({ key }) => key).map(({ key }) => {
return this.$store.state.splatoon.regions.all.map(({ key }) => {
let name = (key) ? this.$t(`regions.${key}.name`) : this.$t('regions.global.name');
return { key, name };
});
},
festivals() {
let data = this.allFestivals[this.region];
return data.festivals.map(festival => {
let results = data.results.find(r => r.festival_id == festival.festival_id);
return { festival, results };
});
return this.allSplatfests[this.region];
},
},
watch: {

View File

@@ -24,12 +24,12 @@
</div>
</div>
<SplatfestResultsRow :festival="festival" :results="results" type="vote" />
<SplatfestResultsRow :festival="festival" :results="results" type="solo" />
<SplatfestResultsRow :festival="festival" :results="results" type="team" />
<SplatfestResultsRow :festival="festival" type="vote" />
<SplatfestResultsRow :festival="festival" type="solo" />
<SplatfestResultsRow :festival="festival" type="team" />
<div class="has-text-centered is-size-5 title-color font-splatoon2" style="margin-top: 10px;" v-if="historyMode">
<SplatfestWinnerBar :festival="festival" :results="results" />
<SplatfestWinnerBar :festival="festival" />
</div>
</div>
</template>
@@ -40,6 +40,6 @@ import SplatfestWinnerBar from './SplatfestWinnerBar.vue';
export default {
components: { SplatfestResultsRow, SplatfestWinnerBar },
props: ['festival', 'results', 'historyMode'],
props: ['festival', 'historyMode'],
}
</script>

View File

@@ -4,7 +4,7 @@
<div class="winner-mark-shadow" v-if="winner == 'alpha'"></div>
<div class="winner-mark" :style="{ background: festival.colors.alpha.css_rgb }" v-if="winner == 'alpha'"></div>
<div class="font-splatoon2 title is-4" :title="alphaTitle | numberFormat">
<div>{{ results.rates[type].alpha | wholePercent }}<span class="percent">.{{ results.rates[type].alpha | partialPercent }}{{ $t('splatfest.results.%') }}</span></div>
<div>{{ rates.alpha | wholePercent }}<span class="percent">.{{ rates.alpha | partialPercent }}{{ $t('splatfest.results.%') }}</span></div>
</div>
</div>
@@ -16,7 +16,7 @@
<div class="winner-mark-shadow" v-if="winner == 'bravo'"></div>
<div class="winner-mark" :style="{ background: festival.colors.bravo.css_rgb }" v-if="winner == 'bravo'"></div>
<div class="font-splatoon2 title is-4" :title="bravoTitle | numberFormat">
<div>{{ results.rates[type].bravo | wholePercent }}<span class="percent">.{{ results.rates[type].bravo | partialPercent }}{{ $t('splatfest.results.%') }}</span></div>
<div>{{ rates.bravo | wholePercent }}<span class="percent">.{{ rates.bravo | partialPercent }}{{ $t('splatfest.results.%') }}</span></div>
</div>
</div>
</div>
@@ -24,10 +24,10 @@
<script>
export default {
props: ['festival', 'results', 'type'],
props: ['festival', 'type'],
computed: {
winner() {
return this.results.summary[this.type] ? 'bravo' : 'alpha';
return this.festival.results.summary[this.type] ? 'bravo' : 'alpha';
},
alphaTitle() {
return this.resultTitle('alpha');
@@ -35,6 +35,9 @@ export default {
bravoTitle() {
return this.resultTitle('bravo');
},
rates() {
return this.festival.results.rates[this.type];
},
},
filters: {
wholePercent(value) {
@@ -47,8 +50,8 @@ export default {
methods: {
resultTitle(team) {
if (this.type == 'vote')
return this.results.team_participants && this.results.team_participants[team];
return this.results.team_scores && this.results.team_scores[`${team}_${this.type}`];
return this.festival.results.team_participants && this.festival.results.team_participants[team];
return this.festival.results.team_scores && this.festival.results.team_scores[`${team}_${this.type}`];
},
},
}

View File

@@ -4,7 +4,7 @@
<script>
export default {
props: ['festival', 'results'],
props: ['festival'],
computed: {
teamNames() {
return {
@@ -13,10 +13,10 @@ export default {
};
},
teamWins() {
if (!this.festival || !this.results)
if (!this.festival || !this.festival.results)
return;
let winner = this.results.summary.total ? 'bravo' : 'alpha';
let winner = this.festival.results.summary.total ? 'bravo' : 'alpha';
let team = `<span style="color: ${this.festival.colors[winner].css_rgb}">${this.teamNames[winner]}</span>`;
return this.$t('splatfest.team_name_wins', { team });

View File

@@ -29,7 +29,7 @@ export default {
stage: {},
showTitle: { default: true },
clickable: { default: true },
isSalmonRun: { default: false },
isSalmonRun: Boolean,
},
data() {
return {

View File

@@ -74,5 +74,10 @@
"get_updates_via_twitter": "Informiere dich über Twitter!",
"all_upcoming_stages": "Alle bevorstehenden Arenen",
"disclaimer": "Diese Website steht in keiner Verbindung zu Nintendo. Alle Produktnamen, Logos und Marken sind Eigentum der jeweiligen Inhaber."
},
"game_mode": {
"regular": "Standardkampf",
"ranked": "Rangkampf",
"league": "Ligakampf"
}
}

View File

@@ -74,5 +74,10 @@
"get_updates_via_twitter": "Get updates via Twitter!",
"all_upcoming_stages": "All Upcoming Stages",
"disclaimer": "This website is not affiliated with Nintendo. All product names, logos, and brands are property of their respective owners."
},
"game_mode": {
"regular": "Regular Battle",
"ranked": "Ranked Battle",
"league": "League Battle"
}
}

View File

@@ -67,5 +67,10 @@
"gear": {
"frequent_ability": "Potenciador común"
},
"ui": {}
"ui": {},
"game_mode": {
"regular": "Combate amistoso",
"ranked": "Combate competitivo",
"league": "Combate de liga"
}
}

View File

@@ -67,5 +67,10 @@
"gear": {
"frequent_ability": "Potenciadores comunes"
},
"ui": {}
"ui": {},
"game_mode": {
"regular": "Combate amistoso",
"ranked": "Combate competitivo",
"league": "Torneo"
}
}

View File

@@ -73,5 +73,10 @@
"get_updates_via_twitter": "Soyez informés par Twitter!",
"all_upcoming_stages": "Les stages à venir",
"disclaimer": "Ce site internet n'est pas affilié à Nintendo. Tous les noms de produits, logos et marques appartiennent à leurs propriétaires respectifs."
},
"game_mode": {
"regular": "Match classique",
"ranked": "Match pro",
"league": "Match de ligue"
}
}

View File

@@ -73,5 +73,10 @@
"get_updates_via_twitter": "Soyez informés par Twitter !",
"all_upcoming_stages": "Les stages à venir",
"disclaimer": "Ce site internet n'est pas affilié à Nintendo. Tous les noms de produits, logos et marques appartiennent à leurs propriétaires respectifs."
},
"game_mode": {
"regular": "Match classique",
"ranked": "Match pro",
"league": "Match de ligue"
}
}

View File

@@ -67,5 +67,10 @@
"gear": {
"frequent_ability": "Abilità comune"
},
"ui": {}
"ui": {},
"game_mode": {
"regular": "Partita amichevole",
"ranked": "Partita pro",
"league": "Partita di lega"
}
}

View File

@@ -67,5 +67,10 @@
"gear": {
"frequent_ability": "つきやすいギアパワー"
},
"ui": {}
"ui": {},
"game_mode": {
"regular": "レギュラーマッチ",
"ranked": "ガチマッチ",
"league": "リーグマッチ"
}
}

View File

@@ -67,5 +67,10 @@
"gear": {
"frequent_ability": "Waarschijnlijk effect"
},
"ui": {}
"ui": {},
"game_mode": {
"regular": "Standaardgevecht",
"ranked": "Profgevecht",
"league": "Toernooigevecht"
}
}

View File

@@ -67,5 +67,10 @@
"gear": {
"frequent_ability": "Обычное свойство снаряжения"
},
"ui": {}
"ui": {},
"game_mode": {
"regular": "Бой салаг",
"ranked": "Бой элиты",
"league": "Бой лиги"
}
}

View File

@@ -8,6 +8,14 @@ const routes = [
path: '/',
component: require('./components/Main.vue').default,
children: [
{
path: 'splatnet',
component: require('./components/splatoon/SplatNetGearDialog.vue').default,
},
{
path: 'splatfests',
component: require('./components/splatoon/SplatfestHistoryDialog.vue').default,
},
{
path: 'about',
component: require('./components/AboutDialog.vue').default,
@@ -16,12 +24,6 @@ const routes = [
path: 'calendars',
component: require('./components/CalendarDialog.vue').default,
},
{
path: 'splatnet',
},
{
path: 'splatfests',
},
],
},
{

View File

@@ -21,12 +21,12 @@ import NewWeapon from './components/screenshots/newweapon/NewWeapon.vue';
import Splatfest from './components/screenshots/splatfest/Splatfest.vue';
const routes = [
{ path: '/', component: ScreenshotHelper },
{ path: '/schedules/:startTime', component: Schedules, props: true },
{ path: '/splatNetGear/:startTime/:endTime', component: SplatNetGear, props: true },
{ path: '/salmonRun/:startTime', component: SalmonRun, props: true },
{ path: '/salmonRunGear/:now', component: SalmonRunGear, props: true },
{ path: '/newWeapon/:releaseTime', component: NewWeapon, props: true },
{ path: '/splatfest/:region/:startTime', component: Splatfest, props: true },
{ path: '/schedules/:now', component: Schedules },
{ path: '/splatNetGear/:now', component: SplatNetGear },
{ path: '/salmonRun/:now', component: SalmonRun },
{ path: '/salmonRunGear/:now', component: SalmonRunGear },
{ path: '/newWeapon/:now', component: NewWeapon },
{ path: '/splatfest/:region/:now', component: Splatfest, props: true },
];
const router = new VueRouter({ routes });

View File

@@ -1,9 +0,0 @@
export default {
loadLocale({ dispatch }, locale) {
// Disabling this for now due to issues with disabling prefetching with the modern build (see vue.config.js)
// import(/* webpackChunkName: "lang-[request]" */ `@/web/locale/${locale}`)
// .then(translations => dispatch('addLocale', { locale, translations: translations.default }));
dispatch('addLocale', { locale, translations: require(`@/web/locale/${locale}`) });
}
}

View File

@@ -1,11 +1,15 @@
import Vue from 'vue';
import Vuex from 'vuex';
import splatoon from './splatoon';
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';
export default new Vuex.Store({
actions: require('./actions').default,
modules: {
splatoon,
},
strict: debug,
});

View File

@@ -0,0 +1,109 @@
import axios from 'axios';
let updateDataTimer;
export const namespaced = true;
// State, actions, and mutators are identical for each data source, so this module
// generates them automatically from a list of data sources.
let dataSources = [
{
name: 'schedules',
url: '/data/schedules.json',
},
{
name: 'coop_schedules',
url: '/data/coop-schedules.json',
},
{
name: 'timeline',
url: '/data/timeline.json',
},
{
name: 'festivals',
url: '/data/festivals.json',
},
{
name: 'merchandises',
url: '/data/merchandises.json',
},
];
// Automatically determine the update action name and mutation name
for (let source of dataSources) {
// Action: updateSourceName
source.actionName = 'update' + source.name[0].toUpperCase() + source.name.substr(1);
// Mutation: UPDATE_SOURCENAME
source.mutationName = 'UPDATE_' + source.name.toUpperCase();
}
export const state = { };
export const actions = {
updateLanguage({ dispatch, rootGetters }) {
let language = rootGetters['splatoon/languages/selectedLanguage'];
if (language) {
axios.get(`/data/locale/${language.language}.json`)
.then(response => dispatch('addLocale', {
locale: language.language,
translations: { splatnet: response.data },
}, { root: true }))
.catch(e => console.error(e));
}
},
updateAll({ dispatch }) {
return Promise.all([
dispatch('updateLanguage'),
...dataSources.map(source => dispatch(source.actionName)),
]);
},
startUpdatingData({ dispatch }) {
if (updateDataTimer)
return;
dispatch('updateAll');
let date = new Date;
// If we're more than 20 seconds past the current hour, schedule the update for the next hour
if (date.getMinutes() !== 0 || date.getSeconds() >= 20)
date.setHours(date.getHours() + 1);
date.setMinutes(0);
// Random number of seconds past the hour (so all open browsers don't hit the server at the same time)
let minSec = 25;
let maxSec = 60;
date.setSeconds(Math.floor(Math.random() * (maxSec - minSec + 1)) + minSec);
// Set the timeout
updateDataTimer = setTimeout(() => {
updateDataTimer = null;
dispatch('startUpdatingData');
}, (date - new Date));
},
stopUpdatingData() {
clearInterval(updateDataTimer);
updateDataTimer = null;
},
};
export const mutations = { };
for (let source of dataSources) {
// State
state[source.name] = null;
// Actions
actions[source.actionName] = ({ commit }) => {
axios.get(source.url)
.then(({ data }) => commit(source.mutationName, { data }))
.catch(e => console.error(e));
};
// Mutations
mutations[source.mutationName] = (state, { data }) => {
state[source.name] = data;
};
}

View File

@@ -0,0 +1,72 @@
import * as settings from './settings';
import * as regions from './regions';
import * as languages from './languages';
import * as data from './data';
import * as schedules from './schedules';
import * as salmonRun from './salmonRun';
import * as splatfests from './splatfests';
import * as splatNetStore from './splatNetStore';
import * as newWeapons from './newWeapons';
let updateNowTimer;
const state = {
now: null,
};
const getters = {
now(state) {
return state.now;
},
};
const actions = {
updateNow({ state, commit }) {
let now = Math.trunc((new Date).getTime() / 1000);
if (state.now != now)
commit('UPDATE_NOW', { now });
},
setNow({ commit }, { now }) {
commit('UPDATE_NOW', { now });
},
startUpdatingNow({ dispatch }) {
if (updateNowTimer)
return;
dispatch('updateNow');
updateNowTimer = setInterval(() => {
dispatch('updateNow');
}, 200);
},
stopUpdatingNow() {
clearInterval(updateNowTimer);
updateNowTimer = null;
},
};
const mutations = {
UPDATE_NOW(state, { now }) {
state.now = now;
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
modules: {
settings,
regions,
languages,
data,
schedules,
salmonRun,
splatfests,
splatNetStore,
newWeapons,
},
};

View File

@@ -0,0 +1,43 @@
import { languages, detectSplatoonLanguage } from '@/common/regions.esm';
export const namespaced = true;
export const state = {
all: languages,
detectedLanguageKey: detectSplatoonLanguage(),
};
export const getters = {
selectedKey(state, getters, { splatoon }) {
let key = splatoon.settings.selectedLanguage;
return (key === null) ? state.detectedLanguageKey : key;
},
selectedLanguage(state, getters) {
return state.all.find(l => l.language === getters.selectedKey);
},
};
export const actions = {
setLanguage({ dispatch }, { language }) {
let key = (language && language.language) ? language.language : 'en';
dispatch('splatoon/settings/updateSetting',
{ name: 'selectedLanguage', value: key },
{ root: true });
},
languageChanged({ getters, dispatch }) {
let language = getters.selectedLanguage;
if (language) {
dispatch('loadLocale', { locale: language.language });
dispatch('splatoon/data/updateLanguage', null, { root: true });
dispatch('setLocale', { locale: language.language }, { root: true });
}
},
loadLocale({ dispatch }, { locale }) {
// Disabling this for now due to issues with disabling prefetching with the modern build (see vue.config.js)
// import(/* webpackChunkName: "lang-[request]" */ `@/web/locale/${locale}`)
// .then(translations => dispatch('addLocale', { locale, translations: translations.default }, { root: true }));
dispatch('addLocale', { locale, translations: require(`@/web/locale/${locale}`) }, { root: true });
},
};

View File

@@ -0,0 +1,17 @@
export const namespaced = true;
export const getters = {
availabilities(state, getters, { splatoon }) {
let now = splatoon.now;
let availabilities = splatoon.data.timeline
&& splatoon.data.timeline.weapon_availability
&& splatoon.data.timeline.weapon_availability.availabilities;
if (now && availabilities)
return availabilities.filter(a => a.release_time <= now);
},
weapons(state, getters) {
return getters.availabilities
&& getters.availabilities.map(a => a.weapon);
},
};

View File

@@ -0,0 +1,31 @@
import { splatoonRegions, detectSplatoonRegion } from '@/common/regions.esm';
export const namespaced = true;
export const state = {
all: splatoonRegions,
detectedRegionKey: detectSplatoonRegion(),
};
export const getters = {
all(state) {
return state.all;
},
selectedKey(state, getters, { splatoon }) {
let key = splatoon.settings.selectedRegion;
return (key === null) ? state.detectedRegionKey : key;
},
selectedRegion(state, getters) {
return state.all.find(r => r.key === getters.selectedKey);
},
};
export const actions = {
setRegion({ dispatch }, { region }) {
let key = (region && region.key) ? region.key : 'global';
dispatch('splatoon/settings/updateSetting',
{ name: 'selectedRegion', value: key },
{ root: true });
},
};

View File

@@ -0,0 +1,38 @@
export const namespaced = true;
export const getters = {
// All current schedules (active & future)
currentSchedules(state, getters, { splatoon }) {
let now = splatoon.now;
let coopSchedules = splatoon.data.coop_schedules;
if (now && coopSchedules) {
let { schedules, details } = coopSchedules;
return schedules.map(schedule => {
let detail = details.find(d => d.start_time == schedule.start_time);
return detail || schedule;
}).filter(s => s.end_time > now);
}
},
// The currently-active schedule
activeSchedule(state, getters, { splatoon }) {
let now = splatoon.now;
if (now && getters.currentSchedules)
return getters.currentSchedules.find(s => s.start_time <= now);
},
// Future upcoming schedules
upcomingSchedules(state, getters, { splatoon }) {
let now = splatoon.now;
if (now && getters.currentSchedules)
return getters.currentSchedules.filter(s => s.start_time > now);
},
rewardGear(state, getters, { splatoon }) {
return splatoon.data.timeline
&& splatoon.data.timeline.coop
&& splatoon.data.timeline.coop.reward_gear
&& splatoon.data.timeline.coop.reward_gear.gear;
},
};

View File

@@ -0,0 +1,37 @@
export const namespaced = true;
function generateModule(name, source) {
return {
namespaced: true,
getters: {
// All current schedules (active & future)
currentSchedules(state, getters, { splatoon }) {
let now = splatoon.now
let schedules = splatoon.data.schedules;
if (now && schedules)
return schedules[source].filter(s => s.end_time > now);
},
// The currently-active schedule
activeSchedule(state, getters, { splatoon }) {
let now = splatoon.now;
if (now && getters.currentSchedules)
return getters.currentSchedules.find(s => s.start_time <= now);
},
// Future upcoming schedules
upcomingSchedules(state, getters, { splatoon }) {
let now = splatoon.now;
if (now && getters.currentSchedules)
return getters.currentSchedules.filter(s => s.start_time > now);
},
},
};
}
export const modules = {
regular: generateModule('regular', 'regular'),
ranked: generateModule('ranked', 'gachi'),
league: generateModule('league', 'league'),
};

View File

@@ -0,0 +1,66 @@
import store from '@/web/store';
export const namespaced = true;
const settings = [
{
name: 'selectedRegion',
key: 'selected-region',
},
{
name: 'selectedLanguage',
key: 'selected-language',
action: 'splatoon/languages/languageChanged',
},
];
function reloadSettings() {
for (let setting of settings)
store.dispatch('splatoon/settings/loadSetting', { name: setting.name });
}
export const state = {};
export const actions = {
initialize({ dispatch }) {
window.addEventListener('storage', reloadSettings);
for (let setting of settings)
dispatch('loadSetting', { name: setting.name });
},
shutdown() {
window.removeEventListener('storage', reloadSettings);
},
loadSetting({ commit, dispatch }, { name }) {
let setting = settings.find(s => s.name === name);
if (setting) {
let value = localStorage.getItem(setting.key);
commit('UPDATE_SETTING', { name, value });
if (setting.action)
dispatch(setting.action, null, { root: true });
}
},
updateSetting({ commit, dispatch }, { name, value }) {
let setting = settings.find(s => s.name === name);
if (setting) {
localStorage.setItem(setting.key, value);
commit('UPDATE_SETTING', { name, value });
if (setting.action)
dispatch(setting.action, null, { root: true });
}
},
};
export const mutations = {
UPDATE_SETTING(store, { name, value }) {
store[name] = value;
},
};
for (let setting of settings) {
state[setting.name] = null;
}

View File

@@ -0,0 +1,11 @@
export const namespaced = true;
export const getters = {
merchandises(state, getters, { splatoon }) {
let now = splatoon.now;
let merchandises = splatoon.data.merchandises && splatoon.data.merchandises.merchandises;
if (now && merchandises)
return merchandises.filter(m => m.end_time > now);
},
};

View File

@@ -0,0 +1,66 @@
export const namespaced = true;
export const getters = {
anyRegionHasCurrentSplatfest(state, getters, rootState, rootGetters) {
return rootGetters['splatoon/splatfests/na/currentSplatfest']
|| rootGetters['splatoon/splatfests/eu/currentSplatfest']
|| rootGetters['splatoon/splatfests/jp/currentSplatfest'];
},
selectedRegionCurrentSplatfest(state, getters, rootState, rootGetters) {
let region = rootGetters[`splatoon/regions/selectedRegion`];
if (region)
return rootGetters[`splatoon/splatfests/${region.key}/currentSplatfest`];
},
selectedRegionHasActiveSplatfest(state, getters) {
return getters.selectedRegionCurrentSplatfest
&& getters.selectedRegionCurrentSplatfest.state === 'active';
},
};
function generateModule(region) {
return {
namespaced: true,
getters: {
allSplatfests(state, getters, { splatoon }) {
let now = splatoon.now;
let data = splatoon.data.festivals && splatoon.data.festivals[region];
if (now && data) {
let { festivals, results } = data;
return festivals.map(festival => {
let state;
if (festival.times.start > now)
state = 'upcoming';
else if (festival.times.end > now)
state = 'active';
else
state = 'past';
return {
...festival,
state,
results: results.find(r => r.festival_id === festival.festival_id),
};
});
}
},
currentSplatfest(state, getters, { splatoon }) {
let now = splatoon.now;
if (now && getters.allSplatfests) {
// We want to show future/current festivals and festivals that ended recently (so we can show their results).
// Filter based on the result time and show festival results for a few days.
let cutoff = now - 86400 * 3; // 3 days
return getters.allSplatfests.find(s => s.times.result > cutoff);
}
},
},
};
}
export const modules = {
na: generateModule('na'),
eu: generateModule('eu'),
jp: generateModule('jp'),
}