From f9e3e9f15a90e595cb5b3227afd597b18198381b Mon Sep 17 00:00:00 2001
From: Thibault <54189871+Asaren1070@users.noreply.github.com>
Date: Thu, 26 Jun 2025 11:06:08 +0100
Subject: [PATCH] 457 upload the last image from hypso 1 and hypso 2 (#461)
* feat: adding a tab bar for the satellite individual page
* feat: uploading the last images of satellite
* fix: lint
* fix lint
---
backend/.env.example | 3 +
backend/package-lock.json | 133 ++++++
backend/package.json | 20 +-
.../api/satellite/controllers/satellite.js | 6 +-
backend/src/api/satellite/routes/satellite.js | 6 +-
backend/src/api/slack/controllers/slack.js | 58 +++
backend/src/api/slack/routes/slack.js | 25 +
backend/src/api/slack/services/slack.js | 45 ++
.../app/satellites/[satelliteSlug]/page.tsx | 72 +--
.../satellites/[satelliteSlug]/satImage.tsx | 132 ++++++
.../satellites/[satelliteSlug]/satInfo.tsx | 70 +++
.../satellites/[satelliteSlug]/satTabs.tsx | 58 +++
.../satellites/[satelliteSlug]/tabBars.tsx | 41 ++
frontend/src/app/satellites/tabContext.tsx | 29 ++
frontend/src/lib/utils.ts | 33 ++
package-lock.json | 431 ++++++++++++++++++
package.json | 1 +
17 files changed, 1082 insertions(+), 81 deletions(-)
create mode 100644 backend/src/api/slack/controllers/slack.js
create mode 100644 backend/src/api/slack/routes/slack.js
create mode 100644 backend/src/api/slack/services/slack.js
create mode 100644 frontend/src/app/satellites/[satelliteSlug]/satImage.tsx
create mode 100644 frontend/src/app/satellites/[satelliteSlug]/satInfo.tsx
create mode 100644 frontend/src/app/satellites/[satelliteSlug]/satTabs.tsx
create mode 100644 frontend/src/app/satellites/[satelliteSlug]/tabBars.tsx
create mode 100644 frontend/src/app/satellites/tabContext.tsx
diff --git a/backend/.env.example b/backend/.env.example
index 680ee45..620a6b0 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -9,6 +9,9 @@ API_TOKEN_SALT=toBeGenerated
ADMIN_JWT_SECRET=toBeGenerated
TRANSFER_TOKEN_SALT=toBeGenerated
JWT_SECRET=toBeGenerated
+SLACK_BOT_TOKEN=toGetFromGitHub
+SLACK_USER_TOKEN=toGetFromGitHub
+SLACK_CHANNEL_ID=toGetFromGitHub
DATABASE_CLIENT=sqlite
DATABASE_FILENAME=dbLocation.sqlite # in dev, will use backend root dir, in prod, will be from server os root dir so should be /var/data/strapi.db or similar
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 74ac57f..62c3631 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.1.0",
"license": "MIT",
"dependencies": {
+ "@slack/web-api": "^7.9.3",
"@strapi/plugin-cloud": "4.20.5",
"@strapi/plugin-graphql": "4.20.5",
"@strapi/plugin-i18n": "4.20.5",
@@ -3108,6 +3109,64 @@
"node": ">=8"
}
},
+ "node_modules/@slack/logger": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz",
+ "integrity": "sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": ">=18.0.0"
+ },
+ "engines": {
+ "node": ">= 18",
+ "npm": ">= 8.6.0"
+ }
+ },
+ "node_modules/@slack/types": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.14.0.tgz",
+ "integrity": "sha512-n0EGm7ENQRxlXbgKSrQZL69grzg1gHLAVd+GlRVQJ1NSORo0FrApR7wql/gaKdu2n4TO83Sq/AmeUOqD60aXUA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0",
+ "npm": ">= 6.12.0"
+ }
+ },
+ "node_modules/@slack/web-api": {
+ "version": "7.9.3",
+ "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.9.3.tgz",
+ "integrity": "sha512-xjnoldVJyoUe61Ltqjr2UVYBolcsTpp5ottqzSI3l41UCaJgHSHIOpGuYps+nhLFvDfGGpDGUeQ7BWiq3+ypqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@slack/logger": "^4.0.0",
+ "@slack/types": "^2.9.0",
+ "@types/node": ">=18.0.0",
+ "@types/retry": "0.12.0",
+ "axios": "^1.8.3",
+ "eventemitter3": "^5.0.1",
+ "form-data": "^4.0.0",
+ "is-electron": "2.2.2",
+ "is-stream": "^2",
+ "p-queue": "^6",
+ "p-retry": "^4",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">= 18",
+ "npm": ">= 8.6.0"
+ }
+ },
+ "node_modules/@slack/web-api/node_modules/axios": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
+ "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/@strapi/admin": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@strapi/admin/-/admin-4.24.0.tgz",
@@ -5082,6 +5141,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
+ "license": "MIT"
+ },
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
@@ -8536,6 +8601,12 @@
"node": ">=4.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "license": "MIT"
+ },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -10471,6 +10542,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-electron": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz",
+ "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==",
+ "license": "MIT"
+ },
"node_modules/is-extendable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
@@ -12714,6 +12791,15 @@
"node": ">=8"
}
},
+ "node_modules/p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/p-is-promise": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz",
@@ -12764,6 +12850,53 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/p-queue": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
+ "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.4",
+ "p-timeout": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-queue/node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "license": "MIT"
+ },
+ "node_modules/p-retry": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
+ "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "0.12.0",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-timeout": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
+ "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
+ "license": "MIT",
+ "dependencies": {
+ "p-finally": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
diff --git a/backend/package.json b/backend/package.json
index 179f92f..32f3970 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,16 +1,20 @@
{
"name": "backend",
- "private": true,
"version": "0.1.0",
+ "private": true,
"description": "A Strapi application",
+ "license": "MIT",
+ "author": {
+ "name": "A Strapi developer"
+ },
"scripts": {
+ "build": "strapi build",
"develop": "strapi develop",
"start": "strapi start",
- "build": "strapi build",
"strapi": "strapi"
},
- "devDependencies": {},
"dependencies": {
+ "@slack/web-api": "^7.9.3",
"@strapi/plugin-cloud": "4.20.5",
"@strapi/plugin-graphql": "4.20.5",
"@strapi/plugin-i18n": "4.20.5",
@@ -22,15 +26,11 @@
"react-router-dom": "5.3.4",
"styled-components": "5.3.3"
},
- "author": {
- "name": "A Strapi developer"
- },
- "strapi": {
- "uuid": "93a36a2f-b139-4841-a8b1-b0d444d507e1"
- },
"engines": {
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
- "license": "MIT"
+ "strapi": {
+ "uuid": "93a36a2f-b139-4841-a8b1-b0d444d507e1"
+ }
}
diff --git a/backend/src/api/satellite/controllers/satellite.js b/backend/src/api/satellite/controllers/satellite.js
index 0f4851d..71170e1 100644
--- a/backend/src/api/satellite/controllers/satellite.js
+++ b/backend/src/api/satellite/controllers/satellite.js
@@ -1,9 +1,9 @@
-'use strict';
+"use strict";
/**
* satellite controller
*/
-const { createCoreController } = require('@strapi/strapi').factories;
+const { createCoreController } = require("@strapi/strapi").factories;
-module.exports = createCoreController('api::satellite.satellite');
\ No newline at end of file
+module.exports = createCoreController("api::satellite.satellite");
diff --git a/backend/src/api/satellite/routes/satellite.js b/backend/src/api/satellite/routes/satellite.js
index 113cfcf..ef0b0e8 100644
--- a/backend/src/api/satellite/routes/satellite.js
+++ b/backend/src/api/satellite/routes/satellite.js
@@ -1,9 +1,9 @@
-'use strict';
+"use strict";
/**
* satellite router
*/
-const { createCoreRouter } = require('@strapi/strapi').factories;
+const { createCoreRouter } = require("@strapi/strapi").factories;
-module.exports = createCoreRouter('api::satellite.satellite');
+module.exports = createCoreRouter("api::satellite.satellite");
diff --git a/backend/src/api/slack/controllers/slack.js b/backend/src/api/slack/controllers/slack.js
new file mode 100644
index 0000000..f1e57d9
--- /dev/null
+++ b/backend/src/api/slack/controllers/slack.js
@@ -0,0 +1,58 @@
+"use strict";
+
+/**
+ * A set of functions called "actions" for `slack`
+ */
+
+// esl-lint-disable-next-line no-unused-vars
+const fetchImagesFromSlack = require("../services/slack");
+const fetch = require("node-fetch");
+
+module.exports = {
+ fetchImages: async (ctx) => {
+ try {
+ const images = await strapi
+ .service("api::slack.slack")
+ .fetchImagesFromSlack();
+ return ctx.send(images);
+ } catch (error) {
+ return ctx.throw(
+ 500,
+ "Error fetching images from Slack: " + error.message
+ );
+ }
+ },
+ getSharedURL: async (ctx) => {
+ const { fileId } = ctx.request.body;
+ const slackToken = process.env.SLACK_BOT_TOKEN;
+
+ if (!fileId) {
+ return ctx.badRequest("File ID is required");
+ }
+
+ try {
+ const response = await fetch(
+ "https://slack.com/api/files.sharedPublicURL",
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${process.env.SLACK_USER_TOKEN}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ file: fileId }),
+ }
+ );
+
+ const data = await response.json();
+ if (!data.ok && !data.error.includes("already_public")) {
+ throw new Error(data.error || "Failed to make the image public");
+ }
+ ctx.send({
+ message: "Image has been made public successfully",
+ });
+ } catch (error) {
+ console.error("Error generating public URL:", error);
+ ctx.internalServerError("Failed to make the image URL " + fileId);
+ }
+ },
+};
diff --git a/backend/src/api/slack/routes/slack.js b/backend/src/api/slack/routes/slack.js
new file mode 100644
index 0000000..73dd28f
--- /dev/null
+++ b/backend/src/api/slack/routes/slack.js
@@ -0,0 +1,25 @@
+"use strict";
+
+/**
+ * slack routes
+ */
+module.exports = {
+ routes: [
+ {
+ method: "GET",
+ path: "/slack-images",
+ handler: "slack.fetchImages",
+ config: {
+ auth: false,
+ },
+ },
+ {
+ method: "POST",
+ path: "/slack-shared-url",
+ handler: "slack.getSharedURL",
+ config: {
+ auth: false,
+ },
+ },
+ ],
+};
diff --git a/backend/src/api/slack/services/slack.js b/backend/src/api/slack/services/slack.js
new file mode 100644
index 0000000..8475735
--- /dev/null
+++ b/backend/src/api/slack/services/slack.js
@@ -0,0 +1,45 @@
+"use strict";
+
+const { WebClient } = require("@slack/web-api");
+const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
+
+/**
+ * slack service
+ */
+
+let cachedImages = null;
+let cacheTimestamp = null;
+
+module.exports = {
+ fetchImagesFromSlack: async () => {
+ const CACHE_DURATION = 60 * 1000; // 1 minute
+ const now = Date.now();
+
+ if (
+ cachedImages &&
+ cacheTimestamp &&
+ now - cacheTimestamp < CACHE_DURATION
+ ) {
+ console.log("Returning cached images");
+ return cachedImages;
+ }
+
+ try {
+ const result = await slack.conversations.history({
+ channel: process.env.SLACK_CHANNEL_ID,
+ limit: 20,
+ });
+ cachedImages = result.messages.filter(
+ (msg) =>
+ msg.bot_profile?.name === "hypso1bot" &&
+ msg.files &&
+ msg.files.some((file) => file.mimetype.startsWith("image/"))
+ );
+ cacheTimestamp = now;
+ return cachedImages;
+ } catch (error) {
+ console.error("Error fetching images from Slack:", error);
+ throw error;
+ }
+ },
+};
diff --git a/frontend/src/app/satellites/[satelliteSlug]/page.tsx b/frontend/src/app/satellites/[satelliteSlug]/page.tsx
index d511f71..52bb99a 100644
--- a/frontend/src/app/satellites/[satelliteSlug]/page.tsx
+++ b/frontend/src/app/satellites/[satelliteSlug]/page.tsx
@@ -2,19 +2,18 @@ import React from "react";
import BlockRendererClient from "@/components/shared/BlockRendererClient";
import RelatedProjectsAndSatellites from "@/components/shared/RelatedProjectsAndSatellites";
import Map2d from "@/app/satellites/[satelliteSlug]/_2dmap/Map2d";
-import SatelliteDataHome from "@/components/satelliteData/SatelliteDataHome";
import LaunchDateCountDown from "@/app/satellites/[satelliteSlug]/launchDateCountDown";
import {
PageHeader,
PageSubtitle,
PageHeaderAndSubtitle,
} from "@/components/layout/PageHeader";
-import Image from "next/image";
import { SatelliteNumber } from "@/lib/store";
import { graphql } from "@/lib/tada/graphql";
import { getClient } from "@/lib/ApolloClient";
import OrbitDataGraph from "./orbitDataGraph";
-import Render3DMod from "../render3DMod";
+import SatInfo from "./satInfo";
+import { SatAttributes } from "@/lib/utils";
export interface ProjectOrSatellite {
id: string;
@@ -64,13 +63,6 @@ export default async function SatelliteInfoPage({
let noradId = Number(satAttributes?.catalogNumberNORAD) as SatelliteNumber;
// Get the satellite image
- let satelliteImage = satAttributes?.satelliteImage?.data?.attributes?.url;
-
- let imageURL = undefined;
- if (STRAPI_URL && satelliteImage) {
- imageURL = STRAPI_URL + satelliteImage;
- }
- const is3DModel = (satelliteImage ?? "").endsWith(".glb");
return (
<>
@@ -84,61 +76,11 @@ export default async function SatelliteInfoPage({
- {/* Container for satname, stats and sat image */}
-
-
- {/* Stats Container */}
-
-
-
-
NORAD ID:
- {noradId ? (
-
- {noradId}
-
- ) : (
-
- No NORAD ID has been assigned yet{" "}
-
- )}
-
-
-
- {satAttributes?.massKg
- ? "Mass: " + satAttributes?.massKg + " kg"
- : null}
-
-
- {satAttributes?.missionStatus === "IN ORBIT" ? (
-
-
-
- ) : null}
-
- {/* Image container */}
-
-
-
- {imageURL ? (
- is3DModel ? (
-
- ) : (
-
- )
- ) : null}
-
-
-
+ {/* Container for satellite info*/}
+
{/* Container for launch date */}
{noradId && satAttributes?.launchDate ? (
diff --git a/frontend/src/app/satellites/[satelliteSlug]/satImage.tsx b/frontend/src/app/satellites/[satelliteSlug]/satImage.tsx
new file mode 100644
index 0000000..a7324ad
--- /dev/null
+++ b/frontend/src/app/satellites/[satelliteSlug]/satImage.tsx
@@ -0,0 +1,132 @@
+"use client";
+import React, { useEffect, useState, useCallback } from "react";
+import { useSatelliteStore } from "@/lib/store";
+import { SatelliteNumber } from "@/lib/store";
+
+export default function SatImage({
+ STRAPI_URL,
+ noradID,
+}: {
+ STRAPI_URL: string | undefined;
+ noradID: number | undefined;
+}) {
+ const [selectedSatellite] = useSatelliteStore((state) => [
+ state.selectedSatellite,
+ ]);
+ const satNumToEntry = useSatelliteStore((state) => state.satNumToEntry);
+
+ const [satImage, setSatImage] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const makeTheImagePublic = useCallback(
+ async (ID: number) => {
+ const requestDetails = {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ fileId: ID,
+ }),
+ };
+
+ const responseUrl = await fetch(
+ STRAPI_URL + "/api/slack-shared-url",
+ requestDetails,
+ );
+ if (!responseUrl.ok) {
+ throw new Error(`HTTP error! status: ${responseUrl.status}`);
+ }
+ },
+ [STRAPI_URL],
+ );
+
+ const createImageUrl = useCallback(
+ (originalURL: string, fileName: string) => {
+ if (originalURL !== undefined) {
+ const lastSegment = originalURL.split("/").pop();
+ if (!lastSegment) return;
+ const arrayInfo = lastSegment.split("-");
+ const userTeam = arrayInfo[0];
+ const fileId = arrayInfo[1];
+ const pubSecret = arrayInfo[2];
+ const fileNameLowered = fileName.toLowerCase();
+ return `https://files.slack.com/files-pri/${userTeam}-${fileId}/${fileNameLowered}?pub_secret=${pubSecret}`;
+ }
+ },
+ [],
+ );
+
+ useEffect(() => {
+ async function fetchSlackImages() {
+ try {
+ setLoading(true);
+ const response = await fetch(STRAPI_URL + "/api/slack-images");
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ interface SlackFile {
+ id: number;
+ name: string;
+ permalink_public: string;
+ }
+
+ interface SlackMessage {
+ text: string;
+ files: SlackFile[];
+ }
+
+ const rightMessage: SlackMessage | undefined = (
+ data as SlackMessage[]
+ ).find((message: SlackMessage) => {
+ if (noradID !== undefined) {
+ const satName: string | undefined =
+ satNumToEntry[noradID as SatelliteNumber]?.name;
+ if (satName && message.text.includes(satName)) {
+ return true;
+ }
+ }
+ return false;
+ });
+
+ const rightFile: SlackFile | undefined = rightMessage?.files[0];
+ makeTheImagePublic(rightFile?.id as number).catch((err) => {
+ console.error("Error making image public:", err);
+ setError("Failed to make image public.");
+ });
+ const imageUrl = createImageUrl(
+ rightFile?.permalink_public as string,
+ rightFile?.name as string,
+ );
+ setSatImage(imageUrl ?? null);
+ } catch (err) {
+ console.error("Error fetching satellite images:", err);
+ setError("Failed to load satellite images.");
+ } finally {
+ setLoading(false);
+ }
+ }
+ fetchSlackImages();
+ }, [satImage, selectedSatellite]);
+
+ if (loading) {
+ return Loading satellite image...
;
+ }
+ if (error) {
+ return {error}
;
+ }
+ return satImage ? (
+
+

+
+ ) : null;
+}
diff --git a/frontend/src/app/satellites/[satelliteSlug]/satInfo.tsx b/frontend/src/app/satellites/[satelliteSlug]/satInfo.tsx
new file mode 100644
index 0000000..f175243
--- /dev/null
+++ b/frontend/src/app/satellites/[satelliteSlug]/satInfo.tsx
@@ -0,0 +1,70 @@
+"use client";
+import React, { useState, useEffect } from "react";
+import Image from "next/image";
+import Render3DMod from "../render3DMod";
+import SatTabs from "./satTabs";
+import { SatAttributes } from "@/lib/utils";
+import TabBar from "./tabBars";
+import { TabProvider } from "../tabContext";
+
+export default function SatInfo({
+ satAttributes,
+ STRAPI_URL,
+}: {
+ satAttributes: SatAttributes;
+ STRAPI_URL: string | undefined;
+}) {
+ const [imageURL, setImageURL] = useState(undefined);
+ const [is3DModel, setIs3DModel] = useState(false);
+ useEffect(() => {
+ let satelliteImage =
+ satAttributes?.satelliteImage?.data?.attributes?.url;
+ if (STRAPI_URL && satelliteImage) {
+ const fullImage = STRAPI_URL + satelliteImage;
+ setImageURL(fullImage);
+ setIs3DModel(
+ satelliteImage.endsWith(".glb") ||
+ satelliteImage.endsWith(".gltf") ||
+ satelliteImage.endsWith(".glb?"),
+ );
+ }
+ }, [satAttributes, STRAPI_URL]);
+
+ return (
+ <>
+ {" "}
+
+
+
+
+ {/* Container for satname, stats and sat image */}
+
+ {/* Stats Container */}
+
+
+ {/* Image container */}
+
+
+ {imageURL ? (
+ is3DModel ? (
+
+ ) : (
+
+ )
+ ) : null}
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/app/satellites/[satelliteSlug]/satTabs.tsx b/frontend/src/app/satellites/[satelliteSlug]/satTabs.tsx
new file mode 100644
index 0000000..7502b53
--- /dev/null
+++ b/frontend/src/app/satellites/[satelliteSlug]/satTabs.tsx
@@ -0,0 +1,58 @@
+"use client";
+import React from "react";
+import SatelliteDataHome from "@/components/satelliteData/SatelliteDataHome";
+import { SatelliteNumber } from "@/lib/store";
+import { SatAttributes } from "@/lib/utils";
+import { useTabContext } from "../tabContext";
+import SatImage from "./satImage";
+
+export default function SatTabs({
+ satAttributes,
+ STRAPI_URL,
+}: {
+ satAttributes: SatAttributes;
+ STRAPI_URL: string | undefined;
+}) {
+ let noradId = Number(satAttributes?.catalogNumberNORAD) as SatelliteNumber;
+ const { selectedTab } = useTabContext();
+ return (
+
+ {selectedTab === "sat parameters" ? (
+ // Render the parameters of the satellite
+
+
+
+
NORAD ID:
+ {noradId ? (
+
+ {noradId}
+
+ ) : (
+
+ No NORAD ID has been assigned yet{" "}
+
+ )}
+
+
+
+ {satAttributes?.massKg
+ ? "Mass: " + satAttributes?.massKg + " kg"
+ : null}
+
+
+ {satAttributes?.missionStatus === "IN ORBIT" ? (
+
+
+
+ ) : null}
+
+ ) : selectedTab === "satellite image" ? (
+
+ ) : null}
+
+ );
+}
diff --git a/frontend/src/app/satellites/[satelliteSlug]/tabBars.tsx b/frontend/src/app/satellites/[satelliteSlug]/tabBars.tsx
new file mode 100644
index 0000000..1478028
--- /dev/null
+++ b/frontend/src/app/satellites/[satelliteSlug]/tabBars.tsx
@@ -0,0 +1,41 @@
+"use client";
+import React from "react";
+import { useTabContext } from "@/app/satellites/tabContext";
+
+export default function TabBar() {
+ const { selectedTab, setSelectedTab } = useTabContext();
+ return (
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/satellites/tabContext.tsx b/frontend/src/app/satellites/tabContext.tsx
new file mode 100644
index 0000000..617904f
--- /dev/null
+++ b/frontend/src/app/satellites/tabContext.tsx
@@ -0,0 +1,29 @@
+"use client";
+import React, { createContext, useContext, useState, ReactNode } from "react";
+
+type TabType = "sat parameters" | "satellite image" | "satellite telemetry";
+
+interface TabContextType {
+ selectedTab: TabType;
+ // eslint-disable-next-line no-unused-vars
+ setSelectedTab: (tab: TabType) => void;
+}
+
+const TabContext = createContext(undefined);
+export function TabProvider({ children }: { children: ReactNode }) {
+ const [selectedTab, setSelectedTab] = useState("sat parameters");
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useTabContext(): TabContextType {
+ const context = useContext(TabContext);
+ if (!context) {
+ throw new Error("useTabContext must be used within a TabProvider");
+ }
+ return context;
+}
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
index a713484..588ad24 100644
--- a/frontend/src/lib/utils.ts
+++ b/frontend/src/lib/utils.ts
@@ -5,3 +5,36 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+
+export interface SatAttributes {
+ catalogNumberNORAD?: string; // NORAD ID as a string
+ missionStatus?: string; // Mission status (e.g., "IN ORBIT")
+ massKg?: number; // Mass in kilograms
+ launchDate?: string; // Launch date as a string
+ historicalOrbitalData?: any[]; // Historical orbital data (adjust type if needed)
+ satelliteImage?: {
+ data?: {
+ attributes?: {
+ url?: string; // URL of the satellite image
+ };
+ };
+ };
+ projects?: {
+ data: {
+ attributes: {
+ title: string;
+ previewImage?: {
+ data?: {
+ attributes?: {
+ url?: string;
+ };
+ };
+ };
+ slug: string;
+ };
+ id: string;
+ }[];
+ };
+ name?: string; // Satellite name
+ content?: any; // Content (adjust type if needed)
+}
diff --git a/package-lock.json b/package-lock.json
index 5b252a8..1749d14 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@react-three/drei": "^9.56.14",
"@react-three/fiber": "^8.15.13",
+ "@slack/web-api": "^7.9.3",
"@types/recharts": "^1.8.29",
"ci": "^2.3.0",
"concurrently": "^8.2.2",
@@ -260,6 +261,59 @@
"loose-envify": "^1.1.0"
}
},
+ "node_modules/@slack/logger": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz",
+ "integrity": "sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": ">=18.0.0"
+ },
+ "engines": {
+ "node": ">= 18",
+ "npm": ">= 8.6.0"
+ }
+ },
+ "node_modules/@slack/types": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.14.0.tgz",
+ "integrity": "sha512-n0EGm7ENQRxlXbgKSrQZL69grzg1gHLAVd+GlRVQJ1NSORo0FrApR7wql/gaKdu2n4TO83Sq/AmeUOqD60aXUA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0",
+ "npm": ">= 6.12.0"
+ }
+ },
+ "node_modules/@slack/web-api": {
+ "version": "7.9.3",
+ "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.9.3.tgz",
+ "integrity": "sha512-xjnoldVJyoUe61Ltqjr2UVYBolcsTpp5ottqzSI3l41UCaJgHSHIOpGuYps+nhLFvDfGGpDGUeQ7BWiq3+ypqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@slack/logger": "^4.0.0",
+ "@slack/types": "^2.9.0",
+ "@types/node": ">=18.0.0",
+ "@types/retry": "0.12.0",
+ "axios": "^1.8.3",
+ "eventemitter3": "^5.0.1",
+ "form-data": "^4.0.0",
+ "is-electron": "2.2.2",
+ "is-stream": "^2",
+ "p-queue": "^6",
+ "p-retry": "^4",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">= 18",
+ "npm": ">= 8.6.0"
+ }
+ },
+ "node_modules/@slack/web-api/node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "license": "MIT"
+ },
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
@@ -327,6 +381,15 @@
"integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "24.0.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz",
+ "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.8.0"
+ }
+ },
"node_modules/@types/offscreencanvas": {
"version": "2019.7.3",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
@@ -378,6 +441,12 @@
"@types/d3-path": "^1"
}
},
+ "node_modules/@types/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
+ "license": "MIT"
+ },
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
@@ -474,6 +543,23 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
+ "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -538,6 +624,19 @@
"ieee754": "^1.2.1"
}
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/camera-controls": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz",
@@ -621,6 +720,18 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/concurrently": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
@@ -807,6 +918,15 @@
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/detect-gpu": {
"version": "5.0.70",
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
@@ -831,11 +951,70 @@
"integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
"license": "Apache-2.0"
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
@@ -897,6 +1076,51 @@
"node": ">=8"
}
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
+ "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -905,6 +1129,43 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -922,6 +1183,18 @@
"integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
"license": "MIT"
},
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -930,6 +1203,45 @@
"node": ">=8"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/highcharts": {
"version": "12.3.0",
"resolved": "https://registry.npmjs.org/highcharts/-/highcharts-12.3.0.tgz",
@@ -974,6 +1286,12 @@
"node": ">=12"
}
},
+ "node_modules/is-electron": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz",
+ "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==",
+ "license": "MIT"
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -1009,6 +1327,18 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/its-fine": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
@@ -1111,6 +1441,15 @@
"three": ">=0.144.0"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -1147,6 +1486,27 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/node-schedule": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz",
@@ -1184,6 +1544,56 @@
"openapi-typescript": "bin/cli.js"
}
},
+ "node_modules/p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-queue": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
+ "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.4",
+ "p-timeout": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-retry": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
+ "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "0.12.0",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-timeout": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
+ "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
+ "license": "MIT",
+ "dependencies": {
+ "p-finally": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -1211,6 +1621,12 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -1405,6 +1821,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -1632,6 +2057,12 @@
"node": ">=14.0"
}
},
+ "node_modules/undici-types": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
+ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+ "license": "MIT"
+ },
"node_modules/utility-types": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
diff --git a/package.json b/package.json
index f0f68d0..4dbdc3c 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"dependencies": {
"@react-three/drei": "^9.56.14",
"@react-three/fiber": "^8.15.13",
+ "@slack/web-api": "^7.9.3",
"@types/recharts": "^1.8.29",
"ci": "^2.3.0",
"concurrently": "^8.2.2",