mirror of
https://github.com/misenhower/splatoon2.ink.git
synced 2026-08-22 02:14:21 -05:00
Use Vue Router for the different homepage views
This enables links like /about, /splatnet, and /splatfests. I'm going to have to decouple some things from the main app view in order to use router components properly, but this handles the basic needs for now.
This commit is contained in:
@@ -15,6 +15,10 @@ server {
|
||||
access_log /var/log/nginx/access.log;
|
||||
access_log /logs/access.log;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Force browsers to check for updated data
|
||||
location /data/ {
|
||||
add_header Cache-Control no-cache;
|
||||
|
||||
@@ -1,440 +1,5 @@
|
||||
<template>
|
||||
<div class="hero is-fullheight" id="main">
|
||||
|
||||
<!-- Site header -->
|
||||
<div class="hero-head">
|
||||
<div class="container is-fluid">
|
||||
<div class="level is-marginless">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<h1 class="title is-1 font-splatoon1 is-inline">Splatoon 2</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<h3 class="subtitle is-3 font-splatoon2 is-inline">{{ $t('ui.map_schedules') }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<a href="https://twitter.com/Splatoon2inkbot" target="_blank" class="button is-translucent-dark is-rounded">
|
||||
<span class="font-splatoon2">{{ $t('ui.get_updates_via_twitter') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item" v-if="showFestivalRegionDropdown">
|
||||
<Dropdown :options="regions" v-model="selectedRegionKey" style="margin: 0 -12px" class="font-splatoon2 is-right-tablet" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="level-item" v-if="showFestivalRegionDropdown">
|
||||
<!-- Empty spacer to add some room when festivals are active -->
|
||||
</div>
|
||||
|
||||
<div class="level-item" v-if="merchandises && merchandises.length">
|
||||
<button class="button is-translucent-dark is-rounded" @click="splatNetGearOpen = true">
|
||||
<span class="font-splatoon2">{{ $t('splatnet_gear.button') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main body area -->
|
||||
<div class="hero-body">
|
||||
<div class="container is-fluid">
|
||||
<!-- Loading spinner -->
|
||||
<div v-if="loading" class="has-text-centered">
|
||||
<span class="icon is-large">
|
||||
<span class="fa fa-3x fa-spin squid-refresh"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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 class="column">
|
||||
<div class="splatfest tilt-left">
|
||||
<div class="hook-box">
|
||||
<SplatfestBox :festival="selectedFestival" :now="now"></SplatfestBox>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
<div class="column">
|
||||
<ScheduleBox class="main-schedule-box tilt-right" cssClass="ranked" :schedules="ranked" :now="now"></ScheduleBox>
|
||||
</div>
|
||||
<div class="column">
|
||||
<ScheduleBox class="main-schedule-box tilt-left" cssClass="league" :schedules="league" :now="now"></ScheduleBox>
|
||||
</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="splatfest tilt-right" style="margin-top: 40px">
|
||||
<div class="hook-box">
|
||||
<SplatfestBox :festival="selectedFestival" :results="selectedFestivalResults" :now="now"></SplatfestBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column" v-if="coopSchedules">
|
||||
<div class="salmon-run tilt-left">
|
||||
<div class="hook-box">
|
||||
<SalmonRunBox :coop="coop" :coopSchedules="coopSchedules" :now="now"></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="new-weapon">
|
||||
<div class="columns">
|
||||
<div class="column" v-for="(weapon, index) in newWeapons">
|
||||
<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="new-weapon">
|
||||
<div style="display: flex; align-items: center; justify-content: center;">
|
||||
<div v-for="(weapon, index) in newWeapons" style="margin: 0 20px">
|
||||
<NewWeaponBox
|
||||
:weapon="weapon"
|
||||
:class="(index % 2 == 0) ? 'tilt-right' : 'tilt-left'"
|
||||
style="min-width: 250px"
|
||||
></NewWeaponBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="hero-foot has-text-centered has-text-grey is-size-7" style="margin-bottom: 0.5rem">
|
||||
<div class="container is-fluid">
|
||||
{{ $t('ui.disclaimer') }}
|
||||
–
|
||||
<a href="#" @click.prevent="aboutOpen = true">{{ $t('ui.about') }}</a>
|
||||
–
|
||||
<a href="https://twitter.com/Splatoon2inkbot" target="_blank">Twitter</a>
|
||||
<template v-if="splatnet.festivals">
|
||||
–
|
||||
<a href="#" @click.prevent="splatfestHistoryOpen = true">{{ $t('splatfest.previous') }}</a>
|
||||
</template>
|
||||
–
|
||||
<Dropdown :options="languages" v-model="selectedLanguageKey" tag="a" class="is-right is-up" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AboutDialog v-if="aboutOpen" @close="aboutOpen = false"></AboutDialog>
|
||||
<SplatNetGearDialog
|
||||
v-if="splatNetGearOpen"
|
||||
:merchandises="merchandises"
|
||||
:now="now"
|
||||
@close="splatNetGearOpen = false"
|
||||
></SplatNetGearDialog>
|
||||
<SplatfestHistoryDialog
|
||||
v-if="splatfestHistoryOpen"
|
||||
:allFestivals="splatnet.festivals"
|
||||
:now="now"
|
||||
:initialRegion="selectedRegionKey"
|
||||
@close="splatfestHistoryOpen = false"
|
||||
/>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#main .limited-width {
|
||||
max-width: 1392px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { mapActions } from 'vuex';
|
||||
import regions from '@/js/regions';
|
||||
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,
|
||||
},
|
||||
aboutOpen: false,
|
||||
splatNetGearOpen: false,
|
||||
splatfestHistoryOpen: false,
|
||||
language: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
language(value) {
|
||||
if (value)
|
||||
this.loadLocale(value);
|
||||
this.$i18n.set(value || 'en');
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
loading() { return !this.splatnet.schedules; },
|
||||
|
||||
// Selected region
|
||||
regions() {
|
||||
return regions.splatoonRegions.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); },
|
||||
},
|
||||
|
||||
// Selected langauge
|
||||
languages() {
|
||||
return regions.languages
|
||||
.reduce((languages, languageInfo) => {
|
||||
// Remove duplicates
|
||||
if (!languages.find(li => li.language == languageInfo.language))
|
||||
languages.push(languageInfo)
|
||||
return languages;
|
||||
}, [])
|
||||
.map(({ language, name }) => ({ key: language, name }));
|
||||
},
|
||||
selectedLanguageKey: {
|
||||
get() { return this.language; },
|
||||
set(value) { this.setLanguage(value); },
|
||||
},
|
||||
|
||||
// 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);
|
||||
},
|
||||
|
||||
// Other
|
||||
bottomRowHasThreeColumns() {
|
||||
return this.selectedFestival && !this.isSelectedFestivalActive && (this.coop || this.coopSchedules) && this.newWeapons;
|
||||
},
|
||||
},
|
||||
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();
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.updateNowTimer);
|
||||
clearInterval(this.updateDataTimer);
|
||||
window.removeEventListener('storage', this.loadLanguage);
|
||||
window.removeEventListener('storage', this.loadRegion);
|
||||
},
|
||||
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;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
446
src/js/components/Main.vue
Normal file
446
src/js/components/Main.vue
Normal file
@@ -0,0 +1,446 @@
|
||||
<template>
|
||||
<div class="hero is-fullheight" id="main">
|
||||
|
||||
<!-- Site header -->
|
||||
<div class="hero-head">
|
||||
<div class="container is-fluid">
|
||||
<div class="level is-marginless">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<h1 class="title is-1 font-splatoon1 is-inline">Splatoon 2</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<h3 class="subtitle is-3 font-splatoon2 is-inline">{{ $t('ui.map_schedules') }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<a href="https://twitter.com/Splatoon2inkbot" target="_blank" class="button is-translucent-dark is-rounded">
|
||||
<span class="font-splatoon2">{{ $t('ui.get_updates_via_twitter') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item" v-if="showFestivalRegionDropdown">
|
||||
<Dropdown :options="regions" v-model="selectedRegionKey" style="margin: 0 -12px" class="font-splatoon2 is-right-tablet" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="level-item" v-if="showFestivalRegionDropdown">
|
||||
<!-- Empty spacer to add some room when festivals are active -->
|
||||
</div>
|
||||
|
||||
<div class="level-item" v-if="merchandises && merchandises.length">
|
||||
<router-link to="/splatnet" class="button is-translucent-dark is-rounded">
|
||||
<span class="font-splatoon2">{{ $t('splatnet_gear.button') }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main body area -->
|
||||
<div class="hero-body">
|
||||
<div class="container is-fluid">
|
||||
<!-- Loading spinner -->
|
||||
<div v-if="loading" class="has-text-centered">
|
||||
<span class="icon is-large">
|
||||
<span class="fa fa-3x fa-spin squid-refresh"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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 class="column">
|
||||
<div class="splatfest tilt-left">
|
||||
<div class="hook-box">
|
||||
<SplatfestBox :festival="selectedFestival" :now="now"></SplatfestBox>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
<div class="column">
|
||||
<ScheduleBox class="main-schedule-box tilt-right" cssClass="ranked" :schedules="ranked" :now="now"></ScheduleBox>
|
||||
</div>
|
||||
<div class="column">
|
||||
<ScheduleBox class="main-schedule-box tilt-left" cssClass="league" :schedules="league" :now="now"></ScheduleBox>
|
||||
</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="splatfest tilt-right" style="margin-top: 40px">
|
||||
<div class="hook-box">
|
||||
<SplatfestBox :festival="selectedFestival" :results="selectedFestivalResults" :now="now"></SplatfestBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column" v-if="coopSchedules">
|
||||
<div class="salmon-run tilt-left">
|
||||
<div class="hook-box">
|
||||
<SalmonRunBox :coop="coop" :coopSchedules="coopSchedules" :now="now"></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="new-weapon">
|
||||
<div class="columns">
|
||||
<div class="column" v-for="(weapon, index) in newWeapons">
|
||||
<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="new-weapon">
|
||||
<div style="display: flex; align-items: center; justify-content: center;">
|
||||
<div v-for="(weapon, index) in newWeapons" style="margin: 0 20px">
|
||||
<NewWeaponBox
|
||||
:weapon="weapon"
|
||||
:class="(index % 2 == 0) ? 'tilt-right' : 'tilt-left'"
|
||||
style="min-width: 250px"
|
||||
></NewWeaponBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="hero-foot has-text-centered has-text-grey is-size-7" style="margin-bottom: 0.5rem">
|
||||
<div class="container is-fluid">
|
||||
{{ $t('ui.disclaimer') }}
|
||||
–
|
||||
<router-link to="/about">{{ $t('ui.about') }}</router-link>
|
||||
–
|
||||
<a href="https://twitter.com/Splatoon2inkbot" target="_blank">Twitter</a>
|
||||
<template v-if="splatnet.festivals">
|
||||
–
|
||||
<router-link to="/splatfests">{{ $t('splatfest.previous') }}</router-link>
|
||||
</template>
|
||||
–
|
||||
<Dropdown :options="languages" v-model="selectedLanguageKey" tag="a" class="is-right is-up" />
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<style>
|
||||
#main .limited-width {
|
||||
max-width: 1392px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { mapActions } from 'vuex';
|
||||
import regions from '@/js/regions';
|
||||
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');
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
loading() { return !this.splatnet.schedules; },
|
||||
|
||||
// Selected region
|
||||
regions() {
|
||||
return regions.splatoonRegions.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); },
|
||||
},
|
||||
|
||||
// Selected langauge
|
||||
languages() {
|
||||
return regions.languages
|
||||
.reduce((languages, languageInfo) => {
|
||||
// Remove duplicates
|
||||
if (!languages.find(li => li.language == languageInfo.language))
|
||||
languages.push(languageInfo)
|
||||
return languages;
|
||||
}, [])
|
||||
.map(({ language, name }) => ({ key: language, name }));
|
||||
},
|
||||
selectedLanguageKey: {
|
||||
get() { return this.language; },
|
||||
set(value) { this.setLanguage(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);
|
||||
},
|
||||
|
||||
// Other
|
||||
bottomRowHasThreeColumns() {
|
||||
return this.selectedFestival && !this.isSelectedFestivalActive && (this.coop || this.coopSchedules) && this.newWeapons;
|
||||
},
|
||||
},
|
||||
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();
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.updateNowTimer);
|
||||
clearInterval(this.updateDataTimer);
|
||||
window.removeEventListener('storage', this.loadLanguage);
|
||||
window.removeEventListener('storage', this.loadRegion);
|
||||
},
|
||||
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;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Modal @close="$emit('close')" class="is-wide splatfest-history-dialog modal-rounded-box">
|
||||
<Modal @close="$emit('close')" class="is-wide splatfest-history-dialog modal-rounded-box" v-if="allFestivals">
|
||||
<div class="modal-card">
|
||||
<div class="modal-card-head">
|
||||
<div class="level">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import store from './store';
|
||||
import router from './router';
|
||||
|
||||
import './i18n';
|
||||
import './directives.js';
|
||||
@@ -10,6 +11,7 @@ import App from './components/App.vue';
|
||||
new Vue({
|
||||
render: h => h(App),
|
||||
store,
|
||||
router,
|
||||
}).$mount('#app');
|
||||
|
||||
// Start analytics
|
||||
|
||||
30
src/js/router.js
Normal file
30
src/js/router.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: require('./components/Main.vue').default,
|
||||
children: [
|
||||
{
|
||||
path: 'about',
|
||||
component: require('./components/AboutDialog.vue').default,
|
||||
},
|
||||
{
|
||||
path: 'splatnet',
|
||||
},
|
||||
{
|
||||
path: 'splatfests',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -30,7 +30,10 @@ module.exports = function(env) {
|
||||
chunkFilename: 'assets/js/[name].[hash:6].js',
|
||||
},
|
||||
devtool: (production) ? false : '#cheap-module-eval-source-map',
|
||||
devServer: { overlay: true },
|
||||
devServer: {
|
||||
overlay: true,
|
||||
historyApiFallback: true,
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user