diff --git a/.env.example b/.env.example
index 649dec7..90a3ffa 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,9 @@
# Site Config
SITE_URL=https://splatoon3.ink
+# Load data from Splatoon3.ink during development
+VITE_DATA_FROM=https://splatoon3.ink
+
# Nintendo API
NINTENDO_TOKEN=
diff --git a/app/social/generators/SplatfestStatus.mjs b/app/social/generators/SplatfestStatus.mjs
new file mode 100644
index 0000000..531e941
--- /dev/null
+++ b/app/social/generators/SplatfestStatus.mjs
@@ -0,0 +1,91 @@
+import StatusGenerator from "./StatusGenerator.mjs";
+import Media from "../Media.mjs";
+import { useUSSplatfestsStore, STATUS_ACTIVE, STATUS_PAST } from '../../../src/stores/splatfests.mjs';
+import { useTimeStore } from "../../../src/stores/time.mjs";
+
+export default class SplatfestStatus extends StatusGenerator
+{
+ key = 'splatfest';
+ name = 'Splatfest';
+
+ async getFestival() {
+ await this.preparePinia();
+ let store = useUSSplatfestsStore();
+
+ return store.upcomingFestival
+ || store.activeFestival
+ || store.recentFestival;
+ }
+
+ async getState() {
+ let festival = await this.getFestival();
+
+ let startTime = Date.parse(festival?.startTime);
+ let now = useTimeStore().now;
+ let hoursAway = (startTime - now) / (1000 * 60 * 60);
+
+ switch (true) {
+ case !festival:
+ return null;
+ case festival.status === STATUS_PAST:
+ return 'ended';
+ case festival.status === STATUS_ACTIVE:
+ return 'active';
+ case hoursAway <= 24:
+ return 'upcoming:1d';
+ case hoursAway <= 72:
+ return 'upcoming:3d';
+ default:
+ return 'upcoming';
+ }
+ }
+
+ async getDataTime() {
+ let festival = await this.getFestival();
+ let state = await this.getState();
+
+ if (!festival || !state) {
+ return null;
+ }
+
+ return `${festival.id}:${state}`;
+ }
+
+ async shouldPost(client) {
+ let currentId = await this.getDataTime();
+
+ let cachedId = await this.lastPostCache(client).getData();
+
+ return currentId && currentId !== cachedId;
+ }
+
+ async _getStatus() {
+ let festival = await this.getFestival();
+ let state = await this.getState();
+
+ if (!festival || !state) {
+ return false;
+ }
+
+ switch (state) {
+ case 'upcoming':
+ return `You can now vote in the next global Splatfest: ${festival.title} #splatfest #splatoon3`;
+ case 'upcoming:3d':
+ return `Reminder: The next global Splatfest starts in 3 DAYS! ${festival.title} #splatfest #splatoon3`
+ case 'upcoming:1d':
+ return `Reminder: The next global Splatfest starts in 24 HOURS! ${festival.title} #splatfest #splatoon3`
+ case 'active':
+ return `The global Splatfest is NOW OPEN! ${festival.title} #splatfest #splatoon3`
+ case 'ended':
+ return `The global Splatfest is now closed. Results are usually posted within 2 hours! #splatfest #splatoon3`
+ }
+ }
+
+ /** @param {ScreenshotHelper} screenshotHelper */
+ async _getMedia(screenshotHelper) {
+ let media = new Media;
+ media.file = await screenshotHelper.capture('splatfest');
+
+ return media;
+ }
+}
diff --git a/app/social/index.mjs b/app/social/index.mjs
index 01ed7bc..5ef6e11 100644
--- a/app/social/index.mjs
+++ b/app/social/index.mjs
@@ -6,6 +6,7 @@ import RegularGearStatus from "./generators/RegularGearStatus.mjs";
import SalmonRunGearStatus from "./generators/SalmonRunGearStatus.mjs";
import SalmonRunStatus from "./generators/SalmonRunStatus.mjs";
import SchedulesStatus from "./generators/SchedulesStatus.mjs";
+import SplatfestStatus from "./generators/SplatfestStatus.mjs";
import SplatfestResultsStatus from "./generators/SplatfestResultsStatus.mjs";
import StatusGeneratorManager from "./StatusGeneratorManager.mjs"
@@ -16,6 +17,7 @@ function defaultStatusGenerators() {
new RegularGearStatus,
new SalmonRunStatus,
new SalmonRunGearStatus,
+ new SplatfestStatus,
new SplatfestResultsStatus,
];
}
diff --git a/src/layouts/ScreenshotLayout.vue b/src/layouts/ScreenshotLayout.vue
index 8991634..b5e8679 100644
--- a/src/layouts/ScreenshotLayout.vue
+++ b/src/layouts/ScreenshotLayout.vue
@@ -18,6 +18,10 @@
@splatoon3ink
+