diff --git a/.DS_Store b/.DS_Store
index a5b591461..c04ddc6b7 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.env.example b/.env.example
index 88181e88f..854ac3a0d 100644
--- a/.env.example
+++ b/.env.example
@@ -1,5 +1,3 @@
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
-SUPABASE_SERVICE_ROLE=
-PRIVATE_KEY=
-NEXT_PUBLIC_THIRDWEB_AUTH_DOMAIN=
\ No newline at end of file
+OPENAI_API_KEY=
\ No newline at end of file
diff --git a/.github/cron/workflows/codeql-analysis.sh b/.github/cron/workflows/codeql-analysis.sh
new file mode 100644
index 000000000..391ed7a9c
--- /dev/null
+++ b/.github/cron/workflows/codeql-analysis.sh
@@ -0,0 +1,66 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [main]
+ schedule:
+ - cron: '0 21 * * 0'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ # Override automatic language detection by changing the below list
+ # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
+ language: ['javascript']
+ # Learn more...
+ # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ # We must fetch at least the immediate parents so that if this is
+ # a pull request then we can checkout the head.
+ fetch-depth: 2
+
+ # If this run was triggered by a pull request event, then checkout
+ # the head of the pull request instead of the merge commit.
+ - run: git checkout HEAD^2
+ if: ${{ github.event_name == 'pull_request' }}
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v1
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v1
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 https://git.io/JvXDl
+
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ #- run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v1
\ No newline at end of file
diff --git a/.github/cron/workflows/deta.sh b/.github/cron/workflows/deta.sh
new file mode 100644
index 000000000..4e5946756
--- /dev/null
+++ b/.github/cron/workflows/deta.sh
@@ -0,0 +1,15 @@
+name: Push to Space
+on: push
+
+jobs:
+ push-to-space:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Deta Space Deployment Github Action
+ uses: neobrains/space-deployment-github-action@v0.5
+ with:
+ access_token: ${{ secrets.ACCESS_TOKEN }}
+ project_id: ${{ secrets.PROJECT_ID }}
+ space_push: true
+ list_on_discovery: true
\ No newline at end of file
diff --git a/.github/cron/workflows/nextjs.sh b/.github/cron/workflows/nextjs.sh
new file mode 100644
index 000000000..5357cf874
--- /dev/null
+++ b/.github/cron/workflows/nextjs.sh
@@ -0,0 +1,96 @@
+# Sample workflow for building and deploying a Next.js site to GitHub Pages
+#
+# To get started with Next.js see: https://nextjs.org/docs/getting-started
+#
+name: Deploy Next.js site to Pages
+
+on:
+ # Runs on pushes targeting the default branch
+ push:
+ branches: ["main"]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
+# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ # Build job
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Detect package manager
+ id: detect-package-manager
+ run: |
+ if [ -f "${{ github.workspace }}/yarn.lock" ]; then
+ echo "manager=yarn" >> $GITHUB_OUTPUT
+ echo "command=install" >> $GITHUB_OUTPUT
+ echo "runner=yarn" >> $GITHUB_OUTPUT
+ exit 0
+ elif [ -f "${{ github.workspace }}/package.json" ]; then
+ echo "manager=npm" >> $GITHUB_OUTPUT
+ echo "command=ci" >> $GITHUB_OUTPUT
+ echo "runner=npx --no-install" >> $GITHUB_OUTPUT
+ exit 0
+ else
+ echo "Unable to determine package manager"
+ exit 1
+ fi
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: "16"
+ cache: ${{ steps.detect-package-manager.outputs.manager }}
+ - name: Setup Pages
+ uses: actions/configure-pages@v3
+ with:
+ # Automatically inject basePath in your Next.js configuration file and disable
+ # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized).
+ #
+ # You may remove this line if you want to manage the configuration yourself.
+ static_site_generator: next
+ - name: Restore cache
+ uses: actions/cache@v3
+ with:
+ path: |
+ .next/cache
+ # Generate a new cache whenever packages or source files change.
+ key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
+ # If source files changed but packages didn't, rebuild from a prior cache.
+ restore-keys: |
+ ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-
+ - name: Install dependencies
+ run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
+ - name: Build with Next.js
+ run: ${{ steps.detect-package-manager.outputs.runner }} next build
+ - name: Static HTML export with Next.js
+ run: ${{ steps.detect-package-manager.outputs.runner }} next export
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v1
+ with:
+ path: ./out
+
+ # Deployment job
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v2
+
diff --git a/.github/deployments/build.sh b/.github/deployments/build.sh
new file mode 100644
index 000000000..af9be52b8
--- /dev/null
+++ b/.github/deployments/build.sh
@@ -0,0 +1,35 @@
+name: Build Next.js Application
+
+on:
+ push:
+ branches:
+ - '**'
+ pull_request:
+ branches:
+ - '**'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ name: Build Next.js App
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: '18'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Build the app
+ run: npm run build
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ name: nextjs-build
+ path: .next
\ No newline at end of file
diff --git a/.github/deployments/docker.sh b/.github/deployments/docker.sh
new file mode 100644
index 000000000..526c3f7e4
--- /dev/null
+++ b/.github/deployments/docker.sh
@@ -0,0 +1,25 @@
+name: Docker Build
+
+on:
+ push:
+ branches:
+ - '**'
+ pull_request:
+ branches:
+ - '**'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ with:
+ submodules: true
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Build Docker images
+ run: docker-compose -f compose.yml build
\ No newline at end of file
diff --git a/.github/deployments/staging.sh b/.github/deployments/staging.sh
new file mode 100644
index 000000000..1d0fd5ea3
--- /dev/null
+++ b/.github/deployments/staging.sh
@@ -0,0 +1,29 @@
+name: Vercel Staging Deployment
+
+env:
+ VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
+ VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
+
+on:
+ push:
+ branches:
+ - development
+
+jobs:
+ Deploy-Staging:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Install Vercel CLI
+ run: npm install --global vercel@latest
+
+ - name: Pull Vercel Environment Information
+ run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
+
+ - name: Build Project Artifacts
+ run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
+
+ - name: Deploy Project Artifacts to Vercel Staging
+ run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_ORG_ID }} --project=${{ secrets.VERCEL_PROJECT_ID }} --env=preview
\ No newline at end of file
diff --git a/.github/deployments/vercel.sh b/.github/deployments/vercel.sh
new file mode 100644
index 000000000..c2ea2870b
--- /dev/null
+++ b/.github/deployments/vercel.sh
@@ -0,0 +1,21 @@
+name: Vercel Production Deployment
+env:
+ VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
+ VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
+on:
+ push:
+ branches:
+ - main
+jobs:
+ Deploy-Production:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Vercel CLI
+ run: npm install --global vercel@latest
+ - name: Pull Vercel Environment Information
+ run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
+ - name: Build Project Artifacts
+ run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
+ - name: Deploy Project Artifacts to Vercel
+ run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 000000000..c6c83f210
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,44 @@
+name: Build Star Sailors
+
+on:
+ push:
+ branches-ignore:
+ - main
+ pull_request:
+ branches-ignore:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18
+
+ - name: Install dependencies
+ run: yarn install
+
+ - name: Debug VAPID keys
+ run: |
+ echo "VAPID_PUBLIC_KEY=$VAPID_PUBLIC_KEY"
+ echo "VAPID_PRIVATE_KEY=$VAPID_PRIVATE_KEY"
+ env:
+ VAPID_PUBLIC_KEY: ${{ secrets.VAPID_PUBLIC_KEY }}
+ VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
+
+ - name: Remove previous build output
+ run: rm -rf .next
+
+ - name: Build the app
+ run: yarn build
+ env:
+ NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
+ VAPID_PUBLIC_KEY: ${{ secrets.VAPID_PUBLIC_KEY }}
+ VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 000000000..82d43db3e
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,77 @@
+name: Build and CodeQL Analysis
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ branches:
+ - '**'
+ schedule:
+ - cron: '0 21 * * 0'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v2
+ with:
+ node-version: '20'
+
+ - name: Install dependencies
+ run: yarn install
+ env:
+ NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
+ VAPID_PUBLIC_KEY: ${{ secrets.VAPID_PUBLIC_KEY }}
+ VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
+
+ - name: Build the app
+ run: yarn build
+ env:
+ NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
+ VAPID_PUBLIC_KEY: ${{ secrets.VAPID_PUBLIC_KEY }}
+ VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
+
+ - name: Cache node modules
+ uses: actions/cache@v3
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+
+ codeql:
+ name: CodeQL Analysis
+ runs-on: ubuntu-latest
+ needs: build # Ensures the build job runs first
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ['javascript']
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 2
+
+ - run: git checkout HEAD^2
+ if: ${{ github.event_name == 'pull_request' }}
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v1
+ with:
+ languages: ${{ matrix.language }}
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v1
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v1
\ No newline at end of file
diff --git a/.github/workflows/eventstable.yml.txt b/.github/workflows/eventstable.yml.txt
new file mode 100644
index 000000000..4f7be618f
--- /dev/null
+++ b/.github/workflows/eventstable.yml.txt
@@ -0,0 +1,41 @@
+name: Supabase Table Check and Query
+
+on:
+ push:
+ branches:
+ - '**'
+ pull_request:
+ branches:
+ - '**'
+
+jobs:
+ query:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Set up PostgreSQL client
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y postgresql-client
+
+ - name: Query Supabase tables (events and zoo)
+ run: |
+ # Set the Supabase credentials as environment variables
+ SUPABASE_URL=${{ secrets.SUPABASE_URL }}
+ SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}
+
+ # Get the PostgreSQL connection string for Supabase
+ DB_HOST="${SUPABASE_URL#https://}"
+ DB_NAME="postgres"
+ DB_USER="postgres"
+ DB_PASSWORD="${SUPABASE_ANON_KEY}"
+
+ # Connect to the Supabase database and query the events and zoo tables
+ PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "SELECT * FROM events;"
+ PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "SELECT * FROM zoo;"
+
+ - name: Post query results to GitHub
+ run: |
+ echo "Query results from events and zoo tables."
\ No newline at end of file
diff --git a/.github/workflows/vercel-staging.yml b/.github/workflows/vercel-staging.yml
new file mode 100644
index 000000000..1d0fd5ea3
--- /dev/null
+++ b/.github/workflows/vercel-staging.yml
@@ -0,0 +1,29 @@
+name: Vercel Staging Deployment
+
+env:
+ VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
+ VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
+
+on:
+ push:
+ branches:
+ - development
+
+jobs:
+ Deploy-Staging:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Install Vercel CLI
+ run: npm install --global vercel@latest
+
+ - name: Pull Vercel Environment Information
+ run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
+
+ - name: Build Project Artifacts
+ run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
+
+ - name: Deploy Project Artifacts to Vercel Staging
+ run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_ORG_ID }} --project=${{ secrets.VERCEL_PROJECT_ID }} --env=preview
\ No newline at end of file
diff --git a/.github/workflows/vercel.yml b/.github/workflows/vercel.yml
new file mode 100644
index 000000000..c2ea2870b
--- /dev/null
+++ b/.github/workflows/vercel.yml
@@ -0,0 +1,21 @@
+name: Vercel Production Deployment
+env:
+ VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
+ VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
+on:
+ push:
+ branches:
+ - main
+jobs:
+ Deploy-Production:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Vercel CLI
+ run: npm install --global vercel@latest
+ - name: Pull Vercel Environment Information
+ run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
+ - name: Build Project Artifacts
+ run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
+ - name: Deploy Project Artifacts to Vercel
+ run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 737d9dc75..78a0a7c2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,4 +54,5 @@ public/sw.js.map
public/sw.js
public/workbox-7144475a.js
public/workbox-7144475a.js.map
-public/workbox-f1770938.js
\ No newline at end of file
+public/workbox-f1770938.js
+certificates
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index 4b8125e9e..082018cd7 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "citizen"]
path = citizen
url = https://github.com/signal-k/sytizen
+[submodule "misc/notifications"]
+ path = misc/notifications
+ url = https://github.com/signal-k/notifications
diff --git a/.vscode/settings.json b/.vscode/settings.json
index ef9a03137..35cff32c2 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -10,5 +10,5 @@
"jira-plugin.workingProject": "",
"liveServer.settings.port": 5501,
"solidity.compileUsingRemoteVersion": "v0.8.11+commit.d7f03943",
- "solidity.defaultCompiler": "localFile"
+ "solidity.defaultCompiler": "localFile",
}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..c0d6b6ea3
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+# Use the official Node.js image
+FROM node:18-bullseye
+
+# Set the working directory
+WORKDIR /app
+
+# Copy the package.json and yarn.lock
+COPY package.json yarn.lock ./
+
+# Install dependencies
+RUN yarn install
+# RUN yarn build
+
+# Copy the rest of the application code
+COPY . .
+
+# For hot-reloading, you can use development mode
+CMD ["yarn", "dev"]
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..28fdc65e8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+up:
+ docker-compose up
+
+build:
+ docker-compose build && yarn build
+
+up-full:
+ yarn && yarn build && supabase start && docker-compose up --build
+
+down:
+ docker-compose down
+
+down-full:
+ docker-compose build && yarn build && docker-compose down && supabase stop
+
+deploy-test:
+ docker-compose build && yarn build && vercel
+
+up-light:
+ docker-compose up -d
\ No newline at end of file
diff --git a/README.md b/README.md
index 47564229a..ed94d5a56 100644
--- a/README.md
+++ b/README.md
@@ -6,4 +6,8 @@ docker exec -it starsailors_db psql -U postgres
\l
docker compose build flaskapp
docker compose up -d flaskapp
-```
\ No newline at end of file
+```
+
+
\ No newline at end of file
diff --git a/app/RootLayoutClient.tsx b/app/RootLayoutClient.tsx
new file mode 100644
index 000000000..b7e82d6da
--- /dev/null
+++ b/app/RootLayoutClient.tsx
@@ -0,0 +1,77 @@
+"use client";
+
+import "@/styles/globals.css";
+import { createPagesBrowserClient } from "@supabase/auth-helpers-nextjs";
+import { SessionContextProvider } from "@supabase/auth-helpers-react";
+import { useEffect, useState, ReactNode } from "react";
+import { ActivePlanetProvider, useActivePlanet } from "@/context/ActivePlanet";
+import { InventoryProvider } from "@/context/InventoryContext";
+import { UserAnomaliesProvider } from "@/context/UserAnomalies";
+import { Analytics } from "@vercel/analytics/react";
+import { SpeedInsights } from "@vercel/speed-insights/next";
+
+function LayoutContent({ children }: { children: ReactNode }) {
+ const { activePlanet } = useActivePlanet();
+
+ useEffect(() => {
+ if (activePlanet) {
+ console.log("Active Planet: ", activePlanet);
+ }
+ }, [activePlanet]);
+
+ return <>{children}>;
+}
+
+export default function RootLayoutClient({ children }: { children: ReactNode }) {
+ const [supabaseClient] = useState(() => createPagesBrowserClient());
+
+ return (
+
+
+
+ {/* */}
+
+ {/* */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+ {/* */}
+
+ {/* */}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/account/page.tsx b/app/account/page.tsx
new file mode 100644
index 000000000..d67204893
--- /dev/null
+++ b/app/account/page.tsx
@@ -0,0 +1,17 @@
+"use client";
+
+import { EarthViewLayout } from "@/components/(scenes)/planetScene/layout";
+import ProfileSetupForm from "@/components/Account/ProfileSetup";
+import Navbar from "@/components/Layout/Navbar";
+import { useState } from "react";
+
+export default function AccountPage() {
+ const [refresh, setRefresh] = useState(false);
+
+ return (
+
+
+ setRefresh((prev) => !prev)} />
+
+ );
+};
\ No newline at end of file
diff --git a/app/actions.ts b/app/actions.ts
new file mode 100644
index 000000000..729ce3743
--- /dev/null
+++ b/app/actions.ts
@@ -0,0 +1,46 @@
+'use server'
+
+import webpush from 'web-push';
+
+webpush.setVapidDetails(
+ '',
+ process.env.VAPID_PUBLIC_KEY!,
+ process.env.VAPID_PRIVATE_KEY!
+)
+
+let subscription: PushSubscription | null = null
+
+export async function subscribeUser(sub: PushSubscription) {
+ subscription = sub
+ // In a production environment, you would want to store the subscription in a database
+ // For example: await db.subscriptions.create({ data: sub })
+ return { success: true }
+}
+
+export async function unsubscribeUser() {
+ subscription = null
+ // In a production environment, you would want to remove the subscription from the database
+ // For example: await db.subscriptions.delete({ where: { ... } })
+ return { success: true }
+}
+
+export async function sendNotification(message: string) {
+ if (!subscription) {
+ throw new Error('No subscription available')
+ }
+
+ try {
+ await webpush.sendNotification(
+ subscription,
+ JSON.stringify({
+ title: 'Test Notification',
+ body: message,
+ icon: '/icon.png',
+ })
+ )
+ return { success: true }
+ } catch (error) {
+ console.error('Error sending push notification:', error)
+ return { success: false, error: 'Failed to send notification' }
+ }
+};
\ No newline at end of file
diff --git a/app/api/balloon/upload-image/route.ts b/app/api/balloon/upload-image/route.ts
new file mode 100644
index 000000000..36006148a
--- /dev/null
+++ b/app/api/balloon/upload-image/route.ts
@@ -0,0 +1,47 @@
+import { NextResponse, NextRequest } from "next/server";
+import axios from "axios";
+
+export async function POST(req: NextRequest) {
+ try {
+ const formData = await req.formData();
+ const file = formData.get('file') as Blob | null;
+
+ if (!file) {
+ console.error("No file found in this request.");
+ return NextResponse.json({
+ error: 'No file uploaded',
+ },
+ {
+ status: 400
+ });
+ };
+
+ const arrayBuffer = await file.arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);
+ const base64Image = buffer.toString('base64');
+
+ console.log('File received. Size: ', buffer.length, ' bytes');
+
+ const openaiRes = await axios.post(
+ 'https://api.openai.com/v1/chat/completions',
+ {
+ model: 'gpt-4-turbo',
+ messages: [
+
+ ],
+ max_tokens: 300,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
+ "Content-Type": "application/json",
+ },
+ },
+ );
+
+ console.log("Raw OpenAI Response: ", openaiRes.data);
+ } catch (error: any) {
+ console.error("Error in API route:", error.response?.data || error.message);
+ return NextResponse.json({ error: "Failed to classify image" }, { status: 500 });
+ };
+};
\ No newline at end of file
diff --git a/app/api/gameplay/inventory/route.ts b/app/api/gameplay/inventory/route.ts
index eef5e5fce..93cba6678 100644
--- a/app/api/gameplay/inventory/route.ts
+++ b/app/api/gameplay/inventory/route.ts
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
interface Recipe {
[key: string]: number;
-}
+};
export interface InventoryItem {
id: number;
@@ -10,7 +10,7 @@ export interface InventoryItem {
description: string;
cost?: number;
icon_url: string;
- ItemCategory: string;
+ ItemCategory: string;
parentItem?: number | null;
itemLevel?: number;
locationType?: string;
@@ -19,6 +19,8 @@ export interface InventoryItem {
};
const inventoryItems: InventoryItem[] = [
+
+ // Items/Minerals
{ id: 11, name: 'Coal', description: 'You can burn this to create power', cost: 1, icon_url: '/assets/Items/Coal.png', ItemCategory: 'Minerals', parentItem: null, itemLevel: 1 },
{ id: 12, name: 'Telescope Signal Receiver', description: 'This tool is used to receive transmissions from your transiting telescope and decode them into readable data. It is also the first component of your main telescope array', cost: 1, icon_url: 'https://github.com/Signal-K/client/blob/SGV2-154/public/assets/Archive/Inventory/Structures/Telescope2.png?raw=true', ItemCategory: 'Structure', parentItem: null, itemLevel: 1, recipe: { '13': 3, '15': 2 }, }, // Originally pointed towards 2 alloy
{ id: 13, name: 'Silicon', description: '', cost: 1, icon_url: '/assets/Items/Silicon.png', ItemCategory: 'Minerals', parentItem: null, itemLevel: 1},
@@ -30,33 +32,138 @@ const inventoryItems: InventoryItem[] = [
{ id: 19, name: 'Copper', description: '', cost: 1, icon_url: '/assets/Items/Copper.png', ItemCategory: 'Minerals', parentItem: null, itemLevel: 1},
{ id: 20, name: 'Chromium', description: '/assets/Items/Chromite.png', cost: 1, icon_url: '', ItemCategory: 'Minerals', parentItem: null, itemLevel: 1},
{ id: 21, name: 'Water-Ice', description: '/assets/Items/Ice.png', cost: 1, icon_url: '', ItemCategory: 'Minerals', parentItem: null, itemLevel: 1},
- { id: 23, name: 'Rover 1', description: '', cost: 1, icon_url: 'https://static-00.iconduck.com/assets.00/mars-rover-illustration-2048x2048-czfy73zy.png', ItemCategory: 'Automaton', parentItem: 22, itemLevel: 1, gif: "/assets/Items/Roover.gif", }, // https://cdn.dribbble.com/users/107759/screenshots/4248752/rover.gif
{ id: 32, name: 'Camera Receiver', description: 'Keep track of all the photos your anomalies have taken', cost: 1, icon_url: 'https://cdn-icons-png.flaticon.com/512/5169/5169909.png', ItemCategory: 'Structure', parentItem: null, itemLevel: 1, recipe: { '11': 1 }, locationType: 'Orbit' },
+
+ // Automatons
+ {
+ id: 23,
+ name: 'Rover 1',
+ description: '',
+ cost: 1,
+ icon_url: '/assets/Automatons/ExploreRover1.png',
+ ItemCategory: 'Automaton',
+ // parentItem: 22,
+ itemLevel: 1,
+ gif: "/assets/Items/Roover.gif", }, // https://cdn.dribbble.com/users/107759/screenshots/4248752/rover.gif
+
{
- id: 3102, name: 'Automaton station', description: 'View, control and upgrade all your automatons and rovers here', cost: 1, icon_url: '/assets/Items/AutoController.png', ItemCategory: 'Structure', parentItem: 30, itemLevel: 1, locationType: 'Surface'
+ id: 24,
+ name: 'Satellite 1',
+ description: '',
+ cost: 1,
+ icon_url: '/assets/Automatons/Sat.png',
+ ItemCategory: 'Automaton',
+ itemLevel: 1,
+ },
+
+
+ // Structures
+ {
+ id: 3102, name: 'Automaton station', description: 'Control and upgrade your automatons here', cost: 1, icon_url: '/assets/Items/AutoController.png', ItemCategory: 'Structure', parentItem: 30, itemLevel: 1, locationType: 'Surface'
},
{
id: 3103, name: 'Telescope', description: 'Space-based observations & classifications', icon_url: '/assets/Items/Telescope.png', ItemCategory: 'Structure', locationType: 'Surface'
},
- {
- id: 3104, name: "Zoodex", description: "Populate your planet with some animals to gain an understanding of animal behaviour on your planet and aide local research back home", cost: 1, icon_url: "/assets/Items/Pokedex.png", ItemCategory: "Structure", parentItem: null, itemLevel: 1, locationType: "Surface"
+ { // Previously referred to as #Zoodex-3104
+ id: 3104, name: "Biodome", description: "Populate your planet with some animals to gain an understanding of animal behaviour on your planet and aide local research back home", cost: 1, icon_url: "/assets/Items/Pokedex.png", ItemCategory: "Structure", parentItem: null, itemLevel: 1, locationType: "Surface"
},
{
id: 3105,
- name: "LIDAR",
+ name: "Weather Balloon", // "LIDAR",
description: "Collect and study weather events and entities",
- icon_url: "/assets/Items/Scoper.png",
+ icon_url: "/assets/Items/WeatherBalloon.png", //Scoper.png",
ItemCategory: "Structure",
- locationType: 'Surface',
+ locationType: 'Atmosphere',
},
{
id: 3106,
name: "Research Station",
description: "Unlock new technology and research",
icon_url: "/assets/Items/Research.png",
- ItemCategory: "Structure",
+ ItemCategory: "StructureResearch",
+ locationType: 'Surface',
+ },
+ // {
+ // id: 3107,
+ // name: "Launchpad",
+ // description: "Launch rockets and satellites",
+ // icon_url: "/assets/Items/Launchpad.jpg",
+ // ItemCategory: "Structure",
+ // locationType: 'Surface',
+ // },
+ // {
+ // id: 3108,
+ // name: "First rocket",
+ // description: "Travel the solar-system",
+ // icon_url: "/assets/Items/Rocket.png",
+ // ItemCategory: "Structure",
+ // locationType: 'Atmosphere',
+ // },
+
+
+ // Classification structures introduced in C2
+ // {
+ // id: 31010,
+ // name: "Physics Lab",
+ // description: "Catalogue results from different particle experiments across the universe",
+ // icon_url: "/assets/Items/PhysicsLab.png",
+ // ItemCategory: "Structure",
+ // locationType: 'Surface',
+ // },
+
+ // Community stations
+ {
+ id: 31011,
+ name: "Greenhouse",
+ description: "Collect and study biological anomalies across multiple locations",
+ icon_url: "/assets/Items/Greenhouse.png",
+ ItemCategory: 'CommunityStation',
locationType: 'Surface',
},
+ {
+ id: 31012,
+ name: "Weather balloon",
+ description: "Collect and study weather events and entities more closely in your planet's atmosphere",
+ icon_url: "/assets/Items/WeatherBalloon.png",
+ ItemCategory: 'CommunityStation',
+ locationType: 'Atmosphere',
+ },
+ {
+ id: 31013,
+ name: "Space Telescope",
+ description: "Collect & compare discoveries more readily per-location", // to-update
+ icon_url: "/assets/Items/SpaceTelescope.png",
+ ItemCategory: 'CommunityStation',
+ locationType: 'Orbital',
+ },
+
+
+ // Greenhouse/Biodome stations
+ {
+ id: 3104001,
+ name: "Desert Observatory",
+ description: "Track & tag animals and plants that originate in desert locations on Earth",
+ icon_url: '/assets/Archive/Inventory/Items/Coal.png', // For test
+ ItemCategory: 'BioDomeStation',
+ locationType: 'Surface',
+ },
+ {
+ id: 3104002,
+ name: "Ocean Observatory",
+ description: "Track & tag animals and plants that originate in the oceans of Earth",
+ icon_url: '/assets/Archive/Inventory/Items/Coal.png', // For test
+ ItemCategory: 'BioDomeStation',
+ locationType: 'Surface', // Maybe underwater?
+ },
+ {
+ id: 31040001,
+ name: "Uploads observatory",
+ description: 'Keep track of all animals and plants that have been uploaded to the database',
+ icon_url: '/assets/Archive/Inventory/Items/Coal.png',
+ ItemCategory: 'BioDomeStation',
+ locationType: 'Surface',
+ },
+
// Tests
{
diff --git a/app/api/gameplay/milestones/route.ts b/app/api/gameplay/milestones/route.ts
new file mode 100644
index 000000000..76cd04646
--- /dev/null
+++ b/app/api/gameplay/milestones/route.ts
@@ -0,0 +1,346 @@
+import { Telescope } from "lucide-react";
+import { NextRequest, NextResponse } from "next/server";
+
+const milestones = [
+ {
+ weekStart: '2025-05-25',
+ data: [
+ {
+ name: 'Propose 5 planet candidates',
+ structure: "Telescope",
+ // group: 'Meteorology',
+ group: 'Astronomy',
+ icon: "Telescope",
+ extendedDescription: '',
+ xp: 5,
+ completionInfo: '',
+ rewardInfo: '',
+ table: "classifications",
+ field: "classificationtype",
+ value: "planet",
+ requiredCount: 5,
+ },
+ ],
+ },
+ {
+ weekStart: '2025-05-18',
+ data: [
+ {
+ name: 'Collect animals around you',
+ structure: "Greenhouse",
+ // group: 'Meteorology',
+ group: 'Bioloy',
+ icon: "PawPrintIcon",
+ extendedDescription: '',
+ xp: 2,
+ completionInfo: '',
+ rewardInfo: '',
+ table: "uploads",
+ field: "source",
+ value: "Webcam",
+ requiredCount: 1,
+ },
+ {
+ name: 'Make 1 Temperature Classification',
+ structure: 'Telescope',
+ group: 'Astronomy',
+ icon: "Telescope",
+ extendedDescription: "",
+ xp: 3,
+ completionInfo: '',
+ rewardInfo: '',
+ table: "comments",
+ field: "category",
+ value: "Temperature",
+ requiredCount: 1,
+ },
+ {
+ name: 'Retrieve satellite images of your planets',
+ structure: 'WeatherBalloon',
+ group: 'Astronomy',
+ icon: "Telescope",
+ extendedDescription: "",
+ xp: 3,
+ completionInfo: '',
+ rewardInfo: '',
+ table: "classifications",
+ field: "classificationtype",
+ value: "satellite-planetFour",
+ requiredCount: 1,
+ },
+ ],
+ },
+ {
+ weekStart: '2025-04-28',
+ data: [
+ {
+ name: 'Discover 2 planets',
+ structure: "Telescope",
+ group: 'Astronomy',
+ icon: "PawPrintIcon",
+ extendedDescription: '',
+ xp: 2,
+ completionInfo: '',
+ rewardInfo: '',
+ table: "classifications",
+ field: "classificationtype",
+ value: "planet",
+ requiredCount: 2,
+ },
+ // {
+ // name: "Create a storm on a gas giant",
+ // structure: "WeatherBalloon",
+ // icon: "SnowflakeIcon",
+ // xp: 1,
+ // extendedDescription: "",
+ // completionInfo: "",
+ // rewardInfo: "",
+ // table: 'events',
+ // field: 'eventtype',
+ // value: 'vortex-storm',
+ // requiredCount: 1,
+ // },
+ // {
+ // name: 'Make 3 Temperature Classifications',
+ // structure: 'Telescope',
+ // icon: "Telescope",
+ // extendedDescription: "",
+ // xp: 3,
+ // completionInfo: '',
+ // rewardInfo: '',
+ // table: "comments",
+ // field: "category",
+ // value: "Temperature",
+ // requiredCount: 3,
+ // },
+ ],
+ },
+ {
+ weekStart: '2025-04-21',
+ data: [
+ // {
+ // name: 'Make 3 Temperature Classifications',
+ // structure: 'Telescope',
+ // icon: "Telescope",
+ // extendedDescription: "",
+ // xp: 3,
+ // completionInfo: '',
+ // rewardInfo: '',
+ // table: "comments",
+ // field: "category",
+ // value: "Temperature",
+ // requiredCount: 3,
+ // },
+ // {
+ // name: 'Discover 2 planets',
+ // structure: "Telescope",
+ // icon: "PawPrintIcon",
+ // extendedDescription: '',
+ // xp: 2,
+ // completionInfo: '',
+ // rewardInfo: '',
+ // table: "classifications",
+ // field: "classificationtype",
+ // value: "planet",
+ // requiredCount: 2,
+ // },
+ {
+ name: "Create a storm on a gas giant",
+ structure: "WeatherBalloon",
+ group: 'Meteorology',
+ icon: "SnowflakeIcon",
+ xp: 1,
+ extendedDescription: "",
+ completionInfo: "",
+ rewardInfo: "",
+ table: 'events',
+ field: 'eventtype',
+ value: 'vortex-storm',
+ requiredCount: 1,
+ },
+ ],
+ },
+ {
+ weekStart: '2025-04-14',
+ data: [
+ // {
+ // name: 'Make 3 Temperature Classifications',
+ // structure: 'Telescope',
+ // icon: "Telescope",
+ // extendedDescription: "",
+ // xp: 3,
+ // completionInfo: '',
+ // rewardInfo: '',
+ // table: "comments",
+ // field: "category",
+ // value: "Temperature",
+ // requiredCount: 3,
+ // },
+ {
+ name: 'Discover 2 planets',
+ structure: "Telescope",
+ group: 'Astronomy',
+ icon: "PawPrintIcon",
+ extendedDescription: '',
+ xp: 2,
+ completionInfo: '',
+ rewardInfo: '',
+ table: "classifications",
+ field: "classificationtype",
+ value: "planet",
+ requiredCount: 2,
+ },
+ // {
+ // name: "Create a storm on a gas giant",
+ // structure: "WeatherBalloon",
+ // icon: "SnowflakeIcon",
+ // xp: 1,
+ // extendedDescription: "",
+ // completionInfo: "",
+ // rewardInfo: "",
+ // table: 'events',
+ // field: 'eventtype',
+ // value: 'vortex-storm',
+ // requiredCount: 1,
+ // },
+ ],
+ },
+ // {
+ // weekStart: "2025-02-18",
+ // data: [
+ // {
+ // name: "Find 3 desert creatures",
+ // structure: "Greenhouse",
+ // icon: "PawPrintIcon",
+ // extendedDescription: "",
+ // xp: null,
+ // completionInfo: "",
+ // rewardInfo: ""
+ // },
+ // {
+ // name: "Find ice on a planet",
+ // structure: "WeatherBalloon",
+ // icon: "SnowflakeIcon",
+ // extendedDescription: "",
+ // xp: null,
+ // completionInfo: "",
+ // rewardInfo: ""
+ // },
+ // {
+ // name: "Find a planet in the habitable zone",
+ // structure: "Telescope",
+ // icon: "GlassWaterIcon",
+ // extendedDescription: "",
+ // xp: null,
+ // completionInfo: "",
+ // rewardInfo: ""
+ // },
+ // ],
+ // },
+ // {
+ // weekStart: "2025-02-11",
+ // data: [
+ // {
+ // name: "Discover a new microbial species",
+ // structure: "Greenhouse",
+ // icon: "PawPrintIcon",
+ // extendedDescription: "",
+ // xp: null,
+ // completionInfo: "",
+ // rewardInfo: ""
+ // },
+ // {
+ // name: "Detect a storm with a weather balloon",
+ // structure: "WeatherBalloon",
+ // icon: "SnowflakeIcon",
+ // extendedDescription: "",
+ // xp: null,
+ // completionInfo: "",
+ // rewardInfo: ""
+ // },
+ // {
+ // name: "Observe a distant exoplanet",
+ // structure: "Telescope",
+ // icon: "GlassWaterIcon",
+ // extendedDescription: "",
+ // xp: null,
+ // completionInfo: "",
+ // rewardInfo: ""
+ // },
+ // ],
+ // },
+];
+
+const communityMilestones = [
+ {
+ weekStart: "2025-02-18",
+ data: [
+ {
+ name: "Discover 100 desert creatures",
+ structure: "Greenhouse",
+ icon: "PawPrintIcon",
+ extendedDescription: "As the community explores, they find 100 desert creatures across planets.",
+ xp: null,
+ completionInfo: "The community achieves a combined total of 100 desert creatures discovered.",
+ rewardInfo: "Unlocks the 'Desert Explorer' badge for all players."
+ },
+ {
+ name: "Discover 50 ice formations on planets",
+ structure: "WeatherBalloon",
+ icon: "SnowflakeIcon",
+ extendedDescription: "The community explores icy terrains, discovering 50 ice formations.",
+ xp: null,
+ completionInfo: "A total of 50 ice formations discovered by the community.",
+ rewardInfo: "Unlocks the 'Arctic Adventurer' badge for all players."
+ },
+ {
+ name: "Observe 75 planets in the habitable zone",
+ structure: "Telescope",
+ icon: "GlassWaterIcon",
+ extendedDescription: "Through collective efforts, the community identifies 75 planets within habitable zones.",
+ xp: null,
+ completionInfo: "The community successfully identifies 75 habitable planets.",
+ rewardInfo: "Unlocks the 'Habitable Zone Pioneer' badge for all players."
+ },
+ ],
+ },
+ // {
+ // weekStart: "2025-02-11",
+ // data: [
+ // {
+ // name: "Community discovers 50 new microbial species",
+ // structure: "Greenhouse",
+ // icon: "PawPrintIcon",
+ // extendedDescription: "The community works together to discover a total of 50 new microbial species.",
+ // xp: null,
+ // completionInfo: "The community has collectively discovered 50 microbial species.",
+ // rewardInfo: "Unlocks the 'Microbial Pioneer' badge for all players."
+ // },
+ // {
+ // name: "Community detects 30 storms with weather balloons",
+ // structure: "WeatherBalloon",
+ // icon: "SnowflakeIcon",
+ // extendedDescription: "The community makes groundbreaking discoveries, detecting 30 storms across planets.",
+ // xp: null,
+ // completionInfo: "30 storms detected by the collective community.",
+ // rewardInfo: "Unlocks the 'Storm Tracker' badge for all players."
+ // },
+ // {
+ // name: "Community observes 100 distant exoplanets",
+ // structure: "Telescope",
+ // icon: "GlassWaterIcon",
+ // extendedDescription: "The community works to observe and catalog 100 distant exoplanets.",
+ // xp: null,
+ // completionInfo: "A total of 100 exoplanets observed by the community.",
+ // rewardInfo: "Unlocks the 'Exoplanet Explorer' badge for all players."
+ // },
+ // ],
+ // },
+];
+
+export async function GET(req: NextRequest) {
+ return NextResponse.json({
+ playerMilestones: milestones,
+ communityMilestones: communityMilestones,
+ });
+};
\ No newline at end of file
diff --git a/app/api/gameplay/missions/archive/route.ts b/app/api/gameplay/missions/archive/route.ts
index 9081f2612..e4dba0644 100644
--- a/app/api/gameplay/missions/archive/route.ts
+++ b/app/api/gameplay/missions/archive/route.ts
@@ -1,6 +1,4 @@
import { NextRequest, NextResponse } from "next/server";
-import { MineralDepositsNoAction } from "@/app/components/(structures)/Mining/AvailableDeposits";
-import { Mission } from "@/app/components/(structures)/StructuresForMission";
interface UserMissionInstance {
id: number;
@@ -9,6 +7,18 @@ interface UserMissionInstance {
mission: number;
};
+export interface Mission {
+ id: number;
+ name: string;
+ sequence?: number;
+ description?: string;
+ rewards?: number[];
+ classificationModule?: string;
+ structure?: number;
+ chapter?: number;
+ component?: React.ComponentType;
+};
+
const missions: Mission[] = [
{ id: 1, name: "Pick planet", description: "Select your starting planet", rewards: [29] }, // Possibly the user should get something, but really they're already getting a planet, so... | After further consideration for the order of missions, I think that giving the spaceship and then the rover will be a good option
{ id: 2, name: "Complete profile", description: "Fill in your profile data", rewards: [22] }, // Give them a rover/automaton without a location, we'll create a button that keeps track of the item that was created and show the user an option to "place" un-locationised items/structures
@@ -120,7 +130,7 @@ const missions: Mission[] = [
chapter: 1,
classificationModule: "Zoodex",
sequence: 3,
- component: MineralDepositsNoAction,
+ // component: MineralDepositsNoAction,
structure: 3104
},
// Telescope mission group (t.m.) (MG-T-##)
@@ -287,7 +297,7 @@ const missions: Mission[] = [
chapter: 1,
classificationModule: "Zoodex",
sequence: 3,
- component: MineralDepositsNoAction,
+ // component: MineralDepositsNoAction,
},
// Telescope mission group (t.m.) (MG-T-##)
{
@@ -312,7 +322,7 @@ const missions: Mission[] = [
{
id: 1372002, name: "Repair your telescope", classificationModule: "Telescope", chapter: 1,
// sequence: 5+,
- component: MineralDepositsNoAction,
+ // component: MineralDepositsNoAction,
},
{
id: 13712602, name: "Repair your Zoodex module", classificationModule: "Zoodex", chapter: 1,
diff --git a/app/api/gameplay/missions/planets/solarsystem/stats/route.ts.rb b/app/api/gameplay/missions/planets/solarsystem/stats/route.ts.rb
new file mode 100644
index 000000000..4bf0888ee
--- /dev/null
+++ b/app/api/gameplay/missions/planets/solarsystem/stats/route.ts.rb
@@ -0,0 +1,246 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export interface PlanetConfig {
+ id: number;
+ name: string;
+ mass: string;
+ radius: string;
+ temperature: {
+ average: string;
+ min?: string;
+ max?: string;
+ };
+ equilibriumTemperature: string;
+ volume: string;
+ density: string;
+ distanceFromSunOrPlanet: string;
+ orbitalPeriod: string;
+ planetType: string;
+};
+
+const planetConfigurations: PlanetConfig[] = [
+ {
+ id: 100001,
+ name: "Mercury",
+ mass: "3.3011 × 10^23 kg",
+ radius: "2,439.7 km",
+ temperature: {
+ average: "167 °C",
+ min: "-173 °C",
+ max: "427 °C"
+ },
+ equilibriumTemperature: "440 K",
+ volume: "6.083 × 10^10 km³",
+ density: "5.427 g/cm³",
+ distanceFromSunOrPlanet: "57.91 million km",
+ orbitalPeriod: "88 days",
+ planetType: "Arid",
+ },
+ {
+ id: 200001,
+ name: "Venus",
+ mass: "4.8675 × 10^24 kg",
+ radius: "6,051.8 km",
+ temperature: {
+ average: "464 °C",
+ min: "462 °C",
+ max: "465 °C"
+ },
+ equilibriumTemperature: "328 K",
+ volume: "9.2843 × 10^11 km³",
+ density: "5.243 g/cm³",
+ distanceFromSunOrPlanet: "108.2 million km",
+ orbitalPeriod: "225 days",
+ planetType: "Hellhole",
+ },
+ {
+ id: 300001,
+ name: "Earth",
+ mass: "5.972 × 10^24 kg",
+ radius: "6,371 km",
+ temperature: {
+ average: "15 °C",
+ min: "-88 °C",
+ max: "58 °C"
+ },
+ equilibriumTemperature: "255 K",
+ volume: "1.08321 × 10^12 km³",
+ density: "5.514 g/cm³",
+ distanceFromSunOrPlanet: "149.6 million km",
+ orbitalPeriod: "365.25 days",
+ planetType: "Lush",
+ },
+ {
+ id: 3000011,
+ name: "Moon",
+ mass: "7.342 × 10^22 kg",
+ radius: "1,737.4 km",
+ temperature: {
+ average: "-53 °C",
+ min: "-173 °C",
+ max: "127 °C"
+ },
+ equilibriumTemperature: "270 K",
+ volume: "2.1968 × 10^10 km³",
+ density: "3.344 g/cm³",
+ distanceFromSunOrPlanet: "384,400 km (from Earth)",
+ orbitalPeriod: "27.3 days (around Earth)",
+ planetType: "Arid",
+ },
+ {
+ id: 400001,
+ name: "Mars",
+ mass: "6.4171 × 10^23 kg",
+ radius: "3,389.5 km",
+ temperature: {
+ average: "-63 °C",
+ min: "-143 °C",
+ max: "35 °C"
+ },
+ equilibriumTemperature: "210 K",
+ volume: "1.6318 × 10^11 km³",
+ density: "3.933 g/cm³",
+ distanceFromSunOrPlanet: "227.9 million km",
+ orbitalPeriod: "687 days",
+ planetType: "Frozen",
+ },
+ {
+ id: 500001,
+ name: "Jupiter",
+ mass: "1.8982 × 10^27 kg",
+ radius: "69,911 km",
+ temperature: {
+ average: "-145 °C"
+ },
+ equilibriumTemperature: "112 K",
+ volume: "1.43128 × 10^15 km³",
+ density: "1.326 g/cm³",
+ distanceFromSunOrPlanet: "778.5 million km",
+ orbitalPeriod: "11.86 years",
+ planetType: "GasGiant",
+ },
+ {
+ id: 500011,
+ name: "Amalthea",
+ mass: "2.08 × 10^18 kg",
+ radius: "83.5 km",
+ temperature: {
+ average: "-153 °C"
+ },
+ equilibriumTemperature: "98 K",
+ volume: "2.43 × 10^5 km³",
+ density: "0.857 g/cm³",
+ distanceFromSunOrPlanet: "181,000 km (from Jupiter)",
+ orbitalPeriod: "0.498 days",
+ planetType: "Rocky",
+ },
+ {
+ id: 500021,
+ name: "Io",
+ mass: "8.9319 × 10^22 kg",
+ radius: "1,821.6 km",
+ temperature: {
+ average: "-143 °C"
+ },
+ equilibriumTemperature: "110 K",
+ volume: "2.53 × 10^10 km³",
+ density: "3.528 g/cm³",
+ distanceFromSunOrPlanet: "421,700 km (from Jupiter)",
+ orbitalPeriod: "1.77 days",
+ planetType: "Volcanic",
+ },
+ {
+ id: 500031,
+ name: "Callisto",
+ mass: "1.0759 × 10^23 kg",
+ radius: "2,410.3 km",
+ temperature: {
+ average: "-139 °C"
+ },
+ equilibriumTemperature: "99 K",
+ volume: "5.9 × 10^10 km³",
+ density: "1.834 g/cm³",
+ distanceFromSunOrPlanet: "1,882,700 km (from Jupiter)",
+ orbitalPeriod: "16.69 days",
+ planetType: "Icy",
+ },
+ {
+ id: 500041,
+ name: "Ganymede",
+ mass: "1.4819 × 10^23 kg",
+ radius: "2,634.1 km",
+ temperature: {
+ average: "-160 °C"
+ },
+ equilibriumTemperature: "110 K",
+ volume: "7.6 × 10^10 km³",
+ density: "1.936 g/cm³",
+ distanceFromSunOrPlanet: "1,070,400 km (from Jupiter)",
+ orbitalPeriod: "7.15 days",
+ planetType: "Icy",
+ },
+ {
+ id: 500051,
+ name: "Europa",
+ mass: "4.7998 × 10^22 kg",
+ radius: "1,560.8 km",
+ temperature: {
+ average: "-160 °C"
+ },
+ equilibriumTemperature: "102 K",
+ volume: "1.593 × 10^10 km³",
+ density: "3.013 g/cm³",
+ distanceFromSunOrPlanet: "671,100 km (from Jupiter)",
+ orbitalPeriod: "3.55 days",
+ planetType: "Icy",
+ },
+ {
+ id: 600001,
+ name: "Saturn",
+ mass: "5.6834 × 10^26 kg",
+ radius: "58,232 km",
+ temperature: {
+ average: "-178 °C"
+ },
+ equilibriumTemperature: "84 K",
+ volume: "8.2713 × 10^14 km³",
+ density: "0.687 g/cm³",
+ distanceFromSunOrPlanet: "1.434 billion km",
+ orbitalPeriod: "29.45 years",
+ planetType: "GasGiant",
+ },
+ {
+ id: 700001,
+ name: "Uranus",
+ mass: "8.6810 × 10^25 kg",
+ radius: "25,362 km",
+ temperature: {
+ average: "-224 °C"
+ },
+ equilibriumTemperature: "59 K",
+ volume: "6.833 × 10^13 km³",
+ density: "1.27 g/cm³",
+ distanceFromSunOrPlanet: "2.871 billion km",
+ orbitalPeriod: "84 years",
+ planetType: "IceGiant",
+ },
+ {
+ id: 800001,
+ name: "Neptune",
+ mass: "1.02413 × 10^26 kg",
+ radius: "24,622 km",
+ temperature: {
+ average: "-214 °C"
+ },
+ equilibriumTemperature: "48 K",
+ volume: "6.254 × 10^13 km³",
+ density: "1.638 g/cm³",
+ distanceFromSunOrPlanet: "4.495 billion km",
+ orbitalPeriod: "164.8 years",
+ planetType: "IceGiant",
+ },
+];
+
+export async function GET(req: NextRequest) {
+ return NextResponse.json(planetConfigurations);
+};
\ No newline at end of file
diff --git a/app/api/gameplay/missions/route.ts b/app/api/gameplay/missions/route.ts
index 04f403b81..f323c4903 100644
--- a/app/api/gameplay/missions/route.ts
+++ b/app/api/gameplay/missions/route.ts
@@ -1,15 +1,17 @@
import { NextRequest, NextResponse } from "next/server";
-import { MineralDepositsNoAction } from "@/app/components/(structures)/Mining/AvailableDeposits";
-import { Mission } from "@/app/components/(structures)/StructuresForMission";
-interface UserMissionInstance {
+export interface Mission {
id: number;
- user: string;
- time_of_completion: string;
- mission: number;
+ name: string;
+ sequence?: number;
+ description?: string;
+ rewards?: number[];
+ classificationModule?: string;
structure?: number;
+ chapter?: number;
+ component?: React.ComponentType;
};
-
+
const missions: Mission[] = [
// Chapter One Missions
{
@@ -37,7 +39,7 @@ const missions: Mission[] = [
{
// id: 100000023,
id: 3000009,
- name: 'Complete Telescpe > Disk Detector tutorial',
+ name: 'Complete telescope > Disk Detector tutorial',
},
{
id: 3000010,
@@ -77,7 +79,7 @@ const missions: Mission[] = [
},
{
id: 100000033,
- name: 'Complete (first) Telescpe > Disk Detector',
+ name: 'Complete (first) telescope > Disk Detector',
},
{
id: 100000034,
@@ -112,7 +114,7 @@ const missions: Mission[] = [
name: 'Research spaceship',
},
{
- id: 100000042,
+ id: 100000042,
name: "Research launchpad",
},
{
@@ -164,9 +166,130 @@ const missions: Mission[] = [
{
id: 100000005,
name: "Pick new planet and travel",
- },
+ }, // Plus planet initialisation missions
// Any resource collection missions...
+
+
+ // Citizen science missions introduced in #Chapter-2
+ {
+ id: 20000001,
+ name: "Complete Count That Neutrino tutorial",
+ },
+ {
+ id: 200000012,
+ name: "Make your first CTN classification",
+ },
+ {
+ id: 20000002,
+ name: 'Complete Cosmic Muon tutorial',
+ },
+ {
+ id: 200000022,
+ name: 'Make your first CM classification',
+ },
+ {
+ id: 20000003,
+ name: 'Complete Minor Planet tutorial'
+ },
+ {
+ id: 200000032,
+ name: 'Make your first MP classification',
+ },
+ {
+ id: 20000004,
+ name: 'Complete the DMP tutorial',
+ },
+ {
+ id: 200000042,
+ name: 'Make your first DMP classification',
+ },
+ {
+ id: 20000005,
+ name: 'Complete the Planet Four tutorial',
+ },
+ {
+ id: 200000052,
+ name: 'Make your first P4 classification',
+ },
+ {
+ id: 20000006,
+ name: 'Complete the AI for Mars tutorial',
+ },
+ {
+ id: 200000062,
+ name: 'Make your first AI4M classification',
+ },
+ {
+ id: 20000007,
+ name: 'Complete the JVH tutorial',
+ },
+ {
+ id: 200000072,
+ name: 'Make your first JVH classification',
+ },
+ {
+ id: 20000008,
+ name: 'Complete the SFNZ tutorial',
+ },
+ {
+ id: 200000082,
+ name: 'Make your first SFNZ classification',
+ },
+ {
+ id: 20000009,
+ name: 'Complete Solar Storm Watch tutorial',
+ },
+ {
+ id: 200000092,
+ name: 'Make your first SSW classification',
+ },
+ {
+ id: 200000010,
+ name: "Complete the Penguin Watch tutorial",
+ },
+ {
+ id: 2000000102,
+ name: 'Make your first PW classification',
+ },
+ {
+ id: 200000012,
+ name: "Complete the Plankton Portal tutorial",
+ },
+ {
+ id: 2000000122,
+ name: 'Make your first Plankton Portal classification',
+ },
+ {
+ id: 200000013,
+ name: "Collect some fuel"
+ },
+ {
+ id: 200000014,
+ name: "Add fuel to your launchpad & rocket"
+ },
+ {
+ id: 200000015,
+ name: "Build a telescope",
+ },
+ {
+ id: 200000016,
+ name: "Planet hunters module",
+ },
+ {
+ id: 200000017,
+ name: "Build a LIDAR structure",
+ },
+
+ // Chapter 2/3 bridge - finding new (exo-)planet
+ {
+ id: 30000001,
+ name: "Classify and discover a new planet", // Requires classification and period/t0
+ },
+ {
+ id: 300000012,
+ name: "Visit your new planet",
+ },
];
export async function GET(req: NextRequest) {
diff --git a/app/api/gameplay/research/structures/route.ts b/app/api/gameplay/research/structures/route.ts
index ba09e25f7..3e66d3ad5 100644
--- a/app/api/gameplay/research/structures/route.ts
+++ b/app/api/gameplay/research/structures/route.ts
@@ -30,7 +30,7 @@ const ResearchedStructures: ResearchedStructureItem[] = [
name: "Rover camera receiver",
description: 'Used to decode messages from your rover and learn more about the terrain of your planet',
icon_url: '/assets/Items/CameraReceiver.png',
- requiresMission: 1370203,
+ // requiresMission: 1370203,
usedFor: 13714101,
classificationType: 'roverImg',
item: 3102,
@@ -41,7 +41,7 @@ const ResearchedStructures: ResearchedStructureItem[] = [
name: "Lidar module",
description: 'Collect and study weather events and entitie',
icon_url: '/assets/Items/Scoper.png',
- requiresMission: 1370203,
+ // requiresMission: 1370203,
usedFor: 137121301,
classificationType: 'clouds',
item: 3105,
@@ -52,12 +52,30 @@ const ResearchedStructures: ResearchedStructureItem[] = [
name: "Zoodex",
description: "Used to discover and collate animal & plant data",
icon_url: '/assets/Items/Pokedex.png',
- requiresMission: 1370203,
+ // requiresMission: 1370203,
usedFor: 137121301,
classificationType: 'zoodex',
item: 3104,
chapter: 1,
},
+ {
+ id: 5,
+ name: "Launchpad",
+ description: "Launch rockets and satellites using your launchpad",
+ icon_url: '/assets/Items/Launchpad.jpg',
+ requiresMission: 10000004,
+ usedFor: 100000042,
+ item: 3107,
+ chapter: 1,
+ },
+ {
+ id: 6,
+ name: "Physics Lab",
+ description: 'Catalogue results from different particle experiments across the universe',
+ icon_url: '/assets/Items/PhysicsLab.png',
+ item: 31010,
+ chapter: 2,
+ }
];
export async function GET(req: NextRequest) {
diff --git a/app/api/gameplay/research/travel/route.ts b/app/api/gameplay/research/travel/route.ts
new file mode 100644
index 000000000..0499209b3
--- /dev/null
+++ b/app/api/gameplay/research/travel/route.ts
@@ -0,0 +1,30 @@
+import { NextRequest, NextResponse } from "next/server";
+
+interface ResearchedTravelMethod {
+ id: number;
+ name: string;
+ description?: string;
+ icon_url: string;
+ requires?: number; // Does it require a research ticket previously?
+ requiresMission?: number;
+ // usedFor?: number; // Mission
+};
+
+const ResearchedTravelMethods: ResearchedTravelMethod[] = [
+ {
+ id: 94,
+ name: "Intra-Solar System Travel",
+ description: 'Travel to planets & moons within the solar system',
+ icon_url: '/assets/Items/Rocket.png',
+ },
+ {
+ id: 95,
+ name: "Unmanned Probe Deployments",
+ description: 'Deploy probes to planets & moons within the solar system',
+ icon_url: '/assets/Items/Probe.png',
+ },
+];
+
+export async function GET(req: NextRequest) {
+ return NextResponse.json(ResearchedTravelMethods);
+};
\ No newline at end of file
diff --git a/app/api/gameplay/research/upgrades/route.ts b/app/api/gameplay/research/upgrades/route.ts
new file mode 100644
index 000000000..f0f12b936
--- /dev/null
+++ b/app/api/gameplay/research/upgrades/route.ts
@@ -0,0 +1,52 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+export interface ResearchLevelInfo {
+ level: number;
+ description: string;
+};
+
+const researchMappings: Record = {
+ probecount: [
+ { level: 1, description: '1 Probe' },
+ { level: 2, description: '2 Probes' },
+ { level: 3, description: '3 Probes' },
+ ],
+ proberange: [
+ { level: 1, description: '100 Lightyears' },
+ { level: 2, description: '500 Lightyears' },
+ { level: 3, description: '1,000 Lightyears' },
+ { level: 4, description: '5,000 Lightyears' },
+ { level: 5, description: '10,000 Lightyears' },
+ ],
+ telescopepower: [
+ { level: 1, description: 'Basic Magnification' },
+ { level: 2, description: 'Medium Magnification' },
+ { level: 3, description: 'High Magnification' },
+ { level: 4, description: 'Ultra Deep Field' },
+ ],
+};
+
+export async function GET(req: NextRequest) {
+ const { searchParams } = new URL(req.url);
+ const techType = searchParams.get('techType');
+ const countStr = searchParams.get('count');
+
+ if (!techType || !countStr) {
+ return NextResponse.json({ error: 'Missing techType or count' }, { status: 400 });
+ }
+
+ const count = parseInt(countStr, 10);
+ const levels = researchMappings[techType];
+
+ if (!levels) {
+ return NextResponse.json({ error: 'Invalid techType' }, { status: 400 });
+ }
+
+ const matchedLevel = levels.find((l) => l.level === count) || levels[levels.length - 1];
+
+ return NextResponse.json({
+ techType,
+ count,
+ description: matchedLevel.description,
+ });
+};
\ No newline at end of file
diff --git a/app/api/send-push/route.ts b/app/api/send-push/route.ts
new file mode 100644
index 000000000..5f110278a
--- /dev/null
+++ b/app/api/send-push/route.ts
@@ -0,0 +1,84 @@
+import webpush from "web-push";
+import { createClient } from "@supabase/supabase-js";
+import { NextResponse } from "next/server";
+
+// Initialize Supabase client (server-side)
+const supabase = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
+);
+
+// Set up VAPID details
+webpush.setVapidDetails(
+ "mailto:test@domain.com",
+ process.env.VAPID_PUBLIC_KEY!,
+ process.env.VAPID_PRIVATE_KEY!
+);
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json();
+
+ const userIds = body.userIds || (body.userId ? [body.userId] : null);
+ const message = body.message;
+
+ if (!Array.isArray(userIds) || !message) {
+ return NextResponse.json(
+ { error: "Invalid input: userIds must be an array and message must be a string." },
+ { status: 400 }
+ );
+ }
+
+ // Fetch user push subscriptions
+ const { data: profiles, error } = await supabase
+ .from("profiles")
+ .select("id, push_subscription")
+ .in("id", userIds);
+
+ if (error) {
+ console.error("Error fetching user subscriptions:", error);
+ return NextResponse.json({ error: "Database query failed" }, { status: 500 });
+ }
+
+ const results = [];
+
+ for (const profile of profiles) {
+ const { id, push_subscription } = profile;
+
+ if (!push_subscription) {
+ results.push({ id, success: false, error: "No subscription" });
+ continue;
+ }
+
+ let subscription;
+ try {
+ subscription =
+ typeof push_subscription === "string"
+ ? JSON.parse(push_subscription)
+ : push_subscription;
+ } catch (parseErr) {
+ console.error(`Failed to parse push_subscription for user ${id}`, parseErr);
+ results.push({ id, success: false, error: "Invalid subscription format" });
+ continue;
+ }
+
+ const payload = JSON.stringify({
+ title: message,
+ message,
+ });
+
+ try {
+ await webpush.sendNotification(subscription, payload);
+ results.push({ id, success: true });
+ } catch (pushErr) {
+ console.error(`Push error for user ${id}:`, pushErr);
+ results.push({ id, success: false, error: (pushErr as Error).message });
+ }
+ }
+
+ return NextResponse.json({ results });
+ } catch (err) {
+ console.error("Unexpected error:", err);
+ return NextResponse.json({ error: "Unexpected server error" }, { status: 500 });
+ }
+};
\ No newline at end of file
diff --git a/app/api/zoodex/upload-image/gpt/route.ts b/app/api/zoodex/upload-image/gpt/route.ts
new file mode 100644
index 000000000..8c84aac44
--- /dev/null
+++ b/app/api/zoodex/upload-image/gpt/route.ts
@@ -0,0 +1,119 @@
+import { NextRequest, NextResponse } from "next/server";
+import axios from "axios";
+
+const starterTraits = [
+ "flying", "wings", "feathered", "social", "intelligent",
+ "amphibious", "poisonous", "fast", "slow", "scaled",
+ "hairy", "bald", "nocturnal"
+];
+
+const anomalousTraits = [
+ "amorphous", "translucent", "bioluminescent", "deep-sea", "no eyes",
+ "multiple limbs", "gelatinous", "blob-like", "tentacled", "spiked",
+ "unusual appendages", "rare", "unknown species", "unclassified"
+];
+
+export async function POST(req: NextRequest) {
+ try {
+ const formData = await req.formData();
+ const file = formData.get("file") as Blob | null;
+
+ if (!file) {
+ console.error("No file found in request.");
+ return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
+ }
+
+ const arrayBuffer = await file.arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);
+ const base64Image = buffer.toString("base64");
+
+ console.log("File received. Size: ", buffer.length, " bytes");
+
+ const openaiRes = await axios.post(
+ "https://api.openai.com/v1/chat/completions",
+ {
+ model: "gpt-4-turbo",
+ messages: [
+ {
+ role: "system",
+ content: `
+ You are a professional biologist AI. Classify the organism in the image with as much detail as possible, including its kingdom, species, bio, traits, and compatible biomes.
+ Additionally, determine how "anomalous" (unusual, unknown, alien-like) this subject is.
+ The anomalous score should be between 0 (completely normal) and 10 (extremely unusual, potentially alien).
+ Organisms with features like amorphous bodies, translucent skin, deep-sea adaptations, or unclassifiable traits should have a high anomalous score.
+ The response must be formatted as pure JSON without markdown or code block syntax.
+ `
+ },
+ {
+ role: "user",
+ content: [
+ { type: "text", text: "What organism is in this image? Provide a detailed classification in pure JSON format, including an anomalous score." },
+ { type: "image_url", image_url: { url: `data:image/jpeg;base64,${base64Image}` } },
+ ],
+ },
+ ],
+ max_tokens: 300,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ console.log("Raw OpenAI response:", openaiRes.data);
+
+ let classification = openaiRes.data.choices[0].message.content;
+ classification = classification.replace(/```json|```/g, "").trim();
+
+ let parsedClassification;
+ try {
+ parsedClassification = JSON.parse(classification);
+ } catch (parseError) {
+ console.error("Failed to parse classification:", parseError);
+ return NextResponse.json({ error: "Invalid classification data" }, { status: 500 });
+ }
+
+ console.log("Parsed Classification:", parsedClassification);
+
+ const detectedTraits = Array.isArray(parsedClassification.traits)
+ ? parsedClassification.traits
+ : typeof parsedClassification.traits === "string"
+ ? parsedClassification.traits.split(",").map((t: string) => t.trim())
+ : [];
+
+ console.log("Detected Traits:", detectedTraits);
+
+ const relevantTraits = starterTraits
+ .filter((trait) => detectedTraits.includes(trait))
+ .slice(0, 3);
+
+ while (relevantTraits.length < 3) {
+ const remainingTraits = starterTraits.filter(
+ (trait) => !relevantTraits.includes(trait)
+ );
+ if (remainingTraits.length === 0) break;
+ relevantTraits.push(
+ remainingTraits[Math.floor(Math.random() * remainingTraits.length)]
+ );
+ }
+
+ parsedClassification.traits = relevantTraits;
+
+ let anomalousScore = 0;
+ for (const trait of detectedTraits) {
+ if (anomalousTraits.includes(trait)) {
+ anomalousScore += 2;
+ }
+ }
+ anomalousScore = Math.min(10, anomalousScore);
+
+ parsedClassification.anomalous_score = anomalousScore;
+
+ return NextResponse.json(parsedClassification);
+ } catch (error: any) {
+ console.error("Error in API route:", error.response?.data || error.message);
+ return NextResponse.json({ error: "Failed to classify image" }, { status: 500 });
+ }
+};
\ No newline at end of file
diff --git a/app/auth/LoginModal.tsx b/app/auth/LoginModal.tsx
index f1a2054bd..53139ac7f 100644
--- a/app/auth/LoginModal.tsx
+++ b/app/auth/LoginModal.tsx
@@ -9,6 +9,8 @@ import { MicroscopeIcon, CodeIcon, FilesIcon } from "lucide-react";
import { Flexbox } from 'react-layout-kit';
import { createStyles } from 'antd-style';
import { rgba } from 'polished';
+import { Card, CardContent } from "@/components/ui/card";
+import { Separator } from "@/components/ui/separator";
interface AuthPageProps {
children: ReactNode;
@@ -57,6 +59,8 @@ const Navbar = () => {
};
function SupabaseAuthWrapper({ children }: { children: ReactNode }) {
+ const supabase = useSupabaseClient();
+
const { styles } = useStyles();
return (
@@ -67,7 +71,7 @@ function SupabaseAuthWrapper({ children }: { children: ReactNode }) {
Star Sailors
- Explore the cosmos & catalogue discoveries in different scientific disciplines
+ Explore the cosmos & catalogue discoveries in different scientific disciplines
-
- {children}
+
+
+
+
+
+
+
+ Or continue with
+
+
+
+
Connect
+
+
+ Threads
+
+
+
+ Github
+
+
+
+ Documentation
+
+
+
+
+
diff --git a/app/auth/landing.tsx b/app/auth/landing.tsx
new file mode 100644
index 000000000..29cbe2790
--- /dev/null
+++ b/app/auth/landing.tsx
@@ -0,0 +1,348 @@
+import Link from "next/link";
+import Image from "next/image";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import { Card } from "@/components/ui/card";
+import { Microscope, Users, Book, Menu, Github, Linkedin, Instagram, Twitter, PaintBucket } from "lucide-react";
+import { TimelinePoint, TimelineSection } from "./landing/timeline";
+import { RoadmapPoint, RoadmapSection } from "./landing/roadmap";
+import Footer from "./landing/footer";
+
+const teamMembers = [
+ {
+ img: "Eric.jpg",
+ name: "L. R. Arbuckle",
+ role: "Technical Lead",
+ bio: "Full stack engineer with a background in mathematics and decentralised science.",
+ social: [
+ { platform: "Github", url: "https://github.com/gizmotronn", icon: },
+ { platform: "Linkedin", url: "https://www.linkedin.com/in/l-arbuckle/", icon: },
+ ],
+ },
+ {
+ img: "Campbells.jpg",
+ name: "Rhys Campbell",
+ role: "Machine Learning Specialist",
+ bio: "Passionate open-source contributor & the brains behind MATR AI.",
+ social: [
+ { platform: "Github", url: "https://github.com/Rhysmalcolm13", icon: },
+ { platform: "Linkedin", url: "https://www.linkedin.com/in/rhys-campbell-96a196294/", icon: },
+ ],
+ },
+ {
+ img: "Fred.webp",
+ name: "Bruce Chooser",
+ role: "UX Designer",
+ bio: "Cybersecurity consultant and creator of all things audio-visual.",
+ social: [
+ { platform: "Instagram", url: "https://instagram.com/choose_bruce", icon: },
+ ],
+ },
+ ];
+
+ const partners = [
+ {
+ logo: "/d.jpg",
+ name: "DeSci Labs",
+ description: "Star Sailors is proud to use and contribute to the DeSci Nodes platform for publishing scientific data.",
+ url: "https://descilabs.com",
+ },
+ {
+ logo: "/orc.webp",
+ name: "ORCID",
+ description: "Star Sailors allows users to link their discoveries with their ORCID profile.",
+ url: "https://ORCID.com",
+ },
+ // {
+ // logo: "https://rsv.org.au/wp-content/uploads/%E2%80%A2RSV-Logo-Final-Type-Small.jpg",
+ // name: "Royal Society of Victoria",
+ // description: "Star Sailors is proudly supported by the RSV",
+ // url: "https://rsv.org.au",
+ // },
+ ];
+
+ const timelinePoints: TimelinePoint[] = [
+ {
+ date: "September 2021",
+ title: "Incorporation",
+ description: "After over a year of building small microservices in the citizen science sector, Star Sailors was officially incorporated in Australia and received its first round of investment.",
+ image: "/placeholder.svg?height=300&width=400",
+ },
+ {
+ date: "January 2023",
+ title: "First releases",
+ description: "Star Sailors went into 'General Availability' after the completion of the Nights & Weekends S3 Incubator, marking the first time the public could interface with the Star Sailors engine.",
+ image: "/placeholder.svg?height=300&width=400",
+ },
+ {
+ date: "June 2024",
+ title: "Version 2 hits GA",
+ description: "Version 2 was released to our users, with the goal to create a product that was more game and less 'webapp'.",
+ image: "/placeholder.svg?height=300&width=400",
+ },
+ {
+ date: "April 2025",
+ title: "Development of V3 begins",
+ description: "The final release for Star Sailors V2 is scheduled for April 2025.",
+ image: "/placeholder.svg?height=300&width=400",
+ },
+ ];
+
+ const roadmapPoints: RoadmapPoint[] = [
+ {
+ quarter: "Q2 2025",
+ title: "Early access to V3",
+ description: "Star Sailors V3 will have better graphics, more content and improved social features. We hope to offer passionate users early access during Q2.",
+ },
+ {
+ quarter: "Q4 2025",
+ title: "First release of V3",
+ description: "The first release of V3 will include a mobile application for iOS and Android, and even more ways to contribute things you see and interact with in your daily life.",
+ },
+ {
+ quarter: "Q1 2026",
+ title: "Multimedia side-projects",
+ description: "The 'Star Sailors Cinematic Universe' will commence with short stories and guidebooks aimed to equip early users with the tools required to enjoy Star Sailors.",
+ },
+];
+
+export default function LandingSS() {
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ {["Open-Source"].map((item) => (
+
+ {item}
+
+ ))}
+
+
+
+
+
+
+
+
+ {/* Hero */}
+
+
+
+
+
+
+
+ New content every week
+
+
+
+ Actively contribute to science
+
+
+ Discover the world around you and contribute to real scientific research in Star Sailors
+
+
+
+
+ Login
+
+
+
+ View Discoveries
+
+
+
+
+
+
+
+
+ {/* About Section */}
+
+
+
+
+
About Star Sailors
+
+ The ability to contribute to science shouldn't require a university degree and a JSTOR account. Star Sailors works with partners like DeSci Labs,
+ ORCID, and the Royal Society of Victoria to give our users opportunities to meaningfully impact the scientific industry. It's 100% free & open-source. And it always will be.
+
+
+
+
+
+
+
+
In active development
+
We've provided weekly updates to SS since 2022.
+
+
+
+
+
+
+
+
Sandbox gameplay
+
Contribute to any projects, in any order, at any time
+
+
+
+
+
+
+
+
+
+ Technical Team
+
+ {teamMembers.map((member, index) => (
+
+
+
+
+
{member.name}
+
{member.role}
+
{member.bio}
+
+
+ ))}
+
+
+
+
+ Our Partners
+
+ {partners.map((partner, index) => (
+
+
+ {partner.name}
+ {partner.description}
+
+ Learn More
+
+
+ ))}
+
+
+
+
+
+ {/* Roadmap Section */}
+
+ {/*
+
+
Game Screenshots
+
+ {[1, 2, 3].map((i) => (
+
+
+
+ ))}
+
+
+ */}
+
+ {/* Footer */}
+
+
+ );
+};
+
+
+{/* News Section */}
+{/*
+
+
+
Latest News
+
+ Stay updated with our latest discoveries and community achievements
+
+
+
+ {[1, 2, 3].map((i) => (
+
+
+
+
+
+
News
+
New Galaxy Cluster Discovered by Players
+
+ Our community has identified a previously unknown galaxy cluster...
+
+
+ Read More →
+
+
+
+ ))}
+
+
+ */}
\ No newline at end of file
diff --git a/app/auth/landing/footer.tsx b/app/auth/landing/footer.tsx
new file mode 100644
index 000000000..0b40aa9fe
--- /dev/null
+++ b/app/auth/landing/footer.tsx
@@ -0,0 +1,146 @@
+import Link from "next/link";
+
+export default function Footer() {
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/app/auth/landing/roadmap.tsx b/app/auth/landing/roadmap.tsx
new file mode 100644
index 000000000..2b5dfcc43
--- /dev/null
+++ b/app/auth/landing/roadmap.tsx
@@ -0,0 +1,44 @@
+export type RoadmapPoint = {
+ quarter: string;
+ title: string;
+ description: string;
+};
+
+interface RoadmapSectionProps {
+ title?: string;
+ subtitle?: string;
+ points: RoadmapPoint[];
+}
+
+export function RoadmapSection({
+ title = "Roadmap",
+ subtitle = "Our vision for the future of Star Sailors",
+ points,
+}: RoadmapSectionProps) {
+ return (
+
+
+
{title}
+
{subtitle}
+
+
+ {points.map((point, index) => (
+
+ {/* Timeline Dot */}
+
+
+ {/* Roadmap Card */}
+
+
+
{point.title}
+ {point.quarter}
+
+
{point.description}
+
+
+ ))}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/auth/landing/timeline.tsx b/app/auth/landing/timeline.tsx
new file mode 100644
index 000000000..782d3f836
--- /dev/null
+++ b/app/auth/landing/timeline.tsx
@@ -0,0 +1,61 @@
+import Image from "next/image";
+import { Calendar } from 'lucide-react';
+
+export type TimelinePoint = {
+ date: string
+ title: string
+ description: string
+ image: string
+};
+
+interface TimelineSectionProps {
+ title?: string
+ points: TimelinePoint[]
+};
+
+export function TimelineSection({
+ title = "Release history",
+ points
+}: TimelineSectionProps) {
+ return (
+
+
+
{title}
+
+
+
+
+ {points.map((point, index) => (
+
+
+ {/*
+
+
*/}
+
+
+
+
{/* md:w-1/2 */}
+
+ {point.date}
+
+
{point.title}
+
{point.description}
+
+ {/*
+
+
*/}
+
+
+
+ ))}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/auth/page.tsx b/app/auth/page.tsx
index 6e04d28fc..1c67524c8 100644
--- a/app/auth/page.tsx
+++ b/app/auth/page.tsx
@@ -16,13 +16,14 @@ const Login = () => {
}, [session, router]);
return (
-
- {!session ? (
-
- ) : (
-
Logged in
- )}
-
+ //
+ // {!session ? (
+ //
+ // ) : (
+ //
Logged in
+ // )}
+ //
+
);
};
diff --git a/app/auth/register/page.tsx b/app/auth/register/page.tsx
index 74ae6ef2e..dde0c78dd 100644
--- a/app/auth/register/page.tsx
+++ b/app/auth/register/page.tsx
@@ -13,7 +13,7 @@ const Register = () => {
useEffect(() => {
if (session) {
router.push('/');
- }
+ };
}, [session, router]);
return (
diff --git a/app/deploy/page.tsx b/app/deploy/page.tsx
new file mode 100644
index 000000000..f90b07dbd
--- /dev/null
+++ b/app/deploy/page.tsx
@@ -0,0 +1,111 @@
+'use client';
+
+import React, { useEffect, useState } from "react";
+import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
+import GameNavbar from "@/components/Layout/Tes";
+import { Dialog, DialogContent } from "@/components/ui/dialog";
+import { Building2Icon, Router } from "lucide-react";
+import { useRouter } from "next/navigation";
+import TelescopeRangeSlider from "@/components/(scenes)/deploy/TelescopeRange";
+import AutomatonDeploySection from "@/components/(scenes)/deploy/AutomatonItems";
+import BiomassOnEarth from "@/components/Data/BiomassEarth";
+import EarthStats from "@/components/Data/EarthStats";
+import BiologySectionDeploy from "@/components/(scenes)/deploy/BiodomeStationCard";
+
+export default function DeployAutomatonsPage() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const router = useRouter();
+
+ // const [selectedLocation, setSelectedLocation] = useState(null);
+
+ return (
+
+
+
+
+
+
+
+
+
{
+ if (!open) {
+ router.push("/")
+ }
+ }}
+ >
+
+ {/* Header */}
+
+
+ {/* <-- CHANGE THIS */}
+
Kickoff
+
+
+
+
+
+
Current energy: 5
+
+
+ {/* Labels */}
+
+
Astronomy
+
Meteorology
+
Biology
+
+
+
+ <>
+
+ {/* Astronomy Section */}
+
+
+
+
+ {/* Meteorology Section */}
+
+
+ {/* Biology Section */}
+
+
+
+
+ {/*
Nearby stats...
*/}
+
+
+ These projects are available for observation:
+
+
+
+
+ >
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index 2c14c183f..d67dc7be0 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,115 +1,27 @@
-"use client";
-
-import "@/styles/globals.css";
-import { createPagesBrowserClient } from "@supabase/auth-helpers-nextjs";
-import { SessionContextProvider, useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
-import { useEffect, useState, ReactNode } from "react";
-import Head from "next/head";
-import { ActivePlanetProvider, useActivePlanet } from "@/context/ActivePlanet";
-import dynamic from 'next/dynamic';
-import { InventoryProvider } from "@/context/InventoryContext";
-import { UserAnomaliesProvider } from "@/context/UserAnomalies";
-import { bgImage, backgroundImages } from "@/constants/backgrounds";
-import { Analytics } from "@vercel/analytics/react"
-import { MissionProvider } from "@/context/MissionContext";
-// import { CreateStructureWithItemRequirementinfo } from "@/components/Gameplay/Inventory/Structures/Structure";
-
-export default function RootLayout({ children }: { children: React.ReactNode }) {
- const [supabaseClient] = useState(() => createPagesBrowserClient());
-
- const { activePlanet } = useActivePlanet();
-
- if (activePlanet) {
- useEffect(() => {
- console.log("Active Planet: ", activePlanet);
- }, [activePlanet]);
- }
-
- return (
-
-
-
- Star Sailors
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* */}
-
-
- {children}
-
-
-
- {/* */}
-
-
-
-
- );
-};
-
-interface LayoutProps {
- children: ReactNode;
- bg: any;
+import { ReactNode } from "react";
+import type { Metadata, Viewport } from "next";
+import RootLayoutClient from "./RootLayoutClient";
+
+export const metadata: Metadata = {
+ title: "Star Sailors",
+ description: "Catalogue the Stars",
+ icons: {
+ icon: "/favicon.ico",
+ apple: [
+ {
+ url: "https://i.ibb.co/qrSP4gq/Removebg.png",
+ sizes: "180x180",
+ type: "image/png",
+ },
+ ],
+ },
+ manifest: "/manifest.json",
};
-interface ResponsiveLayoutProps {
- leftContent: React.ReactNode;
- middleContent: React.ReactNode;
- // rightContent: React.ReactNode;
+export const viewport: Viewport = {
+ themeColor: "#ffffff",
};
-interface SectionProps {
- background: string;
- children: React.ReactNode;
-}
-
-const Section: React.FC = ({ background, children }) => (
-
- {children}
-
-);
\ No newline at end of file
+export default function RootLayout({ children }: { children: ReactNode }) {
+ return {children} ;
+};
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 50c44c145..4b086238e 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,24 +1,456 @@
-"use client"
+"use client";
-import { OnboardingLayout } from "@/app/components/Template";
import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
-import { Landing } from "@/app/components/landing";
import { useEffect, useState } from "react";
import LoginPage from "./auth/LoginModal";
-import OnboardingWindow from "./components/(scenes)/chapters/(onboarding)/window";
-import EarthView from "./scenes/earth/page";
+import { useRouter} from "next/navigation";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { useActivePlanet } from "@/context/ActivePlanet";
+import { subscribeUser, unsubscribeUser, sendNotification } from './actions'
+import StructuresOnPlanet, { AtmosphereStructuresOnPlanet, OrbitalStructuresOnPlanet } from "@/components/Structures/Structures";
+// import EnhancedWeatherEvents from '@/components/(scenes)/mining/enhanced-weather-events';
+import AllAutomatonsOnActivePlanet from "@/components/Structures/Auto/AllAutomatons";
+import { EarthViewLayout } from "@/components/(scenes)/planetScene/layout";
+import Onboarding from "./scenes/onboarding/page";
+import SimpleeMissionGuide from "./tests/singleMissionGuide";
+import AllSatellitesOnActivePlanet from "@/components/Structures/Auto/AllSatellites";
+import LandingSS from "./auth/landing";
+import GameNavbar from "@/components/Layout/Tes";
+import BiomassOnEarth from "@/components/Data/BiomassEarth";
-export default function Home() {
+function urlBase64ToUint8Array(base64String: string) {
+ const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
+ const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
+ const rawData = window.atob(base64);
+ const outputArray = new Uint8Array(rawData.length);
+ for (let i = 0; i < rawData.length; ++i) {
+ outputArray[i] = rawData.charCodeAt(i);
+ }
+ return outputArray;
+};
+
+function PushNotificationManager() {
+ const session = useSession();
const supabase = useSupabaseClient();
+ const [isSupported, setIsSupported] = useState(false);
+ const [subscription, setSubscription] = useState(null);
+ const [hasSubscribedBefore, setHasSubscribedBefore] = useState(false);
+ const [message, setMessage] = useState("");
+
+ useEffect(() => {
+ if ("serviceWorker" in navigator && "PushManager" in window) {
+ setIsSupported(true);
+ registerServiceWorker();
+ }
+ }, []);
+
+ useEffect(() => {
+ if (session?.user?.id) {
+ checkExistingSubscription(session.user.id);
+ }
+ }, [session]);
+
+ async function checkExistingSubscription(userId: string) {
+ const { data, error } = await supabase
+ .from("profiles")
+ .select("push_subscription")
+ .eq("id", userId)
+ .single();
+
+ if (error) {
+ console.error("Failed to fetch subscription info:", error);
+ return;
+ }
+
+ if (data?.push_subscription) {
+ setHasSubscribedBefore(true);
+ }
+ }
+
+ async function registerServiceWorker() {
+ const registration = await navigator.serviceWorker.register("/sw.js", {
+ scope: "/",
+ updateViaCache: "none",
+ });
+
+ const sub = await registration.pushManager.getSubscription();
+ setSubscription(sub);
+ }
+
+ async function subscribeToPush() {
+ const registration = await navigator.serviceWorker.ready;
+
+ const sub = await registration.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: urlBase64ToUint8Array(process.env.VAPID_PUBLIC_KEY!),
+ });
+
+ const subJSON = sub.toJSON();
+
+ setSubscription(sub);
+
+ const { error } = await supabase
+ .from("profiles")
+ .update({ push_subscription: subJSON })
+ .eq("id", session?.user.id);
+
+ if (error) console.error("Error saving subscription:", error);
+ else setHasSubscribedBefore(true);
+ }
+
+ async function unsubscribeFromPush() {
+ await subscription?.unsubscribe();
+ setSubscription(null);
+
+ const { error } = await supabase
+ .from("profiles")
+ .update({ push_subscription: null })
+ .eq("id", session?.user.id);
+
+ if (error) console.error("Error removing subscription:", error);
+ else setHasSubscribedBefore(false);
+ }
+
+ async function sendTestNotification() {
+ if (!session) return;
+
+ try {
+ const res = await fetch("/api/send-push", {
+ method: "POST",
+ body: JSON.stringify({ userId: session.user.id, message }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const json = await res.json();
+ console.log("Push response:", res.status, json);
+
+ if (!res.ok) {
+ alert(`Push failed: ${json?.error || res.status}`);
+ }
+ } catch (err) {
+ console.error("Fetch error:", err);
+ alert("Network or server error");
+ }
+
+ setMessage("");
+ }
+
+ if (!isSupported) {
+ return Push notifications are not supported in this browser.
;
+ };
+
+ return (
+
+
+ {subscription || hasSubscribedBefore ? (
+ <>
+ {/*
You are subscribed to push notifications.
*/}
+
+ {/*
+ Unsubscribe
+ */}
+
+ {/*
setMessage(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-500 rounded-md bg-transparent placeholder-gray-400 text-white"
+ /> */}
+
+ {/*
+ Send Test Notification
+ */}
+ >
+ ) : (
+ <>
+ {/*
You are not subscribed to push notifications.
*/}
+
+ Subscribe
+
+ >
+ )}
+
+ );
+};
+
+function InstallPrompt() {
+ const [isIOS, setIsIOS] = useState(false);
+ const [isStandalone, setIsStandalone] = useState(false);
+ const [deferredPrompt, setDeferredPrompt] = useState(null);
+
+ useEffect(() => {
+ setIsIOS(/iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream);
+ setIsStandalone(window.matchMedia('(display-mode: standalone)').matches);
+
+ const handler = (e: any) => {
+ // Prevent default AND store the prompt
+ e.preventDefault();
+ console.log("beforeinstallprompt event captured");
+ setDeferredPrompt(e);
+ };
+
+ window.addEventListener("beforeinstallprompt", handler);
+
+ return () => window.removeEventListener("beforeinstallprompt", handler);
+ }, []);
+
+ const installApp = () => {
+ if (deferredPrompt) {
+ console.log("Prompting user to install");
+ deferredPrompt.prompt();
+
+ deferredPrompt.userChoice.then((choiceResult: any) => {
+ console.log("User choice result:", choiceResult);
+ if (choiceResult.outcome === "accepted") {
+ console.log("User accepted the install prompt");
+ } else {
+ console.log("User dismissed the install prompt");
+ }
+ setDeferredPrompt(null);
+ });
+ } else {
+ console.log("No install prompt available");
+ }
+ };
+
+
+ if (isStandalone) return null;
+
+ return (
+
+
Install App
+
Add to Home Screen
+ {isIOS && (
+
+ On iOS, tap the Share icon ⎋ and then
+ "Add to Home Screen" ➕ .
+
+ )}
+
+ );
+};
+
+export default function Home() {
const session = useSession();
+ const supabase = useSupabaseClient();
+ const { activePlanet } = useActivePlanet();
+ const router = useRouter();
+
+ const [hasRequiredItems, setHasRequiredItems] = useState(null);
+ const [userClassifications, setUserClassifications] = useState(false);
+ const [planetData, setPlanetData] = useState(null);
+
+ const [showDeployModal, setShowDeployModal] = useState(false);
+
+ useEffect(() => {
+ if (!session) return;
+
+ const checkInventory = async () => {
+ try {
+ const { data, error } = await supabase
+ .from("inventory")
+ .select("id, quantity")
+ .eq("owner", session.user.id)
+ .in("item", [3105, 3104, 3103])
+ .gt("quantity", 0);
+
+ if (error) throw error;
+
+ setHasRequiredItems(data.length > 0);
+ } catch (error: any) {
+ console.error("Error checking inventory:", error.message);
+ setHasRequiredItems(false);
+ }
+ };
+
+ const checkClassifications = async () => {
+ try {
+ const { data, error } = await supabase
+ .from("classifications")
+ .select("*")
+ .eq("author", session.user.id);
+
+ if (error) {
+ setUserClassifications(false);
+ } else {
+ setUserClassifications(data && data.length > 0);
+ }
+ } catch (error: any) {
+ console.error(error);
+ }
+ };
+
+ const checkLinkedAnomaliesLastWeek = async () => {
+ const now = new Date();
+ const oneWeekAgo = new Date(now);
+ oneWeekAgo.setDate(now.getDate() - 7);
+ const fromDate = oneWeekAgo.toISOString();
+ const toDate = now.toISOString();
+
+ const { data, error } = await supabase
+ .from("linked_anomalies")
+ .select("id")
+ .eq("author", session.user.id)
+ .gte("date", fromDate)
+ .lte("date", toDate);
+
+ if (error) {
+ console.error("Error checking linked anomalies:", error);
+ return;
+ }
+
+ if (!data || data.length === 0) {
+ setShowDeployModal(true);
+ }
+ };
+
+ checkClassifications();
+ checkInventory();
+ checkLinkedAnomaliesLastWeek();
+ }, [session, supabase]);
+
+ useEffect(() => {
+ if (!activePlanet) return;
+
+ async function fetchPlanetData() {
+ try {
+ const { data, error } = await supabase
+ .from("anomalies")
+ .select("*")
+ .eq("id", activePlanet.id);
+
+ if (error) {
+ console.error("Error fetching planet data: ", error);
+ } else {
+ setPlanetData(data[0]);
+ }
+ } catch (error: any) {
+ console.error("Error fetching planet data: ", error);
+ }
+ }
+
+ fetchPlanetData();
+ }, [activePlanet, supabase]);
if (!session) {
- return (
-
- );
- };
+ return ;
+ }
+
+ if (hasRequiredItems === null) {
+ return Loading...
;
+ }
+
+ if (!hasRequiredItems) {
+ return ;
+ }
+
+ function handleDeployConfirm() {
+ setShowDeployModal(false);
+ router.push("/deploy");
+ }
+
+ async function handleDeployCancel() {
+ setShowDeployModal(false);
+ try {
+ const { error } = await supabase.from("linked_anomalies").insert([
+ {
+ author: session?.user.id,
+ anomaly_id: 1,
+ automaton: "Empty/Cancel"
+ },
+ ]);
+
+ if (error) {
+ console.error("Error inserting linked anomaly:", error.message);
+ }
+ } catch (error: any) {
+ console.error("Unexpected error inserting linked anomaly:", error.message);
+ }
+ }
return (
-
+
+
+
+
+
+
+
Temperature:
+
{planetData?.temperatureEq} K
+
Humidity:
+
{planetData?.humidity} K
+ {planetData?.id === 30 &&
}
+
+ {showDeployModal && (
+
+
+
+ No Anomaly Records Last Week
+
+ You have no anomaly records from the previous week. Would you like to deploy your automatons to gather more data?
+
+
+
+
+ Cancel
+
+
+ Deploy Automatons
+
+
+
+
+ )}
+
+
+
+
+
+
+
+ {!userClassifications && (
+
+
+
+ )}
+
);
-};
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/app/planets/[id]/page.tsx b/app/planets/[id]/page.tsx
new file mode 100644
index 000000000..b0a46f962
--- /dev/null
+++ b/app/planets/[id]/page.tsx
@@ -0,0 +1,646 @@
+'use client';
+
+import React, { useEffect, useCallback, useState, useMemo } from "react";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import { PostCardSingleWithGenerator } from "@/content/Posts/PostWithGen";
+import Image from "next/image";
+import { Button } from "@/components/ui/button";
+import { ArrowLeft, Droplets, FileText, Info, Thermometer } from "lucide-react";
+import PlanetGenerator from "@/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator";
+// import { PlanetGenerator } from "starsailors-planet-generator";
+import WeatherEventStatus from "@/components/Data/Generator/Weather/EventsCounter";
+import BiomassStats from "@/components/Structures/Missions/Biologists/BiomassOnPlanet";
+import BiomeAggregator from "@/components/Data/Generator/BiomeAggregator";
+import PlanetProgress from "@/components/Structures/Missions/Astronomers/PlanetHunters/PlanetCompletion";
+// import WeatherGenerator from "@/components/Data/Generator/Weather/SimpleWeatherEvents";
+import GameNavbar from "@/components/Layout/Tes";
+
+export interface Classification {
+ id: number;
+ content: string | null;
+ author: string | null;
+ anomaly: Anomaly | null;
+ media: (string | { uploadUrl?: string })[] | null;
+ classificationtype: string | null;
+ classificationConfiguration?: any;
+ created_at: string;
+ title?: string;
+ votes?: number;
+ category?: string;
+ tags?: string[];
+ images?: string[];
+ relatedClassifications?: Classification[];
+};
+
+type PlanetData = {
+ name: string;
+ galaxy: string;
+ diameter: string;
+ dayLength: string;
+ temperature: string;
+ climate: string;
+};
+
+type FocusView = "planet" | "overview" | "Climate" | "atmosphere" | "exploration" | "map" | "edit"
+
+export type Anomaly = {
+ id: number;
+ content: string | null;
+ anomalytype: string | null;
+ mass: number | null;
+ radius: number | null;
+ density: number | null;
+ gravity: number | null;
+ temperature: number | null;
+ orbital_period: number | null;
+avatar_url: string | null;
+ created_at: string;
+};
+
+export interface AggregatedCloud {
+ annotationOptions: Record;
+ classificationOptions: Record>;
+ additionalFields: Record>;
+ cloudColours?: Record;
+};
+
+export interface AggregatedP4 {
+ fanCount: number;
+ blotchCount: number;
+ classificationCounts: Record;
+};
+
+export interface AggregatedAI4M {
+ sandCount: number;
+ soilCount: number;
+ bedrockCount: number;
+ rockCount: number;
+ unlabelledCount: number;
+ classificationCounts: Record;
+};
+
+export interface AggregatedBalloon {
+ shapeCount: number;
+ vortexCount: number;
+ streakCount: number;
+ unlabelledCount: number;
+ classificationCounts: Record;
+};
+
+export interface AI4MClassification {
+ id: number;
+ classificationConfiguration: any;
+ annotationOptions: any[];
+};
+
+import { useParams } from 'next/navigation';
+import Link from "next/link";
+
+export default function TestPlanetWrapper() {
+ const params = useParams();
+ const id = params?.id as string;
+
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [classification, setClassification] = useState(null);
+ const [anomaly, setAnomaly] = useState(null);
+ const [currentView, setCurrentView] = useState("planet");
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [dominantBiome, setDominantBiome] = useState('Barren (Pending)');
+ const [cloudSummary, setCloudSummary] = useState(null);
+ const [p4Summary, setP4Summary] = useState(null);
+ const [ai4MSummary, setAI4MSummary] = useState(null);
+ const [relatedClassifications, setRelatedClassifications] = useState([]);
+ const [period, setPeriod] = useState("");
+ const [showCurrentUser, setShowCurrentUser] = useState(true);
+ const [showMetadata, setShowMetadata] = useState(false);
+ const [currentPlanet, setCurrentPlanet] = useState("orionis")
+ const [density, setDensity] = useState(null);
+ const [temperature, setTemperature] = useState(null);
+ const [biomassScore, setBiomassScore] = useState(0);
+ const [surveyorPeriod, setSurveyorPeriod] = useState("")
+ const [comments, setComments] = useState([]);
+ const [timeLeft, setTimeLeft] = useState(0);
+
+ const getTimeUntilNextSunday = () => {
+ const now = new Date();
+ const nextSunday = new Date(now);
+ nextSunday.setDate(now.getDate() + (7 - now.getDay()) % 7);
+ nextSunday.setHours(10, 1, 0, 0);
+ return nextSunday.getTime() - now.getTime();
+ };
+
+ const calculateBiomass = (temperature?: number, radius?: number, surveyorPeriod?: number): number => {
+ const T = temperature ?? 300;
+ const R = radius ?? 1;
+ const P = surveyorPeriod ?? 1.5; // default to 1.5 if undefined or invalid
+
+ return (
+ 0.1 *
+ (T / (T + 300)) *
+ (1 / (1 + Math.exp(-(R - 1.2)))) *
+ (1 / (1 + Math.exp(-(1.5 - P))))
+ );
+};
+
+ const formatTime = (timeInSeconds: number) => {
+ const hours = String(Math.floor(timeInSeconds / 3600)).padStart(2, "0");
+ const minutes = String(Math.floor((timeInSeconds % 3600) / 60)).padStart(2, "0");
+ const seconds = String(timeInSeconds % 60).padStart(2, "0");
+ return `${hours}:${minutes}:${seconds}`;
+ };
+
+ useEffect(() => {
+ // Ensure all parameters are parsed correctly
+ const orbital = parseFloat(surveyorPeriod);
+ const rad = density !== null && !isNaN(density) ? density : 1; // Default to 1 if density is invalid
+ const temp = temperature !== null && !isNaN(temperature) ? temperature : 300; // Default to 300 if temperature is invalid
+
+ // Ensure orbital is a valid number, fallback to a default value if invalid
+ const validOrbital = !isNaN(orbital) ? orbital : 1.5;
+
+ // Calculate biomass score with valid parameters
+ setBiomassScore(calculateBiomass(temp, rad, validOrbital));
+}, [density, temperature, surveyorPeriod]); // Re-run when these values change
+
+ const MemoizedPlanetGenerator = useMemo(() => {
+ return classification?.id && classification.author ? (
+ //
+
+ ) : null;
+ }, [classification?.id, classification?.author]);
+
+ useEffect(() => {
+ if (!params.id) return;
+
+ const fetchClassifications = async () => {
+ const { data, error } = await supabase
+ .from("classifications")
+ .select("*, anomaly:anomalies(*), classificationConfiguration, media")
+ .eq('id', params.id)
+ .single();
+
+ if (error) {
+ console.error("Error fetching classification", error);
+ setError("Failed to fetch classification data");
+ return;
+ }
+
+ setClassification(data);
+ setAnomaly(data.anomaly);
+
+ const periodFromField = data.classificationConfiguration?.additionalFields?.field_0;
+ if (periodFromField) setPeriod(periodFromField);
+
+ const parentPlanetLocation = data.anomaly?.id;
+ if (parentPlanetLocation) {
+ const query = supabase
+ .from("classifications")
+ .select("*, anomaly:anomalies(*), classificationConfiguration, media")
+ .eq("classificationConfiguration->>parentPlanetLocation", parentPlanetLocation.toString());
+
+ const { data: relatedData, error: relatedError } = await query;
+
+ if (relatedError) {
+ setError("Failed to fetch related classifications")
+ setLoading(false);
+ return;
+ }
+
+ if (relatedData) {
+ const votePromises = relatedData.map(async (related) => {
+ const { count, error: votesError } = await supabase
+ .from("votes")
+ .select("*", { count: "exact" })
+ .eq("classification_id", related.id);
+
+ if (votesError) {
+ console.error("Error fetching votes:", votesError);
+ return { ...related, votes: 0 };
+ }
+
+ return { ...related, votes: count };
+ });
+
+ const relatedClassificationsWithVotes = await Promise.all(votePromises);
+ setRelatedClassifications(relatedClassificationsWithVotes);
+ }
+ }
+
+ setLoading(false);
+ };
+
+ fetchClassifications();
+ }, [session]);
+
+ const fetchComments = async () => {
+ if (!classification) return;
+
+ const { data, error } = await supabase
+ .from("comments")
+ .select("*")
+ .eq("classification_id", classification.id)
+ .order("created_at", { ascending: false });
+
+ if (error) {
+ console.error("Error fetching comments: ", error);
+ } else {
+ let aggregatedComments: any[] = [];
+
+ // Create a map to store the aggregated comments by category and author
+ const aggregatedMap: Record = {};
+ let biomassSum = 0; // Variable to track biomass sum
+
+ data?.forEach((comment) => {
+ // Create a unique key based on category and author
+ const key = `${comment.category}-${comment.author}`;
+
+ if (aggregatedMap[key]) {
+ // If a comment of this type exists, aggregate the values
+ if (comment.value) {
+ aggregatedMap[key].value += parseFloat(comment.value) || 0;
+ }
+ } else {
+ // If this is the first time we encounter this comment type and author, just add it
+ aggregatedMap[key] = { ...comment, value: parseFloat(comment.value) || 0 };
+ }
+
+ // Check for "Biomass" comments and aggregate their value
+ if (comment.category === "Biomass") {
+ biomassSum += parseFloat(comment.value) || 0;
+ }
+ });
+
+ // Convert the map back to an array
+ aggregatedComments = Object.values(aggregatedMap);
+
+ // Now, set the aggregated comments to state
+ setComments(aggregatedComments);
+
+ // Update the biomass score by adding the biomass sum
+ setBiomassScore(prevScore => prevScore + biomassSum);
+
+ // Extract specific comments after aggregation for individual fields like OrbitalPeriod, PlanetType, etc.
+ const orbitalPeriodComment = aggregatedComments.find(
+ (comment) => comment.category === "OrbitalPeriod" && comment.author === classification.author
+ );
+
+ const radiusComment = aggregatedComments.find(
+ (comment) => comment.category === "PlanetType" && comment.author === classification.author
+ );
+
+ const temperatureComment = aggregatedComments.find(
+ (comment) => comment.category === "Temperature" && comment.author === classification.author
+ );
+
+ if (orbitalPeriodComment) setSurveyorPeriod(orbitalPeriodComment.value || "");
+ if (radiusComment) setDensity(radiusComment.value || null);
+ if (temperatureComment) setTemperature(temperatureComment.value || null);
+ };
+ };
+
+ const fetchDensity = async () => {
+ if (!classification) return;
+
+ const { data, error } = await supabase
+ .from("comments")
+ .select("*")
+ .eq("classification_id", classification.id)
+ .eq("category", "Density")
+ .order("created_at", { ascending: false });
+
+ if (error) {
+ console.error("Error fetching comments: ", error);
+ } else {
+ setDensity(data[0].value)
+ };
+ };
+
+ useEffect(() => {
+ fetchComments();
+ fetchDensity();
+ if (cloudSummary && p4Summary && ai4MSummary) {
+ console.log(cloudSummary, p4Summary, ai4MSummary);
+ }
+ }, [classification, supabase]);
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setTimeLeft(prev => {
+ const time = Math.max(getTimeUntilNextSunday() / 1000, 300);
+ return Math.floor(time);
+ });
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, [supabase]);
+
+ const handleViewChange = (view: FocusView) => {
+ setCurrentView(view)
+ };
+
+ const handleBackToMain = () => {
+ setCurrentView('planet');
+ };
+
+ // Planet generator content
+ const PlanetComponent = useMemo(() => (
+
+ {classification && !loading && MemoizedPlanetGenerator}
+
+ ), [classification, loading, MemoizedPlanetGenerator]);
+
+ const ClimateComponent = () => (
+
+ {/*
+
Classification Configuration
+
+
+ {JSON.stringify(classification?.classificationConfiguration, null, 2)}
+
+
+
+
+ {relatedClassifications.length > 0 && (
+
+
Related Classifications
+
+ {relatedClassifications.map((rc, index) => (
+
+
+ {JSON.stringify(rc, null, 2)}
+
+
+ ))}
+
+
+ )} */}
+
+ {(cloudSummary || p4Summary || ai4MSummary) && (
+
setDominantBiome(biome)} biomassVersion={biomassScore} />
+ )}
+
+
+
+ );
+
+ const AtmosphereComponent = () => (
+
+ {classification?.id !== undefined && (
+
+ )}
+
+ );
+
+ const ExplorationComponent = () => (
+
+ {classification && (
+
+ )}
+
+ );
+
+ const MapComponent = () => (
+
+
+
+ );
+
+ const EditComponent = () => (
+
+
+
+ );
+
+ const renderFocusComponent = () => {
+ switch (currentView) {
+ case "planet":
+ return PlanetComponent
+ case "overview":
+ return PlanetComponent
+ case "Climate":
+ return
+ case "atmosphere":
+ return
+ case "exploration":
+ return
+ case "map":
+ return
+ case "edit":
+ return
+ default:
+ return PlanetComponent
+ };
+ };
+
+ return (
+
+
+
+
+
+ {/* {dominantBiome && (
+
+
+
+ )} */}
+
+
+ {currentView !== "planet" && (
+
+ )}
+
+
+
+
Biomass:
+ {biomassScore}
+
+
+
Density:
+ {density}
+ g/cm^3
+
+
+
Main biome:
+ {dominantBiome}
+
+ {/*
+
Period:
+ {surveyorPeriod} Days {/* // or {period}
+
*/}
+
+
+
+ handleViewChange("overview")}
+ >
+
+ Overview
+
+ handleViewChange("Climate")}
+ >
+
+ Climate
+
+ {/* handleViewChange("atmosphere")}
+ >
+
+ Atmosphere
+ */}
+
+ View Post
+
+ handleViewChange("exploration")}
+ >
+
+ Exploration
+
+
+
+
+ {renderFocusComponent()}
+
+
+
+
+
+ handleViewChange("overview")}
+ >
+
+
+ handleViewChange("Climate")}
+ >
+
+
+ handleViewChange("atmosphere")}
+ >
+
+
+ handleViewChange("exploration")}
+ >
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/planets/clouds/[id]/client.tsx b/app/planets/clouds/[id]/client.tsx
new file mode 100644
index 000000000..f5b6aa31e
--- /dev/null
+++ b/app/planets/clouds/[id]/client.tsx
@@ -0,0 +1,103 @@
+'use client';
+
+import React, { useEffect, useState } from "react";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import Navbar from "@/components/Layout/Navbar";
+import { PostCardSingleWithGenerator } from "@/content/Posts/PostWithGen";
+
+interface Classification {
+ id: number;
+ content: string | null;
+ author: string | null;
+ anomaly: Anomaly | null;
+ media: (string | { uploadUrl?: string })[] | null;
+ classificationtype: string | null;
+ classificationConfiguration?: any;
+ created_at: string;
+ title?: string;
+ votes?: number;
+ category?: string;
+ tags?: string[];
+ images?: string[];
+ relatedClassifications?: Classification[];
+};
+
+type Anomaly = {
+ id: number;
+ content: string | null;
+ anomalytype: string | null;
+ mass: number | null;
+ radius: number | null;
+ density: number | null;
+ gravity: number | null;
+ temperature: number | null;
+ orbital_period: number | null;
+ avatar_url: string | null;
+ created_at: string;
+};
+
+export default function CloudDetailsClient({ id }: { id: number }) {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [classification, setClassification] = useState(null);
+ const [anomaly, setAnomaly] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!id || !session) {
+ setLoading(false);
+ return;
+ }
+
+ const fetchClassification = async () => {
+ const { data, error } = await supabase
+ .from("classifications")
+ .select("*, anomaly:anomalies(*), classificationConfiguration, media")
+ .eq("id", id)
+ .single();
+
+ if (error) {
+ setError("Failed to fetch classification.");
+ setLoading(false);
+ return;
+ }
+
+ setClassification(data);
+ setAnomaly(data.anomaly);
+ setLoading(false);
+ };
+
+ fetchClassification();
+ }, [id, session]);
+
+ if (loading) return Loading...
;
+ if (error) return {error}
;
+
+ return (
+
+
+
+
+ {classification?.content || `Cloud #${classification?.id}`}
+
+ {classification?.author && (
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/app/planets/clouds/[id]/page.tsx b/app/planets/clouds/[id]/page.tsx
new file mode 100644
index 000000000..6be2e3882
--- /dev/null
+++ b/app/planets/clouds/[id]/page.tsx
@@ -0,0 +1,11 @@
+import CloudDetailsClient from './client';
+
+type MyCloudPageProps = {
+ params: {
+ id: number;
+ };
+};
+
+export default function Page({ params }: MyCloudPageProps) {
+ return ;
+};
\ No newline at end of file
diff --git a/app/planets/edit/[id]/page.tsx b/app/planets/edit/[id]/page.tsx
new file mode 100644
index 000000000..6b2e03d36
--- /dev/null
+++ b/app/planets/edit/[id]/page.tsx
@@ -0,0 +1,149 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import Navbar from "@/components/Layout/Navbar";
+import { PostCardSingleWithGeneratorEditMode } from "@/content/Posts/PostWithGen";
+import CloudClassificationSummary from "@/components/Structures/Missions/Meteorologists/Cloudspotting/CloudAggregator";
+import BiomeAggregator from "@/components/Data/Generator/BiomeAggregator";
+
+import { Classification } from "../../[id]/page";
+
+export default function EditPlanetAnomaly({ params }: { params: { id: string } }) {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [classification, setClassification] = useState(null);
+ const [relatedClassifications, setRelatedClassifications] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ let isMounted = true; // Prevent setting state if unmounted
+
+ const fetchClassification = async () => {
+ if (!params.id || !session) {
+ setLoading(false);
+ return;
+ }
+
+ const { data, error } = await supabase
+ .from("classifications")
+ .select("*, anomaly:anomalies(*), classificationConfiguration, media")
+ .eq("id", params.id)
+ .single();
+
+ if (error) {
+ if (isMounted) setError("Failed to fetch classification.");
+ setLoading(false);
+ return;
+ }
+
+ if (data && isMounted) {
+ // Extract media URLs correctly
+ let images: string[] = [];
+
+ if (Array.isArray(data.media)) {
+ images = data.media
+ .map((item: { uploadUrl: any; }) => (typeof item === "string" ? item : item?.uploadUrl))
+ .filter(Boolean);
+ } else if (data.media?.uploadUrl) {
+ images.push(data.media.uploadUrl);
+ }
+
+ setClassification({
+ ...data,
+ images,
+ votes: data.classificationConfiguration?.votes || 0,
+ });
+
+ // Fetch related classifications
+ const parentPlanetLocation = data.anomaly?.id;
+ if (parentPlanetLocation) {
+ const { data: relatedData, error: relatedError } = await supabase
+ .from("classifications")
+ .select("*, anomaly:anomalies(*), classificationConfiguration, media")
+ .eq("classificationConfiguration->>parentPlanetLocation", parentPlanetLocation.toString())
+ .eq("author", session.user.id);
+
+ if (relatedError) {
+ if (isMounted) setError("Failed to fetch related classifications.");
+ } else if (relatedData && isMounted) {
+ setRelatedClassifications(relatedData);
+ }
+ }
+ }
+
+ setLoading(false);
+ };
+
+ fetchClassification();
+
+ return () => {
+ isMounted = false; // Cleanup function to avoid setting state on unmounted component
+ };
+ }, [params.id, supabase, session]);
+
+ const cloudClassifications = relatedClassifications.filter(
+ (related) => related.classificationtype === "cloud"
+ );
+
+ // const handleCloudSummaryUpdate = (summary: AggregatedCloud) => {
+ // setCloudSummary(summary);
+ // };
+
+ if (loading) {
+ return (
+ Loading classification data...
+ );
+ };
+
+ if (error) {
+ return (
+ {error}
+ );
+ };
+
+ if (!classification) {
+ return (
+ No classification found.
+ );
+ };
+
+ return (
+
+
+
+
+
+ {/* Related Classifications */}
+ {/* {relatedClassifications.length > 0 && (
+
+
Related Classifications
+
+ {relatedClassifications.map((related) => (
+ {related.classificationtype} - {related.id}
+ ))}
+
+
+ )} */}
+
+ );
+};
\ No newline at end of file
diff --git a/app/planets/jupiter/page.tsx b/app/planets/jupiter/page.tsx
new file mode 100644
index 000000000..dfe940922
--- /dev/null
+++ b/app/planets/jupiter/page.tsx
@@ -0,0 +1,191 @@
+'use client';
+
+import React, { useEffect, useState } from "react";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import Navbar from "@/components/Layout/Navbar";
+// import { PlanetScene } from "@/components/Data/Generator/Astronomers/PlanetHunters/V2/planet-scene";
+// import { calculatePlanetStats, calculateTerrainHeight } from "@/utils/planet-physics";
+import Link from "next/link";
+import JVHCloudAggregator from "@/components/Structures/Missions/Meteorologists/JVH/JVHCloudAggregator";
+
+export default function JupiterClassifications() {
+ const supabase = useSupabaseClient();
+
+ const [classifications, setClassifications] = useState([]);
+ const [showMetadata, setShowMetadata] = useState(false);
+
+ const [jvhClassifications, setJvhClassifications] = useState([]);
+
+ const toggleMetadataVisibility = () => setShowMetadata((prev) => !prev);
+
+ useEffect(() => {
+ const fetchClassifications = async () => {
+ try {
+ const {
+ data,
+ error
+ } = await supabase
+ .from("classifications")
+ .select("*")
+ .in('classificationtype', [
+ 'lidar-jovianVortexHunter'
+ ]);
+
+ if (error) {
+ throw error;
+ };
+
+ setClassifications(data || []);
+ setJvhClassifications(data.filter(c => c.classificationtype === 'lidar-jovianVortexHunter'));
+ } catch (err: any) {
+ console.error("Error fetching classifications: ", err.message);
+ } finally {
+ console.log('finally');
+ };
+ };
+
+ fetchClassifications();
+ }, [supabase]);
+
+ const renderClassificationList = ( typeClassifications: any[], title: string ) => {
+ if (typeClassifications.length === 0) {
+ return null;
+ };
+
+ return (
+
+
{title}
+
+ {typeClassifications.map((classification) => (
+
+ Created At: {new Date(classification.created_at).toLocaleString()}
+ Content: {classification.content}
+ Author: {classification.author}
+ Anomaly: {classification.anomaly ?? "None"}
+
+ {classification.media && (
+
+
Media:
+
{JSON.stringify(classification.media, null, 2)}
+
+ )}
+
+ {classification.classificationConfiguration && (
+
+
Classification Configuration:
+
{JSON.stringify(classification.classificationConfiguration, null, 2)}
+
+ )}
+
+ {classification.annotationOptions?.length > 0 && (
+
+
Annotations:
+
+ {classification.annotationOptions.map((option: string, index: number) => (
+ {option}
+ ))}
+
+
+ )}
+
+ ))}
+
+
+ );
+ };
+
+ const [mass, setMass] = useState(317.8);
+ const [radius, setRadius] = useState(11.21);
+ const [temperature, setTemperature] = useState(165.0);
+ const [orbitalPeriod, setOrbitalPeriod] = useState(4333.0);
+ const [typeOverride, setTypeOverride] = useState<"terrestrial" | "gaseous" | null>("gaseous");
+ const [atmosphereStrength, setAtmosphereStrength] = useState(1.0);
+ const [cloudCount, setCloudCount] = useState(0); // Jupiter doesn't really have a "cloud count," but this can stay 0 or be repurposed
+ const [waterHeight, setWaterHeight] = useState(0.0);
+ const [surfaceRoughness, setSurfaceRoughness] = useState(0.0);
+ const [biomeFactor, setBiomeFactor] = useState(0.0);
+ const [cloudContribution, setCloudContribution] = useState(1.0);
+ const [terrainVariation, setTerrainVariation] = useState<"flat" | "moderate" | "chaotic">("chaotic");
+ // const [terrainVariation, setTerrainVariation] = useState<"flat" | "moderate" | "chaotic" | "turbulent">("turbulent");
+ const [terrainErosion, setTerrainErosion] = useState(0.0);
+ const [plateTectonics, setPlateTectonics] = useState(0.0);
+ // const [soilType, setSoilType] = useState<"rocky" | "sandy" | "volcanic" | "organic" | "dusty" | "frozen" | "muddy" | "none">("none");
+ const [biomassLevel, setBiomassLevel] = useState(0.0);
+ const [waterLevel, setWaterLevel] = useState(0.0);
+ const [salinity, setSalinity] = useState(0.0);
+ const [subsurfaceWater, setSubsurfaceWater] = useState(0.0);
+ const [atmosphericDensity, setAtmosphericDensity] = useState(2.5);
+ const [weatherVariability, setWeatherVariability] = useState(0.9);
+ const [stormFrequency, setStormFrequency] = useState(0.95);
+ const [volcanicActivity, setVolcanicActivity] = useState(0.0);
+ const [biome, setBiome] = useState("Gas Giant");
+ // const [cloudTypes, setCloudTypes] = useState([""]); // Ammonia Crystals", "Water Ice", "Ammonium Hydrosulfide
+ // const [cloudDensity, setCloudDensity] = useState(0.98);
+ const [atmosphereVisibility, setAtmosphereVisibility] = useState(1);
+ const [atmosphereHeight, setAtmosphereHeight] = useState(1);
+
+ // const stats = calculatePlanetStats(
+ // mass,
+ // radius,
+ // temperature,
+ // orbitalPeriod,
+ // typeOverride,
+ // atmosphereStrength,
+ // cloudCount,
+ // waterHeight,
+ // surfaceRoughness,
+ // undefined,
+ // biomeFactor,
+ // cloudContribution,
+ // terrainVariation,
+ // terrainErosion,
+ // plateTectonics,
+ // biomassLevel,
+ // waterLevel,
+ // salinity,
+ // subsurfaceWater,
+ // atmosphericDensity,
+ // weatherVariability,
+ // stormFrequency,
+ // volcanicActivity,
+ // biome,
+ // );
+
+ // const terrainHeight = calculateTerrainHeight(stats);
+
+ return (
+
+
+
+
Jupiter Discoveries
+ {/*
*/}
+
+ {showMetadata ? 'Hide metadata' : 'Show metadata'}
+
+
+ {/* Aggregators go here when implemented
+ {showMetadata && (
+
+ {renderClassificationList(satelliteP4Classifications, 'Satellite - Planet Four')}
+
+
+ */}
+ {showMetadata && (
+
+ {renderClassificationList(jvhClassifications, 'Jovian Vortex Hunters')}
+
+ {renderClassificationList(jvhClassifications, "Jovian Vortex Hunters")}
+
+
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/planets/mars/page.tsx b/app/planets/mars/page.tsx
new file mode 100644
index 000000000..e2b07dd3a
--- /dev/null
+++ b/app/planets/mars/page.tsx
@@ -0,0 +1,254 @@
+'use client';
+
+import React, { useEffect, useState } from "react";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import Navbar from "@/components/Layout/Navbar";
+import CloudClassificationSummary from "@/components/Structures/Missions/Meteorologists/Cloudspotting/CloudAggregator";
+import SatellitePlanetFourAggregator from "@/components/Structures/Missions/Astronomers/SatellitePhotos/P4/P4Aggregator";
+import AI4MAggregator from "@/components/Structures/Missions/Astronomers/SatellitePhotos/AI4M/AI4MAggregator";
+import { AggregatedCloud, AggregatedBalloon, AggregatedAI4M, AggregatedP4 } from "../[id]/page";
+// import { PlanetScene } from "@/components/Data/Generator/Astronomers/PlanetHunters/V2/planet-scene";
+// import { calculatePlanetStats, calculateTerrainHeight } from "@/utils/planet-physics";
+import MarsCloudsShapesAggregator from "@/components/Structures/Missions/Meteorologists/Cloudspotting/Shapes/ShapesAggregator";
+
+export default function MarsClassifications() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [classifications, setClassifications] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const [showMetadata, setShowMetadata] = useState(false);
+
+ // Anomaly/classification summaries
+ const [cloudSummary, setCloudSummary] = useState(null);
+ const [p4Summary, setP4Summary] = useState(null);
+ const [ai4MSummary, setAI4MSummary] = useState(null);
+ const [balloonSummary, setBalloonSummary] = useState(null);
+
+ // Individual classification type states
+ const [cloudClassifications, setCloudClassifications] = useState([]);
+ const [satelliteP4Classifications, setSatelliteP4Classifications] = useState([]);
+ const [ai4MClassifications, setAi4MClassifications] = useState([]);
+ const [balloonClassifications, setBalloonClassifications] = useState([]);
+
+ // Toggle for metadata visibility
+ const toggleMetadataVisibility = () => setShowMetadata((prev) => !prev);
+
+ useEffect(() => {
+ const fetchClassifications = async () => {
+ setLoading(true);
+ setError(null);
+
+ try {
+ const { data, error } = await supabase
+ .from("classifications")
+ .select('*')
+ .in('classificationtype', [
+ 'satellite-planetFour',
+ 'automaton-aiForMars',
+ 'cloud',
+ 'balloon-marsCloudShapes'
+ ]);
+
+ if (error) throw error;
+
+ setClassifications(data || []);
+
+ // Filter classifications into their respective groups
+ setCloudClassifications(data.filter(c => c.classificationtype === 'cloud'));
+ setSatelliteP4Classifications(data.filter(c => c.classificationtype === 'satellite-planetFour'));
+ setAi4MClassifications(data.filter(c => c.classificationtype === 'automaton-aiForMars'));
+ setBalloonClassifications(data.filter(c => c.classificationtype === 'balloon-marsCloudShapes'));
+
+ } catch (err: any) {
+ console.error("Error fetching classifications: ", err.message);
+ setError("Failed to fetch classifications");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchClassifications();
+ }, [supabase]);
+
+ // if (loading) return Loading classifications...
;
+ // if (error) return Error: {error}
;
+
+ // Summary update handlers
+ const handleCloudSummaryUpdate = (summary: AggregatedCloud) => setCloudSummary(summary);
+ const handleP4SummaryUpdate = (summary: AggregatedP4) => setP4Summary(summary);
+ const handleAI4MSummaryUpdate = (summary: AggregatedAI4M) => setAI4MSummary(summary);
+ const handleBalloonSummaryUpdate = (summary: AggregatedBalloon) => setBalloonSummary(summary);
+
+ // Classification content rendering
+ const renderClassificationList = (typeClassifications: any[], title: string) => {
+ if (typeClassifications.length === 0) return null;
+
+ return (
+
+
{title}
+
+ {typeClassifications.map((classification) => (
+
+ Created At: {new Date(classification.created_at).toLocaleString()}
+ Content: {classification.content || "No content available"}
+ Author: {classification.author || "Unknown"}
+ Anomaly: {classification.anomaly ?? "None"}
+
+ {/* Handle optional media */}
+ {classification.media && (
+
+
Media:
+
{JSON.stringify(classification.media, null, 2)}
+
+ )}
+
+ {/* Handle optional configuration */}
+ {classification.classificationConfiguration && (
+
+
Classification Configuration:
+
{JSON.stringify(classification.classificationConfiguration, null, 2)}
+
+ )}
+
+ {/* Safely handle annotationOptions */}
+ {classification.annotationOptions?.length > 0 && (
+
+
Annotations:
+
+ {classification.annotationOptions.map((option: string, index: number) => (
+ {option}
+ ))}
+
+
+ )}
+
+ ))}
+
+
+ );
+ };
+
+ const [mass, setMass] = useState(0.107);
+ const [radius, setRadius] = useState(0.532);
+ const [temperature, setTemperature] = useState(210.0);
+ const [orbitalPeriod, setOrbitalPeriod] = useState(687.0);
+ const [typeOverride, setTypeOverride] = useState<"terrestrial" | "gaseous" | null>("terrestrial");
+ const [atmosphereStrength, setAtmosphereStrength] = useState(0.01);
+ const [cloudCount, setCloudCount] = useState(5.412);
+ const [waterHeight, setWaterHeight] = useState(0.02);
+ const [surfaceRoughness, setSurfaceRoughness] = useState(0.72);
+ const [biomeFactor, setBiomeFactor] = useState(0.14);
+ const [cloudContribution, setCloudContribution] = useState(0.12);
+ const [terrainVariation, setTerrainVariation] = useState<"flat" | "moderate" | "chaotic">("chaotic");
+ const [terrainErosion, setTerrainErosion] = useState(0.03);
+ const [plateTectonics, setPlateTectonics] = useState(0.01);
+ const [soilType, setSoilType] = useState<"rocky" | "sandy" | "volcanic" | "organic" | "dusty" | "frozen" | "muddy">("dusty");
+ const [biomassLevel, setBiomassLevel] = useState(0.0);
+ const [waterLevel, setWaterLevel] = useState(0.01);
+ const [salinity, setSalinity] = useState(0.22);
+ const [subsurfaceWater, setSubsurfaceWater] = useState(0.38);
+ const [atmosphericDensity, setAtmosphericDensity] = useState(0.02);
+ const [weatherVariability, setWeatherVariability] = useState(0.41);
+ const [stormFrequency, setStormFrequency] = useState(0.14);
+ const [volcanicActivity, setVolcanicActivity] = useState(0.001);
+ const [biome, setBiome] = useState("Barren Wasteland");
+ const [cloudTypes, setCloudTypes] = useState([""]);
+ const [cloudDensity, setCloudDensity] = useState(0.5);
+ const [atmosphereVisibility, setAtmosphereVisibility] = useState(1);
+ const [atmosphereHeight, setAtmosphereHeight] = useState(1);
+
+ // const stats = calculatePlanetStats(
+ // mass,
+ // radius,
+ // temperature,
+ // orbitalPeriod,
+ // typeOverride,
+ // atmosphereStrength,
+ // cloudCount,
+ // waterHeight,
+ // surfaceRoughness,
+ // undefined,
+ // biomeFactor,
+ // cloudContribution,
+ // terrainVariation,
+ // terrainErosion,
+ // plateTectonics,
+ // biomassLevel,
+ // waterLevel,
+ // salinity,
+ // subsurfaceWater,
+ // atmosphericDensity,
+ // weatherVariability,
+ // stormFrequency,
+ // volcanicActivity,
+ // biome,
+ // );
+
+ // const terrainHeight = calculateTerrainHeight(stats);
+
+ return (
+
+
+
+
Mars Classifications
+ {/*
*/}
+
+ {showMetadata ? 'Hide metadata' : 'Show metadata'}
+
+
+ {/* Cloud Classification Summary */}
+ {cloudClassifications.length > 0 && (
+
+ )}
+
+ {/* SP4 Classification Summary */}
+ {satelliteP4Classifications.length > 0 && (
+
+ )}
+
+ {/* AI4M Classification Summary */}
+ {ai4MClassifications.length > 0 && (
+
+ )}
+
+ {balloonClassifications.length > 0 && (
+
+ )}
+
+ {/* Metadata Section */}
+ {showMetadata && (
+
+ {renderClassificationList(satelliteP4Classifications, 'Satellite - Planet Four')}
+ {renderClassificationList(ai4MClassifications, 'Automaton - AI for Mars')}
+ {renderClassificationList(cloudClassifications, 'Cloud')}
+ {renderClassificationList(balloonClassifications, 'Balloon - Mars Cloud Shapes')}
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/planets/related/[id]/page.tsx b/app/planets/related/[id]/page.tsx
new file mode 100644
index 000000000..31cf9f512
--- /dev/null
+++ b/app/planets/related/[id]/page.tsx
@@ -0,0 +1,12 @@
+'use client';
+
+import RelatedClassifications from "@/components/Data/RelatedClassifications";
+import { useSupabaseClient } from "@supabase/auth-helpers-react";
+
+export default function PlanetWrapperWithRelatedClassifications({ params }: { params: { id: string } }) {
+ const supabase = useSupabaseClient();
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/app/planets/tests/[id]/page.tsx b/app/planets/tests/[id]/page.tsx
new file mode 100644
index 000000000..71d0628d5
--- /dev/null
+++ b/app/planets/tests/[id]/page.tsx
@@ -0,0 +1,759 @@
+'use client';
+
+import React, { useCallback, useEffect, useState } from "react";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import Navbar from "@/components/Layout/Navbar";
+import { PostCardSingleWithGenerator } from "@/content/Posts/PostWithGen";
+import CloudClassificationSummary from "@/components/Structures/Missions/Meteorologists/Cloudspotting/CloudAggregator";
+import BiomeAggregator from "@/components/Data/Generator/BiomeAggregator";
+import SatellitePlanetFourAggregator, { SatellitePlanetFourClassification } from "@/components/Structures/Missions/Astronomers/SatellitePhotos/P4/P4Aggregator";
+import AI4MAggregator from "@/components/Structures/Missions/Astronomers/SatellitePhotos/AI4M/AI4MAggregator";
+import BuildTerrariumStructures from "@/components/Structures/Build/BuildOnTerrariums";
+import StructuresOnTerrarium from "@/components/Structures/StructuresOnTerrarium";
+import { Badge } from "@/components/ui/badge";
+import { Camera, Clock } from "lucide-react";
+import Image from "next/image"
+import { Edit, Map, ArrowLeft, Info, FileText, Droplets, Thermometer, Globe } from "lucide-react"
+import SimplePlanetGenerator from "@/components/Data/Generator/Astronomers/PlanetHunters/SimplePlanetGenerator";
+import { Button } from "@/components/ui/button"
+import BiomassStats from "@/components/Structures/Missions/Biologists/BiomassOnPlanet";
+import WeatherGenerator from "@/components/Data/Generator/Weather/SimpleWeatherEvents";
+// import { PlanetGenerator } from "@/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator";
+import PlanetProgress from "@/components/Structures/Missions/Astronomers/PlanetHunters/PlanetCompletion";
+
+export interface Classification {
+ id: number;
+ content: string | null;
+ author: string | null;
+ anomaly: Anomaly | null;
+ media: (string | { uploadUrl?: string })[] | null;
+ classificationtype: string | null;
+ classificationConfiguration?: any;
+ created_at: string;
+ title?: string;
+ votes?: number;
+ category?: string;
+ tags?: string[];
+ images?: string[];
+ relatedClassifications?: Classification[];
+};
+
+type PlanetData = {
+ name: string
+ galaxy: string
+ diameter: string
+ dayLength: string
+ temperature: string
+ climate: string
+};
+
+const planets: Record = {
+ orionis: {
+ name: "ORIONIS",
+ galaxy: "Virgo A",
+ diameter: "120,780 km",
+ dayLength: "4 Earth hours",
+ temperature: "10°C to 40°C",
+ climate: "Temperate",
+ },
+ etheron: {
+ name: "ETHERON",
+ galaxy: "Sombrero",
+ diameter: "56,780 km",
+ dayLength: "12 Earth hours",
+ temperature: "60°C to 90°C",
+ climate: "Tropical",
+ },
+ theronix: {
+ name: "THERONIX",
+ galaxy: "Andromeda",
+ diameter: "89,320 km",
+ dayLength: "36 Earth hours",
+ temperature: "-20°C to 15°C",
+ climate: "Arctic",
+ },
+};
+
+type FocusView = "planet" | "overview" | "Climate" | "atmosphere" | "exploration" | "map" | "edit"
+
+export type Anomaly = {
+ id: number;
+ content: string | null;
+ anomalytype: string | null;
+ mass: number | null;
+ radius: number | null;
+ density: number | null;
+ gravity: number | null;
+ temperature: number | null;
+ orbital_period: number | null;
+ avatar_url: string | null;
+ created_at: string;
+};
+
+export interface AggregatedCloud {
+ annotationOptions: Record;
+ classificationOptions: Record>;
+ additionalFields: Record>;
+ cloudColours?: Record;
+};
+
+export interface AggregatedP4 {
+ fanCount: number;
+ blotchCount: number;
+ classificationCounts: Record;
+};
+
+export interface AggregatedAI4M {
+ sandCount: number;
+ soilCount: number;
+ bedrockCount: number;
+ rockCount: number;
+ unlabelledCount: number;
+ classificationCounts: Record;
+};
+
+export interface AggregatedBalloon {
+ shapeCount: number;
+ vortexCount: number;
+ streakCount: number;
+ unlabelledCount: number;
+ classificationCounts: Record;
+};
+
+export interface AI4MClassification {
+ id: number;
+ classificationConfiguration: any;
+ annotationOptions: any[];
+};
+
+export default function ClassificationDetail({ params }: { params: { id: string } }) {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [classification, setClassification] = useState(null);
+ const [anomaly, setAnomaly] = useState(null);
+
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const [dominantBiome, setDominantBiome] = useState('Barren (Pending)');
+ const [cloudSummary, setCloudSummary] = useState(null);
+ const [p4Summary, setP4Summary] = useState(null);
+ const [ai4MSummary, setAI4MSummary] = useState(null);
+ const [relatedClassifications, setRelatedClassifications] = useState([]);
+ const [period, setPeriod] = useState("");
+
+ const [showCurrentUser, setShowCurrentUser] = useState(true);
+ const [showMetadata, setShowMetadata] = useState(false);
+
+ const [currentPlanet, setCurrentPlanet] = useState("orionis")
+ const [currentView, setCurrentView] = useState("planet");
+ const [density, setDensity] = useState(null);
+ const [temperature, setTemperature] = useState(null);
+ const planet = planets[currentPlanet];
+ const [biomassScore, setBiomassScore] = useState(0);
+
+ const [surveyorPeriod, setSurveyorPeriod] = useState("")
+ const [comments, setComments] = useState([]);
+
+ const [timeLeft, setTimeLeft] = useState(0);
+
+ const getTimeUntilNextSunday = () => {
+ const now = new Date();
+ const nextSunday = new Date(now);
+ nextSunday.setDate(now.getDate() + (7 - now.getDay()) % 7);
+ nextSunday.setHours(10, 1, 0, 0);
+ return nextSunday.getTime() - now.getTime();
+ };
+
+ const calculateBiomass = (temperature?: number, radius?: number, orbitalPeriod?: number): number => {
+ const T = temperature ?? 300;
+ const R = radius ?? 1;
+ const P = orbitalPeriod ?? 1.5;
+
+ return (
+ 0.1 *
+ (T / (T + 300)) *
+ (1 / (1 + Math.exp(-(R - 1.2)))) *
+ (1 / (1 + Math.exp(-(1.5 - P))))
+ );
+ };
+
+ const formatTime = (timeInSeconds: number) => {
+ const hours = String(Math.floor(timeInSeconds / 3600)).padStart(2, "0");
+ const minutes = String(Math.floor((timeInSeconds % 3600) / 60)).padStart(2, "0");
+ const seconds = String(timeInSeconds % 60).padStart(2, "0");
+ return `${hours}:${minutes}:${seconds}`;
+ };
+
+ const fetchComments = async () => {
+ if (!classification) return;
+
+ const { data, error } = await supabase
+ .from("comments")
+ .select("*")
+ .eq("classification_id", classification.id)
+ .order("created_at", { ascending: false });
+
+ if (error) {
+ console.error("Error fetching comments:", error);
+ } else {
+ setComments(data || []);
+
+ // Find the first comment by the author with type 'OrbitalPeriod'
+ const orbitalPeriodComment = data?.find(
+ (comment) => comment.category === "OrbitalPeriod" && comment.author === classification.author
+ );
+
+ const radiusComment = data?.find(
+ (comment) => comment.category === "PlanetType" && comment.author === classification.author
+ );
+
+ const temperatureComment = data?.find(
+ (comment) => comment.category === "Temperature" && comment.author === classification.author
+ )
+
+ if (orbitalPeriodComment) {
+ setSurveyorPeriod(orbitalPeriodComment.value || "");
+ };
+
+ if (radiusComment) {
+ setDensity(radiusComment.content || null);
+ };
+
+ if (temperatureComment) {
+ setTemperature(temperatureComment.value || null);
+ };
+
+ setBiomassScore(calculateBiomass(temperature ?? undefined, density ?? undefined, parseFloat(surveyorPeriod) || undefined));
+ };
+ };
+
+ useEffect(() => {
+ fetchComments();
+ {cloudSummary && p4Summary && ai4MSummary && (
+ console.log(cloudSummary, p4Summary, ai4MSummary)
+ )}
+ }, [classification, supabase]);
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setTimeLeft(prev => {
+ const time = Math.max(getTimeUntilNextSunday() / 1000, 300); // Ensure it's always at least 5 minutes
+ return Math.floor(time);
+ });
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, []);
+
+ const handleViewChange = (view: FocusView) => {
+ setCurrentView(view)
+ };
+
+ const handleBackToMain = () => {
+ setCurrentView("planet")
+ };
+
+ useEffect(() => {
+ if (!params.id) return;
+
+ const fetchClassification = async () => {
+ if (!params.id || !session) return;
+
+ const { data, error } = await supabase
+ .from("classifications")
+ .select("*, anomaly:anomalies(*), classificationConfiguration, media")
+ .eq("id", params.id)
+ .single();
+
+ if (error) {
+ setError("Failed to fetch classification.");
+ setLoading(false);
+ return;
+ };
+
+ setClassification(data);
+ setAnomaly(data.anomaly);
+
+ const periodFromField = data.classificationConfiguration?.additionalFields?.field_0;
+ if (periodFromField) {
+ setPeriod(periodFromField);
+ }
+
+ const parentPlanetLocation = data.anomaly?.id;
+ if (parentPlanetLocation) {
+ const query = supabase
+ .from("classifications")
+ .select("*, anomaly:anomalies(*), classificationConfiguration, media")
+ .eq("classificationConfiguration->>parentPlanetLocation", parentPlanetLocation.toString());
+
+ if (showCurrentUser) {
+ query.eq("author", session.user.id);
+ }
+
+ const { data: relatedData, error: relatedError } = await query;
+
+ if (relatedError) {
+ setError("Failed to fetch related classifications.");
+ setLoading(false);
+ return;
+ };
+
+ if (relatedData) {
+ const votePromises = relatedData.map(async (related) => {
+ const { count, error: votesError } = await supabase
+ .from("votes")
+ .select("*", { count: "exact" })
+ .eq("classification_id", related.id);
+
+ if (votesError) {
+ console.error("Error fetching votes:", votesError);
+ return { ...related, votes: 0 };
+ }
+
+ return { ...related, votes: count };
+ });
+
+ const relatedClassificationsWithVotes = await Promise.all(votePromises);
+ setRelatedClassifications(relatedClassificationsWithVotes);
+ }
+ }
+
+ setLoading(false);
+ };
+
+ fetchClassification();
+ }, [params.id, supabase, session, showCurrentUser]);
+
+ if (loading) return Loading classification data...
;
+ if (error) return {error}
;
+ if (!classification) return Classification not found.
;
+
+ const cloudClassifications = relatedClassifications.filter(
+ (related) => related.classificationtype === "cloud"
+ );
+
+ const satelliteP4Classifications: SatellitePlanetFourClassification[] = relatedClassifications
+ .filter(
+ (related): related is Classification & SatellitePlanetFourClassification =>
+ related.classificationtype === "satellite-planetFour" &&
+ Array.isArray(related.classificationConfiguration?.annotationOptions)
+ )
+ .map((related) => ({
+ id: related.id,
+ annotationOptions: related.classificationConfiguration?.annotationOptions || [],
+ classificationConfiguration: related.classificationConfiguration || {}
+ }));
+
+ const ai4MClassifications: AI4MClassification[] = relatedClassifications.filter(
+ (related) => related.classificationtype === "automaton-aiForMars"
+ ).map((related) => ({
+ id: related.id,
+ classificationConfiguration: related.classificationConfiguration || {},
+ annotationOptions: related.classificationConfiguration?.annotationOptions || []
+ }));
+
+ const handleCloudSummaryUpdate = (summary: AggregatedCloud) => {
+ setCloudSummary(summary);
+ console.log("Cloud Summary:", summary);
+ };
+
+ const handleP4SummaryUpdate = (summary: AggregatedP4) => {
+ setP4Summary(summary);
+ };
+
+ // const handleAI4MSummaryUpdate = useCallback((summary: AggregatedAI4M) => {
+ // setAI4MSummary(summary);
+ // }, []);
+
+ const toggleUserClassifications = () => {
+ setShowCurrentUser((prev) => !prev);
+ };
+
+ const toggleMetadataVisibility = () => {
+ setShowMetadata((prev) => !prev);
+ };
+
+ const getBackgroundImage = (planetType: string | null) => {
+ switch (planetType) {
+ case "Terrestrial":
+ return "url('/assets/Backdrops/Venus.png')";
+ case "Gaseous":
+ return "url('/assets/Backdrops/gasgiant.jpeg')";
+ default:
+ return "url('/assets/Backdrops/Earth.png')";
+ };
+ };
+
+ // Component for the Climate information
+ const ClimateComponent = () => (
+
+ {(cloudSummary || p4Summary || ai4MSummary) && (
+
setDominantBiome(biome)} biomassVersion={biomassScore} />
+ )}
+
+
+ );
+
+ // Component for the atmosphere information
+ const AtmosphereComponent = () => (
+
+ <>
+ {/* Cloud Classification Summary */}
+ {cloudClassifications.length > 0 && (
+
+ )}
+
+ {/* SP4 Classification Summary */}
+ {satelliteP4Classifications.length > 0 && (
+
+ )}
+
+ {/* AI4M Classification Summary */}
+ {/* {ai4MClassifications.length > 0 && (
+
+ )} */}
+ >
+
+ )
+
+ // Component for the exploration information
+ const ExplorationComponent = () => (
+
+ );
+
+ // Component for the map view
+ const MapComponent = () => (
+
+
Planet Map
+
+
+
+
+
Interactive Map
+
Topographical data is currently loading...
+
+
+
+
+ Terrain
+
+
+ Satellite
+
+
+ Resources
+
+
+
+
+
+
Northern Hemisphere
+
3 continents, 2 major oceans
+
+
+
Equatorial Region
+
Dense rainforests, island chains
+
+
+
Southern Hemisphere
+
Ice caps, mountain ranges
+
+
+
+
+ )
+
+ // Component for the edit view
+ const EditComponent = () => (
+
+ )
+
+ const PlanetComponent = () => (
+
+ {classification && (
+ <>
+ {/*
*/}
+ {/*
*/}
+ >
+ )}
+
+ );
+
+ const renderFocusComponent = () => {
+ switch (currentView) {
+ case "planet":
+ return
+ case "overview":
+ return
+ case "Climate":
+ return
+ case "atmosphere":
+ return
+ case "exploration":
+ return
+ case "map":
+ return
+ case "edit":
+ return
+ default:
+ return
+ };
+ };
+
+ return (
+
+
+
+
+
+ {dominantBiome && (
+
+
+
+ )}
+
+
+ {/* Back button */}
+ {currentView !== "planet" && (
+
+ )}
+
+ {/* Action buttons - Desktop */}
+
+ handleViewChange("overview")}
+ >
+
+ Overview
+
+ handleViewChange("Climate")}
+ >
+
+ Climate
+
+ handleViewChange("atmosphere")}
+ >
+
+ Atmosphere
+
+ handleViewChange("exploration")}
+ >
+
+ Exploration
+
+
+
+ {/* Stats - Desktop */}
+
+
+
Biomass:
+ {biomassScore}
+
+
+
Density:
+
{density} Earth radii
+
+
+
Biome:
+
{dominantBiome}
+
+
+
Period:
+
{surveyorPeriod}
+
+
+
+ {/* Planet generator or view */}
+
+ {renderFocusComponent()}
+
+
+ {/* Stats - Mobile */}
+
+
+
Galaxy
+
{planet.galaxy}
+
+
+
Diameter
+
{planet.diameter}
+
+
+
Day Length
+
{planet.dayLength}
+
+
+
Avg Temperature
+
{planet.temperature}
+
+
+
+ {/* Action buttons - Mobile */}
+
+ handleViewChange("overview")}
+ >
+
+
+ handleViewChange("Climate")}
+ >
+
+
+ handleViewChange("atmosphere")}
+ >
+
+
+ handleViewChange("exploration")}
+ >
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/posts/[id]/page.tsx b/app/posts/[id]/page.tsx
new file mode 100644
index 000000000..a857345fd
--- /dev/null
+++ b/app/posts/[id]/page.tsx
@@ -0,0 +1,111 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import { useSupabaseClient } from "@supabase/auth-helpers-react";
+import { SimplePostSingle } from "@/content/Posts/SimplePostSingle";
+import StructuresOnPlanet from "@/components/Structures/Structures";
+// import PlanetGenerator from "@/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator";
+import Navbar from "@/components/Layout/Navbar";
+
+interface Classification {
+ id: number;
+ created_at: string;
+ content: string | null;
+ author: string | null;
+ anomaly: number | null;
+ media: string[] | null;
+ classificationtype: string | null;
+ classificationConfig?: any | null;
+};
+
+export default function SinglePostPage({ params }: { params: { id: string } }) {
+ const supabase = useSupabaseClient();
+
+ const [classification, setClassification] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchClassification = async () => {
+ const id = parseInt(params.id, 10);
+ if (isNaN(id)) {
+ setError("Invalid classification ID.");
+ setLoading(false);
+ return;
+ };
+
+ try {
+ const { data, error } = await supabase
+ .from("classifications")
+ .select("*")
+ .eq("id", id)
+ .single();
+
+ if (error || !data) {
+ setError("Classification not found.");
+ } else {
+ const flattenedMedia = (data.media || [])
+ .flat()
+ .filter(
+ (url: string) => typeof url === "string" && url.startsWith("http")
+ );
+
+ setClassification({
+ ...data,
+ media: flattenedMedia,
+ });
+ }
+ } catch (err) {
+ console.error(err);
+ setError("An error occurred while fetching the classification.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchClassification();
+ }, [params.id, supabase]);
+
+ if (loading)
+ return (
+
+
+
+
Loading classification...
+
+
+ );
+
+ if (error) return {error}
;
+ if (!classification) return No classification found.
;
+
+ return (
+
+
+
+
+ {classification.author && (
+
+
+
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/posts/surveyor/[id]/page.tsx b/app/posts/surveyor/[id]/page.tsx
new file mode 100644
index 000000000..464684c4b
--- /dev/null
+++ b/app/posts/surveyor/[id]/page.tsx
@@ -0,0 +1,109 @@
+'use client';
+
+import React, { useEffect, useState } from "react";
+import { useSupabaseClient } from "@supabase/auth-helpers-react";
+import PostCard from "@/content/Posts/TestPostCard";
+import GameNavbar from "@/components/Layout/Tes";
+import StructuresOnPlanet from "@/components/Structures/Structures";
+
+interface Classification {
+ id: number;
+ created_at: string;
+ content: string | null;
+ author: string | null;
+ anomaly: number | null;
+ media: string[] | null;
+ classificationtype: string | null;
+ classificationConfig?: any | null;
+};
+
+export default function SurveyorPostPage({ params }: { params: { id: string } }) {
+ const supabase = useSupabaseClient();
+
+ const [classification, setClassification] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchClassification = async () => {
+ const id = parseInt(params.id, 10);
+ if (isNaN(id)) {
+ setError("Invalid classification ID.");
+ setLoading(false);
+ return;
+ };
+
+ try {
+ const { data, error } = await supabase
+ .from("classifications")
+ .select("*")
+ .eq("id", id)
+ .single();
+
+ if (error || !data) {
+ setError("Classification not found.");
+ } else {
+ const flattenedMedia = (data.media || [])
+ .flat()
+ .filter(
+ (url: string) => typeof url === "string" && url.startsWith("http")
+ );
+
+ setClassification({
+ ...data,
+ media: flattenedMedia,
+ });
+ }
+ } catch (err) {
+ console.error(err);
+ setError("An error occurred while fetching the classification.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchClassification();
+ }, [params.id, supabase]);
+
+ if (loading)
+ return (
+
+
+
+
Loading classification...
+
+
+ );
+
+ if (error) return {error}
;
+ if (!classification) return No classification found.
;
+
+ return (
+
+
+
+
+ {classification.author && (
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/research/page.tsx b/app/research/page.tsx
new file mode 100644
index 000000000..28aead86c
--- /dev/null
+++ b/app/research/page.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useState } from "react";
+import GameNavbar from "@/components/Layout/Tes";
+import AstronomyResearch from "@/components/Research/AstronomyItems";
+import MeteorologyResearch from "@/components/Research/MeteorologyItems";
+import BiologyResearch from "@/components/Research/BiologyItems";
+import TotalPoints from "@/components/Structures/Missions/Stardust/Total";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+
+type CapacityKey =
+ | "probeCount"
+ | "probeDistance"
+ | "stationCount"
+ | "balloonCount"
+ | "cameraCount"
+ | "stationSize";
+
+type UserCapacities = Record;
+
+export default function TechTreeResearchPage() {
+ const [availablePoints, setAvailablePoints] = useState(0);
+ const [userCapacities, setUserCapacities] = useState({
+ probeCount: 1,
+ probeDistance: 1,
+ stationCount: 0,
+ balloonCount: 0,
+ cameraCount: 1,
+ stationSize: 0,
+ });
+
+ const getUpgradeCost = (currentLevel: number): number => {
+ return (currentLevel + 1) * 2;
+ };
+
+ const handleUpgrade = (capacity: CapacityKey, currentLevel: number) => {
+ const cost = getUpgradeCost(currentLevel);
+ if (availablePoints >= cost) {
+ setAvailablePoints((prev) => prev - cost);
+ setUserCapacities((prev) => ({
+ ...prev,
+ [capacity]: prev[capacity] + 1,
+ }));
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ {/* setAvailablePoints(total)} /> */}
+
+
+
+ Home
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/scenes/desert/page.tsx b/app/scenes/desert/page.tsx
new file mode 100644
index 000000000..c1605f3b3
--- /dev/null
+++ b/app/scenes/desert/page.tsx
@@ -0,0 +1,450 @@
+'use client';
+
+import React, { useState, useEffect, type ReactNode, type ReactElement } from "react";
+import { BurrowingOwl } from "@/components/Projects/Zoodex/burrowingOwls";
+import { ZoodexIguanas } from "@/components/Projects/Zoodex/iguanasFromAbove";
+import { BiomePattern } from "@/components/Structures/Missions/Biologists/BiomePattern";
+import { iconMap } from "@/components/Structures/Missions/Biologists/StationCard";
+import Navbar from "@/components/Layout/Navbar";
+import DesertGallery from "@/components/Structures/Missions/Biologists/gallery/desertGallery";
+import DesertCounter from "@/components/Structures/Missions/Biologists/timer/desertTimer";
+import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ ArrowRight,
+ Camera,
+ Clock,
+ Leaf,
+ MapPin,
+ Thermometer,
+ Sprout,
+ Droplets,
+ Sun,
+ Wind,
+ Bird,
+ ChevronLeft,
+ Plus,
+ PawPrint,
+} from "lucide-react";
+import { Bolt, BoltedCard, MetricItem } from "@/components/Structures/Greenhouses/Views";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+
+interface Project {
+ id: number
+ station: number
+ biome: string
+ title: string
+ icon: React.ElementType
+ completedCount: number
+ internalComponent?: React.ComponentType>
+ color: string; // Ensure this is explicitly typed as a string
+ images?: string[]
+ stats?: {
+ cameras: number
+ habitat: string
+ temperature: string
+ humidity: string
+ };
+};
+
+interface Animal {
+ name: string;
+ icon: string;
+};
+
+const fetchProjects = (): Project[] => [
+ {
+ id: 1,
+ station: 3104001,
+ biome: "Desert",
+ title: "Burrowing Owls",
+ icon: Bird,
+ completedCount: 0,
+ internalComponent: () => ,
+ color: "text-green-400",
+ },
+ {
+ id: 2,
+ station: 3104001,
+ biome: "Desert",
+ title: "Iguanas from Above",
+ icon: PawPrint,
+ completedCount: 5,
+ internalComponent: () => ,
+ color: "text-green-700",
+ },
+];
+
+const DesertBaseEarthScene = () => {
+ const [activeStation] = useState({
+ id: 3104001,
+ name: "Desert Station",
+ description: "A station located in the desert biome.",
+ biome: "Desert",
+ animals: [
+ { name: "Burrowing Owl", icon: "Bird" },
+ { name: "Iguana", icon: "PawPrint" },
+ ],
+ location: { coordinates: "23.6345° N, 46.8475° E", depth: "30m", altitude: "500m" },
+ });
+
+ useEffect(() => {
+ if (activeStation) {
+ setProjects(fetchProjects().filter((p) => p.station === Number(activeStation.id)));
+ }
+ }, [activeStation]);
+
+ const [timeLeft, setTimeLeft] = useState(0);
+ const [activeProject, setActiveProject] = useState(null);
+ const [projects, setProjects] = useState([]);
+
+ const getTimeUntilNextSunday = () => {
+ const now = new Date();
+ const nextSunday = new Date(now);
+ nextSunday.setDate(now.getDate() + (7 - now.getDay()) % 7); // Next Sunday
+ nextSunday.setHours(10, 1, 0, 0); // Set to 00:01 AEST (UTC+10)
+ return nextSunday.getTime() - now.getTime(); // Milliseconds until next Sunday
+ };
+
+ const handleBack = () => {
+ console.log('Back button clicked');
+ setActiveProject(null); // Reset activeProject
+ };
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setTimeLeft(prev => {
+ const time = Math.max(getTimeUntilNextSunday() / 1000, 300); // Ensure it's always at least 5 minutes
+ return Math.floor(time);
+ });
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, []);
+
+ const formatTime = (timeInSeconds: number) => {
+ const hours = String(Math.floor(timeInSeconds / 3600)).padStart(2, "0");
+ const minutes = String(Math.floor((timeInSeconds % 3600) / 60)).padStart(2, "0");
+ const seconds = String(timeInSeconds % 60).padStart(2, "0");
+ return `${hours}:${minutes}:${seconds}`;
+ };
+
+ return (
+
+
+
+
+
+ {/* Station Overview */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{activeStation.name} | {activeStation.id}
+
{activeStation.description}
+
+
+
+
+ Next: {formatTime(timeLeft)}
+
+
+
+ 12 Active
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Biome
+
+ {activeStation.biome}
+
+
+
+
+ {activeProject === null ? (
+
+ ) : (
+
p.id === activeProject)!}
+ onBack={() => handleBack()}
+ />
+ )}
+
+
+
+ );
+};
+
+export default DesertBaseEarthScene;
+
+interface MainViewProps {
+ projects: Project[]
+ archiveImages?: string[]
+ onProjectClick: (id: number) => void
+};
+
+function MainView({ projects, archiveImages, onProjectClick }: MainViewProps): ReactElement {
+ return (
+
+ {/* Projects Section */}
+
+
+
+
+ Active Projects
+
+
+ 2 Stations
+
+
+
+
+ {projects.map((project: Project) => {
+ const Icon = project.icon
+ return (
+
+
+
+
+ {/* */}
+ {project.title}
+ {project.completedCount > 0 && (
+
+ {project.completedCount} completed
+
+ )}
+
+ onProjectClick(project.id)}>
+
+
+
+
+
+
+
+ {project.images?.map((img: string, idx: number) => (
+
+
+
+ ))}
+
+
+
+
+
+
+ {project.biome}
+
+
+ Station {project.station}
+
+
+
+
+ )
+ })}
+
+
+
+ );
+};
+
+interface ProjectDetailViewProps {
+ project: Project
+ onBack: () => void
+};
+
+function ProjectDetailView({ project, onBack }: ProjectDetailViewProps): ReactElement {
+ const recentCaptures = Array(8).fill("/placeholder.svg?height=100&width=100")
+ const Icon = project.icon
+ const InternalComponent = project.internalComponent
+
+ return (
+
+
+
+
+
+
+
+ {/* */}
+ {project.title}
+
+
+
+ Station ID: {project.station}
+
+
+
+ {InternalComponent &&
}
+
+ );
+ };
+
+ {/*
+
+ Recent Captures
+
+
+
+
+ {recentCaptures.map((img: string, idx: number) => (
+
+
+
+ ))}
+
+
+
+
+
+
+ Station Metrics
+
+
+
+
+
+
+
+
+
+
+ */}
+
+ {/*
+
+ Biome: {project.biome}
+
+
+
+
+
+
+
+
+
+
+ */}
+ {/* Archive Section */}
+ {/*
+
+
+
+ Archive
+
+
+ 142 Items
+
+
+
+
+
+
+
+
+ Studied
+
+
+ Community
+
+
+
+ {["studied", "community"].map((tab: string) => (
+
+
+ {archiveImages.map((img: string, idx: number) => (
+
+
+
+ ))}
+
+
+ ))}
+
+
+
+
*/}
+
+ {/* Dashboard Grid */}
+ {/*
+ {/* Animals Section
+
+
Animals available for study
+
+ {activeStation.animals.map((animal: Animal) => {
+ const AnimalIcon = iconMap[animal.icon as keyof typeof iconMap];
+ return (
+
+ {AnimalIcon &&
}
+
{animal.name}
+
+ );
+ })}
+
+
+
Your discoveries
+
+
+
+
+
+ {/* Projects Section
+
+
Projects
+ {projects.length > 0 ? (
+
+ {projects.map((project) => (
+
+
{project.title}
+
Completed: {project.completedCount}
+
+ {project.internalComponent &&
}
+
+
+ ))}
+
+ ) : (
+
No projects available
+ )}
+
+
*/}
\ No newline at end of file
diff --git a/app/scenes/earth/page.tsx b/app/scenes/earth/page.tsx
index 0ea4a5174..6bbc10d28 100644
--- a/app/scenes/earth/page.tsx
+++ b/app/scenes/earth/page.tsx
@@ -1,32 +1,46 @@
"use client";
-import React, { useCallback, useEffect, useState } from "react";
+import React, { ReactNode, useCallback, useEffect, useState } from "react";
import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
import { useActivePlanet } from "@/context/ActivePlanet";
-import { EarthViewLayout } from "@/app/components/(scenes)/planetScene/layout";
+import { EarthViewLayout } from "@/components/(scenes)/planetScene/layout";
import { InventoryStructureItem } from "@/types/Items";
-import { PlanetarySystem } from "@/app/components/(scenes)/planetScene/orbitals/system";
-import StructuresOnPlanet, { AtmosphereStructuresOnPlanet, OrbitalStructuresOnPlanet } from "@/app/components/(structures)/Structures";
-import { SciFiPopupMenu } from "@/components/ui/popupMenu";
-import AllAutomatonsOnActivePlanet from "@/app/components/(vehicles)/(automatons)/AllAutomatons";
-import InitialiseChapterOneUser from "@/app/components/(scenes)/chapters/one/InitialiseUser";
+import { PlanetarySystem } from "@/components/(scenes)/planetScene/orbitals/system";
+import StructuresOnPlanet, {
+ AtmosphereStructuresOnPlanet,
+ OrbitalStructuresOnPlanet,
+} from "@/components/Structures/Structures";
+import AllAutomatonsOnActivePlanet from "@/components/Structures/Auto/AllAutomatons";
+import InventoryPage from "@/components/Inventory/Grid/Grid";
+// import EnhancedWeatherEvents from '@/components/(scenes)/mining/enhanced-weather-events';
+// import TopographicMap from "@/components/topographic-map";
const EarthView: React.FC = () => {
- const supabase = useSupabaseClient();
- const session = useSession();
-
const { activePlanet, updatePlanetLocation } = useActivePlanet();
+ const handleUpdateToEarth = () => {
+ updatePlanetLocation(69);
+ };
- if (activePlanet?.id !== 69) {
+ if (activePlanet?.id !== 50) {
return (
-
+
+
+
Current planet is not "old" Earth.
+
Switch to 'old' Earth
+
+
);
- };
+};
return (
-
+
+
+
-
+
);
};
@@ -34,87 +48,35 @@ const EarthView: React.FC = () => {
export default EarthView;
const EarthStructures: React.FC = () => {
- const supabase = useSupabaseClient();
- const session = useSession();
-
- const { activePlanet } = useActivePlanet();
-
- const [orbitalStructures, setOrbitalStructures] = useState
([]);
- const [atmosphereStructures, setAtmosphereStructures] = useState([]);
- const [surfaceStructures, setSurfaceStructures] = useState([]);
-
- const handleStructuresFetch = (
- orbital: InventoryStructureItem[],
- atmosphere: InventoryStructureItem[],
- surface: InventoryStructureItem[]
- ) => {
- setOrbitalStructures(orbital);
- setAtmosphereStructures(atmosphere);
- setSurfaceStructures(surface);
- };
-
- const fetchStructures = useCallback(async () => {
- if (!session?.user?.id || !activePlanet?.id) return;
-
- try {
- const { data: inventoryData, error } = await supabase
- .from('inventory')
- .select('*')
- .eq('owner', session.user.id)
- .eq('anomaly', activePlanet.id)
- .not('item', 'lte', 100);
-
- if (error) throw error;
-
- const orbital = inventoryData.filter((item) => item.locationType === 'Orbital');
- const atmosphere = inventoryData.filter((item) => item.locationType === 'Atmosphere');
- const surface = inventoryData.filter((item) => item.locationType === 'Surface');
-
- handleStructuresFetch(orbital, atmosphere, surface);
- } catch (error) {
- console.error('Error fetching structures:', error);
- }
- }, [session?.user?.id, activePlanet?.id, supabase]);
-
- useEffect(() => {
- fetchStructures();
- }, [fetchStructures]);
+ return (
+
+ {/* */}
+
+
+
+
+
+
+
+
+ );
+};
- return (
-
-
-
- {orbitalStructures.map((structure) => (
-
-
-
- ))}
-
+ {/*
-
-
-
-
-
-
- );
-};
\ No newline at end of file
+
*/}
+ {/* */}
\ No newline at end of file
diff --git a/app/scenes/earth/scene.tsx b/app/scenes/earth/scene.tsx
new file mode 100644
index 000000000..378185f08
--- /dev/null
+++ b/app/scenes/earth/scene.tsx
@@ -0,0 +1,81 @@
+'use client';
+
+import { ReactNode, CSSProperties } from "react";
+
+export const EarthScene: React.FC<{
+ toolbar?: ReactNode;
+ topSection?: ReactNode;
+ middleSection?: ReactNode;
+ bottomSection?: ReactNode;
+ middleSectionTwo?: ReactNode;
+}> = ({ toolbar, topSection, middleSection, bottomSection, middleSectionTwo }) => {
+ // Collect all sections to determine height distribution
+ const sections = [topSection, middleSection, middleSectionTwo, bottomSection].filter(Boolean);
+ const totalSections = sections.length;
+
+ // Define the flex properties for each section
+ const sectionStyles: CSSProperties[] = sections.map((_, index) => {
+ // Distribute height for each section
+ let flexValue = 0;
+
+ if (index === 0) {
+ flexValue = 22.5; // Top section
+ } else if (index === 1 || index === 2) {
+ flexValue = 15; // Middle sections, reduced height
+ } else if (index === 3) {
+ flexValue = 22.5; // Bottom section
+ }
+
+ return {
+ flex: flexValue,
+ };
+ });
+
+ return (
+
+
+
+
+ {topSection && (
+
+ {topSection}
+
+ )}
+
+ {middleSection && (
+
+
+ {middleSection}
+
+
+ {toolbar && (
+
+ {toolbar}
+
+ )}
+
+ )}
+
+ {middleSectionTwo && (
+
+
+ {middleSectionTwo}
+
+
+ )}
+
+ {bottomSection && (
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/scenes/index.ts b/app/scenes/index.ts
new file mode 100644
index 000000000..a36fc855f
--- /dev/null
+++ b/app/scenes/index.ts
@@ -0,0 +1,31 @@
+// Earth and Moon
+export { default as EarthView } from './earth/page';
+
+// Jupiter and Moons
+
+// Mars and Moons
+
+// Mercury
+
+// Mining
+
+// // Neptune and Triton
+// export { default as NeptuneView } from './neptune/page';
+// export { default as TritonView } from './neptune/triton/page';
+
+// Planet (generic or custom planets)
+
+
+// // Pluto
+// export { default as PlutoView } from './pluto/page';
+
+// // Saturn and Moons
+// export { default as SaturnView } from './saturn/page';
+// export { default as EnceladusView } from './saturn/enceladus/page';
+// export { default as TitanView } from './saturn/titan/page';
+
+// // Uranus and Miranda
+// export { default as UranusView } from './uranus/page';
+// export { default as MirandaView } from './uranus/miranda/page';
+
+// Venus
\ No newline at end of file
diff --git a/app/scenes/ocean/page.tsx b/app/scenes/ocean/page.tsx
new file mode 100644
index 000000000..b0cf47b07
--- /dev/null
+++ b/app/scenes/ocean/page.tsx
@@ -0,0 +1,228 @@
+'use client';
+
+import React, { useState, useEffect } from "react";
+import { Anchor, Camera, Clock, Droplets, Fish, GlassWater, Sun, Thermometer, Wind } from "lucide-react";
+import { PlanktonPortalFrame } from "@/components/Projects/Zoodex/planktonPortal";
+import { ClickACoral } from "@/components/Projects/Zoodex/click-a-coral";
+import { BiomePattern } from "@/components/Structures/Missions/Biologists/BiomePattern";
+import { iconMap } from "@/components/Structures/Missions/Biologists/StationCard";
+import Navbar from "@/components/Layout/Navbar";
+import OceanGallery from "@/components/Structures/Missions/Biologists/gallery/oceanGallery";
+import OceanCounter from "@/components/Structures/Missions/Biologists/timer/oceanTimer";
+import { Bolt, MainView, MetricItem, ProjectDetailView } from "@/components/Structures/Greenhouses/Views";
+import { Badge } from "@/components/ui/badge";
+import GameNavbar from "@/components/Layout/Tes";
+
+interface Project {
+ id: number
+ station: number
+ biome: string
+ title: string
+ icon: React.ElementType
+ completedCount: number
+ internalComponent?: React.ElementType
+ color: string
+ images?: string[]
+ stats?: {
+ cameras: number
+ habitat: string
+ temperature: string
+ humidity: string
+ };
+};
+
+interface Animal {
+ name: string;
+ icon: string;
+};
+
+const fetchProjects = (): Project[] => [
+ {
+ id: 1,
+ station: 3104002,
+ biome: "Ocean",
+ title: "Plankton Portal",
+ icon: Fish,
+ completedCount: 0,
+ internalComponent: () => ,
+ color: "text-blue-400",
+ },
+ {
+ id: 2,
+ station: 3104002,
+ biome: "Ocean",
+ title: "Click a Coral",
+ icon: Anchor,
+ completedCount: 0,
+ internalComponent: () => ,
+ color: "text-red-200",
+ },
+];
+
+const OceanBaseEarthScene = () => {
+ const [activeStation] = useState({
+ id: 3104002,
+ name: "Ocean Station",
+ description: "A station located in the ocean biome focused on plankton research.",
+ biome: "Ocean",
+ animals: [
+ { name: "Plankton", icon: "Fish" },
+ ],
+ location: { coordinates: "3.123° N, 51.567° W", depth: "200m" },
+ });
+ const [projects, setProjects] = useState([]);
+
+ useEffect(() => {
+ if (activeStation) {
+ setProjects(fetchProjects().filter((p) => p.station === Number(activeStation.id)));
+ };
+ }, [activeStation]);
+
+ const [timeLeft, setTimeLeft] = useState(0);
+ const [activeProject, setActiveProject] = useState(null);
+
+ const getTimeUntilNextSunday = () => {
+ const now = new Date();
+ const nextSunday = new Date(now);
+ nextSunday.setDate(now.getDate() + (7 - now.getDay()) % 7); // Next Sunday
+ nextSunday.setHours(10, 1, 0, 0); // Set to 00:01 AEST (UTC+10)
+ return nextSunday.getTime() - now.getTime(); // Milliseconds until next Sunday
+ };
+
+ const handleBack = () => {
+ console.log('Back button clicked');
+ setActiveProject(null); // Reset activeProject
+ };
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setTimeLeft(prev => {
+ const time = Math.max(getTimeUntilNextSunday() / 1000, 300); // Ensure it's always at least 5 minutes
+ return Math.floor(time);
+ });
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, []);
+
+ const formatTime = (timeInSeconds: number) => {
+ const hours = String(Math.floor(timeInSeconds / 3600)).padStart(2, "0");
+ const minutes = String(Math.floor((timeInSeconds % 3600) / 60)).padStart(2, "0");
+ const seconds = String(timeInSeconds % 60).padStart(2, "0");
+ return `${hours}:${minutes}:${seconds}`;
+ };
+
+ return (
+
+
+
+
+
+ {/* Station Overview */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{activeStation.name} | {activeStation.id}
+
{activeStation.description}
+
+
+
+
+ Next: {formatTime(timeLeft)}
+
+
+
+ 12 Active
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Biome
+
+ {activeStation.biome}
+
+
+
+
+ {activeProject === null ? (
+
+ ) : (
+
p.id === activeProject)!}
+ onBack={() => handleBack()}
+ />
+ )}
+
+
+
+ );
+};
+
+export default OceanBaseEarthScene;
+
+{/* Dashboard Grid */}
+ {/*
+
+
Animals available for study
+
+ {activeStation.animals.map((animal: Animal) => {
+ const AnimalIcon = iconMap[animal.icon as keyof typeof iconMap];
+ return (
+
+ {AnimalIcon &&
}
+
{animal.name}
+
+ );
+ })}
+
+
+
Your discoveries
+
+
+
+
+
+
+
Projects
+ {projects.length > 0 ? (
+
+ {projects.map((project) => (
+
+
{project.title}
+
Completed: {project.completedCount}
+
+ {project.internalComponent &&
}
+
+
+ ))}
+
+ ) : (
+
No projects available
+ )}
+
+
*/}
\ No newline at end of file
diff --git a/app/scenes/onboarding/page.tsx b/app/scenes/onboarding/page.tsx
new file mode 100644
index 000000000..8d5a579f4
--- /dev/null
+++ b/app/scenes/onboarding/page.tsx
@@ -0,0 +1,24 @@
+'use client';
+
+import React from "react";
+import { EarthScene } from "@/app/scenes/earth/scene";
+import InventoryPage from "@/components/Inventory/Grid/Grid";
+import MissionSelector from "@/components/Missions/mission-selector";
+import Navbar from "@/components/Layout/Navbar";
+import GameNavbar from "@/components/Layout/Tes";
+
+export default function Onboarding () {
+ return (
+ <>
+
+
+ >}
+ middleSection={ }
+ // toolbar={ }
+ // bottomSection={ }
+ />
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/app/scenes/uploads/page.tsx b/app/scenes/uploads/page.tsx
new file mode 100644
index 000000000..7985c5484
--- /dev/null
+++ b/app/scenes/uploads/page.tsx
@@ -0,0 +1,142 @@
+'use client';
+
+import React, { useState, useEffect } from "react";
+import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
+import Navbar from "@/components/Layout/Navbar";
+import Link from "next/link";
+
+const BiodomeStructureBase = () => {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+ const [uploads, setUploads] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!session) return;
+
+ const fetchUploads = async () => {
+ setLoading(true);
+ const { data, error } = await supabase
+ .from("uploads")
+ .select("*")
+ .eq("author", session.user.id)
+ .order("created_at", { ascending: false });
+
+ if (error) {
+ console.error("Error fetching uploads:", error);
+ } else {
+ setUploads(data || []);
+ }
+ setLoading(false);
+ };
+
+ fetchUploads();
+ }, [session, supabase]);
+
+ if (!session) {
+ return Please log in to view your uploads.
;
+ };
+
+ return (
+
+
+
Your Uploads
+
+ {loading ? (
+
Loading...
+ ) : uploads.length === 0 ? (
+
No uploads found.
+ ) : (
+
+ {uploads.map((upload) => {
+ const config = upload.configuration || {};
+ const biome = config.natural_habitat?.biomes?.toLowerCase();
+ const scenePath =
+ biome === "ocean"
+ ? "/scenes/ocean"
+ : biome === "desert"
+ ? "/scenes/desert"
+ : null;
+
+ return (
+
+
+ {new Date(upload.created_at).toLocaleDateString()}
+
+
+
+
+
+ {config.common_name || "Unknown Species"}
+
+
+ {config.kingdom} → {config.phylum} → {config.class}
+
+
+
+
Traits:
+
+ {(Array.isArray(config.traits) ? config.traits : []).map((trait: string) => (
+
+ {trait}
+
+ ))}
+
+ {(!config.traits || (Array.isArray(config.traits) && config.traits.length === 0)) && (
+ No traits available
+ )}
+
+
+
+
+
Habitat:
+
+ {config.natural_habitat?.biomes || "Unknown"}
+
+
+ {config.natural_habitat?.geographical_distribution || "Unknown"}
+
+
+
+ {config.characteristics && (
+
+
Characteristics:
+
+ {Object.entries(config.characteristics).map(([key, value]) => (
+
+ {key}: {String(value)}
+
+ ))}
+
+
+ )}
+
+ {scenePath && (
+
+ View {biome.charAt(0).toUpperCase() + biome.slice(1)} Scene
+
+ )}
+
+ );
+ })}
+
+ )}
+
+ );
+};
+
+export default BiodomeStructureBase;
\ No newline at end of file
diff --git a/app/structures/balloon/page.tsx b/app/structures/balloon/page.tsx
new file mode 100644
index 000000000..746bf596c
--- /dev/null
+++ b/app/structures/balloon/page.tsx
@@ -0,0 +1,181 @@
+'use client';
+
+import Home from "@/app/page";
+import WeatherEventsOverview from "@/components/Data/Generator/Weather/EventsAcrossMyLocations";
+import { DataSourcesModal } from "@/components/Data/unlockNewDataSources";
+import GameNavbar from "@/components/Layout/Tes";
+import AI4M from "@/components/Structures/Missions/Astronomers/SatellitePhotos/AI4M/AIForMars";
+import PlanetFour from "@/components/Structures/Missions/Astronomers/SatellitePhotos/P4/PlanetFour";
+import CloudspottingOnMars from "@/components/Structures/Missions/Meteorologists/Cloudspotting/CloudspottingOnMars";
+import JovianVortexHunters from "@/components/Structures/Missions/Meteorologists/JVH/JovianVortexHunters";
+import { Dialog, DialogContent } from "@/components/ui/dialog";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import {
+ BuildingIcon,
+ CameraOffIcon,
+ CloudCogIcon,
+ CloudDrizzleIcon,
+ MicroscopeIcon,
+ RssIcon,
+ UmbrellaIcon
+} from "lucide-react";
+import { useRouter } from "next/navigation";
+import React, { useState } from "react";
+
+export default function WeatherBalloonOnEarthPage() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const router = useRouter();
+
+ const [activeComponent, setActiveComponent] = useState(null);
+
+ if (!session) return ;
+
+ const actions = [
+ {
+ icon: ,
+ text: "Research",
+ dynamicComponent: ,
+ },
+ {
+ icon: ,
+ text: "Upcoming weather events",
+ dynamicComponent: ,
+ },
+ ];
+
+ const buttons = [
+ {
+ icon: ,
+ text: "Search your clouds",
+ dynamicComponent: ,
+ },
+ {
+ icon: ,
+ text: "Map storms on gas planets",
+ dynamicComponent: ,
+ },
+ {
+ icon: ,
+ text: "Identify landmarks on terrestrial planets",
+ dynamicComponent: ,
+ },
+ {
+ icon: ,
+ text: "Map the surface of planets",
+ dynamicComponent: ,
+ },
+ ];
+
+ const handleComponentChange = (component: React.ReactNode) => {
+ setActiveComponent(component);
+ };
+
+ const handleBack = () => {
+ setActiveComponent(null);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
{
+ if (!open) {
+ router.push("/");
+ }
+ }}
+ >
+
+ {/* Back button when showing dynamic content */}
+ {activeComponent && (
+
+
+ ← Back
+
+
+ )}
+
+ {/* Content switcher */}
+
+ {activeComponent ? (
+
{activeComponent}
+ ) : (
+ <>
+
+
+
+
Weather Balloon
+
+
+
+
+
+
+
+
+ {actions.map((action, index) => (
+
handleComponentChange(action.dynamicComponent)}
+ >
+ {action.icon}
+
{action.text}
+
+ ))}
+
+
+
+
+ {buttons.map((button, index) => (
+
handleComponentChange(button.dynamicComponent)}
+ style={{ width: "100%", maxWidth: "240px" }}
+ >
+
+
{button.icon}
+
{button.text}
+
+
+ ))}
+
+
+ >
+ )}
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/structures/cameras/page.tsx b/app/structures/cameras/page.tsx
new file mode 100644
index 000000000..d98100845
--- /dev/null
+++ b/app/structures/cameras/page.tsx
@@ -0,0 +1,126 @@
+'use client';
+
+import React, { act, useEffect, useState } from "react";
+import Home from "@/app/page";
+import { useSession } from "@supabase/auth-helpers-react";
+import { useRouter } from "next/navigation";
+import { BuildingIcon, CloudCogIcon, MicroscopeIcon } from "lucide-react";
+import ChatGPTImageClassifier from "@/app/tests/pleaseWork";
+import GameNavbar from "@/components/Layout/Tes";
+import { Dialog, DialogContent } from "@/components/ui/dialog";
+
+export default function CameraAggregatesPage() {
+ const session = useSession();
+
+ const router = useRouter();
+
+ const [activeComponent, setActiveComponent] = useState(null);
+
+ if (!session) {
+ return (
+
+ );
+ };
+
+ const buttons = [
+ {
+ icon: ,
+ text: "Animal tracker",
+ dynamicComponent: ,
+ },
+ {
+ icon: ,
+ text: "Storm chaser - coming soon",
+ },
+ ];
+
+ const handleComponentChange = ( component: React.ReactNode ) => {
+ setActiveComponent( component );
+ };
+
+ const handleBack = () => {
+ setActiveComponent( null );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
{
+ if (!open) {
+ router.push("/");
+ }
+ }}
+ >
+
+ {activeComponent ? (
+
+ {activeComponent}
+
+ ) : (
+ <>
+
+
+
+
+ Your cameras (across all structures)
+
+
+
+
+
+
+
+
+
+
+ {buttons.map((button, index) => (
+
handleComponentChange(button.dynamicComponent)}
+ style={{ width: "100%", maxWidth: "240px" }}
+ >
+
+
{button.icon}
+
{button.text}
+
+
+ ))}
+
+
+ >
+ )}
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/structures/greenhouse/page.tsx b/app/structures/greenhouse/page.tsx
new file mode 100644
index 000000000..cd53bb97d
--- /dev/null
+++ b/app/structures/greenhouse/page.tsx
@@ -0,0 +1,132 @@
+'use client';
+
+import Home from "@/app/page";
+import ChatGPTImageClassifier from "@/app/tests/pleaseWork";
+import GameNavbar from "@/components/Layout/Tes";
+import { GreenhouseResearchStations } from "@/components/Structures/Missions/Biologists/ResearchStations";
+import { Dialog, DialogContent } from "@/components/ui/dialog";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import { BeanIcon, BuildingIcon, CameraIcon } from "lucide-react";
+import { useRouter } from "next/navigation";
+import React, { useState } from "react";
+
+export default function GreenhouseBiodomeOnEarthPage() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+ const router = useRouter();
+
+ const [activeComponent, setActiveComponent] = useState(null);
+
+ if (!session) return ;
+
+ const buttons = [
+ {
+ icon: ,
+ text: "My Greenhouse Stations",
+ dynamicComponent: ,
+ },
+ {
+ icon: ,
+ text: "Scan animals around you",
+ dynamicComponent: ,
+ },
+ ];
+
+ const handleComponentChange = (component: React.ReactNode) => {
+ setActiveComponent(component);
+ };
+
+ const handleBack = () => {
+ setActiveComponent(null);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
{
+ if (!open) {
+ router.push("/");
+ }
+ }}
+ >
+
+ {/* Back button when showing dynamic content */}
+ {activeComponent && (
+
+
+ ← Back
+
+
+ )}
+
+ {/* Content switcher */}
+
+ {activeComponent ? (
+
{activeComponent}
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+ {buttons.map((button, index) => (
+
handleComponentChange(button.dynamicComponent)}
+ style={{ width: "100%", maxWidth: "240px" }}
+ >
+
+
{button.icon}
+
{button.text}
+
+
+ ))}
+
+
+ >
+ )}
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/structures/telescope/page.tsx b/app/structures/telescope/page.tsx
new file mode 100644
index 000000000..80f528695
--- /dev/null
+++ b/app/structures/telescope/page.tsx
@@ -0,0 +1,173 @@
+'use client';
+
+import { useRouter } from "next/navigation";
+import Home from "@/app/page";
+import GameNavbar from "@/components/Layout/Tes";
+import { TelescopeDiskDetector } from "@/components/Projects/Telescopes/DiskDetector";
+import SunspotSteps from "@/components/Projects/Telescopes/Sunspots/SunspotShell";
+import DailyMinorPlanetMissions from "@/components/Structures/Missions/Astronomers/DailyMinorPlanet/DailyMinorPlanet";
+import PlanetHuntersSteps from "@/components/Structures/Missions/Astronomers/PlanetHunters/PlanetHunters";
+import AstronomyResearch from "@/components/Research/AstronomyItems";
+import { Dialog, DialogContent } from "@/components/ui/dialog";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import {
+ BuildingIcon,
+ MoonStarIcon,
+ SunIcon,
+ TelescopeIcon,
+ TestTubeDiagonalIcon,
+ Trophy
+} from "lucide-react";
+import { useState } from "react";
+
+export default function TelescopeOnEarthPage() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+ const router = useRouter();
+
+ const [activeComponent, setActiveComponent] = useState(null);
+
+ if (!session) return ;
+
+ const actions = [
+ {
+ icon: ,
+ text: "Upgrades",
+ dynamicComponent: ,
+ },
+ ];
+
+ const buttons = [
+ {
+ icon: ,
+ text: "Discover planets",
+ dynamicComponent: ,
+ },
+ {
+ icon: ,
+ text: "Sunspot data",
+ dynamicComponent: ,
+ },
+ {
+ icon: ,
+ text: "Find early solar systems",
+ dynamicComponent: ,
+ },
+ {
+ icon: ,
+ text: "Discover asteroids",
+ dynamicComponent: ,
+ },
+ ];
+
+ const handleComponentChange = (component: React.ReactNode) => {
+ setActiveComponent(component);
+ };
+
+ const handleBack = () => {
+ setActiveComponent(null);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
{
+ if (!open) {
+ router.push("/");
+ }
+ }}
+ >
+
+ {/* Back button when showing dynamic content */}
+ {activeComponent && (
+
+
+ ← Back
+
+
+ )}
+
+ {/* Content switcher */}
+
+ {activeComponent ? (
+
{activeComponent}
+ ) : (
+ <>
+
+
+
+
+
+
+
+ {actions.map((action, index) => (
+
handleComponentChange(action.dynamicComponent)}
+ >
+ {action.icon}
+
{action.text}
+
+ ))}
+
+
+
+
+ {buttons.map((button, index) => (
+
handleComponentChange(button.dynamicComponent)}
+ style={{ width: "100%", maxWidth: "240px" }}
+ >
+
+
{button.icon}
+
{button.text}
+
+
+ ))}
+
+
+ >
+ )}
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/tests/page.tsx b/app/tests/page.tsx
index e3f77ff02..3b4bf0c80 100644
--- a/app/tests/page.tsx
+++ b/app/tests/page.tsx
@@ -1,12 +1,76 @@
"use client";
-import React from "react";
-import IntroduceUserToResearch from "../components/(scenes)/chapters/(onboarding)/initialiseResearch";
+// import PlanetGenerator from "@/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator";
+import WeatherEventsOverview from "@/components/Data/Generator/Weather/EventsAcrossMyLocations";
+import WeatherEventStatus from "@/components/Data/Generator/Weather/EventsCounter";
+import MilestoneTotalCompleted from "@/components/Structures/Missions/Milestones/Completed";
+import TotalPoints from "@/components/Structures/Missions/Stardust/Total";
+import MyLocationIds from "@/content/Classifications/UserLocationPK";
+import MySettlementsLocations from "@/content/Classifications/UserLocations";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import { useEffect, useState } from "react";
+// import { PlanetGenerator } from "starsailors-planet-generator";
export default function TestPage() {
- return (
-
- {/* */}
-
- );
-};
\ No newline at end of file
+ const supabase = useSupabaseClient();
+ const session = useSession();
+ const [locationIds, setLocationIds] = useState<{ id: number; biome: string; biomass: number; density: number; anomaly_id: number }[]>([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ async function fetchLocationIds() {
+ if (!session) return;
+ const { data, error } = await supabase
+ .from("classifications")
+ .select("id, anomaly")
+ .eq("author", session.user.id)
+ .in("classificationtype", ["planet", "telescope-minorPlanet"]);
+
+ if (!error && data) {
+ const parsedData = data.map((item) => ({
+ id: item.id,
+ biome: "RockyHighlands",// biome: item.biome ?? "Unknown",
+ biomass: 0.01, //item.biomass ?? 0,
+ density: 3.5, // item.density ?? 1,
+ anomaly_id: item.anomaly,
+ }));
+ setLocationIds(parsedData);
+ }
+ setLoading(false);
+ }
+
+ fetchLocationIds();
+ }, [session]);
+
+ return (
+
+ {/*
*/}
+ {/*
+
+
+
*/}
+ {/*
*/}
+
+ {/*
*/}
+
+ );
+};
+
+
+/*
+
+ anomalies = {[
+ {
+ id: "1",
+ name: "Hardened owl",
+ description:
+ "A hardened owl that is ready to be transported to another lush location.",
+ },
+ ]} />
+
+*/
\ No newline at end of file
diff --git a/app/tests/pleaseWork.tsx b/app/tests/pleaseWork.tsx
new file mode 100644
index 000000000..b101fa00a
--- /dev/null
+++ b/app/tests/pleaseWork.tsx
@@ -0,0 +1,555 @@
+"use client"
+
+import type React from "react"
+
+import { useState, useRef, useEffect } from "react"
+import Webcam from "react-webcam"
+import axios from "axios"
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react"
+import { useActivePlanet } from "@/context/ActivePlanet"
+
+export default function ChatGPTImageClassifier() {
+ const supabase = useSupabaseClient()
+ const session = useSession()
+
+ const { activePlanet } = useActivePlanet()
+
+ const [image, setImage] = useState(null)
+ const [preview, setPreview] = useState(null)
+ const [response, setResponse] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [useWebcam, setUseWebcam] = useState(false)
+ const [location, setLocation] = useState("")
+ const [comment, setComment] = useState("")
+ const webcamRef = useRef(null)
+ const [scanPosition, setScanPosition] = useState(0);
+ const [fileUrlTO, setFileUrlTo] = useState(null)
+
+ // Animation for scanner line
+ useEffect(() => {
+ if (!loading) return
+
+ let animationId: number
+ let position = 0
+ const animate = () => {
+ position = (position + 1) % 100
+ setScanPosition(position)
+ animationId = requestAnimationFrame(animate)
+ }
+
+ animationId = requestAnimationFrame(animate)
+ return () => cancelAnimationFrame(animationId)
+ }, [loading])
+
+ // Handle file upload
+ const handleFileChange = (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0] || null
+ if (file && file.type.startsWith("image/")) {
+ setImage(file)
+ setPreview(URL.createObjectURL(file))
+ } else {
+ setImage(null)
+ setPreview(null)
+ }
+ }
+
+// Capture image from webcam
+const captureWebcamImage = () => {
+ console.log('Capture button clicked');
+ if (webcamRef.current) {
+ const imageSrc = webcamRef.current.getScreenshot();
+ if (imageSrc) {
+ console.log('Image captured:', imageSrc);
+ setPreview(imageSrc);
+ setImage(null); // Clear the image file
+ // Convert base64 image to Blob (File)
+ const byteString = atob(imageSrc.split(',')[1]);
+ const arrayBuffer = new ArrayBuffer(byteString.length);
+ const view = new Uint8Array(arrayBuffer);
+ for (let i = 0; i < byteString.length; i++) {
+ view[i] = byteString.charCodeAt(i);
+ }
+ const blob = new Blob([arrayBuffer], { type: 'image/jpeg' });
+ const file = new File([blob], `webcam-image-${Date.now()}.jpg`, { type: 'image/jpeg' });
+ setImage(file); // Set the file for upload
+ // Stop webcam stream after capture
+ const videoStream = webcamRef.current.video?.srcObject;
+ if (videoStream) {
+ const tracks = (videoStream as MediaStream).getTracks();
+ tracks.forEach(track => track.stop());
+ }
+ setUseWebcam(false); // Optionally toggle off webcam if you don't want it to continue
+ }
+ }
+};
+
+// Upload image to Supabase
+const uploadImageToSupabase = async (file: File) => {
+ if (!file) return;
+ setLoading(true);
+
+ const fileName = `${Date.now()}-${file.name}`;
+
+ try {
+ const { data: uploadData, error: uploadError } = await supabase.storage.from("uploads").upload(fileName, file);
+
+ if (uploadError) {
+ console.error("Upload error:", uploadError.message);
+ return;
+ };
+
+ if (uploadData) {
+ const fileUrl = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/uploads/${uploadData.path}`;
+ setFileUrlTo(fileUrl);
+ console.log('test')
+
+ const { data: uploadEntry, error: entryError } = await supabase
+ .from("uploads")
+ .insert({
+ author: session?.user?.id,
+ location: activePlanet?.id || 30,
+ content: comment,
+ file_url: fileUrl,
+ configuration: response,
+ source: "Webcam",
+ })
+ .single();
+
+ console.log(uploadData);
+
+ if (entryError) {
+ console.error("Entry error:", entryError.message);
+ } else {
+ console.log("Upload entry:", uploadEntry);
+ window.location.reload();
+ // saveLifeEntity();
+ }
+ }
+ } catch (err) {
+ console.error("Unexpected error during file upload:", err);
+ } finally {
+ setLoading(false);
+ }
+};
+
+// Send image for classification
+const sendImage = async () => {
+ if (!preview && !image) {
+ console.log("No image selected or captured.");
+ return;
+ }
+
+ console.log("Sending image...");
+
+ // Logging image format
+ console.log("Preview image base64 format: ", preview);
+
+ setLoading(true);
+ setResponse(null);
+
+ const formData = new FormData();
+
+ if (image) {
+ // If file upload is used
+ console.log("Uploading image file...");
+ formData.append("file", image);
+ } else {
+ // Convert base64 to Blob for webcam image
+ console.log("Converting base64 to Blob...");
+ if (!preview) {
+ console.error("Preview is null. Cannot fetch image.");
+ return;
+ }
+ const blob = await fetch(preview).then((res) => res.blob());
+ formData.append("file", blob);
+ }
+
+ try {
+ console.log("Sending API request...");
+ const res = await axios.post("/api/zoodex/upload-image/gpt", formData);
+ console.log("Full API Response:", res.data); // Log full response
+
+ if (res.data.refresh) {
+ window.location.reload();
+ return;
+ }
+
+ setResponse(res.data);
+ } catch (error) {
+ console.error("Error fetching classification:", error);
+ setResponse({ error: "Error: Unable to get classification." });
+ } finally {
+ setLoading(false);
+ }
+};
+
+const saveLifeEntity = async () => {
+ if (!session) {
+ return
+ }
+
+ try {
+ // Save a new entry to the "zoo" table with reference to the image and data
+ const { data: entityEntry, error: entityError } = await supabase.from("zoo").insert({
+ author: session?.user?.id,
+ file_url: fileUrlTO || '',
+ owner: session?.user?.id,
+ // location: activePlanet?.id || 30,
+ configuration: response, // Ensure that the classification data is saved
+ // uploaded: "", // You can store the URL or other related information
+ })
+
+ if (entityError) {
+ console.error("Entity error:", entityError.message)
+ } else {
+ console.log("Entity saved:", entityEntry)
+ }
+ } catch (err: any) {
+ console.error("Error saving entity:", err)
+ }
+}
+
+ return (
+
+ {/* Decorative elements - bolts and mechanical details */}
+
+
+
+ {/* Circuit board lines */}
+
+
+
+
+
+
+
+
+
+
+ {/* Header with glowing effect */}
+
+
+
+ {/* Status lights */}
+
+
+
+ {/* Main content - circular viewfinder */}
+
+
+ {/* Mechanical ring with bolts */}
+
+ {/* Decorative bolts around the ring */}
+ {[...Array(8)].map((_, i) => (
+
+ ))}
+
+
+ {/* Black outer ring with tech details */}
+
+ {/* Tech pattern on black ring */}
+
+
+
+
+
+
+ {/* Inner viewfinder with glowing border */}
+
+ {useWebcam ? (
+
+ ) : preview ? (
+
+ ) : (
+
+
+
+
+
+
+
Upload an image or switch to camera
+
+
+ )}
+
+ {/* Scanning animation when loading */}
+ {loading && (
+
+ )}
+
+
+
+ {/* Scan button with mechanical housing */}
+
+
+ {useWebcam ? (
+
+ Capture
+
+ ) : preview ? (
+
+ {loading ? "..." : "Uploaded"}
+
+ ) : (
+
+ upload
+
+
+ )}
+
+ Scan
+
+
+
+ {/* Camera toggle with mechanical look */}
+
+
+ setUseWebcam(!useWebcam)}
+ className={`px-6 py-2 rounded-full text-sm font-medium transition-all ${
+ useWebcam
+ ? "bg-gradient-to-r from-blue-500 to-blue-600 text-white"
+ : "bg-gradient-to-r from-amber-500 to-orange-500 text-white"
+ }`}
+ >
+ {useWebcam ? "Upload Image" : "Use Camera"}
+
+
+
+
+ {/* Bottom panel with mechanical details */}
+
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+
+
+ {/* Results panel - only show when we have results */}
+ {response && !response.error && (
+
+
+ {/* Mechanical header with bolts */}
+
+
Classification Results
+
+
+
+
+
+ Common Name: {response.common_name}
+
+
+ Scientific Name: {response.genus} {response.species}
+
+
+ Kingdom: {response.kingdom}
+
+
+ Phylum: {response.phylum}
+
+
+ Class: {response.class}
+
+
+ Order: {response.order}
+
+
+ Family: {response.family}
+
+
+ {response.traits && (
+
+
Traits:
+
+ {Object.entries(response.traits).map(([trait, value]) => (
+
+ {trait}: {String(value)}
+
+ ))}
+
+
+ )}
+
+ {response.compatible_biomes && response.compatible_biomes.length > 0 && (
+
+
Compatible Biomes:
+
+ {response.compatible_biomes.map((biome: string, index: number) => (
+ {biome}
+ ))}
+
+
+ )}
+
+
+ Conservation Status: {response.conservation_status}
+
+
+
+
+
+
+ )}
+
+ {response?.error && (
+
+
+
Error
+
{response.error}
+
setResponse(null)}
+ className="mt-4 px-4 py-2 bg-gray-800 border border-gray-700 rounded-md hover:bg-gray-700 transition-colors"
+ >
+ Close
+
+
+
+ )}
+
+ {/* QR code icon in bottom right with mechanical housing */}
+
+
+ {/* Add keyframes for pulse animation */}
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/tests/singleMissionGuide.tsx b/app/tests/singleMissionGuide.tsx
new file mode 100644
index 000000000..be82376cf
--- /dev/null
+++ b/app/tests/singleMissionGuide.tsx
@@ -0,0 +1,163 @@
+import React, { useEffect, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { CloudDrizzleIcon, LightbulbIcon, Telescope, ArrowUpIcon, ArrowDownIcon } from "lucide-react";
+import { motion, AnimatePresence } from "framer-motion";
+import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
+import JournalPage from "@/components/Structures/Missions/Stardust/Journal";
+import MilestoneCard from "@/components/Structures/Missions/Milestones/MilestoneCard";
+
+export interface Mission {
+ id: number;
+ name: string;
+ description: string;
+ additionalDescription: string | React.ReactNode;
+ icon: React.ElementType;
+ color: string;
+ identifier: string;
+};
+
+const projects: Record = {
+ "Refracting Telescope": [
+ {
+ id: 1,
+ name: "Planet Hunting",
+ description: "Click on the Telescope structure icon to rate different transit events and find planets.",
+ additionalDescription: "You'll work alongside other users to classify and verify planet candidates. Once they've been confirmed, you'll be able to visit them, determine their properties and start construction & exploration surveys.",
+ icon: Telescope,
+ color: "text-cyan-300",
+ identifier: "planet"
+ },
+ {
+ id: 2,
+ name: "Asteroid Detection",
+ description: "Click on the Telescope structure icon to discover new asteroids in your telescope's data.",
+ additionalDescription: "Using the latest telescope data, you will identify potential asteroids that might pose a threat to the Earth or serve as future targets for exploration. Additionally, exo-asteroids can be discovered and tracked to identify their interactions with planets discovered in the Planet Hunters project",
+ icon: Telescope,
+ color: "text-green-300",
+ identifier: "asteroid"
+ },
+ {
+ id: 3,
+ name: "Sunspot Observations",
+ description: "Click on the Telescope structure icon to keep track of our sun's health & electrical storms",
+ additionalDescription: "This mission requires monitoring sunspots and solar activity to understand how these phenomena affect space weather and our planet's magnetosphere. Sunspots can trigger electrical outages here on Earth (and on any planets in the firing line) - so we need to be prepared",
+ icon: Telescope,
+ color: "text-yellow-300",
+ identifier: "sunspot"
+ },
+ ],
+ Biodome: [
+ {
+ id: 4,
+ name: "Wildwatch Burrowing Owls",
+ description: "Click on the Biodome structure icon to classify pictures of burrowing owls in captivity.",
+ additionalDescription: "Help track the health and behavior of burrowing owls by classifying pictures from the Biodome. You will analyze the owls' nesting and foraging behaviors.",
+ icon: LightbulbIcon,
+ color: "text-cyan-300",
+ identifier: "burrowingOwl"
+ },
+ {
+ id: 5,
+ name: "Iguanas from Above",
+ description: "Count Galapagos Marine Iguanas from aerial photos that have been sent to your Biodome structure.",
+ additionalDescription: "Use aerial images to count and classify marine iguanas on the Galapagos Islands. This is important for conservation efforts to track their population.",
+ icon: LightbulbIcon,
+ color: "text-green-300",
+ identifier: "iguana"
+ },
+ ],
+ WeatherBalloon: [
+ {
+ id: 6,
+ name: "Martian Cloud Survey",
+ description: "Use your Weather Balloon to identify clouds & cloud behaviour on Mars & Mars-like planets",
+ additionalDescription: "Analyze cloud patterns on Mars and other planets. This mission will involve classifying clouds based on their shape, size, and movement. We can use this information to understand more about the composition and climate cycle on these planets",
+ icon: CloudDrizzleIcon,
+ color: "text-blue-400",
+ identifier: "martianClouds"
+ },
+ {
+ id: 7,
+ name: "Jovian Vortx Hunters",
+ description: "Classify storms & storm behaviour on gas giant planets using data collected in your Weather Balloon",
+ additionalDescription: "Study the massive storms on gas giants like Jupiter, tracking their movements and behaviors to understand their atmospheric dynamics, composition and climate.",
+ icon: CloudDrizzleIcon,
+ color: "text-yellow-400",
+ identifier: "jovianStorms"
+ },
+ ],
+};
+
+const defaultMission: Mission = {
+ id: 0,
+ name: "Welcome to Star Sailors",
+ description: "You've been given some basic structures to start your journey. Click on their icons to classify the data they've collected for you. New data & projects are being added weekly.",
+ // additionalDescription: "You'll get to explore various missions, starting with basic tasks such as classifying objects in the sky and on Earth. Each task you complete unlocks new insights.", //
+ additionalDescription: ,
+ icon: LightbulbIcon,
+ color: "text-blue-400",
+ identifier: "default-starting-mission",
+};
+
+const SimpleeMissionGuide = () => {
+ const [isMinimized, setIsMinimized] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const openModal = () => {
+ setIsModalOpen(true);
+ };
+
+ const closeModal = () => {
+ setIsModalOpen(false);
+ };
+
+ return (
+
+
+ {!isMinimized && (
+
+
+
+
+
+
{defaultMission.name}
+
{defaultMission.description}
+
+ Weekly goals
+
+
+
+
+
+ )}
+
+
+ {/* Modal for additional description */}
+ {isModalOpen && (
+
+
+
{defaultMission.name}
+
{defaultMission.additionalDescription}
+
+ Close
+
+
+
+ )}
+
+ );
+};
+
+export default SimpleeMissionGuide;
\ No newline at end of file
diff --git a/citizen b/citizen
index c92dcbcaa..6ffae7671 160000
--- a/citizen
+++ b/citizen
@@ -1 +1 @@
-Subproject commit c92dcbcaa92ef5a1621b660b6db11bc65e9151e0
+Subproject commit 6ffae7671fb92faa0c2ffba22cc1cd712a21e72d
diff --git a/components.json b/components.json
index c68463308..d9ef0ae53 100644
--- a/components.json
+++ b/components.json
@@ -1,16 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
- "rsc": false,
+ "rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
- "css": "styles/globals.css",
- "baseColor": "gray",
- "cssVariables": true
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
},
"aliases": {
+ "components": "@/components",
"utils": "@/lib/utils",
- "components": "@/components"
- }
-}
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/components/(scenes)/deploy/AutomatonItems.tsx b/components/(scenes)/deploy/AutomatonItems.tsx
new file mode 100644
index 000000000..e11d2b323
--- /dev/null
+++ b/components/(scenes)/deploy/AutomatonItems.tsx
@@ -0,0 +1,401 @@
+'use client';
+
+import { Button } from "@/components/ui/button";
+import { Vehicle } from "@/types/Vehicles";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import React, { useEffect, useState } from "react";
+import SelectLocationForAutomatonDeploymentDropdown from "./SelectDeployLocation";
+
+// Helper to get ISO week and year for a date
+function getISOWeekYear(date: Date) {
+ const target = new Date(date.valueOf());
+ const dayNr = (date.getDay() + 6) % 7;
+ target.setDate(target.getDate() - dayNr + 3);
+ const firstThursday = target.valueOf();
+ target.setMonth(0, 1);
+ if (target.getDay() !== 4) {
+ target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7);
+ }
+ const week = 1 + Math.ceil((firstThursday - target.valueOf()) / 604800000);
+ return { year: target.getFullYear(), week };
+}
+
+const automatons: Vehicle[] = [
+ {
+ id: "1",
+ name: "Probe",
+ description: "Simple and versatile satellite used to obtain data of nearby exoplanets",
+ image: "/assets/Automatons/Sat.png",
+ stats: { speed: 100, armor: 100, capacity: 100 },
+ cost: 1,
+ quantity: 0,
+ },
+ {
+ id: "2",
+ name: "Scout Roover",
+ description: "Train roover",
+ image: "/assets/Automatons/ExploreRover1.png",
+ stats: { speed: 100, armor: 100, capacity: 100 },
+ cost: 1,
+ quantity: 0,
+ },
+];
+
+interface Classification {
+ author: string;
+ id: number;
+ content: string;
+ classificationtype: string;
+ anomaly: number;
+ media: (string | { uploadUrl?: string })[] | null;
+ image_url?: string;
+ images?: string[];
+ anomalyContent?: string;
+ relatedClassifications?: Classification[];
+}
+
+type CapacityKey = "probeCount" | "probeDistance" | "roverCount";
+type UserCapacities = Record;
+
+export default function AutomatonDeploySection() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [probeLocations, setProbeLocations] = useState<(Classification | null)[]>([]);
+ const [roverLocations, setRoverLocations] = useState<(Classification | null)[]>([]);
+ const [userCapacities, setUserCapacities] = useState({
+ probeCount: 1,
+ probeDistance: 1,
+ roverCount: 1,
+ });
+
+ const [dropdownOpenState, setDropdownOpenState] = useState>({});
+ const [deployDisabled, setDeployDisabled] = useState(false);
+
+ // Check for existing anomalies linked this week and disable deploy if found
+ useEffect(() => {
+ if (!session) return;
+
+ async function checkDeployAvailability() {
+ const { data, error } = await supabase
+ .from("linked_anomalies")
+ .select("date")
+ .eq("author", session?.user.id)
+ .eq("automaton", "Probe")
+ .order("date", { ascending: false });
+
+ if (error) {
+ console.error("Error fetching linked anomalies:", error);
+ return;
+ }
+
+ console.log("linked_anomalies data:", data);
+
+ if (!data || data.length === 0) {
+ setDeployDisabled(false);
+ return;
+ }
+
+ const now = new Date();
+ const currentWeek = getISOWeekYear(now);
+
+ // Check if any linked anomalies are in this week
+ const foundThisWeek = data.some((entry) => {
+ if (!entry.date) return false;
+ const d = new Date(entry.date);
+ const { year, week } = getISOWeekYear(d);
+ return year === currentWeek.year && week === currentWeek.week;
+ });
+
+ setDeployDisabled(false);
+ setDeployDisabled(foundThisWeek);
+ }
+
+ checkDeployAvailability();
+ }, [session, supabase]);
+
+ useEffect(() => {
+ setProbeLocations((prev) => {
+ if (prev.length === userCapacities.probeCount) return prev;
+ return Array(userCapacities.probeCount).fill(null);
+ });
+ setRoverLocations((prev) => {
+ if (prev.length === userCapacities.roverCount) return prev;
+ return Array(userCapacities.roverCount).fill(null);
+ });
+ }, [userCapacities]);
+
+ useEffect(() => {
+ if (!session) return;
+
+ async function fetchResearchData() {
+ try {
+ const { data: researched, error } = await supabase
+ .from("researched")
+ .select("tech_type")
+ .eq("user_id", session?.user.id);
+
+ if (error) throw error;
+
+ let probeCount = 1;
+ let probeDistance = 1;
+ let roverCount = 1;
+
+ researched?.forEach((item) => {
+ if (item.tech_type === "probecount") probeCount += 1;
+ if (item.tech_type === "proberange") probeDistance += 1;
+ if (item.tech_type === "rovercount") roverCount += 1;
+ });
+
+ setUserCapacities({ probeCount, probeDistance, roverCount });
+
+ await fetch(`api/gameplay/research/upgrades?techType=proberange&count=${probeDistance}`);
+ } catch (error) {
+ console.error("Error fetching research data:", error);
+ }
+ }
+
+ fetchResearchData();
+ }, [session, supabase]);
+
+ const handleSelectLocation = (
+ index: number,
+ type: "probe" | "rover",
+ location: Classification
+ ) => {
+ if (type === "probe") {
+ const updated = [...probeLocations];
+ updated[index] = location;
+ setProbeLocations(updated);
+ } else {
+ const updated = [...roverLocations];
+ updated[index] = location;
+ setRoverLocations(updated);
+ }
+ };
+
+ const handleDropdownOpenChange = (key: string, isOpen: boolean) => {
+ setDropdownOpenState((prev) => ({ ...prev, [key]: isOpen }));
+ };
+
+ const handleSubmit = async () => {
+ if (!session) return;
+
+ // Before inserting new linked anomalies:
+ // Delete linked anomalies older than this week for this user
+
+ // Fetch existing linked anomalies by author
+ const { data: existingLinked, error: fetchErr } = await supabase
+ .from("linked_anomalies")
+ .select("id, date")
+ .eq("author", session.user.id);
+
+ if (fetchErr) {
+ console.error("Error fetching existing linked anomalies:", fetchErr);
+ return;
+ }
+
+ const now = new Date();
+ const currentWeek = getISOWeekYear(now);
+
+ // Filter anomalies NOT in current week (to delete)
+ const toDeleteIds = (existingLinked || [])
+ .filter((entry) => {
+ if (!entry.date) return false;
+ const d = new Date(entry.date);
+ const { year, week } = getISOWeekYear(d);
+ return !(year === currentWeek.year && week === currentWeek.week);
+ })
+ .map((entry) => entry.id);
+
+ if (toDeleteIds.length > 0) {
+ const { error: deleteErr } = await supabase
+ .from("linked_anomalies")
+ .delete()
+ .in("id", toDeleteIds);
+
+ if (deleteErr) {
+ console.error("Error deleting old linked anomalies:", deleteErr);
+ return;
+ }
+ }
+
+ // Now proceed with deployment logic:
+
+ const probes = probeLocations.filter(Boolean) as Classification[];
+ const rovers = roverLocations.filter(Boolean) as Classification[];
+
+ const anomalySetMap: Record = {
+ AI4M: "automaton-aiForMars",
+ JVH: "lidar-jovianVortexHunter",
+ P4: "satellite-planetFour",
+ Clouds: "cloudspottingOnMars",
+ };
+
+ const getRandomTypes = (types: string[]) => {
+ const shuffled = [...types].sort(() => 0.5 - Math.random());
+ return Math.random() > 0.5 ? [shuffled[0], shuffled[1]] : [shuffled[0], shuffled[0]];
+ };
+
+ const neededSets: Record = {};
+
+ rovers.forEach(() => {
+ getRandomTypes(["AI4M", "JVH"]).forEach((type) => {
+ const key = anomalySetMap[type];
+ neededSets[key] = (neededSets[key] || 0) + 1;
+ });
+ });
+
+ probes.forEach(() => {
+ getRandomTypes(["P4", "Clouds"]).forEach((type) => {
+ const key = anomalySetMap[type];
+ neededSets[key] = (neededSets[key] || 0) + 1;
+ });
+ });
+
+ const anomalyIdPool: Record = {};
+
+ for (const [set, count] of Object.entries(neededSets)) {
+ const { data, error } = await supabase
+ .from("anomalies")
+ .select("id")
+ .eq("anomalySet", set);
+
+ if (error) {
+ console.error(`Error fetching anomalies for ${set}:`, error);
+ continue;
+ }
+
+ const ids = (data?.map((a) => a.id) || []).slice(0, count);
+ anomalyIdPool[set] = ids;
+ }
+
+ const assignments: {
+ classification: Classification;
+ anomalySet: string;
+ anomalyId: number | null;
+ automaton: "Rover" | "Probe";
+ }[] = [];
+
+ const assignAnomalies = (
+ locations: Classification[],
+ types: string[],
+ automaton: "Rover" | "Probe"
+ ) => {
+ locations.forEach((loc) => {
+ const selected = getRandomTypes(types);
+ selected.forEach((type) => {
+ const set = anomalySetMap[type];
+ const ids = anomalyIdPool[set] || [];
+
+ const assignedId = ids.length ? ids.shift()! : null;
+
+ assignments.push({
+ classification: loc,
+ anomalySet: set,
+ anomalyId: assignedId,
+ automaton,
+ });
+ });
+ });
+ };
+
+ assignAnomalies(rovers, ["AI4M", "JVH"], "Rover");
+ assignAnomalies(probes, ["P4", "Clouds"], "Probe");
+
+ const entriesToInsert = assignments
+ .filter((a) => a.anomalyId !== null)
+ .map((a) => ({
+ author: session.user.id,
+ classification_id: a.classification.id,
+ anomaly_id: a.anomalyId!,
+ automaton: a.automaton,
+ date: new Date().toISOString(),
+ }));
+
+ if (entriesToInsert.length === 0) {
+ alert("No anomalies available to deploy.");
+ return;
+ }
+
+ const { error: insertError } = await supabase
+ .from("linked_anomalies")
+ .insert(entriesToInsert);
+
+ if (insertError) {
+ console.error("Error inserting into linked_anomalies:", insertError);
+ alert("Deployment failed. Please try again.");
+ } else {
+ alert("Successfully deployed automatons!");
+ setDeployDisabled(true);
+ }
+ };
+
+ return (
+ <>
+ Available automatons
+ {!deployDisabled && (
+
+ {automatons.map((vehicle) => {
+ const isProbe = vehicle.name === "Probe";
+ const count = isProbe ? userCapacities.probeCount : userCapacities.roverCount;
+ const slots = Array.from({ length: count });
+
+ return (
+
+
+
+
{vehicle.name}
+
{vehicle.description}
+
+ {isProbe
+ ? `Count: ${userCapacities.probeCount}`
+ : `Count: ${userCapacities.roverCount}`}
+
+
+
+
+ {slots.map((_, i) => {
+ const selectedLoc = isProbe ? probeLocations[i] : roverLocations[i];
+ const dropdownKey = `${isProbe ? "probe" : "rover"}-${i}`;
+ return (
+
+ {vehicle.name} #{i + 1}:
+
+ handleSelectLocation(i, isProbe ? "probe" : "rover", loc)
+ }
+ open={!!dropdownOpenState[dropdownKey]}
+ onOpenChange={(open) =>
+ handleDropdownOpenChange(dropdownKey, open)
+ }
+ />
+
+ );
+ })}
+
+
+ );
+ })}
+
+ )}
+
+
+
+ {deployDisabled ? "Already deployed this week" : "Deploy Automatons"}
+
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/deploy/BiodomeStationCard.tsx b/components/(scenes)/deploy/BiodomeStationCard.tsx
new file mode 100644
index 000000000..ce8815729
--- /dev/null
+++ b/components/(scenes)/deploy/BiodomeStationCard.tsx
@@ -0,0 +1,202 @@
+import Link from "next/link";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { BiomePattern } from "@/components/Structures/Missions/Biologists/BiomePattern";
+import {
+ Anchor,
+ Trees,
+ Snowflake,
+ Sun,
+ Waves,
+ Fish,
+ Turtle,
+ Flower2,
+ Bird,
+ MoonIcon as Monkey,
+ Bug,
+ BeakerIcon as Bear,
+ Cat,
+ Rabbit,
+ Wand2,
+ Globe,
+} from "lucide-react";
+import type { Station } from "@/types/station";
+
+// --- Icon Mapping ---
+const iconMap = {
+ Anchor,
+ Trees,
+ Snowflake,
+ Sun,
+ Waves,
+ Fish,
+ Turtle,
+ Flower2,
+ Bird,
+ Monkey,
+ Bug,
+ Bear,
+ Cat,
+ Rabbit,
+ Wand2,
+};
+
+// --- Station Config ---
+const stationConfig: Station[] = [
+ {
+ id: "3104001",
+ name: "Desert Observatory",
+ icon: "Sun",
+ biome: {
+ name: "Desert",
+ color: "rgb(234, 255, 114)",
+ accentColor: "rgb(234, 255, 100)",
+ darkColor: "rgb(30, 58, 138)",
+ },
+ animals: [
+ {
+ name: "Burrowing Owl",
+ icon: "Bird",
+ biomassType: "Fauna",
+ mass: 0.5,
+ },
+ {
+ name: "Iguanas (from above)",
+ icon: "Turtle",
+ biomassType: "Fauna",
+ mass: 0.25,
+ },
+ ],
+ location: {
+ coordinates: "23.4162° N, 75.2397° W",
+ depth: "+2m",
+ },
+ built: false,
+ },
+ {
+ id: "3104002",
+ name: "Ocean Observatory",
+ icon: "Waves",
+ biome: {
+ name: "Ocean",
+ color: "rgb(22, 163, 255)",
+ accentColor: "rgb(34, 87, 245)",
+ darkColor: "rgb(20, 83, 45)",
+ },
+ animals: [
+ {
+ name: "Plankton Portal",
+ icon: "Fish",
+ biomassType: "Fauna",
+ mass: 0.0000001,
+ },
+ ],
+ location: {
+ coordinates: "2.4162° S, 54.2397° W",
+ altitude: "-10m",
+ },
+ built: false,
+ },
+];
+
+// --- Animal-Centric Mapping ---
+const animalProjects = (() => {
+ const map: Record = {};
+
+ for (const station of stationConfig) {
+ for (const animal of station.animals) {
+ if (!map[animal.name]) {
+ map[animal.name] = {
+ ...animal,
+ stations: [],
+ };
+ }
+ map[animal.name].stations.push(station);
+ }
+ }
+
+ return Object.values(map);
+})();
+
+// --- Main Component ---
+export default function BiologySectionDeploy() {
+ return (
+
+
+ {animalProjects.map((animal) => (
+
+ ))}
+
+
+
+
+ View Greenhouse
+
+
+
+ );
+}
+
+// --- Animal Card ---
+interface AnimalCardProps {
+ animal: {
+ name: string;
+ icon: string;
+ biomassType: string;
+ mass: number;
+ stations: Station[];
+ };
+}
+
+function AnimalCard({ animal }: AnimalCardProps) {
+ const AnimalIcon = iconMap[animal.icon as keyof typeof iconMap];
+
+ return (
+
+ {/* Blend biome backgrounds from all associated stations */}
+ {animal.stations.map((station, index) => (
+ 0 ? "mix-blend-lighten" : ""}`}
+ />
+ ))}
+
+
+
+ {AnimalIcon && }
+ {animal.name}
+
+
+
+
+
+ Biomass: {animal.mass} kg
+
+
+
+ Type: {animal.biomassType}
+
+
+ Stations:
+
+ {animal.stations.map((station) => (
+
+ {station.name} —{" "}
+ {station.biome.name}
+
+ ))}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/deploy/SelectDeployLocation.tsx b/components/(scenes)/deploy/SelectDeployLocation.tsx
new file mode 100644
index 000000000..cd17c5988
--- /dev/null
+++ b/components/(scenes)/deploy/SelectDeployLocation.tsx
@@ -0,0 +1,133 @@
+'use client';
+
+import React, { Fragment, useEffect, useState } from "react";
+import { Menu, Transition } from "@headlessui/react";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import { Button } from "@/components/ui/button";
+
+interface Classification {
+ author: string;
+ id: number;
+ content: string;
+ classificationtype: string;
+ anomaly: number;
+ media: (string | { uploadUrl?: string })[] | null;
+ image_url?: string;
+ images?: string[];
+ anomalyContent?: string;
+ relatedClassifications?: Classification[];
+ anomalies?: {
+ content: string;
+ };
+}
+
+interface Props {
+ selected?: Classification | null;
+ setSelectedLocation: (location: Classification) => void;
+ open?: boolean; // Optional controlled open state
+ onOpenChange?: (open: boolean) => void; // Optional handler
+};
+
+export default function SelectLocationForAutomatonDeploymentDropdown({
+ setSelectedLocation,
+ selected,
+}: Props) {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [myLocations, setMyLocations] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ function formatLabel(classification: Classification): string {
+ return classification.anomalyContent || classification.content || `#${classification.id}`;
+ }
+
+ async function fetchUserLocationClassifications() {
+ if (!session?.user?.id) {
+ setError("No user session found.");
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const { data, error: lcError } = await supabase
+ .from("classifications")
+ .select("*, anomalies(content)")
+ .in("classificationtype", ["planet", "telescope-minorPlanet"])
+ .eq("author", session.user.id)
+ .limit(10);
+
+ if (lcError) throw lcError;
+ setMyLocations(data);
+ } catch (error) {
+ console.error("Error occurred while fetching user locations:", error);
+ setError("Error fetching locations.");
+ } finally {
+ setLoading(false);
+ };
+ };
+
+ useEffect(() => {
+ if (session) fetchUserLocationClassifications();
+ }, [session]);
+
+ if (loading) return <>Loading...>;
+ if (error) return {error}
;
+ if (!myLocations || myLocations.length === 0) {
+ return You need to discover more objects to visit!
;
+ }
+
+ return (
+
+
+ {selected ? formatLabel(selected) : "Select Location"}
+
+
+
+
+
+
+
+ {myLocations.map((location) => (
+
+ {({ active }) => (
+ setSelectedLocation(location)}
+ className={`${
+ active ? "bg-yellow-500/30 text-yellow-100" : "text-yellow-300"
+ } group flex w-full items-start px-4 py-2 text-sm truncate`}
+ >
+
+
{location.anomalies?.content || "Unnamed object"}
+
+ {location.classificationtype === "planet"
+ ? "Planet"
+ : location.classificationtype === "telescope-minorPlanet"
+ ? "Asteroid"
+ : "Unknown Type"}
+
+
+
+ )}
+
+ ))}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/deploy/TelescopeRange.tsx b/components/(scenes)/deploy/TelescopeRange.tsx
new file mode 100644
index 000000000..142da0c2e
--- /dev/null
+++ b/components/(scenes)/deploy/TelescopeRange.tsx
@@ -0,0 +1,333 @@
+'use client';
+
+import type React from "react";
+import { useState, useEffect } from "react";
+import { Slider } from "@/components/ui/slider";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ Telescope,
+ AsteriskIcon as Asteroids,
+ Sun,
+ Star,
+ SpaceIcon as Planet,
+} from "lucide-react";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+
+export default function TelescopeRangeSlider() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [range, setRange] = useState([50]);
+ const [dropRates, setDropRates] = useState>({});
+ const [fetchedAnomalies, setFetchedAnomalies] = useState([]);
+ const [alreadyDeployed, setAlreadyDeployed] = useState(false);
+
+ // Calculate weighted anomaly drop rates whenever the slider changes
+ useEffect(() => {
+ const position = range[0];
+ const nearWeight = 100 - position;
+ const farWeight = position;
+
+ const nearItems = [
+ {
+ key: 'asteroids',
+ label: 'Asteroids',
+ icon: ,
+ colorClass: 'bg-destructive',
+ },
+ {
+ key: 'sunspots',
+ label: 'Sunspots',
+ icon: ,
+ colorClass: 'bg-secondary',
+ },
+ ];
+
+ const farItems = [
+ {
+ key: 'exoplanets',
+ label: 'Exoplanet Candidates',
+ icon: ,
+ colorClass: 'bg-primary',
+ },
+ // Uncomment if enabled
+ // {
+ // key: 'newStars',
+ // label: 'New Stars',
+ // icon: ,
+ // colorClass: 'bg-accent',
+ // },
+ ];
+
+ const newDropRates: Record = {};
+
+ const nearSplit = nearWeight / nearItems.length;
+ nearItems.forEach((item) => {
+ newDropRates[item.key] = Math.round(nearSplit);
+ });
+
+ const farSplit = farWeight / farItems.length;
+ farItems.forEach((item) => {
+ newDropRates[item.key] = Math.round(farSplit);
+ });
+
+ setDropRates(newDropRates);
+ }, [range]);
+
+ // Check on mount if telescope was already deployed this week
+ useEffect(() => {
+ const checkDeployment = async () => {
+ const oneWeekAgo = new Date();
+ oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
+
+ const { data: recentDeployments, error } = await supabase
+ .from("linked_anomalies")
+ .select("*")
+ .eq("automaton", "Telescope")
+ .gte("date", oneWeekAgo.toISOString());
+
+ if (error) {
+ console.error("Error checking telescope deployment:", error);
+ return;
+ }
+
+ if (recentDeployments && recentDeployments.length > 0) {
+ setAlreadyDeployed(true);
+ }
+ };
+
+ checkDeployment();
+ }, [supabase]);
+
+ const activeDropTypes = [
+ {
+ key: 'asteroids',
+ label: 'Asteroids',
+ icon: ,
+ colorClass: 'bg-destructive',
+ },
+ {
+ key: 'sunspots',
+ label: 'Sunspots',
+ icon: ,
+ colorClass: 'bg-secondary',
+ },
+ // {
+ // key: 'newStars',
+ // label: 'New Stars',
+ // icon: ,
+ // colorClass: 'bg-accent',
+ // },
+ {
+ key: 'exoplanets',
+ label: 'Exoplanet Candidates',
+ icon: ,
+ colorClass: 'bg-primary',
+ },
+ ];
+
+ const anomalyTypeMap: Record = {
+ asteroids: "telescope-minorPlanet",
+ sunspots: "sunspot",
+ exoplanets: "telescope-tess",
+ };
+
+ const handleDeploy = async () => {
+ // This code block should never run if alreadyDeployed is true,
+ // but is retained for logic completeness.
+ const oneWeekAgo = new Date();
+ oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
+
+ const { data: userDeployments, error: userFetchError } = await supabase
+ .from("linked_anomalies")
+ .select("*")
+ .eq("author", session?.user?.id)
+ .eq("automaton", "Telescope")
+ .gte("date", oneWeekAgo.toISOString());
+
+ if (userFetchError) {
+ console.error("Error fetching linked anomalies:", userFetchError);
+ return;
+ }
+
+ if (userDeployments && userDeployments.length >= 4) {
+ return;
+ }
+
+ const totalAnomalies = 4;
+ const weightedAnomalies: string[] = [];
+
+ for (const [key, rate] of Object.entries(dropRates)) {
+ const count = Math.round((rate / 100) * totalAnomalies);
+ for (let i = 0; i < count; i++) {
+ weightedAnomalies.push(key);
+ }
+ }
+
+ while (weightedAnomalies.length < totalAnomalies) {
+ const sorted = Object.entries(dropRates).sort((a, b) => b[1] - a[1]);
+ for (const [key] of sorted) {
+ if (weightedAnomalies.length < totalAnomalies) {
+ weightedAnomalies.push(key);
+ }
+ }
+ }
+
+ const selectedAnomalies = weightedAnomalies.slice(0, totalAnomalies);
+ console.log("Deployed anomalies:", selectedAnomalies);
+
+ const anomalies: any[] = [];
+
+ for (const anomalyKey of selectedAnomalies) {
+ const anomalySet = anomalyTypeMap[anomalyKey];
+ const { data, error } = await supabase
+ .from("anomalies")
+ .select("*")
+ .eq("anomalySet", anomalySet);
+
+ if (error) {
+ console.error(`Error fetching anomaly of type ${anomalyKey}:`, error);
+ continue;
+ }
+
+ if (data && data.length > 0) {
+ const anomaly = data[0];
+ anomalies.push(anomaly);
+
+ const { error: insertError } = await supabase
+ .from("linked_anomalies")
+ .insert([
+ {
+ author: session?.user?.id,
+ anomaly_id: anomaly.id,
+ classification_id: null,
+ automaton: "Telescope",
+ },
+ ]);
+
+ if (insertError) {
+ console.error("Error inserting into linked_anomalies:", insertError);
+ } else {
+ console.log("Inserted linked anomaly:", anomaly.id);
+ }
+ }
+ }
+
+ console.log("Fetched anomalies:", anomalies);
+ setFetchedAnomalies(anomalies);
+ };
+
+ // If already deployed this week, show only a message
+ if (alreadyDeployed) {
+ return (
+
+
+ The Telescope has already been deployed this week. You’ll be able to recalibrate and search again next week.
+
+
+ );
+ }
+
+ // Main UI if not yet deployed
+ return (
+
+
+
+
+
+ Telescope Range Calibration
+
+
+ Adjust the slider to determine how far your telescope will look into space (for this week). This will determine the anomalies that are visible to your structures
+
+
+
+
+
+
+
+
+ Solar System
+
+
+ Deep Space
+
+
+
+
+
+
+
+
+
+ Current Focus: {range[0] < 33
+ ? "Near Space"
+ : range[0] < 66
+ ? "Mid-Range Objects"
+ : "Deep Space"}
+
+
+
+
+
+
+
+ Deploy Telescope
+
+
+
+
+
+
+ );
+}
+
+interface DropRateItemProps {
+ icon: React.ReactNode;
+ label: string;
+ rate: number;
+ colorClass: string;
+}
+
+function DropRateItem({
+ icon,
+ label,
+ rate,
+ colorClass,
+}: DropRateItemProps) {
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/mining/Inventory.tsx b/components/(scenes)/mining/Inventory.tsx
new file mode 100644
index 000000000..46bc9e35a
--- /dev/null
+++ b/components/(scenes)/mining/Inventory.tsx
@@ -0,0 +1,132 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Diamond, Zap, Battery, Magnet, Crown, Gem } from 'lucide-react';
+import { useSupabaseClient, useSession } from '@supabase/auth-helpers-react';
+
+type InventoryItem = {
+ id: string;
+ name: string;
+ amount: number;
+};
+
+const getMineralColor = (name: string) => {
+ switch (name) {
+ case 'Iron':
+ return 'bg-red-100';
+ case 'Copper':
+ return 'bg-orange-100';
+ case 'Coal':
+ return 'bg-gray-100';
+ case 'Nickel':
+ return 'bg-green-100';
+ case 'Fuel':
+ return 'bg-cyan-100';
+ default:
+ return 'bg-blue-100';
+ }
+};
+
+export function Inventory() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [inventory, setInventory] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [hoveredItem, setHoveredItem] = useState(null);
+
+ const itemNames: Record = {
+ 11: 'Coal',
+ 13: 'Silicon',
+ 15: 'Iron',
+ 16: 'Nickel',
+ 18: 'Fuel',
+ 19: 'Copper',
+ };
+
+ useEffect(() => {
+ const fetchInventory = async () => {
+ if (!session?.user.id) {
+ console.error('Session or user ID is missing');
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const { data, error } = await supabase
+ .from('inventory')
+ .select('id, item, quantity')
+ .eq('owner', session.user.id)
+ .in('item', Object.keys(itemNames).map(Number));
+
+ if (error) {
+ throw error;
+ }
+
+ const formattedInventory = Object.entries(itemNames).map(([key, name]) => {
+ const foundItem = data?.find((item) => item.item === Number(key));
+ return {
+ id: foundItem ? foundItem.id.toString() : key,
+ name: name,
+ amount: foundItem ? foundItem.quantity : 0,
+ };
+ });
+
+ setInventory(formattedInventory);
+ } catch (error) {
+ console.error('Error fetching inventory:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchInventory();
+ }, [session, supabase]);
+
+ const getIcon = (name: string) => {
+ switch (name) {
+ case 'Iron':
+ return ;
+ case 'Copper':
+ return ;
+ case 'Coal':
+ return ;
+ case 'Silicon':
+ return ;
+ case 'Fuel':
+ return ;
+ case 'Nickel':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ if (loading) {
+ return Loading...
;
+ }
+
+ return (
+
+
Inventory
+
+ {inventory.map((item) => (
+
setHoveredItem(item.name)} // Use name for hover
+ onMouseLeave={() => setHoveredItem(null)}
+ >
+ {getIcon(item.name)}
+
{item.amount}
+ {hoveredItem === item.name && ( // Compare against name
+
+ {item.name}
+
+ )}
+
+ ))}
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/mining/mineral-deposit-list.tsx b/components/(scenes)/mining/mineral-deposit-list.tsx
new file mode 100644
index 000000000..ddd0ed674
--- /dev/null
+++ b/components/(scenes)/mining/mineral-deposit-list.tsx
@@ -0,0 +1,113 @@
+'use client';
+
+import { Button } from "@/components/ui/button";
+import React, { useEffect, useState } from "react";
+import { Diamond, Zap, Battery, Magnet, Crown, Gem } from 'lucide-react';
+import { useSession } from "@supabase/auth-helpers-react";
+
+export type MineralDeposit = {
+ id: string;
+ name: string;
+ mineral: string;
+ amount: number;
+ position: { x: number; y: number };
+ icon_url: string;
+ level: number;
+ uses: any[]; // Adjust this according to your actual data structure
+};
+
+export interface InventoryItem {
+ id: number;
+ name: string;
+ description: string;
+ cost?: number;
+ icon_url: string;
+ ItemCategory: string;
+};
+
+type Props = {
+ deposits: MineralDeposit[];
+ onSelect: (deposit: MineralDeposit) => void;
+ selectedDeposit: MineralDeposit | null;
+};
+
+const itemNames: Record = {
+ 11: 'Coal',
+ 13: 'Silicon',
+ 15: 'Iron',
+ 16: 'Nickel',
+ 18: 'Fuel',
+ 19: 'Copper',
+};
+
+const getIcon = (name: string) => {
+ switch (name) {
+ case 'Iron':
+ return ;
+ case 'Copper':
+ return ;
+ case 'Coal':
+ return ;
+ case 'Silicon':
+ return ;
+ case 'Fuel':
+ return ;
+ case 'Nickel':
+ return ;
+ default:
+ return ;
+ }
+};
+
+export function MineralDepositList({ deposits = [], onSelect, selectedDeposit }: Props) {
+ const [inventoryItems, setInventoryItems] = useState([]);
+
+ useEffect(() => {
+ const fetchInventoryItems = async () => {
+ try {
+ const response = await fetch("/api/gameplay/inventory");
+ const data = await response.json();
+ setInventoryItems(data);
+ } catch (error) {
+ console.error("Error fetching inventory items:", error);
+ }
+ };
+
+ fetchInventoryItems();
+ }, []);
+
+ const getMineralDetails = (deposit: MineralDeposit) => {
+ // Map the mineral name using `deposit.mineral` or fallback to `deposit.name`
+ const mineralName = deposit.mineral ? itemNames[deposit.mineral] || deposit.mineral : deposit.name || "Unknown Mineral";
+
+ return {
+ icon: getIcon(mineralName),
+ name: mineralName,
+ };
+ };
+
+ if (inventoryItems.length === 0) {
+ return Loading mineral data...
;
+ }
+
+ return (
+
+
Mineral Deposits
+ {deposits.map((deposit) => {
+ const { icon, name } = getMineralDetails(deposit);
+ return (
+
onSelect(deposit)}
+ >
+
+ {icon}
+ {name} - {deposit.amount} units
+
+
+ );
+ })}
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/mining/mining-component.tsx b/components/(scenes)/mining/mining-component.tsx
new file mode 100644
index 000000000..daf0f4711
--- /dev/null
+++ b/components/(scenes)/mining/mining-component.tsx
@@ -0,0 +1,483 @@
+import { useState, useEffect } from 'react';
+import { MineralDepositList } from './mineral-deposit-list';
+import { TopographicMap } from './topographic-map';
+import { TerrainMap } from './terrain-map';
+import { Inventory } from './Inventory';
+import { Button } from "@/components/ui/button";
+import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react';
+import { useActivePlanet } from '@/context/ActivePlanet';
+import { MineralDeposit } from './mineral-deposit-list';
+
+type Rover = {
+ id: string
+ name: string
+ speed: number
+ efficiency: number
+ miningLevel: number
+};
+
+type InventoryItem = {
+ id: string
+ name: string
+ amount: number
+};
+
+type Landmark = {
+ id: string
+ name: string
+ description: string
+ position: { x: number; y: number }
+ isOpen: boolean
+};
+
+export function MiningComponentComponent() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const { activePlanet, updatePlanetLocation } = useActivePlanet();
+
+ const [mineralDeposits, setMineralDeposits] = useState([]);
+ const [rover, setRover] = useState(null)
+ const [selectedDeposit, setSelectedDeposit] = useState(null)
+ const [roverPosition, setRoverPosition] = useState<{ x: number; y: number } | null>(null)
+ const [inventory, setInventory] = useState([])
+ const [isMining, setIsMining] = useState(false)
+ const [activeMap, setActiveMap] = useState<'2D' | '3D'>('2D')
+ const [landmarks, setLandmarks] = useState([]);
+ const [timeRemaining, setTimeRemaining] = useState(0);
+
+ useEffect(() => {
+ const fetchLandmarks = async () => {
+ if (!session?.user?.id || !activePlanet?.id) {
+ console.error("User or activePlanet is undefined.");
+ return;
+ };
+
+ const { data: inventoryData, error: inventoryError } = await supabase
+ .from("inventory")
+ .select("id, item, quantity")
+ .eq("owner", session.user.id)
+ .eq("anomaly", activePlanet.id)
+ .gt("quantity", 0);
+
+ if (inventoryError) {
+ console.error("Error fetching inventory:", inventoryError);
+ return;
+ }
+
+ // Fetch all item details from API
+ const res = await fetch('/api/gameplay/inventory');
+ const items = await res.json();
+
+ // Filter for structures and map to landmarks
+ const structures = inventoryData
+ ?.filter((inventoryItem) =>
+ items.some(
+ (item: { id: any; ItemCategory: string }) =>
+ item.id === inventoryItem.item && item.ItemCategory === "Structure"
+ )
+ )
+ .map((inventoryItem) => {
+ const itemDetails = items.find(
+ (item: { id: any }) => item.id === inventoryItem.item
+ );
+
+ return {
+ id: inventoryItem.id.toString(),
+ name: itemDetails?.name || "Unknown",
+ description: itemDetails?.description || "No description available",
+ position: itemDetails?.position || { x: Math.random() * 100, y: Math.random() * 100 }, // Default to random position
+ isOpen: false,
+ };
+ });
+
+ setLandmarks(structures || []);
+ refreshParent();
+ };
+
+ fetchLandmarks();
+ }, [session, activePlanet, supabase]);
+
+ useEffect(() => {
+ const fetchDepositsAndInventory = async () => {
+ if (!session?.user?.id || !activePlanet?.id) {
+ console.error("User or activePlanet is undefined.");
+ return;
+ }
+
+ // Fetch deposits
+ const { data: deposits, error: depositsError } = await supabase
+ .from("mineralDeposits")
+ .select("id, mineralconfiguration")
+ .eq("owner", session?.user.id)
+ .eq("anomaly", activePlanet?.id);
+
+ if (depositsError) {
+ console.error("Error fetching mineral deposits:", depositsError);
+ return;
+ }
+
+ const formattedDeposits = deposits?.map((deposit, index) => ({
+ id: `${deposit.id}-${index}`, // Ensure uniqueness
+ name: deposit.mineralconfiguration.mineral || "Unknown",
+ mineral: deposit.mineralconfiguration.mineral || "Unknown",
+ amount: deposit.mineralconfiguration.quantity || 0,
+ icon_url: deposit.mineralconfiguration.icon_url || "",
+ level: deposit.mineralconfiguration.level || 1,
+ uses: deposit.mineralconfiguration.uses || [],
+ position: deposit.mineralconfiguration.position || { x: 50, y: 50 },
+ }));
+
+ setMineralDeposits(formattedDeposits || []);
+
+ const { data: inventoryData, error: inventoryError } = await supabase
+ .from("inventory")
+ .select("id, item, quantity")
+ .eq("owner", session?.user.id)
+ .eq("anomaly", activePlanet?.id)
+ .gt("quantity", 0);
+
+ if (inventoryError) {
+ console.error("Error fetching inventory:", inventoryError);
+ return;
+ }
+
+ const res = await fetch('/api/gameplay/inventory');
+ const items = await res.json();
+
+ const structures = inventoryData
+ ?.filter((inventoryItem) =>
+ items.some(
+ (item: { id: any; ItemCategory: string; }) =>
+ item.id === inventoryItem.item &&
+ item.ItemCategory === "Structure"
+ )
+ )
+ .map((inventoryItem) => {
+ const itemDetails = items.find(
+ (item: { id: any; }) => item.id === inventoryItem.item
+ );
+ return {
+ id: inventoryItem.id.toString(),
+ name: itemDetails?.name || "Unknown",
+ description: itemDetails?.description || "",
+ amount: inventoryItem.quantity || 0,
+ icon_url: itemDetails?.icon_url || "",
+ locationType: itemDetails?.locationType || "Unknown",
+ };
+ });
+
+ setInventory(structures || []);
+ setRover({
+ id: "1",
+ name: "Mars Rover",
+ speed: 15,
+ efficiency: 0.8,
+ miningLevel: 2,
+ });
+ };
+
+ fetchDepositsAndInventory();
+ }, [session, activePlanet, supabase]);
+
+ const handleDepositSelect = (deposit: MineralDeposit) => {
+ console.log("Deposit selected:", deposit);
+ setSelectedDeposit(deposit);
+ };
+
+ const handleStartMining = () => {
+ if (selectedDeposit && rover) {
+ console.log("Starting mining:", selectedDeposit, rover);
+ setIsMining(true);
+ setRoverPosition({ x: 5, y: 5 });
+
+ const duration = 5000;
+ const startTime = Date.now();
+ const endTime = Date.now() + duration;
+ const timer = setInterval(() => {
+ const remaining = Math.max(0, endTime - Date.now());
+ setTimeRemaining(Math.ceil(remaining / 1000));
+ if (remaining <= 0) {
+ clearInterval(timer);
+ setIsMining(false);
+ setTimeRemaining(0);
+ setRoverPosition({ x: 5, y: 5 });
+ updateInventory(selectedDeposit.name, 10);
+ setSelectedDeposit(null);
+ }
+ }, 1000);
+
+ const animateRover = () => {
+ const elapsedTime = Date.now() - startTime;
+ const progress = Math.min(elapsedTime / duration, 1);
+
+ setRoverPosition({
+ x: 5 + (selectedDeposit.position.x - 5) * progress,
+ y: 5 + (selectedDeposit.position.y - 5) * progress,
+ });
+
+ if (progress < 1) {
+ requestAnimationFrame(animateRover);
+ } else {
+ setTimeout(() => {
+ setRoverPosition({ x: 5, y: 5 });
+ setIsMining(false);
+ updateInventory(selectedDeposit.name, 50);
+ setSelectedDeposit(null);
+ }, 5000);
+ }
+ };
+
+ requestAnimationFrame(animateRover);
+ } else {
+ console.error("No deposit selected or rover not available.");
+ }
+ };
+
+ const updateInventory = async (resourceName: string, minedAmount: number) => {
+ if (!session?.user?.id || !activePlanet?.id) {
+ console.error("User or activePlanet is undefined.");
+ return;
+ }
+
+ const existingItem = inventory.find(item => item.name === resourceName);
+
+ if (existingItem) {
+ const { error } = await supabase
+ .from("inventory")
+ .update({ quantity: existingItem.amount + minedAmount })
+ .eq("id", existingItem.id)
+ .eq("owner", session.user.id)
+ .eq("anomaly", activePlanet.id);
+
+ if (error) {
+ console.error("Error updating inventory:", error);
+ } else {
+ setInventory(prev =>
+ prev.map(item =>
+ item.id === existingItem.id
+ ? { ...item, amount: item.amount + minedAmount }
+ : item
+ )
+ );
+ }
+ } else {
+ const { data, error } = await supabase
+ .from("inventory")
+ .insert([
+ {
+ item: resourceName,
+ owner: session.user.id,
+ quantity: minedAmount,
+ anomaly: activePlanet.id,
+ configuration: { Uses: 1, CommunityExpedition: "1 Mars 29 Nov 2024" },
+ },
+ ])
+ .select();
+
+ if (error) {
+ console.error("Error inserting new item into inventory:", error);
+ } else {
+ setInventory(prev => [
+ ...prev,
+ { id: data[0].id.toString(), name: resourceName, amount: minedAmount },
+ ]);
+ }
+ }
+ };
+
+ const toggleMap = () => {
+ setActiveLandmark(null);
+ setLandmarks((prev) => [...prev]);
+ setActiveMap((prev) => (prev === "2D" ? "3D" : "2D"));
+ };
+
+ const [activeLandmark, setActiveLandmark] = useState(null);
+
+ const handleLandmarkClick = (id: string) => {
+ console.log("Landmark clicked:", id);
+ const landmark = landmarks.find((l) => l.id === id);
+ if (landmark) {
+ setActiveLandmark({ ...landmark, isOpen: true });
+ }
+ };
+
+ const closeModal = () => {
+ if (activeLandmark) {
+ setLandmarks((prev: Landmark[] = []) =>
+ prev.map((landmark: Landmark) =>
+ landmark.id === activeLandmark.id
+ ? { ...landmark, isOpen: false }
+ : landmark
+ )
+ );
+ setActiveLandmark(null);
+ }
+ };
+
+ const [refreshKey, setRefreshKey] = useState(0);
+
+ const refreshParent = () => {
+ setRefreshKey((prevKey) => prevKey + 1);
+ };
+
+ useEffect(() => {
+ console.log('Parent component refreshed.');
+ }, [refreshKey]);
+
+ return (
+
+
+
+
Mining Operations
+
+ Switch to {activeMap === '2D' ? '3D' : '2D'} Map
+
+
+
+
+ {activeMap === '2D' ? (
+
+ ) : (
+
+ )}
+
+
+
+ {selectedDeposit ? (
+
+
Selected Deposit
+
{selectedDeposit.name}
+
+ {isMining
+ ? `Mining in progress, time remaining: ${timeRemaining}s`
+ : "Start Mining"}
+
+
+ ) : (
+
Select a deposit to start mining.
+ )}
+
+
+
+
+
+
+
+
+ {activePlanet !== 30 && (
+
updatePlanetLocation(30)}
+ className="mt-2 bg-green-500 hover:bg-green-600 text-white"
+ >
+ Return to Earth
+
+ )}
+ {activeLandmark && (
+
+
e.stopPropagation()}
+ >
+
{activeLandmark.name}
+
{activeLandmark.description}
+
+ Close
+
+
+
+ )}
+
+
+ );
+};
+
+const MineralDepositsGenerator: React.FC = () => {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const { activePlanet } = useActivePlanet();
+
+ const availableMinerals = [11, 13, 15, 16, 18, 19];
+ const [creatingDeposits, setCreatingDeposits] = useState(false);
+
+ const handleCreateDeposits = async () => {
+ if (!session?.user?.id || !activePlanet?.id) {
+ console.error("User or activePlanet is undefined.");
+ return;
+ }
+
+ setCreatingDeposits(true);
+
+ const randomMineral = availableMinerals[Math.floor(Math.random() * availableMinerals.length)];
+
+ const newDeposit = {
+ mineralconfiguration: {
+ mineral: randomMineral,
+ quantity: 100,
+ icon_url: `https://example.com/mineral-icon-${randomMineral}.png`,
+ level: 1,
+ uses: ["Mining", "Excavation"],
+ position: { x: Math.random() * 100, y: Math.random() * 100 },
+ },
+ owner: session.user.id,
+ anomaly: activePlanet.id,
+ };
+
+ const { error } = await supabase
+ .from("mineralDeposits")
+ .insert([newDeposit]);
+
+ if (error) {
+ console.error("Error creating deposit:", error);
+ setCreatingDeposits(false);
+ return;
+ }
+
+ // Trigger full reload
+ window.location.reload();
+ };
+
+ return (
+
+
+
Create Mineral Deposits
+
+ {creatingDeposits ? "Creating..." : "Create Deposits"}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/mining/terrain-map.tsx b/components/(scenes)/mining/terrain-map.tsx
new file mode 100644
index 000000000..aa1343de6
--- /dev/null
+++ b/components/(scenes)/mining/terrain-map.tsx
@@ -0,0 +1,152 @@
+import { useEffect, useRef } from 'react';
+import { motion } from 'framer-motion';
+import { MapPin, Pickaxe, Zap, Battery, Magnet } from 'lucide-react';
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { MineralDeposit } from './mineral-deposit-list';
+
+type Landmark = {
+ id: string
+ name: string
+ description: string
+ position: { x: number; y: number }
+ isOpen: boolean
+};
+
+type Props = {
+ deposits: MineralDeposit[]
+ roverPosition: { x: number; y: number } | null
+ selectedDeposit: MineralDeposit | null
+ landmarks: Landmark[]
+ onLandmarkClick: (id: string) => void
+};
+
+const getMineralIcon = (name: string) => {
+ switch (name) {
+ case 'Iron':
+ return
+ case 'Copper':
+ return
+ case 'Coal':
+ return
+ case 'Nickel':
+ return
+ default:
+ return
+ };
+};
+
+export function TerrainMap({ deposits = [], roverPosition, selectedDeposit, landmarks, onLandmarkClick }: Props) {
+ const canvasRef = useRef(null)
+
+ useEffect(() => {
+ const canvas = canvasRef.current
+ if (!canvas) return
+
+ const ctx = canvas.getContext('2d')
+ if (!ctx) return
+
+ const drawMap = () => {
+ const { width, height } = canvas
+
+ // Clear the canvas
+ ctx.clearRect(0, 0, width, height)
+
+ // Draw a placeholder for the 3D terrain
+ ctx.fillStyle = '#276e4b' // A brown color for the terrain
+ ctx.fillRect(0, 0, width, height)
+
+ // Draw some basic terrain features
+ ctx.strokeStyle = '#654321'
+ ctx.lineWidth = 2
+ for (let i = 0; i < 10; i++) {
+ ctx.beginPath()
+ ctx.moveTo(0, Math.random() * height)
+ ctx.lineTo(width, Math.random() * height)
+ ctx.stroke()
+ }
+
+ // Draw deposits
+ deposits.forEach(deposit => {
+ ctx.fillStyle = '#FFD700' // Gold color for deposits
+ ctx.beginPath()
+ ctx.arc(deposit.position.x * (width / 100), deposit.position.y * (height / 100),
+ Math.sqrt(deposit.amount) / 5, 0, 2 * Math.PI)
+ ctx.fill()
+ })
+ }
+
+ const resizeCanvas = () => {
+ canvas.width = canvas.offsetWidth
+ canvas.height = canvas.offsetHeight
+ drawMap()
+ }
+
+ window.addEventListener('resize', resizeCanvas)
+ resizeCanvas()
+
+ return () => window.removeEventListener('resize', resizeCanvas)
+ }, [deposits])
+
+ return (
+
+
+ {deposits.map((deposit) => (
+
+ {getMineralIcon(deposit.name)}
+
+ ))}
+ {landmarks.map((landmark) => (
+
+
+ onLandmarkClick(landmark.id)}
+ >
+
+
+
+
+
+ {landmark.name}
+ {landmark.description}
+
+
+
+ ))}
+{roverPosition && (
+
+ {/* Rover icon */}
+
+)}
+ {selectedDeposit && (
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/components/(scenes)/mining/topographic-map.tsx b/components/(scenes)/mining/topographic-map.tsx
new file mode 100644
index 000000000..cc75516fc
--- /dev/null
+++ b/components/(scenes)/mining/topographic-map.tsx
@@ -0,0 +1,176 @@
+import { useEffect, useRef, useState } from "react";
+import { MapPin, Pickaxe, Zap, Battery, Magnet } from "lucide-react";
+import { createNoise2D } from "simplex-noise";
+import alea from "alea";
+
+export type MineralDeposit = {
+ id: string;
+ name: string;
+ mineral: string;
+ amount: number;
+ position: { x: number; y: number };
+ icon_url: string;
+ level: number;
+ uses: any[];
+};
+
+type Landmark = {
+ id: string;
+ name: string;
+ description: string;
+ position: { x: number; y: number };
+ isOpen: boolean;
+};
+
+type TopographicMapProps = {
+ deposits: MineralDeposit[];
+ roverPosition: { x: number; y: number } | null;
+ selectedDeposit: MineralDeposit | null;
+ landmarks: Landmark[];
+ onLandmarkClick: (id: string) => void;
+ onDepositSelect: (deposit: MineralDeposit) => void;
+};
+
+const getMineralIcon = (name: string) => {
+ switch (name) {
+ case "Iron":
+ return ;
+ case "Copper":
+ return ;
+ case "Coal":
+ return ;
+ case "Nickel":
+ return ;
+ default:
+ return ;
+ }
+};
+
+const BackgroundMap = ({ deposits, seed }: { deposits: MineralDeposit[]; seed: string }) => {
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+
+ const prng = alea(seed);
+ const noise2D = createNoise2D(prng);
+
+ const drawMap = () => {
+ const { width, height } = canvas;
+
+ ctx.clearRect(0, 0, width, height);
+ ctx.fillStyle = "#a6142a" // "#F5F5DC";
+ ctx.fillRect(0, 0, width, height);
+
+ const step = 10;
+ for (let y = 0; y < height; y += step) {
+ for (let x = 0; x < width; x += step) {
+ const elevation = noise2D(x * 0.005, y * 0.005);
+ const color = elevation > 0 ? `rgba(96, 165, 250, ${elevation})` : `rgba(30, 58, 138, ${-elevation})`;
+ ctx.fillStyle = color;
+ ctx.fillRect(x, y, step, step);
+ }
+ }
+
+ deposits.forEach((deposit) => {
+ ctx.fillStyle = "#A52A2A";
+ ctx.beginPath();
+ ctx.arc(
+ deposit.position.x * (width / 100),
+ deposit.position.y * (height / 100),
+ Math.sqrt(deposit.amount) / 5,
+ 0,
+ 2 * Math.PI
+ );
+ ctx.fill();
+ ctx.strokeStyle = "#2C4F64";
+ ctx.lineWidth = 2;
+ ctx.stroke();
+ });
+ };
+
+ const resizeCanvas = () => {
+ canvas.width = canvas.offsetWidth;
+ canvas.height = canvas.offsetHeight;
+ drawMap();
+ };
+
+ window.addEventListener("resize", resizeCanvas);
+ resizeCanvas();
+
+ return () => window.removeEventListener("resize", resizeCanvas);
+ }, [deposits, seed]);
+
+ return ;
+};
+
+export function TopographicMap({
+ deposits,
+ roverPosition,
+ selectedDeposit,
+ landmarks,
+ onDepositSelect,
+ onLandmarkClick,
+}: TopographicMapProps) {
+ const [terrainSeed, setTerrainSeed] = useState("default");
+ const [dynamicLandmarks, setDynamicLandmarks] = useState(landmarks);
+
+ const generateNewSector = () => {
+ const newSeed = Math.random().toString(); // Generate a new random seed
+ setTerrainSeed(newSeed);
+
+ setDynamicLandmarks(
+ dynamicLandmarks.map((landmark) => ({
+ ...landmark,
+ position: {
+ x: Math.random() * 100,
+ y: Math.random() * 100,
+ },
+ }))
+ );
+ };
+
+ return (
+
+
+ {deposits.map((deposit) => (
+
onDepositSelect(deposit)}
+ >
+ {deposit.icon_url ? (
+
+ ) : (
+ getMineralIcon(deposit.mineral)
+ )}
+
+))}
+ {dynamicLandmarks.map((landmark) => (
+
onLandmarkClick(landmark.id)}
+ >
+
+
+ ))}
+
+ Generate Next Sector
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/planetScene/layout.tsx b/components/(scenes)/planetScene/layout.tsx
new file mode 100644
index 000000000..dc9ded7aa
--- /dev/null
+++ b/components/(scenes)/planetScene/layout.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import React, { useState, CSSProperties } from "react";
+// import { Header } from "@/app/components/sections/header";
+import {
+ Essendonitis,
+ background2,
+ background3,
+ callme,
+ mining,
+} from "@/constants/backgrounds";
+import backgroundImage from "@/public/assets/Backdrops/Negotiations.jpg";
+import Header from "@/components/ui/Header";
+
+interface PlanetViewLayoutProps {
+ children: React.ReactNode[];
+};
+
+export const EarthViewLayout: React.FC = ({ children }) => {
+ const childrenArray = React.Children.toArray(children);
+
+ const sectionStyles: CSSProperties[] = [
+ {
+ flex: 8,
+ },
+ {
+ flex: 6,
+ },
+ {
+ display: "flex",
+ flexDirection: "column" as const,
+ flex: 20,
+ },
+ ];
+
+ return (
+
+
+
+
+
+ {/*
goes somewhere here */}
+ {childrenArray.slice(0, 2).map((child, index) => (
+
+ {child}
+
+ ))}
+
+
+
{childrenArray[2]}
+ {childrenArray[3] && (
+
+ {childrenArray[3]}
+
+ )}
+
+
{childrenArray[4]}
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/planetScene/orbitals/system.tsx b/components/(scenes)/planetScene/orbitals/system.tsx
new file mode 100644
index 000000000..f709cceba
--- /dev/null
+++ b/components/(scenes)/planetScene/orbitals/system.tsx
@@ -0,0 +1,69 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import Image from 'next/image'
+
+export function PlanetarySystem() {
+ const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 0)
+
+ useEffect(() => {
+ const handleResize = () => setWindowWidth(window.innerWidth)
+ window.addEventListener('resize', handleResize)
+ return () => window.removeEventListener('resize', handleResize)
+ }, [])
+
+ const size = 100;
+
+ const orbits = [
+ { radius: size * 0.25, duration: 20 },
+ { radius: size * 0.35, duration: 40 },
+ { radius: size * 0.45, duration: 55 }
+ ]
+
+ return (
+
+
+ {/* Central Object */}
+
+
+
+
+ {/* Orbits and Orbiting Objects */}
+ {orbits.map((orbit, index) => (
+
+ ))}
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/travel/BoardingPass.tsx b/components/(scenes)/travel/BoardingPass.tsx
new file mode 100644
index 000000000..b44764e5a
--- /dev/null
+++ b/components/(scenes)/travel/BoardingPass.tsx
@@ -0,0 +1,84 @@
+import React from 'react'
+import { motion } from 'framer-motion'
+import { User, Planet } from "@/types/Travel";
+import Image from 'next/image'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
+
+interface BoardingPassProps {
+ user: User
+ planet: Planet
+ onBeginTrip: () => void
+ onCancelBooking: () => void
+};
+
+function QRCodePattern() {
+ const size = 10
+ const cells = Array(size * size).fill(0).map(() => Math.random() > 0.5)
+
+ return (
+
+
+ {cells.map((filled, i) => (
+
+ ))}
+
+
+ );
+};
+
+export default function BoardingPass({ user, planet, onBeginTrip, onCancelBooking }: BoardingPassProps) {
+ return (
+
+
+
+ Boarding Pass
+
+
+
+
+ Passenger
+ {user.name}
+
+
+ Destination
+ {planet.name}
+
+
+ Station
+ {planet.station}
+
+
+ Temperature
+ {planet.temperature}°C
+
+
+
+
+
+
+ Begin Trip
+
+
+ Cancel Booking
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/travel/BoardingPassCard.tsx b/components/(scenes)/travel/BoardingPassCard.tsx
new file mode 100644
index 000000000..de2b3d0f0
--- /dev/null
+++ b/components/(scenes)/travel/BoardingPassCard.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import Image from 'next/image';
+
+// Define the BoardingPass interface
+interface BoardingPass {
+ userName: string;
+ frequentFlyerNumber: string;
+ frequentFlyerStatus: string;
+ departurePlanet: string;
+ departureTemperature: number;
+ destinationPlanet: string;
+ destinationTemperature: number;
+ rocketType: string;
+ departureTime: string; // ISO 8601 string
+}
+
+interface BoardingPassCardProps {
+ boardingPass: BoardingPass;
+}
+
+const BoardingPassCard: React.FC = ({ boardingPass }) => {
+ return (
+
+
Boarding Pass
+
+
+
Passenger Information
+
Name: {boardingPass.userName}
+
Frequent Flyer: {boardingPass.frequentFlyerNumber}
+
Status: {boardingPass.frequentFlyerStatus}
+
+
+
Flight Information
+
+ From: {boardingPass.departurePlanet}
+ ({boardingPass.departureTemperature}°C)
+
+
+ To: {boardingPass.destinationPlanet}
+ ({boardingPass.destinationTemperature}°C)
+
+
Rocket: {boardingPass.rocketType}
+
+ Departure:
+ {new Date(boardingPass.departureTime).toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default BoardingPassCard;
\ No newline at end of file
diff --git a/components/(scenes)/travel/BookingConfirmation.tsx b/components/(scenes)/travel/BookingConfirmation.tsx
new file mode 100644
index 000000000..3cf16838e
--- /dev/null
+++ b/components/(scenes)/travel/BookingConfirmation.tsx
@@ -0,0 +1,109 @@
+'use client'
+
+import { motion } from 'framer-motion'
+import { ChevronLeft } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { Card } from '@/components/ui/card'
+import Image from 'next/image'
+import { Planet, Departure, User } from '@/types/Travel'
+
+interface BookingConfirmationProps {
+ planet: Planet
+ departure: Departure
+ user: User
+ onBack: () => void
+};
+
+function QRCodePattern() {
+ const size = 10
+ const cells = Array(size * size).fill(0).map(() => Math.random() > 0.5)
+
+ return (
+
+
+ {cells.map((filled, i) => (
+
+ ))}
+
+
+ );
+};
+
+export default function BookingConfirmation({ planet, departure, user, onBack }: BookingConfirmationProps) {
+ return (
+
+
+
+
+
+ Booking Confirmation
+
+
+
+
+
+
+
{planet.name}
+
{planet.station}
+
+
+
+
+
+
+ Passenger
+ {user.name
+ }
+
+
+ Departure
+ {new Date(departure.departureTime).toLocaleString()}, Gate {departure.gate}
+
+
+ Duration
+ {departure.duration}
+
+
+ Rocket
+ {departure.rocketType}
+
+
+
+
+
+
+
Total Price
+
${departure.price}
+
+
+
Booking Reference
+
{planet.name.substring(0, 3).toUpperCase()}-{Math.random().toString(36).substring(2, 7).toUpperCase()}
+
+
+
+
+
+
+
+
+
+
+ Download Ticket
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/travel/DepartureList.tsx b/components/(scenes)/travel/DepartureList.tsx
new file mode 100644
index 000000000..fd3f1ba93
--- /dev/null
+++ b/components/(scenes)/travel/DepartureList.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Departure } from '@/types/Travel';
+
+interface DepartureListProps {
+ departures: Departure[];
+ onSelect: (departure: Departure) => void;
+ selectedDeparture: Departure | null;
+}
+
+const DepartureList: React.FC = ({ departures, onSelect, selectedDeparture }) => {
+ return (
+
+
Select Your Departure
+
+ {departures.map(departure => (
+ onSelect(departure)}
+ >
+ {departure.rocketType} - Departs at{' '}
+ {new Date(departure.departureTime).toLocaleString()}
+
+ ))}
+
+
+ );
+};
+
+export default DepartureList;
\ No newline at end of file
diff --git a/components/(scenes)/travel/DepartureSelector.tsx b/components/(scenes)/travel/DepartureSelector.tsx
new file mode 100644
index 000000000..e6197254f
--- /dev/null
+++ b/components/(scenes)/travel/DepartureSelector.tsx
@@ -0,0 +1,101 @@
+'use client'
+
+import { motion } from 'framer-motion'
+import { ChevronLeft, Rocket, Clock, CreditCard } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { Card } from '@/components/ui/card'
+import { Planet, Departure } from '@/types/Travel'
+
+interface DepartureSelectorProps {
+ planet: Planet
+ onSelect: (departure: Departure) => void
+ onBack: () => void
+}
+
+const departures: Departure[] = [
+ {
+ id: '1',
+ rocketType: 'Falcon Heavy',
+ departureTime: '2023-07-01T09:00:00Z',
+ price: 2499,
+ duration: '6 months',
+ gate: 'A1'
+ },
+ {
+ id: '2',
+ rocketType: 'Starship',
+ departureTime: '2023-07-02T14:30:00Z',
+ price: 3999,
+ duration: '4 months',
+ gate: 'B3'
+ },
+ {
+ id: '3',
+ rocketType: 'New Glenn',
+ departureTime: '2023-07-03T20:45:00Z',
+ price: 2999,
+ duration: '5 months',
+ gate: 'C2'
+ }
+]
+
+export default function DepartureSelector({ planet, onSelect, onBack }: DepartureSelectorProps) {
+ return (
+
+
+
+
+
+ Select Departure to {planet.name}
+
+
+
+ {departures.map((departure, index) => (
+
+ onSelect(departure)}
+ >
+
+
+
{departure.rocketType}
+
Gate {departure.gate}
+
+
+
${departure.price}
+
per person
+
+
+
+
+
+ {new Date(departure.departureTime).toLocaleString()}
+
+
+
+ {departure.duration}
+
+
+
+
+ ))}
+
+
+
+ onSelect(departures[0])}
+ >
+
+ Continue to Payment
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/travel/DestinationList.tsx b/components/(scenes)/travel/DestinationList.tsx
new file mode 100644
index 000000000..f25ab9d69
--- /dev/null
+++ b/components/(scenes)/travel/DestinationList.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+
+// Define the Destination interface
+interface Destination {
+ id: string; // Unique identifier for each destination
+ name: string; // Name of the destination
+ type: 'solar' | 'exoplanet'; // Type of destination
+}
+
+interface DestinationListProps {
+ destinations: Destination[]; // List of destinations
+ onSelect: (destination: Destination) => void; // Callback when a destination is selected
+ selectedDestination: Destination | null; // Currently selected destination
+}
+
+const DestinationList: React.FC = ({
+ destinations,
+ onSelect,
+ selectedDestination,
+}) => {
+ const solarDestinations = destinations.filter((d) => d.type === 'solar');
+ const exoplanets = destinations.filter((d) => d.type === 'exoplanet');
+
+ return (
+
+
Select Your Destination
+
+
+
Solar System
+
+ {solarDestinations.map((destination) => (
+ onSelect(destination)}
+ >
+ {destination.name}
+
+ ))}
+
+
+
+
Exoplanets
+
+ {exoplanets.map((destination) => (
+ onSelect(destination)}
+ >
+ {destination.name}
+
+ ))}
+
+
+
+
+ );
+};
+
+export default DestinationList;
\ No newline at end of file
diff --git a/components/(scenes)/travel/PlanetSelector.tsx b/components/(scenes)/travel/PlanetSelector.tsx
new file mode 100644
index 000000000..92f5abafb
--- /dev/null
+++ b/components/(scenes)/travel/PlanetSelector.tsx
@@ -0,0 +1,285 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { motion } from 'framer-motion'
+import { User, Planet } from '@/types/Travel';
+import Image from 'next/image';
+import { ChevronRight } from 'lucide-react';
+import BoardingPass from './BoardingPass';
+import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react';
+
+interface PlanetSelectorProps {
+ user: User
+ onSelect: (planet: Planet) => void
+};
+
+const initialPlanets: Planet[] = [
+ {
+ id: 'mars',
+ name: 'MARS',
+ station: 'Olympus Base',
+ image: '/placeholder.svg?height=400&width=400',
+ color: '#FF4D4D',
+ temperature: -63,
+ type: 'intra-solar',
+ planetType: 'Terrestrial',
+ description: 'The Red Planet, fourth from the Sun.',
+ orbitPosition: 0,
+ available: true
+ },
+ {
+ id: 'moon',
+ name: 'MOON',
+ station: 'Tranquility Base',
+ image: '/placeholder.svg?height=400&width=400',
+ color: '#F7F5E9',
+ temperature: -20,
+ planetType: 'Terrestrial',
+ type: 'intra-solar',
+ description: 'Earth\'s only natural satellite.',
+ orbitPosition: 1,
+ available: true
+ },
+ {
+ id: 'europa',
+ name: 'EUROPA',
+ station: 'Galileo Station',
+ image: '/placeholder.svg?height=400&width=400',
+ color: '#5FCBC3',
+ temperature: -160,
+ planetType: "Frozen",
+ type: 'intra-solar',
+ description: 'Jupiter\'s icy moon with a subsurface ocean.',
+ orbitPosition: 2,
+ available: false
+ },
+ {
+ id: 'venus',
+ name: 'VENUS',
+ station: 'Cloud City',
+ image: '/placeholder.svg?height=400&width=400',
+ color: '#FFD700',
+ temperature: 462,
+ planetType: "Hellish",
+ type: 'intra-solar',
+ description: 'The hottest planet in our solar system.',
+ orbitPosition: 3,
+ available: false
+ },
+];
+
+const getParentStarColor = (type: 'intra-solar' | 'interstellar') => {
+ if (type === 'intra-solar') {
+ return 'bg-gradient-radial from-yellow-400 to-orange-500';
+ }
+ const hue = Math.floor(Math.random() * 60);
+ return `bg-gradient-radial from-[hsl(${hue},100%,50%)] to-[hsl(${hue},100%,30%)]`;
+};
+
+export default function PlanetSelector({ user, onSelect }: PlanetSelectorProps) {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [planets, setPlanets] = useState([...initialPlanets]); // Use renamed constant here
+ const [selectedPlanet, setSelectedPlanet] = useState(initialPlanets[0]);
+ const [selectedType, setSelectedType] = useState<'intra-solar' | 'interstellar'>('intra-solar');
+ const [isBooked, setIsBooked] = useState(false);
+
+ useEffect(() => {
+ const fetchExoplanets = async () => {
+ try {
+ const userId = session?.user?.id; // Ensure `session.user.id` is available
+ if (!userId) {
+ console.error('User is not logged in');
+ return;
+ }
+
+ // Fetch classifications first
+ const { data: classifications, error: classificationsError } = await supabase
+ .from('classifications')
+ .select('anomaly')
+ .eq('author', userId)
+ .neq('anomaly', null);
+
+ if (classificationsError) throw classificationsError;
+
+ // Extract anomaly IDs
+ const anomalyIds = classifications.map((classification) => classification.anomaly);
+
+ if (anomalyIds.length === 0) {
+ console.warn('No anomalies found for the user.');
+ return;
+ };
+
+ // Fetch anomalies using the anomaly IDs
+ const { data, error } = await supabase
+ .from('anomalies')
+ .select(`
+ id,
+ content,
+ anomalytype,
+ avatar_url,
+ anomalySet
+ `)
+ .in('id', anomalyIds)
+ .eq('anomalySet', 'tess');
+
+ if (error) throw error;
+
+ // Map data to Planet objects
+ const exoplanetsData: Planet[] = data.map((exoplanet) => ({
+ id: exoplanet.id as string,
+ name: exoplanet.content || 'Unknown Name',
+ color: '#5FCBC3',
+ station: 'Unknown Station',
+ temperature: NaN, // No temperature in the query
+ planetType: exoplanet.anomalytype || 'Unknown',
+ type: 'interstellar',
+ description: exoplanet.content || 'No description available',
+ image: exoplanet.avatar_url || '/assets/Planets/DefaultExoplanet.png',
+ orbitPosition: Math.floor(Math.random() * 5),
+ available: true,
+ }));
+
+ setPlanets((prevPlanets) => [...prevPlanets, ...exoplanetsData]);
+ } catch (error) {
+ console.error('Error fetching exoplanets:', error);
+ }
+ };
+
+
+ fetchExoplanets();
+ }, [supabase, session]);
+
+ const filteredPlanets = planets.filter((planet) => planet.type === selectedType);
+
+ const handlePlanetSelect = (planet: Planet) => {
+ if (planet.available) {
+ setSelectedPlanet(planet);
+ setIsBooked(false);
+ }
+ };
+
+ const handleBook = () => {
+ setIsBooked(true);
+ onSelect(selectedPlanet);
+ };
+
+ const handleBeginTrip = () => {
+ console.log('Beginning trip to', selectedPlanet.name);
+ };
+
+ if (isBooked) {
+ return (
+ setIsBooked(false)}
+ />
+ );
+ };
+
+ return (
+
+
+
+
+
+
Let's book your
+ trip to
+
+
+
+ setSelectedType('intra-solar')}
+ >
+ Intra-Solar
+
+ setSelectedType('interstellar')}
+ >
+ Interstellar
+
+
+
+
+
+
p.id === selectedPlanet.id) + 1) / filteredPlanets.length) * 100}%` }}
+ />
+
+
+
+ {filteredPlanets.map((planet, index) => (
+
handlePlanetSelect(planet)}
+ style={{
+ backgroundColor: planet.id === selectedPlanet.id ? planet.color : (planet.available ? '#2C4F64' : '#4A4A4A')
+ }}
+ >
+
+ {planet.name}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
{selectedPlanet.name}
+
{selectedPlanet.station}
+
+
+ Book Trip
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/(scenes)/travel/SolarSystem.tsx b/components/(scenes)/travel/SolarSystem.tsx
new file mode 100644
index 000000000..74b783a7b
--- /dev/null
+++ b/components/(scenes)/travel/SolarSystem.tsx
@@ -0,0 +1,598 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import { Star, MapPin, Clock, ThermometerSun, Users, ChevronLeft, ChevronRight } from "lucide-react";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import { useActivePlanet } from "@/context/ActivePlanet";
+import { lidarDataSources, physicsLabDataSources, roverDataSources, telescopeDataSources, zoodexDataSources } from "@/components/Data/ZoodexDataSources";
+import InventoryList from "@/components/Inventory/items/ItemsOnPlanet";
+
+type Exoplanet = {
+ id: number;
+ name: string;
+ distance: number;
+ anomaly?: number;
+ planetType?: string;
+ initialisationMissionId?: number;
+ image?: string;
+ description?: string;
+};
+
+type SolarSystemPlanet = {
+ id: number;
+ name: string;
+ color: string;
+ stats: {
+ gravity: string;
+ temp: string;
+ };
+ anomaly: number;
+ planetType: string;
+ initialisationMissionId: number;
+ travelTime: string;
+ description: string;
+ image: string;
+};
+
+type Planet = Exoplanet | SolarSystemPlanet;
+
+export default function SwitchPlanet() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const { activePlanet, updatePlanetLocation } = useActivePlanet();
+
+ const [activeTab, setActiveTab] = useState<"solarSystem" | "exoplanets">("solarSystem");
+ const [currentIndex, setCurrentIndex] = useState(0);
+
+ const [planetStats, setPlanetStats] = useState
([]);
+ const [visitedPlanets, setVisitedPlanets] = useState<{ [key: number]: boolean }>({});
+ const [classificationsByPlanet, setClassificationsByPlanet] = useState>({});
+ const [hasRocket, setHasRocket] = useState(false);
+
+ const [showMissions, setShowMissions] = useState(false);
+ const [selectedMission, setSelectedMission] = useState(null);
+ const [compatibleMissions, setCompatibleMissions] = useState([]);
+
+ const [planets, setPlanets] = useState<{
+ solarSystem: SolarSystemPlanet[];
+ exoplanets: Exoplanet[];
+ }>({
+ solarSystem: [
+ // {
+ // id: 10,
+ // name: "Mercury",
+ // color: "bg-[#2C3A4A]",
+ // stats: { gravity: "3.7 m/s²", temp: "430°C" },
+ // anomaly: 10,
+ // planetType: 'Arid',
+ // initialisationMissionId: 100001,
+ // travelTime: '30 seconds',
+ // description: '',
+ // image: '/assets/Planets/Mercury.png',
+ // },
+ // {
+ // id: 20,
+ // name: "Venus",
+ // color: "bg-yellow-200",
+ // stats: { gravity: "8.87 m/s²", temp: "462°C" },
+ // anomaly: 20,
+ // planetType: 'Arid',
+ // initialisationMissionId: 200001,
+ // travelTime: '30 seconds',
+ // description: '',
+ // image: '/assets/Planets/Venus.png',
+ // },
+ {
+ id: 40,
+ name: "Mars",
+ color: "bg-red-500",
+ stats: { gravity: "3.71 m/s²", temp: "-63°C" },
+ anomaly: 40,
+ planetType: 'Arid',
+ initialisationMissionId: 400001,
+ travelTime: '30 seconds',
+ description: '',
+ image: '/assets/Planets/Mars.png',
+ },
+ {
+ id: 30, // 69
+ name: "Earth",
+ color: "bg-blue-500",
+ stats: { gravity: "9.8 m/s²", temp: "15°C" },
+ anomaly: 30,
+ planetType: 'Lush',
+ initialisationMissionId: 300001,
+ travelTime: '30 seconds',
+ description: '',
+ image: '/assets/Planets/Earth.png',
+ },
+ ],
+ exoplanets: [],
+ });
+
+ const currentPlanets = planets[activeTab];
+ const currentPlanet = currentPlanets[currentIndex];
+ // const planetType = currentPlanet?.planetType || 'Unknown';
+ // const anomaly = currentPlanet?.anomaly ?? 'Unknown';
+ // const initialisationMissionId = currentPlanet?.initialisationMissionId ?? null;
+ // const image = currentPlanet?.image ?? '/assets/Planets/DefaultExoplanet.png';
+ // const description = currentPlanet?.description ?? 'No description available';
+ const currentPlanetAnomaly = currentPlanet?.anomaly ?? 0;
+
+ useEffect(() => {
+ const fetchExoplanets = async () => {
+ try {
+ const { data, error } = await supabase
+ .from("anomalies")
+ .select("*")
+ .eq("anomalySet", "tess");
+
+ if (error) throw error;
+
+ const exoplanetsData = data.map((exoplanet) => ({
+ id: exoplanet.id,
+ name: exoplanet.content,
+ color: "bg-purple-500",
+ stats: { gravity: "Unknown", temp: "Unknown" },
+ anomaly: exoplanet.id,
+ planetType: exoplanet.anomalytype || "Unknown",
+ initialisationMissionId: null,
+ travelTime: "Unknown",
+ description: exoplanet.content || "No description available",
+ image: exoplanet.avatar_url || '/assets/Planets/DefaultExoplanet.png',
+ }));
+
+ setPlanets((prevState) => ({
+ ...prevState,
+ exoplanets: exoplanetsData as unknown as Exoplanet[],
+ }));
+ } catch (error: any) {
+ console.error("Error fetching exoplanets: ", error.message);
+ }
+ };
+
+ fetchExoplanets();
+ }, [supabase]);
+
+ useEffect(() => {
+ const fetchExoplanets = async () => {
+ try {
+ const { data: exoplanetData, error: exoplanetError } = await supabase
+ .from("anomalies")
+ .select("*")
+ // .eq("anomalySet", "tess");
+ .eq("anomalytype", 'planet');
+
+ if (exoplanetError) throw exoplanetError;
+
+ if (session?.user?.id) {
+ const { data: classificationData, error: classificationError } = await supabase
+ .from("classifications")
+ .select("anomaly")
+ .eq("author", session.user.id)
+ .in(
+ "anomaly",
+ exoplanetData.map((exoplanet) => exoplanet.id)
+ );
+
+ if (classificationError) throw classificationError;
+
+ const classifiedExoplanetIds = classificationData.map((classification) => classification.anomaly);
+
+ const filteredExoplanets = exoplanetData
+ .filter((exoplanet) => classifiedExoplanetIds.includes(exoplanet.id))
+ .map((exoplanet) => ({
+ id: exoplanet.id,
+ name: exoplanet.content,
+ color: "bg-purple-500",
+ stats: { gravity: "Unknown", temp: "Unknown" },
+ anomaly: exoplanet.id,
+ planetType: "Frozen",
+ initialisationMissionId: null,
+ travelTime: "Unknown",
+ description: exoplanet.deepnote || "No description available",
+ image: exoplanet.avatar_url || '/assets/Planets/DefaultExoplanet.png',
+ }));
+
+ setPlanets((prevState) => ({
+ ...prevState,
+ exoplanets: filteredExoplanets as unknown as Exoplanet[],
+ }));
+ };
+ } catch (error: any) {
+ console.error("Error fetching exoplanets: ", error.message);
+ };
+ };
+
+ fetchExoplanets();
+ }, [supabase, session?.user?.id]);
+
+
+ useEffect(() => {
+ const fetchVisitedPlanets = async () => {
+ if (!session) return;
+
+ try {
+ const { data: missionsData, error } = await supabase
+ .from("missions")
+ .select("mission")
+ .eq("user", session.user.id);
+
+ if (error) throw error;
+
+ const visited = missionsData.reduce(
+ (acc: { [key: number]: boolean }, mission: { mission: number }) => {
+ acc[mission.mission] = true;
+ return acc;
+ },
+ {}
+ );
+
+ setVisitedPlanets(visited);
+ } catch (error: any) {
+ console.error("Error fetching visited planets: ", error.message);
+ };
+ };
+
+ const fetchPlanetStats = async () => {
+ try {
+ const response = await fetch("/api/gameplay/missions/planets/solarsystem/stats");
+ const data = await response.json();
+ setPlanetStats(data);
+ } catch (error) {
+ console.error("Error fetching planet stats:", error);
+ };
+ };
+
+ const fetchClassifications = async () => {
+ if (session?.user?.id) {
+ try {
+ const { data: classificationsData, error } = await supabase
+ .from("classifications")
+ .select("classificationtype, classificationConfiguration")
+ .eq("author", session.user.id);
+
+ if (error) throw error;
+
+ const classificationsByPlanetTemp: Record = {};
+
+ classificationsData.forEach((classification: any) => {
+ const planetAnomaly = parseInt(classification.classificationConfiguration.activePlanet, 10);
+
+ if (!classificationsByPlanetTemp[planetAnomaly]) {
+ classificationsByPlanetTemp[planetAnomaly] = [];
+ }
+
+ classificationsByPlanetTemp[planetAnomaly].push(classification);
+ });
+
+ setClassificationsByPlanet(classificationsByPlanetTemp);
+ } catch (error: any) {
+ console.error("Error fetching classifications:", error.message);
+ };
+ };
+ };
+
+ const checkRocketInInventory = async () => {
+ if (session?.user?.id) {
+ try {
+ const { data, error } = await supabase
+ .from("inventory")
+ .select("id")
+ .eq("item", 3108)
+ .eq("anomaly", activePlanet?.id)
+ .eq("owner", session.user.id);
+
+ if (error) throw error;
+
+ setHasRocket(data.length > 0);
+ } catch (error: any) {
+ console.error("Error checking inventory:", error.message);
+ };
+ };
+ };
+
+ fetchVisitedPlanets();
+ fetchPlanetStats();
+ fetchClassifications();
+ checkRocketInInventory();
+ }, [session, supabase, activePlanet]);
+
+ useEffect(() => {
+ const planetType = currentPlanet?.planetType;
+
+ if (planetType) {
+ const filterCompatibleMissions = () => {
+ const planetType = currentPlanet?.planetType;
+ if (planetType) {
+ // const zoodexMissions = zoodexDataSources.flatMap((category) =>
+ // category.items.filter((item) => item.compatiblePlanetTypes.includes(planetType))
+ // );
+ const physicsLabMissions = physicsLabDataSources.flatMap((category) =>
+ category.items.filter((item) => item.compatiblePlanetTypes.includes(planetType))
+ );
+ const telescopeMissions = telescopeDataSources.flatMap((category) =>
+ category.items.filter((item) => item.compatiblePlanetTypes.includes(planetType))
+ );
+ const roverMissions = roverDataSources.flatMap((category) =>
+ category.items.filter((item) => item.compatiblePlanetTypes.includes(planetType))
+ );
+ const lidarMissions = lidarDataSources.flatMap((category) =>
+ category.items.filter((item) => item.compatiblePlanetTypes.includes(planetType))
+ );
+ setCompatibleMissions([
+ // ...zoodexMissions,
+ ...physicsLabMissions,
+ ...telescopeMissions,
+ ...roverMissions,
+ ...lidarMissions,
+ ]);
+ }
+ };
+
+
+ filterCompatibleMissions();
+ };
+ }, [currentPlanet]);
+
+ const nextPlanet = () => {
+ setCurrentIndex((currentIndex + 1) % currentPlanets.length);
+ setShowMissions(false);
+ setSelectedMission(null);
+ };
+
+ const prevPlanet = () => {
+ setCurrentIndex((currentIndex - 1 + currentPlanets.length) % currentPlanets.length);
+ setShowMissions(false);
+ setSelectedMission(null);
+ };
+
+ const handleSelectMission = () => {
+ setShowMissions(true);
+ };
+
+ const handleMissionClick = (mission: any) => {
+ setSelectedMission(mission);
+ setMissionSelected(true);
+ };
+
+ const handleVisitPlanet = async () => {
+ if (currentPlanet.anomaly !== activePlanet?.id) {
+ const initialisationMission = currentPlanet.initialisationMissionId;
+ const initialisePlanetMissionData = {
+ user: session?.user.id,
+ time_of_completion: new Date().toISOString(),
+ mission: initialisationMission,
+ };
+
+ const { error: missionsError } = await supabase
+ .from("missions")
+ .insert([initialisePlanetMissionData]);
+
+ if (missionsError) {
+ console.error("Error inserting mission data:", missionsError);
+ return;
+ };
+
+ if (selectedMission?.activeStructure) {
+ const structureId = selectedMission.activeStructure;
+
+ const { data: existingInventory, error: fetchError } = await supabase
+ .from("inventory")
+ .select("*")
+ .eq("item", structureId)
+ .eq("owner", session?.user?.id)
+ .eq("anomaly", currentPlanet.anomaly);
+
+ if (fetchError) {
+ console.error("Error fetching inventory data:", fetchError);
+ return;
+ };
+
+ if (!existingInventory || existingInventory.length === 0) {
+ const newConfig = {
+ Uses: 1,
+ "missions unlocked": [selectedMission?.identifier],
+ };
+
+ const inventoryData = {
+ item: structureId,
+ owner: session?.user?.id,
+ quantity: 1,
+ anomaly: currentPlanet.anomaly,
+ configuration: newConfig,
+ };
+
+ const { error: inventoryError } = await supabase
+ .from("inventory")
+ .insert([inventoryData]);
+
+ if (inventoryError) {
+ console.error("Error inserting inventory data:", inventoryError);
+ return;
+ }
+ } else {
+ const currentConfig = existingInventory[0].configuration || {
+ Uses: 1,
+ "missions unlocked": [],
+ };
+
+ const newMission = selectedMission?.identifier;
+ if (!currentConfig["missions unlocked"]) {
+ currentConfig["missions unlocked"] = [];
+ }
+ if (!currentConfig["missions unlocked"].includes(newMission)) {
+ currentConfig["missions unlocked"].push(newMission);
+ }
+
+ const { error: updateError } = await supabase
+ .from("inventory")
+ .update({ configuration: currentConfig })
+ .eq("id", existingInventory[0].id);
+
+ if (updateError) {
+ console.error("Error updating inventory configuration:", updateError);
+ return;
+ };
+ };
+ };
+
+ const validAnomaly = currentPlanet?.anomaly ?? 0;
+
+ await moveItemsToNewPlanet(validAnomaly);
+ updatePlanetLocation(validAnomaly);
+ };
+ };
+
+ const [missionSelected, setMissionSelected] = useState(false);
+
+ const moveItemsToNewPlanet = async (newPlanetId: number) => {
+ if (session?.user?.id) {
+ const itemsToMove = [3108, 3107];
+
+ try {
+ await Promise.all(
+ itemsToMove.map(async (itemId) => {
+ const { data, error } = await supabase
+ .from("inventory")
+ .update({ anomaly: newPlanetId })
+ .eq("item", itemId)
+ .eq("owner", session.user.id);
+
+ if (error) throw error;
+ return data;
+ })
+ );
+ } catch (error: any) {
+ console.error("Error moving items:", error.message);
+ };
+ };
+ };
+
+ const isVisited = currentPlanet?.anomaly !== undefined && classificationsByPlanet[currentPlanet.anomaly]?.length > 0;
+ const handleTabSwitch = (tab: "solarSystem" | "exoplanets") => {
+ setActiveTab(tab);
+ setCurrentIndex(0);
+ };
+
+ return (
+
+
+
+ handleTabSwitch("solarSystem")}
+ >
+ Solar System
+
+ handleTabSwitch("exoplanets")}
+ >
+ Exoplanets
+
+
+
+
+ {currentPlanets.length > 0 && (
+
+
+
+
+
{currentPlanet.name}
+
{currentPlanet.description}
+ {/*
+ Select Mission
+ */}
+
+
+ Visit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showMissions && (
+
+
Compatible Missions
+ {compatibleMissions.length > 0 ? (
+
+ {compatibleMissions.map((mission) => (
+ handleMissionClick(mission)}
+ className={`cursor-pointer p-2 text-center rounded-lg transition-colors ${
+ selectedMission?.id === mission.id
+ ? "bg-[#4BB3A5]"
+ : "bg-gray-700 hover:bg-gray-600"
+ }`}
+ >
+ {mission.name}
+
+ ))}
+
+ ) : (
+
No compatible missions found for this planet.
+ )}
+
+ {selectedMission && (
+
+
Selected Mission:
+
{selectedMission.name}
+
+ )}
+
+ )}
+
+ {missionSelected && (
+
+
+ Visit
+
+
+ )}
+
+ {/* {!missionSelected && (
+
+
+
+ )} */}
+
+
+ )}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Account/Avatar.tsx b/components/Account/Avatar.tsx
new file mode 100644
index 000000000..fbcda3a14
--- /dev/null
+++ b/components/Account/Avatar.tsx
@@ -0,0 +1,120 @@
+import React, { useEffect, useState } from "react";
+import Image from "next/image";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+
+interface Props {
+ author: string;
+};
+
+export const AvatarGenerator: React.FC = ({ author }) => {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [avatarUrl, setAvatarUrl] = useState("");
+
+ useEffect(() => {
+ let ignore = false;
+
+ async function fetchAvatar () {
+ const { data, error } = await supabase
+ .from("profiles")
+ .select("avatar_url")
+ .eq("id", session?.user.id)
+ .single();
+
+ if (!data?.avatar_url) {
+ const generateAvatar = () => {
+ const apiUrl = `https://api.dicebear.com/6.x/bottts/svg?seed=${encodeURIComponent(author)}`;
+ setAvatarUrl(apiUrl);
+ };
+
+ generateAvatar();
+ } else {
+ setAvatarUrl(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/avatars/${data.avatar_url}`)
+ };
+ }
+
+ fetchAvatar();
+ }, [author]);
+
+ return (
+
+ {avatarUrl && (
+
+
+
+ )}
+
+ );
+};
+
+export function Avatar() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [avatarUrl, setAvatarUrl] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let ignore = false;
+
+ async function fetchAvatar() {
+ if (!session?.user?.id) return;
+
+ const { data, error } = await supabase
+ .from("profiles")
+ .select("avatar_url")
+ .eq("id", session.user.id)
+ .single();
+
+ if (!ignore) {
+ if (error) {
+ console.warn("Error fetching avatar: ", error.message);
+ } else if (data) {
+ setAvatarUrl(data.avatar_url || null);
+ }
+ setLoading(false);
+ }
+ }
+
+ fetchAvatar();
+
+ return () => {
+ ignore = true;
+ };
+ }, [session, supabase]);
+
+ if (loading) {
+ return
;
+ }
+
+ return (
+
+ {avatarUrl ? (
+
+ ) : (
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/components/Account/ProfileSetup.tsx b/components/Account/ProfileSetup.tsx
new file mode 100644
index 000000000..42e47c8b4
--- /dev/null
+++ b/components/Account/ProfileSetup.tsx
@@ -0,0 +1,242 @@
+import React, { useEffect, useState, type ChangeEvent } from "react";
+import Image from "next/image";
+import { useRouter } from "next/navigation";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import { useActivePlanet } from "@/context/ActivePlanet";
+
+interface ProfileSetupFormProps {
+ onProfileUpdate: () => void | null;
+}
+
+export default function ProfileSetupForm({ onProfileUpdate }: ProfileSetupFormProps) {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+ const { activePlanet } = useActivePlanet();
+ const router = useRouter();
+
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const [username, setUsername] = useState("");
+ const [firstName, setFirstName] = useState("");
+ const [avatar, setAvatar] = useState(null);
+ const [avatarPreview, setAvatarPreview] = useState(null);
+
+ // const [inventoryItems, setInventoryItems] = useState<{ item: number }[] | null>(null);
+
+ useEffect(() => {
+ let ignore = false;
+ async function getProfile() {
+ setLoading(true);
+ const { data, error } = await supabase
+ .from("profiles")
+ .select("username, full_name, avatar_url")
+ .eq("id", session?.user?.id)
+ .single();
+
+ if (!ignore && data) {
+ setUsername(data.username || "");
+ setFirstName(data.full_name || "");
+ setAvatarPreview(data.avatar_url || null);
+ } else if (error) {
+ console.warn(error.message);
+ }
+
+ setLoading(false);
+ }
+
+ if (session?.user?.id) {
+ getProfile();
+ }
+
+ return () => {
+ ignore = true;
+ };
+ }, [session, supabase]);
+
+ // useEffect(() => {
+ // async function fetchInventory() {
+ // if (!session?.user?.id) return;
+
+ // const { data, error } = await supabase
+ // .from("inventory")
+ // .select("item")
+ // .eq("owner", session.user.id)
+ // .in("item", [23, 24]);
+
+ // if (error) {
+ // console.warn(error);
+ // } else {
+ // setInventoryItems(data);
+ // }
+ // }
+
+ // fetchInventory();
+ // }, [session, supabase]);
+
+ const handleAvatarChange = (e: ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ setAvatar(file);
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setAvatarPreview(reader.result as string);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ async function updateProfile(event: React.FormEvent) {
+ event.preventDefault();
+ if (!username || !session?.user?.email) {
+ setError("Username and email are required.");
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ // Ensure inventory contains items 23 and 24
+ // if (inventoryItems) {
+ // const existingItems = inventoryItems.map((item) => item.item);
+ // const itemsToAdd = [23, 24].filter((item) => !existingItems.includes(item)).map((item) => ({
+ // item,
+ // owner: session.user.id,
+ // anomay: activePlanet?.id,
+ // quantity: 1,
+ // time_of_deploy: new Date(),
+ // }));
+
+ // if (itemsToAdd.length > 0) {
+ // const { error: insertError } = await supabase.from("inventory").insert(itemsToAdd);
+ // if (insertError) {
+ // setError(insertError.message);
+ // setLoading(false);
+ // return;
+ // }
+ // }
+ // }
+
+ let avatar_url = avatarPreview;
+
+ if (avatar) {
+ const fileName = `${Date.now()}-${session.user.id}-avatar.png`;
+ const { data, error } = await supabase.storage
+ .from("avatars")
+ .upload(fileName, avatar, {
+ contentType: avatar.type,
+ });
+
+ if (error) {
+ setError(error.message);
+ setLoading(false);
+ return;
+ }
+
+ avatar_url = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/avatars/${data?.path}`;
+ }
+
+ const updates = {
+ id: session.user.id,
+ username,
+ full_name: firstName,
+ avatar_url,
+ updated_at: new Date(),
+ };
+
+ const { error: updateError } = await supabase.from("profiles").upsert(updates);
+
+ if (updateError) {
+ setError(updateError.message);
+ } else {
+ router.push("/");
+ onProfileUpdate?.();
+ }
+
+ setLoading(false);
+ }
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/BiomassEarth.tsx b/components/Data/BiomassEarth.tsx
new file mode 100644
index 000000000..505579c4f
--- /dev/null
+++ b/components/Data/BiomassEarth.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import React, { useEffect, useState } from "react";
+import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
+
+export default function BiomassOnEarth() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const [zoodexCount, setZoodexCount] = useState(null);
+ const biomass = zoodexCount !== null ? 0.831 * 0.001 * zoodexCount : null;
+
+ useEffect(() => {
+ const fetchZoodexCount = async () => {
+ try {
+ const { count, error } = await supabase
+ .from("classifications")
+ .select("id", { count: "exact" })
+ .like("classificationtype", "%zoodex%");
+
+ if (error) throw error;
+ setZoodexCount(count);
+ } catch (err: any) {
+ console.error("Error fetching Zoodex classifications:", err.message);
+ setZoodexCount(0);
+ }
+ };
+
+ fetchZoodexCount();
+ }, [supabase]);
+
+ return (
+
+
Biomass:
+
{biomass !== null ? biomass.toFixed(6) : "Loading..."}
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/EarthStats.tsx b/components/Data/EarthStats.tsx
new file mode 100644
index 000000000..3f5b85bf3
--- /dev/null
+++ b/components/Data/EarthStats.tsx
@@ -0,0 +1,8 @@
+export default function EarthStats() {
+ return (
+
+
Temperature on Earth this week:
+
15º C
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/DailyMinorPlanet/Asteroid.tsx b/components/Data/Generator/Astronomers/DailyMinorPlanet/Asteroid.tsx
new file mode 100644
index 000000000..9c29e92ed
--- /dev/null
+++ b/components/Data/Generator/Astronomers/DailyMinorPlanet/Asteroid.tsx
@@ -0,0 +1,54 @@
+import { useRef, useMemo } from "react"
+import { useFrame } from "@react-three/fiber"
+import * as THREE from "three"
+import { createNoise3D } from "simplex-noise"
+
+export function Asteroid({ metallic }: { metallic: number }) {
+ const meshRef = useRef(null)
+ const noise3D = useMemo(() => createNoise3D(), [])
+
+ // Generate geometry with noise-based displacement
+ const geometry = useMemo(() => {
+ const geo = new THREE.IcosahedronGeometry(1, 4)
+ const positions = geo.attributes.position
+ const vector = new THREE.Vector3()
+
+ for (let i = 0; i < positions.count; i++) {
+ vector.fromBufferAttribute(positions, i)
+ const noise = noise3D(vector.x * 2, vector.y * 2, vector.z * 2)
+ vector.normalize().multiplyScalar(1 + 0.3 * noise)
+ positions.setXYZ(i, vector.x, vector.y, vector.z)
+ }
+
+ geo.computeVertexNormals()
+ return geo
+ }, [noise3D])
+
+ // Interpolate between rocky and metallic materials based on slider
+ const material = useMemo(() => {
+ return new THREE.MeshStandardMaterial({
+ color: new THREE.Color().lerpColors(
+ new THREE.Color("#8B7355"), // Rocky brown
+ new THREE.Color("#A8A8A8"), // Metallic gray
+ metallic
+ ),
+ metalness: THREE.MathUtils.lerp(0.2, 0.8, metallic),
+ roughness: THREE.MathUtils.lerp(0.8, 0.3, metallic),
+ })
+ }, [metallic])
+
+ // Slow rotation
+ useFrame((state, delta) => {
+ if (meshRef.current) {
+ meshRef.current.rotation.y += delta * 0.2
+ }
+ })
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/DailyMinorPlanet/asteroid-viewer.tsx b/components/Data/Generator/Astronomers/DailyMinorPlanet/asteroid-viewer.tsx
new file mode 100644
index 000000000..2ce865fd7
--- /dev/null
+++ b/components/Data/Generator/Astronomers/DailyMinorPlanet/asteroid-viewer.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { Canvas } from "@react-three/fiber";
+import { OrbitControls } from "@react-three/drei";
+import { Slider } from "@/components/ui/slider";
+import { useState } from "react";
+import { Asteroid } from "./Asteroid";
+import { Background } from "./background";
+
+interface AsteroidGeneratorProps {
+ classificationConfig?: any;
+ classificationId: string;
+};
+
+export default function AsteroidViewer({ classificationConfig, classificationId }: AsteroidGeneratorProps) {
+ const [metallic, setMetallic] = useState(0.5)
+
+ return (
+
+ {/*
} /> */}
+
+
+
+ {/* */}
+
+
Material Type
+
setMetallic(value)}
+ max={1}
+ step={0.01}
+ className="w-full"
+ />
+
+ Rocky
+ Metallic
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/DailyMinorPlanet/background.tsx b/components/Data/Generator/Astronomers/DailyMinorPlanet/background.tsx
new file mode 100644
index 000000000..1009a076a
--- /dev/null
+++ b/components/Data/Generator/Astronomers/DailyMinorPlanet/background.tsx
@@ -0,0 +1,63 @@
+import { useRef, useMemo } from "react"
+import * as THREE from "three"
+import { useFrame } from "@react-three/fiber"
+
+const vertexShader = `
+ varying vec2 vUv;
+ void main() {
+ vUv = uv;
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+ }
+`
+
+const fragmentShader = `
+ uniform float time;
+ varying vec2 vUv;
+
+ float random(vec2 st) {
+ return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
+ }
+
+ void main() {
+ vec2 uv = vUv;
+
+ // Create a noisy background
+ float noise = random(uv + time * 0.1);
+ vec3 color = vec3(noise * 0.15);
+
+ // Add stars
+ float star = step(0.998, random(uv));
+ color += star;
+
+ gl_FragColor = vec4(color, 1.0);
+ }
+`
+
+export function Background() {
+ const shaderRef = useRef(null)
+
+ const uniforms = useMemo(
+ () => ({
+ time: { value: 0 },
+ }),
+ []
+ )
+
+ useFrame((state) => {
+ if (shaderRef.current) {
+ shaderRef.current.uniforms.time.value = state.clock.elapsedTime
+ }
+ })
+
+ return (
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/DailyMinorPlanet/effects.tsx b/components/Data/Generator/Astronomers/DailyMinorPlanet/effects.tsx
new file mode 100644
index 000000000..98a431523
--- /dev/null
+++ b/components/Data/Generator/Astronomers/DailyMinorPlanet/effects.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { Effect } from "postprocessing";
+import { forwardRef } from "react";
+import { extend, useThree } from "@react-three/fiber";
+import { EffectComposer, Noise } from "@react-three/postprocessing";
+import { BlendFunction } from "postprocessing";
+import { Uniform, WebGLRenderer } from "three";
+
+// Custom grain effect to match telescope image
+class GrainEffect extends Effect {
+ constructor({ blendFunction = BlendFunction.MULTIPLY } = {}) {
+ super(
+ "GrainEffect",
+ /* glsl */ `
+ uniform float time;
+
+ float random(vec2 p) {
+ return fract(sin(dot(p.xy, vec2(12.9898,78.233))) * 43758.5453123);
+ }
+
+ void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
+ vec2 grain = vec2(random(uv + time * 0.1));
+ outputColor = vec4(inputColor.rgb * (0.85 + 0.15 * grain.x), inputColor.a);
+ }
+ `,
+ {
+ blendFunction,
+ uniforms: new Map([["time", new Uniform(0)]]),
+ }
+ );
+ }
+
+ update(renderer: WebGLRenderer, inputBuffer: any, deltaTime: number) {
+ this.uniforms.get("time")!.value += deltaTime;
+ }
+}
+
+extend({ GrainEffect });
+
+export const Effects = forwardRef(() => {
+ return (
+
+
+ {/* */}
+
+ );
+});
+
+Effects.displayName = "Effects";
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/PlanetFour/map.tsx b/components/Data/Generator/Astronomers/PlanetFour/map.tsx
new file mode 100644
index 000000000..ebded49c0
--- /dev/null
+++ b/components/Data/Generator/Astronomers/PlanetFour/map.tsx
@@ -0,0 +1,384 @@
+import { useEffect, useRef, useState } from 'react';
+import { Card } from '@/components/ui/card';
+import { Slider } from '@/components/ui/slider';
+import { Button } from '@/components/ui/button';
+import { MapData, LayerState, Season, Feature, SpiderFeature } from '../../../../../types/Generators/planet-4';
+
+const SEASONS: Season[] = ['spring', 'summer']
+
+// Colors based on HiRISE imagery
+const COLORS = {
+ fan: {
+ spring: 'rgba(64, 64, 64, 0.8)',
+ summer: 'rgba(139, 69, 19, 0.7)'
+ },
+ blotch: {
+ spring: 'rgba(32, 32, 32, 0.9)',
+ summer: 'rgba(101, 67, 33, 0.8)'
+ },
+ windVector: 'rgba(255, 255, 255, 0.8)',
+ ice: {
+ spring: 'rgba(200, 200, 255, 0.4)',
+ summer: 'rgba(255, 200, 150, 0.2)'
+ },
+ spiders: 'rgba(30, 30, 30, 0.8)',
+ terrain: {
+ spring: '#1a1a2e',
+ summer: '#2d1810'
+ }
+}
+
+// Mock data with realistic features
+const MOCK_DATA: MapData = {
+ features: [
+ { id: '1', type: 'fan', position: { lat: 0.3, lng: 0.2 }, orientation: 45, size: 0.8, season: 'spring', intensity: 0.9 },
+ { id: '2', type: 'fan', position: { lat: 0.4, lng: 0.6 }, orientation: 120, size: 0.6, season: 'spring', intensity: 0.7 },
+ { id: '3', type: 'blotch', position: { lat: 0.6, lng: 0.4 }, size: 0.5, season: 'spring', intensity: 0.8 },
+ { id: '4', type: 'blotch', position: { lat: 0.7, lng: 0.7 }, size: 0.7, season: 'spring', intensity: 0.9 },
+ ],
+ spiders: [
+ { position: { lat: 0.5, lng: 0.5 }, size: 1, branches: 8 },
+ { position: { lat: 0.3, lng: 0.7 }, size: 0.8, branches: 6 },
+ ],
+ season: 'spring',
+ solarLongitude: 180, // Start of spring
+ region: 'South Pole'
+}
+
+export default function Planet4Map() {
+ const canvasRef = useRef(null)
+ const [currentSeason, setCurrentSeason] = useState('spring')
+ const [timelinePosition, setTimelinePosition] = useState(0)
+ const [layers, setLayers] = useState({
+ fans: true,
+ blotches: true,
+ windVectors: true,
+ seasonalOverlay: true,
+ })
+
+ useEffect(() => {
+ const canvas = canvasRef.current
+ if (!canvas) return
+
+ const ctx = canvas.getContext('2d')
+ if (!ctx) return
+
+ canvas.width = canvas.offsetWidth * window.devicePixelRatio
+ canvas.height = canvas.offsetHeight * window.devicePixelRatio
+ ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
+
+ // Draw base terrain
+ drawBaseMap(ctx, currentSeason)
+
+ // Draw spider features (always visible but more prominent in summer)
+ drawSpiderFeatures(ctx, MOCK_DATA.spiders, currentSeason)
+
+ // Draw seasonal features based on layers
+ if (layers.seasonalOverlay) {
+ drawSeasonalOverlay(ctx, currentSeason, timelinePosition)
+ }
+
+ if (layers.blotches) {
+ drawFeatures(ctx, MOCK_DATA.features.filter(f => f.type === 'blotch'), currentSeason, timelinePosition)
+ }
+
+ if (layers.fans) {
+ drawFeatures(ctx, MOCK_DATA.features.filter(f => f.type === 'fan'), currentSeason, timelinePosition)
+ }
+
+ if (layers.windVectors) {
+ drawWindVectors(ctx, MOCK_DATA.features, timelinePosition)
+ }
+
+ }, [layers, currentSeason, timelinePosition])
+
+ return (
+
+
+
+
+
+
+
+
+ setLayers(prev => ({ ...prev, fans: !prev.fans }))}
+ className={`
+ ${layers.fans ? 'bg-zinc-700 hover:bg-zinc-600' : 'hover:bg-zinc-800'}
+ transition-colors
+ `}
+ >
+ Fans
+
+ setLayers(prev => ({ ...prev, blotches: !prev.blotches }))}
+ className={`
+ ${layers.blotches ? 'bg-stone-700 hover:bg-stone-600' : 'hover:bg-stone-800'}
+ transition-colors
+ `}
+ >
+ Blotches
+
+ setLayers(prev => ({ ...prev, windVectors: !prev.windVectors }))}
+ className={`
+ ${layers.windVectors ? 'bg-slate-600 hover:bg-slate-500' : 'hover:bg-slate-700'}
+ transition-colors
+ `}
+ >
+ Wind Vectors
+
+ setLayers(prev => ({ ...prev, seasonalOverlay: !prev.seasonalOverlay }))}
+ className={`
+ ${layers.seasonalOverlay ? 'bg-blue-900 hover:bg-blue-800' : 'hover:bg-blue-950'}
+ transition-colors
+ `}
+ >
+ CO₂ Ice Layer
+
+
+
+
+
+ Ls = {180 + Math.round(timelinePosition * 0.9)}°
+ {currentSeason === 'spring' ? 'Spring → Summer' : 'Summer → Autumn'}
+
+
setTimelinePosition(value)}
+ max={100}
+ step={1}
+ className="w-full"
+ />
+
+
+
+ {SEASONS.map((season) => (
+ setCurrentSeason(season)}
+ className={`
+ ${currentSeason === season ? 'bg-indigo-900 hover:bg-indigo-800' : 'hover:bg-indigo-950'}
+ transition-colors
+ `}
+ >
+ {season.charAt(0).toUpperCase() + season.slice(1)}
+
+ ))}
+
+
+
+
+ )
+}
+
+function drawBaseMap(ctx: CanvasRenderingContext2D, season: Season) {
+ // Create textured background
+ const gradient = ctx.createLinearGradient(0, 0, ctx.canvas.width, ctx.canvas.height)
+ gradient.addColorStop(0, COLORS.terrain[season])
+ gradient.addColorStop(1, COLORS.terrain[season])
+
+ ctx.fillStyle = gradient
+ ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height)
+
+ // Add terrain texture
+ for (let i = 0; i < ctx.canvas.width; i += 2) {
+ for (let j = 0; j < ctx.canvas.height; j += 2) {
+ if (Math.random() > 0.5) {
+ ctx.fillStyle = `rgba(255, 255, 255, ${Math.random() * 0.03})`
+ ctx.fillRect(i, j, 2, 2)
+ }
+ }
+ }
+}
+
+function drawSpiderFeatures(ctx: CanvasRenderingContext2D, spiders: SpiderFeature[], season: Season) {
+ spiders.forEach(spider => {
+ const centerX = spider.position.lng * ctx.canvas.width
+ const centerY = spider.position.lat * ctx.canvas.height
+ const radius = spider.size * 50
+
+ // Draw spider channels
+ for (let i = 0; i < spider.branches; i++) {
+ const angle = (i / spider.branches) * Math.PI * 2
+ ctx.beginPath()
+ ctx.moveTo(centerX, centerY)
+
+ // Create curved branch
+ const cp1x = centerX + Math.cos(angle) * radius * 0.5
+ const cp1y = centerY + Math.sin(angle) * radius * 0.5
+ const cp2x = centerX + Math.cos(angle) * radius * 0.75
+ const cp2y = centerY + Math.sin(angle) * radius * 0.75
+ const endX = centerX + Math.cos(angle) * radius
+ const endY = centerY + Math.sin(angle) * radius
+
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY)
+
+ ctx.strokeStyle = COLORS.spiders
+ ctx.lineWidth = season === 'summer' ? 3 : 1
+ ctx.stroke()
+ }
+ })
+}
+
+function drawFeatures(ctx: CanvasRenderingContext2D, features: Feature[], season: Season, timelinePosition: number) {
+ features.forEach(feature => {
+ const color = feature.type === 'fan' ? COLORS.fan[season] : COLORS.blotch[season]
+ const visibility = Math.max(0, Math.min(1,
+ season === 'spring' ?
+ timelinePosition / 50 : // Appear in spring
+ 1 - (timelinePosition - 50) / 50 // Disappear in summer
+ ))
+
+ if (visibility === 0) return
+
+ if (feature.type === 'fan') {
+ // Draw fan shape
+ ctx.beginPath()
+ ctx.fillStyle = color
+
+ const centerX = feature.position.lng * ctx.canvas.width
+ const centerY = feature.position.lat * ctx.canvas.height
+ const radius = feature.size * 30 * visibility
+
+ if (feature.orientation !== undefined) {
+ const startAngle = (feature.orientation - 45) * Math.PI / 180
+ const endAngle = (feature.orientation + 45) * Math.PI / 180
+
+ ctx.moveTo(centerX, centerY)
+ ctx.arc(centerX, centerY, radius, startAngle, endAngle)
+ ctx.closePath()
+ }
+
+ ctx.fill()
+
+ // Add texture to fan
+ ctx.save()
+ ctx.clip()
+ for (let i = 0; i < radius * 2; i += 2) {
+ for (let j = 0; j < radius * 2; j += 2) {
+ if (Math.random() > 0.7) {
+ ctx.fillStyle = `rgba(0, 0, 0, ${Math.random() * 0.2})`
+ ctx.fillRect(centerX - radius + i, centerY - radius + j, 2, 2)
+ }
+ }
+ }
+ ctx.restore()
+ } else {
+ // Draw blotch with radial pattern
+ const centerX = feature.position.lng * ctx.canvas.width
+ const centerY = feature.position.lat * ctx.canvas.height
+ const radius = feature.size * 25 * visibility
+
+ // Create radial gradient for blotch
+ const gradient = ctx.createRadialGradient(
+ centerX, centerY, 0,
+ centerX, centerY, radius
+ )
+ gradient.addColorStop(0, color)
+ gradient.addColorStop(1, 'transparent')
+
+ ctx.beginPath()
+ ctx.fillStyle = gradient
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2)
+ ctx.fill()
+ }
+ })
+}
+
+function drawWindVectors(ctx: CanvasRenderingContext2D, features: Feature[], timelinePosition: number) {
+ const fans = features.filter(f => f.type === 'fan' && f.orientation !== undefined)
+
+ // Only show wind vectors in spring when fans are forming
+ const visibility = Math.max(0, Math.min(1, 1 - timelinePosition / 100))
+
+ if (visibility === 0) return
+
+ fans.forEach(fan => {
+ ctx.beginPath()
+ ctx.strokeStyle = COLORS.windVector
+ ctx.lineWidth = 2 * visibility
+
+ const startX = fan.position.lng * ctx.canvas.width
+ const startY = fan.position.lat * ctx.canvas.height
+ const length = 30 * visibility
+ const angle = (fan.orientation || 0) * Math.PI / 180
+
+ // Draw arrow shaft with slight wave
+ ctx.beginPath()
+ ctx.moveTo(startX, startY)
+
+ const controlPoint1X = startX + Math.cos(angle) * length * 0.5
+ const controlPoint1Y = startY + Math.sin(angle) * length * 0.5 + Math.sin(timelinePosition / 10) * 5
+
+ const endX = startX + Math.cos(angle) * length
+ const endY = startY + Math.sin(angle) * length
+
+ ctx.quadraticCurveTo(controlPoint1X, controlPoint1Y, endX, endY)
+
+ // Draw arrow head
+ const headLength = 8 * visibility
+ const headAngle = Math.PI / 6
+
+ ctx.lineTo(
+ endX - headLength * Math.cos(angle - headAngle),
+ endY - headLength * Math.sin(angle - headAngle)
+ )
+ ctx.moveTo(endX, endY)
+ ctx.lineTo(
+ endX - headLength * Math.cos(angle + headAngle),
+ endY - headLength * Math.sin(angle + headAngle)
+ )
+
+ ctx.stroke()
+ })
+}
+
+function drawSeasonalOverlay(ctx: CanvasRenderingContext2D, season: Season, timelinePosition: number) {
+ ctx.save()
+
+ // Calculate ice coverage based on season and timeline
+ const coverage = Math.max(0, Math.min(1,
+ season === 'spring' ?
+ 1 - timelinePosition / 100 : // Ice melts in spring
+ timelinePosition / 100 // Ice forms in summer
+ ))
+
+ if (coverage === 0) return
+
+ // Create ice texture
+ const icePattern = ctx.createLinearGradient(0, 0, ctx.canvas.width, ctx.canvas.height)
+ icePattern.addColorStop(0, COLORS.ice[season])
+ icePattern.addColorStop(1, `rgba(255, 255, 255, ${0.1 * coverage})`)
+
+ ctx.fillStyle = icePattern
+ ctx.globalAlpha = coverage
+
+ // Draw ice layer with crystalline pattern
+ for (let i = 0; i < ctx.canvas.width; i += 20) {
+ for (let j = 0; j < ctx.canvas.height; j += 20) {
+ if (Math.random() > 0.3) {
+ ctx.beginPath()
+ ctx.moveTo(i, j)
+ ctx.lineTo(i + 10 + Math.random() * 10, j + Math.random() * 10)
+ ctx.lineTo(i + Math.random() * 20, j + 10 + Math.random() * 10)
+ ctx.closePath()
+ ctx.fill()
+ }
+ }
+ }
+
+ ctx.restore()
+}
+
diff --git a/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator.tsx b/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator.tsx
new file mode 100644
index 000000000..ff2416b5a
--- /dev/null
+++ b/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator.tsx
@@ -0,0 +1,448 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import PlanetViewer from "./planetViewer";
+import { type PlanetConfig, defaultPlanetConfig } from "@/utils/planet-physics";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { useSupabaseClient } from "@supabase/auth-helpers-react";
+
+interface PlanetGeneratorProps {
+ classificationId: string;
+};
+
+interface PlanetStats {
+ mass?: number;
+ radius?: number;
+ [key: string]: any;
+};
+
+export default function PlanetGenerator({
+ classificationId,
+}: PlanetGeneratorProps) {
+ const supabase = useSupabaseClient();
+
+ const [planetConfig, setPlanetConfig] = useState(defaultPlanetConfig);
+ const [importDialogOpen, setImportDialogOpen] = useState(false);
+ const [exportDialogOpen, setExportDialogOpen] = useState(false);
+ const [importText, setImportText] = useState("");
+ const [importError, setImportError] = useState("");
+
+ const [planetStats, setPlanetStats] = useState({});
+
+ const handleConfigChange = ( newConfig: Partial ) => {
+ setPlanetConfig(( prev ) => ({
+ ...prev,
+ ...newConfig
+ }))
+ };
+
+ const handleImport = () => {
+ try {
+ const config = JSON.parse(importText);
+ setPlanetConfig(config);
+ setImportDialogOpen(false);
+ setImportError("");
+ } catch (error: any) {
+ setImportError("Invalid JSON configuration");
+ };
+ };
+
+ const handleExport = async () => {
+ const idAsNumber = Number.parseInt(classificationId);
+ if (isNaN(idAsNumber)) return;
+
+ const { data, error: fetchError } = await supabase
+ .from("classifications")
+ .select("classificationConfiguration")
+ .eq("id", idAsNumber)
+ .single();
+
+ if (fetchError || !data) {
+ console.error("Failed to fetch planet configuration: ", fetchError);
+ return;
+ };
+
+ const currentConfig = data.classificationConfiguration || {};
+ const updatedConfig = {
+ ...currentConfig,
+ planetConfiguration: planetConfig,
+ };
+
+ const { error: updateError } = await supabase
+ .from("classifications")
+ .update({ classificationConfiguration: updatedConfig })
+ .eq("id", idAsNumber);
+
+ if (updateError) {
+ console.error("Failed to update planet configuration, ", updateError);
+ };
+ };
+
+ // Fetch planet data
+ useEffect(() => {
+ const fetchExportedValues = async () => {
+ const idAsNumber = Number.parseInt(classificationId)
+ if (isNaN(idAsNumber)) return
+
+ const { data, error } = await supabase
+ .from("classifications")
+ .select("classificationConfiguration")
+ .eq("id", idAsNumber)
+ .single()
+
+ if (error || !data?.classificationConfiguration?.exportedValue) return
+
+ const { exportedValue } = data.classificationConfiguration
+
+ setPlanetStats((prev) => ({
+ ...prev,
+ ...(exportedValue.mass && { mass: exportedValue.mass }),
+ ...(exportedValue.radius && { radius: exportedValue.radius }),
+ }))
+ }
+
+ fetchExportedValues()
+ }, [classificationId, supabase])
+
+ // Listen for custom events from the settings panel
+ useEffect(() => {
+ const handleOpenImport = () => setImportDialogOpen(true);
+ const handleOpenExport = () => setExportDialogOpen(true);
+
+ window.addEventListener("open-import-dialog", handleOpenImport);
+ window.addEventListener("open-export-dialog", handleOpenExport);
+
+ return () => {
+ window.removeEventListener("open-import-dialog", handleOpenImport);
+ window.removeEventListener("open-export-dialog", handleOpenExport);
+ };
+ }, []);
+
+ const exportConfig = JSON.stringify(planetConfig, null, 2);
+
+ return (
+
+
+
+ {JSON.stringify(planetStats, null, 2)}
+
+
+
+
+ Import Planet Configuration
+ Paste a previously exported planet configuration JSON below.
+
+
+
+
+
+
+
+
+ Export Planet Configuration
+ Copy this JSON to save your planet configuration.
+
+
+
+
+
+ );
+};
+
+
+// "use client"
+
+// import { useState, useEffect } from "react"
+// import { Canvas } from "@react-three/fiber"
+// import { OrbitControls, Stars, Environment } from "@react-three/drei"
+// import { PlanetShader } from "./PlanetShader"
+// import { SimplePlanet } from "./Simple/simple"
+// import { StylizedPlanetShader } from "./Shader/Styliized/stylized-planet-shader"
+// import { StylizedClouds } from "./Shader/Styliized/stylized-cloud-shader"
+// import { Planet } from "./Planet/planet-main"
+// import { SettingsPanel } from "./SettingsPanel"
+// import { Cog, ChevronDown } from "lucide-react";
+// import { Button } from "@/components/ui/button"
+// import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
+// import {
+// type PlanetStats,
+// calculateDensity,
+// defaultPlanetStats,
+// mergeWithDefaults,
+// generateDefaultLandmarks,
+// } from "@/lib/planet-physics"
+// import { useSupabaseClient } from "@supabase/auth-helpers-react"
+
+// export interface PlanetGeneratorProps {
+// classificationConfig?: any
+// content?: string
+// classificationId: string
+// author: string
+// type?: string
+// biome?: string
+// planetConfiguration?: any
+// };
+
+// export function PlanetGenerator({
+// classificationId = "UNCLASSIFIED",
+// author = "UNKNOWN",
+// type,
+// biome,
+// planetConfiguration,
+// classificationConfig,
+// content,
+// }: PlanetGeneratorProps) {
+// const supabase = useSupabaseClient()
+
+// const [showSettings, setShowSettings] = useState(false)
+// const [renderMode, setRenderMode] = useState<"detailed" | "simple" | "stylized" | "standard">("detailed")
+// const [planetStats, setPlanetStats] = useState(() => {
+// const initialStats = planetConfiguration
+// ? mergeWithDefaults(planetConfiguration)
+// : {
+// ...defaultPlanetStats,
+// ...(biome ? { biome } : {}),
+// ...(type ? { type } : {}),
+// }
+
+// if (!initialStats.landmarks || initialStats.landmarks.length === 0) {
+// initialStats.landmarks = generateDefaultLandmarks(initialStats)
+// }
+
+// return initialStats
+// })
+
+// // Update planet stats when configuration changes
+// useEffect(() => {
+// const updatedStats = planetConfiguration
+// ? mergeWithDefaults(planetConfiguration)
+// : {
+// ...defaultPlanetStats,
+// ...(biome ? { biome } : {}),
+// ...(type ? { type } : {}),
+// }
+
+// if (!updatedStats.landmarks || updatedStats.landmarks.length === 0) {
+// updatedStats.landmarks = generateDefaultLandmarks(updatedStats)
+// } else {
+// const hasTerrestrial = updatedStats.landmarks.some((l) => l.category === "terrestrial")
+// const hasGaseous = updatedStats.landmarks.some((l) => l.category === "gaseous")
+
+// if (!hasTerrestrial || !hasGaseous) {
+// const defaultLandmarks = generateDefaultLandmarks(updatedStats)
+// if (!hasTerrestrial) {
+// const terrestrial = defaultLandmarks.find((l) => l.category === "terrestrial")
+// if (terrestrial) updatedStats.landmarks.push(terrestrial)
+// }
+// if (!hasGaseous) {
+// const gaseous = defaultLandmarks.find((l) => l.category === "gaseous")
+// if (gaseous) updatedStats.landmarks.push(gaseous)
+// }
+// }
+// }
+
+// setPlanetStats(updatedStats)
+// }, [planetConfiguration, biome, type])
+
+// // Fetch exported values from Supabase
+// useEffect(() => {
+// const fetchExportedValues = async () => {
+// const idAsNumber = Number.parseInt(classificationId)
+// if (isNaN(idAsNumber)) return
+
+// const { data, error } = await supabase
+// .from("classifications")
+// .select("classificationConfiguration")
+// .eq("id", idAsNumber)
+// .single()
+
+// if (error || !data?.classificationConfiguration?.exportedValue) return
+
+// const { exportedValue } = data.classificationConfiguration
+
+// setPlanetStats((prev) => ({
+// ...prev,
+// ...(exportedValue.mass && { mass: exportedValue.mass }),
+// ...(exportedValue.radius && { radius: exportedValue.radius }),
+// }))
+// }
+
+// fetchExportedValues()
+// }, [classificationId, supabase])
+
+// // Update density when mass or radius changes
+// useEffect(() => {
+// setPlanetStats((prev) => ({
+// ...prev,
+// density: calculateDensity(prev.mass, prev.radius),
+// }))
+// }, [planetStats.mass, planetStats.radius])
+
+// // Get render mode label
+// const getRenderModeLabel = () => {
+// switch (renderMode) {
+// case "detailed":
+// return "Detailed View"
+// case "stylized":
+// return "Stylized View"
+// case "simple":
+// return "Simple View"
+// case "standard":
+// return "Standard Render"
+// default:
+// return "Detailed View"
+// }
+// }
+
+// return (
+//
+// {/* Classification info - kept but commented out as in original */}
+// {/*
+//
+// ID: {classificationId}
+//
+//
+// AUTHOR: {author}
+//
+// {type && (
+//
+// TYPE: {type}
+//
+// )}
+// {biome && (
+//
+// BIOME: {biome}
+//
+// )}
+// {planetStats.landmarks && planetStats.landmarks.length > 0 && (
+//
+// LANDMARKS: {planetStats.landmarks.length}
+//
+// )}
+//
*/}
+
+// {/* Classification config - kept but commented out as in original */}
+// {/* {classificationConfig && (
+//
+// {JSON.stringify(classificationConfig, null, 2)}
+//
+// )} */}
+
+// {/* Use a key on the Canvas to force a complete remount when switching modes */}
+//
+//
+//
+//
+//
+//
+
+// {renderMode === "detailed" && }
+// {renderMode === "stylized" && (
+// <>
+//
+//
+// >
+// )}
+// {renderMode === "simple" && }
+// {renderMode === "standard" && }
+
+//
+//
+//
+//
+//
+//
+//
+
+//
+//
+//
+//
+// {getRenderModeLabel()}
+//
+//
+//
+//
+// setRenderMode("detailed")}
+// >
+// Detailed View
+//
+// setRenderMode("stylized")}
+// >
+// Stylized View
+//
+// setRenderMode("simple")}
+// >
+// Simple View
+//
+// setRenderMode("standard")}
+// >
+// Standard Render
+//
+//
+//
+// setShowSettings(!showSettings)}
+// >
+//
+//
+//
+
+// {showSettings && (
+//
+// )}
+//
+// )
+// };
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/PlanetHunters/SettingsPanel.tsx b/components/Data/Generator/Astronomers/PlanetHunters/SettingsPanel.tsx
new file mode 100644
index 000000000..70a28f8a9
--- /dev/null
+++ b/components/Data/Generator/Astronomers/PlanetHunters/SettingsPanel.tsx
@@ -0,0 +1,635 @@
+"use client"
+import { useState, useEffect } from "react"
+import type { PlanetConfig } from "@/utils/planet-physics"
+import { getLiquidType, getTemperatureAdjustedColors } from "@/utils/planet-physics";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { Slider } from "@/components/ui/slider"
+import { Label } from "@/components/ui/label"
+import { Switch } from "@/components/ui/switch"
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import { Download, Upload, RefreshCw } from "lucide-react"
+import ColorPicker from "@/utils/Generators/PH/color-picker"
+
+interface SettingsPanelProps {
+ planetConfig: PlanetConfig
+ onChange: (config: Partial) => void
+}
+
+export default function SettingsPanel({ planetConfig, onChange }: SettingsPanelProps) {
+ const [showImportExport, setShowImportExport] = useState(false)
+
+ // Update liquid type based on temperature
+ useEffect(() => {
+ const liquidType = getLiquidType(planetConfig.temperature)
+
+ // Only update if the colors don't match the expected liquid type
+ if (
+ planetConfig.colors.ocean !== liquidType.color ||
+ planetConfig.colors.oceanPattern !== liquidType.patternColor
+ ) {
+ onChange({
+ colors: {
+ ...planetConfig.colors,
+ ocean: liquidType.color,
+ oceanPattern: liquidType.patternColor,
+ },
+ })
+ }
+ }, [planetConfig.temperature])
+
+ // Update terrain colors based on temperature
+ useEffect(() => {
+ const terrainColors = getTemperatureAdjustedColors(planetConfig.temperature, planetConfig.biomass)
+
+ // Only update if colors have changed
+ if (
+ planetConfig.colors.beach !== terrainColors.beach ||
+ planetConfig.colors.lowland !== terrainColors.lowland ||
+ planetConfig.colors.midland !== terrainColors.midland ||
+ planetConfig.colors.highland !== terrainColors.highland ||
+ planetConfig.colors.mountain !== terrainColors.mountain ||
+ planetConfig.colors.snow !== terrainColors.snow
+ ) {
+ onChange({
+ colors: {
+ ...planetConfig.colors,
+ ...terrainColors,
+ },
+ })
+ }
+ }, [planetConfig.temperature, planetConfig.biomass])
+
+ // Check if planet should be gaseous or terrestrial based on mass and radius
+ useEffect(() => {
+ if ((planetConfig.mass > 7.5 || planetConfig.radius > 2.5) && planetConfig.type !== "gaseous") {
+ onChange({ type: "gaseous" })
+ } else if (planetConfig.mass <= 7.5 && planetConfig.radius <= 2.5 && planetConfig.type === "gaseous") {
+ onChange({ type: "terrestrial" })
+ }
+ }, [planetConfig.mass, planetConfig.radius, planetConfig.type])
+
+ const regenerateSeed = () => {
+ onChange({ seed: Math.floor(Math.random() * 10000) })
+ }
+
+ // Get current liquid type name
+ const liquidType = getLiquidType(planetConfig.temperature)
+
+ return (
+
+
+ setShowImportExport(!showImportExport)}
+ className="flex items-center gap-2 bg-slate-800 text-slate-200 border-slate-700 hover:bg-slate-700"
+ >
+ {showImportExport ? "Hide" : "Show"} Import/Export
+ {showImportExport ? : }
+
+
+
+ {showImportExport && (
+
+
Import/Export Configuration
+
+ {
+ const event = new CustomEvent("open-import-dialog")
+ window.dispatchEvent(event)
+ }}
+ className="flex-1 bg-slate-700 text-slate-200 hover:bg-slate-600"
+ >
+ Import
+
+ {
+ const event = new CustomEvent("open-export-dialog")
+ window.dispatchEvent(event)
+ }}
+ className="flex-1 bg-slate-700 text-slate-200 hover:bg-slate-600"
+ >
+ Export
+
+
+
+ )}
+
+
+
+
+ Basic
+
+
+ Terrain
+
+
+ Colors
+
+
+
+
+
+
+
Planet Type
+
+ onChange({ type: "terrestrial" })}
+ className="flex-1"
+ >
+ Terrestrial
+
+ onChange({ type: "gaseous" })}
+ className="flex-1"
+ >
+ Gaseous
+
+
+ {(planetConfig.mass > 7.5 || planetConfig.radius > 2.5) && planetConfig.type === "gaseous" && (
+
+ Note: High mass or radius has automatically set this to a gas giant
+
+ )}
+ {planetConfig.mass <= 7.5 && planetConfig.radius <= 2.5 && planetConfig.type === "terrestrial" && (
+
Note: Values are in Earth radii/mass units
+ )}
+
+
+
+
+
Seed: {planetConfig.seed}
+
+
+
+
+
+
+
+
+ Radius (Earth radii)
+
+ {planetConfig.radius.toFixed(1)}
+
+
onChange({ radius: value[0] })}
+ className="[&_[role=slider]]:bg-slate-200"
+ />
+
+
+
+
+
+ Temperature
+
+
+ {planetConfig.temperature.toFixed(0)}K ({(planetConfig.temperature - 273.15).toFixed(0)}°C)
+
+
+
onChange({ temperature: value[0] })}
+ className="[&_[role=slider]]:bg-slate-200"
+ />
+ Liquid Solvent: {liquidType.name}
+
+
+
+
+
+ Mass (Earth masses)
+
+ {planetConfig.mass.toFixed(1)} M⊕
+
+
onChange({ mass: value[0] })}
+ className="[&_[role=slider]]:bg-slate-200"
+ />
+
+
+
+
+ Density
+
+ {(planetConfig.mass / ((planetConfig.radius ** 3 * Math.PI * 4) / 3)).toFixed(2)} g/cm³
+
+
+
+
+
+
+
+
+ {planetConfig.type === "terrestrial" && (
+
+
Terrain Properties
+
+
+
+
+ Terrain Roughness
+
+ {planetConfig.terrainRoughness.toFixed(2)}
+
+
onChange({ terrainRoughness: value[0] })}
+ className="[&_[role=slider]]:bg-slate-200"
+ />
+
+
+
+
+
+ Ocean Level
+
+ {planetConfig.liquidHeight.toFixed(2)}
+
+
onChange({ liquidHeight: value[0] })}
+ className="[&_[role=slider]]:bg-slate-200"
+ />
+
+
+
+
+
+ Continent Size
+
+ {planetConfig.continentSize.toFixed(2)}
+
+
onChange({ continentSize: value[0] })}
+ className="[&_[role=slider]]:bg-slate-200"
+ />
+
+
+
+
+
+ Continent Count
+
+ {planetConfig.continentCount.toFixed(0)}
+
+
onChange({ continentCount: value[0] })}
+ className="[&_[role=slider]]:bg-slate-200"
+ />
+
+
+
+
+
+ Detail Level
+
+ {planetConfig.noiseScale.toFixed(2)}
+
+
onChange({ noiseScale: value[0] })}
+ className="[&_[role=slider]]:bg-slate-200"
+ />
+
+
+
+
+
+ Volcanic Activity
+
+ {planetConfig.volcanicActivity.toFixed(2)}
+
+
onChange({ volcanicActivity: value[0] })}
+ className="[&_[role=slider]]:bg-slate-200"
+ />
+ Note: Volcanic activity is currently a static value
+
+
+
+
+
+ Biomass
+
+ {planetConfig.biomass.toFixed(2)}
+
+
onChange({ biomass: value[0] })}
+ className="[&_[role=slider]]:bg-slate-200"
+ />
+ Note: Biomass is currently a static value
+
+
+ )}
+
+
+
+
Debug Mode
+ onChange({ debugMode: checked })}
+ />
+
+
+ {planetConfig.debugMode && (
+
+
Show/hide specific terrain types:
+
+
+
+ onChange({
+ visibleTerrains: {
+ ...planetConfig.visibleTerrains,
+ ocean: !!checked,
+ },
+ })
+ }
+ />
+
+
+
+
+
+
+
+ onChange({
+ visibleTerrains: {
+ ...planetConfig.visibleTerrains,
+ beach: !!checked,
+ },
+ })
+ }
+ />
+
+
+
+
+
+
+
+ onChange({
+ visibleTerrains: {
+ ...planetConfig.visibleTerrains,
+ lowland: !!checked,
+ },
+ })
+ }
+ />
+
+
+
+
+
+
+
+ onChange({
+ visibleTerrains: {
+ ...planetConfig.visibleTerrains,
+ midland: !!checked,
+ },
+ })
+ }
+ />
+
+
+
+
+
+
+
+ onChange({
+ visibleTerrains: {
+ ...planetConfig.visibleTerrains,
+ highland: !!checked,
+ },
+ })
+ }
+ />
+
+
+
+
+
+
+
+ onChange({
+ visibleTerrains: {
+ ...planetConfig.visibleTerrains,
+ mountain: !!checked,
+ },
+ })
+ }
+ />
+
+
+
+
+
+
+
+ onChange({
+ visibleTerrains: {
+ ...planetConfig.visibleTerrains,
+ snow: !!checked,
+ },
+ })
+ }
+ />
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
Planet Colors
+
+
+ onChange({ colors: { ...planetConfig.colors, atmosphere: color } })}
+ />
+
+ onChange({ colors: { ...planetConfig.colors, ocean: color } })}
+ />
+
+ onChange({ colors: { ...planetConfig.colors, oceanPattern: color } })}
+ />
+
+ onChange({ colors: { ...planetConfig.colors, beach: color } })}
+ />
+
+ onChange({ colors: { ...planetConfig.colors, lowland: color } })}
+ />
+
+ onChange({ colors: { ...planetConfig.colors, midland: color } })}
+ />
+
+ onChange({ colors: { ...planetConfig.colors, highland: color } })}
+ />
+
+ onChange({ colors: { ...planetConfig.colors, mountain: color } })}
+ />
+
+ onChange({ colors: { ...planetConfig.colors, snow: color } })}
+ />
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/PlanetHunters/SimplePlanetGenerator.tsx b/components/Data/Generator/Astronomers/PlanetHunters/SimplePlanetGenerator.tsx
new file mode 100644
index 000000000..7ba5feb2e
--- /dev/null
+++ b/components/Data/Generator/Astronomers/PlanetHunters/SimplePlanetGenerator.tsx
@@ -0,0 +1,56 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import SimplePlanetViewer from "./planetViewer-simple"
+import { type PlanetConfig, defaultPlanetConfig } from "@/utils/planet-physics";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
+import { Button } from "@/components/ui/button"
+import { Textarea } from "@/components/ui/textarea"
+
+export default function SimplePlanetPage() {
+ const [planetConfig, setPlanetConfig] = useState(defaultPlanetConfig)
+ const [importDialogOpen, setImportDialogOpen] = useState(false)
+ const [exportDialogOpen, setExportDialogOpen] = useState(false)
+ const [importText, setImportText] = useState("")
+ const [importError, setImportError] = useState("")
+
+ const handleConfigChange = (newConfig: Partial) => {
+ setPlanetConfig((prev) => ({ ...prev, ...newConfig }))
+ }
+
+ const handleImport = () => {
+ try {
+ const config = JSON.parse(importText)
+ setPlanetConfig(config)
+ setImportDialogOpen(false)
+ setImportError("")
+ setImportText("")
+ } catch (error) {
+ setImportError("Invalid JSON configuration")
+ }
+ }
+
+ // Listen for custom events from the settings panel
+ useEffect(() => {
+ const handleOpenImport = () => setImportDialogOpen(true)
+ const handleOpenExport = () => setExportDialogOpen(true)
+
+ window.addEventListener("open-simple-import-dialog", handleOpenImport)
+ window.addEventListener("open-simple-export-dialog", handleOpenExport)
+
+ return () => {
+ window.removeEventListener("open-simple-import-dialog", handleOpenImport)
+ window.removeEventListener("open-simple-export-dialog", handleOpenExport)
+ }
+ }, [])
+
+ const exportConfig = JSON.stringify(planetConfig, null, 2)
+
+ return (
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/PlanetHunters/planet-info.tsx b/components/Data/Generator/Astronomers/PlanetHunters/planet-info.tsx
new file mode 100644
index 000000000..11b15df29
--- /dev/null
+++ b/components/Data/Generator/Astronomers/PlanetHunters/planet-info.tsx
@@ -0,0 +1,46 @@
+import { Sun, Thermometer, Droplets } from 'lucide-react';
+import { Planet } from '@/types/greenhouse';
+
+interface PlanetInfoProps {
+ planet: Planet;
+};
+
+export function PlanetInfo({ planet }: PlanetInfoProps) {
+ return (
+
+
+
+
{planet.name}
+
Orbiting {planet.starName}
+
+
+
+
+
+
+
+
+ Temperature
+
+
{planet.temperature}°C
+
+
+
+
+
+ Humidity
+
+
{planet.humidity}%
+
+
+
+
+
+ Atmosphere
+
+
{planet.atmosphere}%
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/PlanetHunters/planet-scene.tsx b/components/Data/Generator/Astronomers/PlanetHunters/planet-scene.tsx
new file mode 100644
index 000000000..921503da1
--- /dev/null
+++ b/components/Data/Generator/Astronomers/PlanetHunters/planet-scene.tsx
@@ -0,0 +1,71 @@
+'use client'
+
+import { Canvas } from '@react-three/fiber';
+import { OrbitControls, Stars } from '@react-three/drei';
+// import { PlanetMesh } from './planet-mesh';
+// import type { PlanetStats } from '@/utils/planet-physics';
+import { Button } from '@/components/ui/button';
+import SimplePlanetMesh from './planet-simple';
+// import { SimplePlanetMesh } from './Simple/simple-planet-mesh';
+
+interface PlanetSceneProps {
+ // stats: PlanetStats
+ type?: string;
+};
+
+// export function PlanetScene({ stats, type }: PlanetSceneProps) {
+// const planetType = stats.type ?? "terrestrial";
+
+// return (
+// {/* bg-black */}
+// {type && (
+//
+// {type} Planet
+//
+// )}
+//
+//
+//
+//
+//
+//
+//
+//
+// );
+// };
+
+// interface SimplePlanetSceneProps {
+// stats: PlanetStats;
+// };
+
+// export function SimplePlanetScene() {
+// return (
+//
+//
+//
+//
+// {/* stats={stats} */}
+//
+//
+//
+//
+// );
+// };
+
+export function TEST () {
+ return (
+ <>>
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/PlanetHunters/planet-simple.tsx b/components/Data/Generator/Astronomers/PlanetHunters/planet-simple.tsx
new file mode 100644
index 000000000..5ff058232
--- /dev/null
+++ b/components/Data/Generator/Astronomers/PlanetHunters/planet-simple.tsx
@@ -0,0 +1,186 @@
+"use client"
+
+import { useRef, useMemo } from "react"
+import { useFrame } from "@react-three/fiber"
+import * as THREE from "three"
+import type { PlanetConfig } from "@/utils/planet-physics"
+
+interface SimplePlanetMeshProps {
+ config: PlanetConfig
+};
+
+export default function SimplePlanetMesh({ config }: SimplePlanetMeshProps) {
+ const meshRef = useRef(null)
+
+ const material = useMemo(() => {
+ // Get simplified colors for the simple interface
+ const primaryColor = config.type === "gaseous" ? config.colors.atmosphere : config.colors.ocean
+ const secondaryColor = config.type === "gaseous" ? config.colors.ocean : config.colors.lowland
+ const accentColor = config.type === "gaseous" ? config.colors.oceanPattern : config.colors.highland
+
+ return new THREE.ShaderMaterial({
+ uniforms: {
+ time: { value: 0 },
+ seed: { value: config.seed },
+ primaryColor: { value: new THREE.Color(primaryColor) },
+ secondaryColor: { value: new THREE.Color(secondaryColor) },
+ accentColor: { value: new THREE.Color(accentColor) },
+ density: { value: config.mass / ((config.radius ** 3 * Math.PI * 4) / 3) },
+ mass: { value: config.mass },
+ temperature: { value: config.temperature },
+ biomass: { value: config.biomass },
+ isGaseous: { value: config.type === "gaseous" ? 1.0 : 0.0 },
+ },
+ vertexShader: `
+ varying vec2 vUv;
+ varying vec3 vNormal;
+ varying vec3 vPosition;
+ varying float vElevation;
+
+ uniform float time;
+ uniform float seed;
+ uniform float isGaseous;
+ uniform float temperature;
+
+ // Simple noise function
+ float noise(vec3 p) {
+ float seedValue = seed;
+ return fract(sin(dot(p, vec3(12.9898, 78.233, 45.164))) * 43758.5453 + seedValue);
+ }
+
+ float fbm(vec3 p) {
+ float value = 0.0;
+ float amplitude = 0.5;
+ for(int i = 0; i < 4; i++) {
+ value += amplitude * (noise(p) - 0.5);
+ p *= 2.0;
+ amplitude *= 0.5;
+ }
+ return value;
+ }
+
+ void main() {
+ vUv = uv;
+ vNormal = normal;
+ vPosition = position;
+
+ vec3 pos = position;
+ float elevation = 0.0;
+
+ if (isGaseous > 0.5) {
+ // Gas giant - smooth bands and swirls
+ float bands = sin(pos.y * 8.0 + time * 0.1) * 0.02;
+ float swirls = fbm(pos * 2.0 + vec3(time * 0.05, 0.0, 0.0)) * 0.03;
+ elevation = bands + swirls;
+ } else {
+ // Terrestrial - more varied terrain
+ float continents = fbm(pos * 1.5) * 0.1;
+ float mountains = max(0.0, fbm(pos * 4.0)) * 0.05;
+ float details = fbm(pos * 8.0) * 0.02;
+
+ // Add temperature-based variation
+ float tempFactor = clamp((temperature - 200.0) / 500.0, 0.0, 1.0);
+ elevation = continents + mountains * tempFactor + details;
+ }
+
+ vElevation = elevation;
+ pos += normal * elevation;
+
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
+ }
+ `,
+ fragmentShader: `
+ uniform vec3 primaryColor;
+ uniform vec3 secondaryColor;
+ uniform vec3 accentColor;
+ uniform float density;
+ uniform float mass;
+ uniform float temperature;
+ uniform float biomass;
+ uniform float isGaseous;
+ uniform float time;
+ uniform float seed;
+
+ varying vec2 vUv;
+ varying vec3 vNormal;
+ varying vec3 vPosition;
+ varying float vElevation;
+
+ // Simple noise function
+ float noise(vec3 p) {
+ float seedValue = seed;
+ return fract(sin(dot(p, vec3(12.9898, 78.233, 45.164))) * 43758.5453 + seedValue);
+ }
+
+ void main() {
+ vec3 lightDirection = normalize(vec3(1.0, 1.0, 1.0));
+ float lightIntensity = max(0.2, dot(vNormal, lightDirection));
+
+ vec3 color;
+
+ if (isGaseous > 0.5) {
+ // Gas giant coloring
+ float bands = sin(vPosition.y * 8.0 + time * 0.1) * 0.5 + 0.5;
+ float storms = noise(vPosition * 3.0 + vec3(time * 0.1, 0.0, 0.0));
+
+ color = mix(primaryColor, secondaryColor, bands);
+ color = mix(color, accentColor, smoothstep(0.6, 0.8, storms) * 0.3);
+
+ // Add atmospheric glow
+ float fresnel = 1.0 - abs(dot(vNormal, vec3(0.0, 0.0, 1.0)));
+ color += primaryColor * fresnel * 0.2;
+ } else {
+ // Terrestrial coloring
+ float height = vElevation * 10.0 + 0.5;
+ float landmass = smoothstep(0.3, 0.7, height);
+
+ // Water vs land
+ vec3 waterColor = primaryColor;
+ vec3 landColor = mix(secondaryColor, accentColor, smoothstep(0.5, 0.8, height));
+
+ color = mix(waterColor, landColor, landmass);
+
+ // Add biomass effects
+ if (biomass > 0.3 && temperature > 250.0 && temperature < 350.0) {
+ float vegetation = biomass * smoothstep(0.4, 0.6, height);
+ color = mix(color, vec3(0.2, 0.6, 0.2), vegetation * 0.5);
+ }
+
+ // Add some variation
+ float variation = noise(vPosition * 5.0);
+ color = mix(color, accentColor, variation * 0.1);
+
+ // Polar caps for terrestrial planets
+ float latitude = abs(vNormal.y);
+ if (mass > 0.5 && mass < 3.0 && temperature < 300.0) {
+ color = mix(color, vec3(0.9, 0.95, 1.0), smoothstep(0.8, 1.0, latitude) * 0.7);
+ }
+ }
+
+ // Apply lighting
+ color *= lightIntensity;
+
+ // Add rim lighting
+ float rimLight = 1.0 - max(0.0, dot(vNormal, vec3(0.0, 0.0, 1.0)));
+ color += rimLight * 0.1;
+
+ gl_FragColor = vec4(color, 1.0);
+ }
+ `,
+ })
+ }, [config])
+
+ useFrame((state) => {
+ if (meshRef.current) {
+ meshRef.current.rotation.y += 0.002
+ material.uniforms.time.value = state.clock.elapsedTime
+ }
+ })
+
+ return (
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/PlanetHunters/planet.tsx b/components/Data/Generator/Astronomers/PlanetHunters/planet.tsx
new file mode 100644
index 000000000..351fc3636
--- /dev/null
+++ b/components/Data/Generator/Astronomers/PlanetHunters/planet.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import { useRef, useMemo, useEffect } from "react";
+import { useFrame } from "@react-three/fiber";
+import * as THREE from "three";
+import type { PlanetConfig } from "@/utils/planet-physics";
+import { vertexShader } from "@/utils/Generators/PH/vertex-shader";
+import { fragmentShader } from "@/utils/Generators/PH/fragment-shader";
+
+interface PlanetProps {
+ config: PlanetConfig
+};
+
+export default function Planet({ config }: PlanetProps) {
+ const meshRef = useRef(null)
+ const materialRef = useRef(null)
+ const atmosphereRef = useRef(null)
+
+ // Create shader material with uniforms based on planet config
+ const shaderMaterial = useMemo(() => {
+ const material = new THREE.ShaderMaterial({
+ vertexShader,
+ fragmentShader,
+ uniforms: {
+ time: { value: 0 },
+ seed: { value: config.seed },
+ radius: { value: config.radius },
+ isGaseous: { value: config.type === "gaseous" ? 1.0 : 0.0 },
+ surfaceRoughness: { value: config.terrainRoughness },
+ liquidHeight: { value: config.liquidHeight },
+ volcanicActivity: { value: config.volcanicActivity },
+ temperature: { value: config.temperature },
+ biomass: { value: config.biomass },
+ continentSize: { value: config.continentSize },
+ continentCount: { value: config.continentCount },
+ noiseScale: { value: config.noiseScale },
+ debugMode: { value: config.debugMode ? 1.0 : 0.0 },
+ visibleTerrains: {
+ value: new THREE.Vector4(
+ config.visibleTerrains?.ocean ? 1.0 : 0.0,
+ config.visibleTerrains?.beach ? 1.0 : 0.0,
+ config.visibleTerrains?.lowland ? 1.0 : 0.0,
+ config.visibleTerrains?.midland ? 1.0 : 0.0,
+ ),
+ },
+ visibleTerrains2: {
+ value: new THREE.Vector4(
+ config.visibleTerrains?.highland ? 1.0 : 0.0,
+ config.visibleTerrains?.mountain ? 1.0 : 0.0,
+ config.visibleTerrains?.snow ? 1.0 : 0.0,
+ 0.0,
+ ),
+ },
+
+ // Colors
+ atmosphereColor: { value: new THREE.Color(config.colors.atmosphere) },
+ oceanColor: { value: new THREE.Color(config.colors.ocean) },
+ oceanPatternColor: { value: new THREE.Color(config.colors.oceanPattern) },
+ beachColor: { value: new THREE.Color(config.colors.beach) },
+ lowlandColor: { value: new THREE.Color(config.colors.lowland) },
+ midlandColor: { value: new THREE.Color(config.colors.midland) },
+ highlandColor: { value: new THREE.Color(config.colors.highland) },
+ mountainColor: { value: new THREE.Color(config.colors.mountain) },
+ snowColor: { value: new THREE.Color(config.colors.snow) },
+ },
+ })
+
+ return material
+ }, [config]) // Update when any config property changes
+
+ // Set the material ref after the material is created
+ useEffect(() => {
+ if (shaderMaterial) {
+ materialRef.current = shaderMaterial
+ }
+ }, [shaderMaterial])
+
+ // Auto-rotate the planet and update time uniform
+ useFrame((state) => {
+ if (meshRef.current) {
+ meshRef.current.rotation.y += 0.001
+ }
+
+ if (atmosphereRef.current) {
+ atmosphereRef.current.rotation.y += 0.0005
+ }
+
+ if (materialRef.current) {
+ // Only animate certain aspects with time
+ materialRef.current.uniforms.time.value = state.clock.getElapsedTime()
+ }
+ })
+
+ // Create planet mesh with appropriate geometry based on type
+ return (
+
+
+
+
+
+
+ {/* Add atmosphere */}
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/PlanetHunters/planetViewer-simple.tsx b/components/Data/Generator/Astronomers/PlanetHunters/planetViewer-simple.tsx
new file mode 100644
index 000000000..3f4b58859
--- /dev/null
+++ b/components/Data/Generator/Astronomers/PlanetHunters/planetViewer-simple.tsx
@@ -0,0 +1,59 @@
+"use client"
+
+import { useState } from "react"
+import { Canvas } from "@react-three/fiber"
+import { OrbitControls, Stars } from "@react-three/drei"
+import { Cog } from "lucide-react"
+import type { PlanetConfig } from "@/utils/planet-physics"
+import SimplePlanetMesh from "./planet-simple"
+// import SimplePlanetSettings from "./simple-planet-settings"
+
+interface SimplePlanetViewerProps {
+ planetConfig: PlanetConfig
+ onConfigChange: (config: Partial) => void
+}
+
+export default function SimplePlanetViewer({ planetConfig, onConfigChange }: SimplePlanetViewerProps) {
+ const [showSettings, setShowSettings] = useState(false)
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {showSettings && (
+
+
+ {/* setShowSettings(false)}
+ className="text-slate-300 hover:text-slate-100"
+ aria-label="Close settings"
+ >
+
+
+
+
+ */}
+
+ {/*
*/}
+
+ )}
+
+ )
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Astronomers/PlanetHunters/planetViewer.tsx b/components/Data/Generator/Astronomers/PlanetHunters/planetViewer.tsx
new file mode 100644
index 000000000..84c48980a
--- /dev/null
+++ b/components/Data/Generator/Astronomers/PlanetHunters/planetViewer.tsx
@@ -0,0 +1,67 @@
+"use client"
+
+import { useState } from "react"
+import { Canvas } from "@react-three/fiber"
+import { OrbitControls, Stars } from "@react-three/drei"
+import { Cog } from "lucide-react"
+import type { PlanetConfig } from "@/utils/planet-physics"
+import Planet from "./planet"
+import SettingsPanel from "./SettingsPanel"
+
+interface PlanetViewerProps {
+ planetConfig: PlanetConfig
+ onConfigChange: (config: Partial) => void
+};
+
+export default function PlanetViewer({ planetConfig, onConfigChange }: PlanetViewerProps) {
+ const [showSettings, setShowSettings] = useState(false)
+
+ return (
+
+
+
+
+
+
+
+
+
+
setShowSettings(!showSettings)}
+ className="absolute top-4 right-4 bg-slate-800 p-2 rounded-full text-white hover:bg-slate-700 transition-colors z-10 border border-slate-700"
+ aria-label="Toggle settings"
+ >
+
+
+
+ {showSettings && (
+
+
+
Planet Settings
+ setShowSettings(false)}
+ className="text-slate-300 hover:text-slate-100"
+ aria-label="Close settings"
+ >
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/BiomeAggregator.tsx b/components/Data/Generator/BiomeAggregator.tsx
new file mode 100644
index 000000000..b8c136b40
--- /dev/null
+++ b/components/Data/Generator/BiomeAggregator.tsx
@@ -0,0 +1,204 @@
+'use client';
+
+import React, { useState, useEffect } from "react";
+
+interface AggregatedCloud {
+ annotationOptions: Record;
+ classificationOptions: Record>;
+ cloudColours?: Record;
+}
+
+interface AggregatedP4 {
+ fanCount: number;
+ blotchCount: number;
+ classificationCounts: Record;
+}
+
+interface AggregatedAI4M {
+ sandCount: number;
+ soilCount: number;
+ bedrockCount: number;
+ rockCount: number;
+ unlabelledCount: number;
+ classificationCounts: Record;
+}
+
+interface BiomeAggregatorProps {
+ cloudSummary: AggregatedCloud | null;
+ p4Summary: AggregatedP4 | null;
+ ai4MSummary: AggregatedAI4M | null;
+ biomassVersion: number;
+ onBiomeUpdate?: (biome: string) => void;
+}
+
+const biomeScores: Record> = {
+ "white": {
+ "barren wasteland": 0.0,
+ "arid dunes": 0.05,
+ "frigid expanse": 0.1,
+ "volcanic terrain": 0.1,
+ "tundra basin": 0.2,
+ "temperate highlands": 0.3,
+ "oceanic world": 0.4,
+ "tropical jungle": 0.5,
+ "barren jungle": 0.0,
+ "sparse desert": 0.025,
+ "empty ocean": 0.05,
+ },
+ "blue": {
+ "barren wasteland": -0.05,
+ "arid dunes": -0.1,
+ "frigid expanse": -0.2,
+ "volcanic terrain": -0.05,
+ "tundra basin": 0.0,
+ "temperate highlands": 0.05,
+ "oceanic world": 0.1,
+ "tropical jungle": 0.2,
+ "barren jungle": 0.05,
+ "sparse desert": 0.025,
+ "empty ocean": 0.1,
+ },
+};
+
+const traitsByBiome: Record = {
+ "volcanic terrain": ["Poisonous", "Fast", "Scaled", "Bald"],
+ "frigid expanse": ["Hairy", "Slow", "Nocturnal", "Intelligent"],
+ "arid dunes": ["Fast", "Scaled", "Bald", "Nocturnal"],
+ "tropical jungle": ["Flying", "Wings", "Feathered", "Social", "Intelligent", "Poisonous"],
+ "oceanic world": ["Amphibious", "Fast", "Social", "Intelligent", "Poisonous", "Nocturnal"],
+ "temperate highlands": ["Hairy", "Social", "Intelligent", "Fast", "Feathered"],
+ "rocky highlands": ["Scaled", "Fast", "Bald", "Intelligent"],
+ "barren wasteland": ["Slow", "Bald", "Poisonous", "Nocturnal"],
+ "barren jungle": ["Slow", "Nocturnal", "Scattered Vegetation"],
+ "sparse desert": ["Minimal", "Slow", "Nocturnal"],
+ "empty ocean": ["Amphibious", "Minimal"],
+};
+
+const weatherByBiome: Record = {
+ "rocky highlands": ["Rain", "Thunderstorms", "Dust Storms", "Seismic Activity"],
+ "rocky terrain": ["Rain", "Thunderstorms", "Dust Storms", "Seismic Activity"],
+ "consolidated soil": ["Rain", "Thunderstorms", "Dust Storms", "Seismic Activity"],
+ "wind-affected terrain": ["Rain", "Thunderstorms", "Dust Storms", "Seismic Activity"],
+ "barren wasteland": ["Acid Rain", "Dust Storms", "Ion Storms", "Impact Events"],
+ "arid dunes": ["Dust Storms", "Ion Storms", "Occasional Rain"],
+ "dusty surface": ["Dust Storms", "Ion Storms", "Occasional Rain"],
+ "sandy desert": ["Dust Storms", "Ion Storms", "Occasional Rain"],
+ "frigid expanse": ["Snow", "Sleet", "Ice Storms", "Cryovolcanism"],
+ "volcanic terrain": ["Volcanic Eruptions", "Geysers", "Seismic Activity", "Acid Rain"],
+ "temperate highlands": ["Rain", "Thunderstorms", "Hail", "Hurricanes"],
+ "oceanic world": ["Rain", "Hurricanes", "Tsunamis", "Deep Ocean Storms"],
+ "tropical jungle": ["Rain", "Superstorms", "Flooding", "Thunderstorms"],
+ "barren jungle": ["Sparse Rain", "Occasional Winds"],
+ "sparse desert": ["Dust Storms", "Occasional Winds"],
+ "empty ocean": ["Calm Seas", "Occasional Winds"],
+ "basalt plains": ["Rain", "Dust Storms", "Occasional Thunder"],
+ "sediment flats": ["Rain", "Flash Floods", "Hail"],
+ "cratered terrain": ["Dust Storms", "Impact Events", "Seismic Activity"],
+ "tundra basin": ["Snow", "Permafrost Winds", "Ice Storms"],
+ "flood basin": ["Flooding", "Heavy Rain", "Thunderstorms"],
+ "coral reefs": ["Tropical Storms", "Tsunamis", "Heavy Rain"],
+ "dune fields": ["Sandstorms", "Ion Storms", "High Wind Gusts"],
+};
+
+const biomeTraits: Record = {
+ "barren wasteland": ["slow", "nocturnal", "poisonous"],
+ "arid dunes": ["fast", "scaled", "bald", "nocturnal"],
+ "frigid expanse": ["slow", "scaled", "hairy", "nocturnal"],
+ "volcanic terrain": ["slow", "poisonous", "scaled"],
+ "temperate highlands": ["fast", "social", "feathered"],
+ "oceanic world": ["amphibious", "social", "fast", "intelligent"],
+ "tropical jungle": ["fast", "social", "feathered", "intelligent"],
+ "rocky highlands": ["scaled", "fast", "bald", "intelligent"],
+ "wind-affected terrain": ["scaled", "fast", "bald", "intelligent"],
+ "flood basin": ["amphibious", "social", "intelligent"],
+ "barren jungle": ["slow", "nocturnal", "scattered vegetation"],
+ "sparse desert": ["minimal", "slow", "nocturnal"],
+ "empty ocean": ["amphibious", "minimal"],
+};
+
+const getDominantBiome = (
+ cloudSummary: AggregatedCloud | null,
+ p4Summary: AggregatedP4 | null,
+ ai4MSummary: AggregatedAI4M | null,
+ biomassVersion: number
+): string => {
+ const biomeTotals: Record = {};
+
+ if (cloudSummary?.cloudColours) {
+ Object.entries(cloudSummary.cloudColours).forEach(([colour, count]) => {
+ const normalizedColour = colour.toLowerCase().trim();
+ if (biomeScores[normalizedColour]) {
+ Object.entries(biomeScores[normalizedColour]).forEach(([biome, score]) => {
+ biomeTotals[biome] = (biomeTotals[biome] || 0) + Number(score) * Number(count);
+ });
+ }
+ });
+ }
+
+ if (p4Summary) {
+ biomeTotals["wind-affected terrain"] = (biomeTotals["wind-affected terrain"] || 0) + p4Summary.fanCount * 0.2;
+ biomeTotals["dusty surface"] = (biomeTotals["dusty surface"] || 0) + p4Summary.blotchCount * 0.1;
+ }
+
+ if (ai4MSummary) {
+ biomeTotals["sandy desert"] = (biomeTotals["sandy desert"] || 0) + ai4MSummary.sandCount * 0.15;
+ biomeTotals["rocky terrain"] = (biomeTotals["rocky terrain"] || 0) + ai4MSummary.rockCount * 0.2;
+ biomeTotals["consolidated soil"] = (biomeTotals["consolidated soil"] || 0) + ai4MSummary.soilCount * 0.1;
+ }
+
+ Object.keys(biomeTotals).forEach((biome) => {
+ biomeTotals[biome] = biomeTotals[biome] * biomassVersion;
+ });
+
+ const sortedBiomes = Object.entries(biomeTotals).sort((a, b) => b[1] - a[1]);
+ return sortedBiomes.length > 0 ? String(sortedBiomes[0][0]) : "Unknown";
+};
+
+const BiomeAggregator: React.FC = ({ cloudSummary, p4Summary, ai4MSummary, biomassVersion, onBiomeUpdate }) => {
+ const [dominantBiome, setDominantBiome] = useState("Unknown");
+ const [weatherEvents, setWeatherEvents] = useState([]);
+ const [traits, setTraits] = useState([]);
+
+ useEffect(() => {
+ if (cloudSummary || p4Summary || ai4MSummary) {
+ const biome = getDominantBiome(cloudSummary, p4Summary, ai4MSummary, biomassVersion);
+ setDominantBiome(biome);
+ setWeatherEvents(weatherByBiome[biome] || []);
+ setTraits(biomeTraits[biome] || []);
+ if (onBiomeUpdate) {
+ onBiomeUpdate(biome);
+ }
+ }
+ }, [cloudSummary, p4Summary, ai4MSummary, biomassVersion, onBiomeUpdate]);
+
+ return (
+
+
Biome Aggregation
+
+
Dominant Biome: {dominantBiome}
+
+ {traits.map((trait, index) => (
+ {trait}
+ ))}
+
+ {cloudSummary && (
+
{JSON.stringify(cloudSummary)}
+ )}
+ {p4Summary && (
+
{JSON.stringify(p4Summary)}
+ )}
+ {ai4MSummary && (
+
{JSON.stringify(ai4MSummary)}
+ )}
+
Weather Events:
+
+ {weatherEvents.map((event, index) => (
+ {event}
+ ))}
+
+
+
+ );
+};
+
+export default BiomeAggregator;
\ No newline at end of file
diff --git a/components/Data/Generator/EarthIcon.tsx b/components/Data/Generator/EarthIcon.tsx
new file mode 100644
index 000000000..8af466b15
--- /dev/null
+++ b/components/Data/Generator/EarthIcon.tsx
@@ -0,0 +1,44 @@
+import React, { useMemo } from 'react'
+
+interface GenerativeEarthIconProps {
+ size?: number
+ seed?: number
+}
+
+export function GenerativeEarthIcon({ size = 24, seed = 1 }: GenerativeEarthIconProps) {
+ const continents = useMemo(() => {
+ const rng = mulberry32(seed)
+ return Array.from({ length: 5 }, () => ({
+ cx: rng() * size,
+ cy: rng() * size,
+ rx: rng() * (size / 4) + size / 8,
+ ry: rng() * (size / 4) + size / 8,
+ }))
+ }, [size, seed])
+
+ return (
+
+
+ {continents.map((continent, index) => (
+
+ ))}
+
+ )
+}
+
+// Simple random number generator
+function mulberry32(a: number) {
+ return function() {
+ let t = a += 0x6D2B79F5
+ t = Math.imul(t ^ t >>> 15, t | 1)
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61)
+ return ((t ^ t >>> 14) >>> 0) / 4294967296
+ };
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Meteorologists/JVH/cloud-canvas.tsx b/components/Data/Generator/Meteorologists/JVH/cloud-canvas.tsx
new file mode 100644
index 000000000..d7deb2e64
--- /dev/null
+++ b/components/Data/Generator/Meteorologists/JVH/cloud-canvas.tsx
@@ -0,0 +1,205 @@
+import { useEffect, useRef } from 'react'
+import { CloudPattern } from '../../../../../types/Generators/JVH/atmosphere'
+import { getColorsByAltitude } from '../../../../../data/cloud-compositions'
+
+interface CloudCanvasProps {
+ patterns: CloudPattern[]
+ altitude: number
+}
+
+export function CloudCanvas({ patterns, altitude }: CloudCanvasProps) {
+ const canvasRef = useRef(null)
+
+ useEffect(() => {
+ const canvas = canvasRef.current
+ if (!canvas) return
+
+ const ctx = canvas.getContext('2d')
+ if (!ctx) return
+
+ canvas.width = canvas.offsetWidth * window.devicePixelRatio
+ canvas.height = canvas.offsetHeight * window.devicePixelRatio
+ ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
+
+ // Clear canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
+
+ patterns.forEach(pattern => {
+ const colors = getColorsByAltitude(altitude, pattern)
+ renderCloudPattern(ctx, pattern, canvas.width, canvas.height, colors)
+ })
+ }, [patterns, altitude])
+
+ return (
+ 300 ? '#1a365d' : altitude > 100 ? '#2d3748' : '#4a5568'},
+ ${altitude > 300 ? '#2d3748' : altitude > 100 ? '#4a5568' : '#718096'})`
+ }}
+ />
+ )
+}
+
+function renderCloudPattern(
+ ctx: CanvasRenderingContext2D,
+ pattern: CloudPattern,
+ width: number,
+ height: number,
+ colors: string[]
+) {
+ ctx.save()
+
+ switch (pattern) {
+ case 'featureless':
+ renderFeaturelessCloud(ctx, width, height, colors)
+ break
+ case 'turbulent':
+ renderTurbulentCloud(ctx, width, height, colors)
+ break
+ case 'vortex':
+ renderVortex(ctx, width, height, colors)
+ break
+ case 'bands':
+ renderBands(ctx, width, height, colors)
+ break
+ }
+
+ ctx.restore()
+}
+
+function renderFeaturelessCloud(
+ ctx: CanvasRenderingContext2D,
+ width: number,
+ height: number,
+ colors: string[]
+) {
+ ctx.filter = 'blur(15px)'
+
+ // Create layered smooth clouds
+ colors.forEach((color, index) => {
+ const offset = index * (height / colors.length)
+
+ for (let i = 0; i < 3; i++) {
+ ctx.fillStyle = color
+ ctx.beginPath()
+ ctx.ellipse(
+ width * (0.3 + Math.random() * 0.4),
+ height * (0.3 + Math.random() * 0.4) + offset,
+ width * 0.4,
+ height * 0.2,
+ 0,
+ 0,
+ Math.PI * 2
+ )
+ ctx.fill()
+ }
+ })
+}
+
+function renderTurbulentCloud(
+ ctx: CanvasRenderingContext2D,
+ width: number,
+ height: number,
+ colors: string[]
+) {
+ ctx.filter = 'blur(8px)'
+
+ colors.forEach((color, index) => {
+ ctx.fillStyle = color
+
+ for (let i = 0; i < 15; i++) {
+ const x = width * Math.random()
+ const y = height * Math.random()
+ const size = Math.min(width, height) * 0.3
+
+ ctx.beginPath()
+ ctx.moveTo(x, y)
+
+ for (let j = 0; j < 5; j++) {
+ const cp1x = x + size * (Math.random() - 0.5)
+ const cp1y = y + size * (Math.random() - 0.5)
+ const cp2x = x + size * (Math.random() - 0.5)
+ const cp2y = y + size * (Math.random() - 0.5)
+ const endX = x + size * (Math.random() - 0.5)
+ const endY = y + size * (Math.random() - 0.5)
+
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY)
+ }
+
+ ctx.closePath()
+ ctx.fill()
+ }
+ })
+}
+
+function renderVortex(
+ ctx: CanvasRenderingContext2D,
+ width: number,
+ height: number,
+ colors: string[]
+) {
+ const centerX = width / 2
+ const centerY = height / 2
+ const maxRadius = Math.min(width, height) * 0.4
+
+ ctx.filter = 'blur(4px)'
+
+ colors.forEach((color, colorIndex) => {
+ ctx.fillStyle = color
+
+ for (let i = 0; i < 60; i++) {
+ const angle = (i / 30) * Math.PI * 2
+ const radiusOffset = (colorIndex / colors.length) * maxRadius * 0.3
+ const radius = maxRadius - radiusOffset
+
+ const x = centerX + Math.cos(angle) * radius * (1 - i / 120)
+ const y = centerY + Math.sin(angle) * radius * (1 - i / 120)
+
+ ctx.beginPath()
+ ctx.arc(x, y, radius * 0.15, 0, Math.PI * 2)
+ ctx.fill()
+ }
+ })
+}
+
+function renderBands(
+ ctx: CanvasRenderingContext2D,
+ width: number,
+ height: number,
+ colors: string[]
+) {
+ ctx.filter = 'blur(6px)'
+
+ const bandCount = colors.length * 2
+ const bandHeight = height / bandCount
+
+ colors.forEach((color, index) => {
+ const y = index * bandHeight * 2
+
+ // Main band
+ ctx.fillStyle = color
+ ctx.fillRect(0, y, width, bandHeight * 2)
+
+ // Add texture to bands
+ for (let i = 0; i < 8; i++) {
+ const x = width * Math.random()
+ const localY = y + bandHeight * Math.random()
+
+ ctx.beginPath()
+ ctx.ellipse(
+ x,
+ localY,
+ width * 0.2,
+ bandHeight * 0.8,
+ 0,
+ 0,
+ Math.PI * 2
+ )
+ ctx.fill()
+ }
+ })
+}
+
diff --git a/components/Data/Generator/Meteorologists/JVH/cloud-classifier.tsx b/components/Data/Generator/Meteorologists/JVH/cloud-classifier.tsx
new file mode 100644
index 000000000..c6735b28a
--- /dev/null
+++ b/components/Data/Generator/Meteorologists/JVH/cloud-classifier.tsx
@@ -0,0 +1,181 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { Slider } from '@/components/ui/slider'
+import { Badge } from '@/components/ui/badge'
+import { CloudPattern, CloudConfiguration } from '../../../../../types/Generators/JVH/atmosphere'
+import { cloudCompositions } from '../../../../../data/cloud-compositions'
+import { Cloud, Wind, TornadoIcon as Hurricane, Waves, Activity } from 'lucide-react'
+import { CloudCanvas } from './cloud-canvas'
+import { SciFiPanel } from '../../../../ui/styles/sci-fi/panel'
+import { SciFiButton } from '../../../../ui/styles/sci-fi/button'
+import { ConfigIOPanel } from './configuration'
+
+interface CloudSignalProps {
+ classificationConfig?: CloudConfiguration
+ classificationId: string
+};
+
+export default function CloudClassifier({ classificationConfig, classificationId }: CloudSignalProps) {
+ const [selectedPatterns, setSelectedPatterns] = useState([])
+ const [altitude, setAltitude] = useState(500)
+
+ const patterns = [
+ { type: 'featureless' as const, icon: , label: 'Featureless' },
+ { type: 'turbulent' as const, icon: , label: 'Turbulent' },
+ { type: 'vortex' as const, icon: , label: 'Vortex' },
+ { type: 'bands' as const, icon: , label: 'Bands' }
+ ];
+
+ const togglePattern = (pattern: CloudPattern) => {
+ setSelectedPatterns((current) =>
+ current.includes(pattern) ? current.filter((p) => p !== pattern) : [...current, pattern]
+ );
+ };
+
+ const getCompositionAnalysis = () => {
+ const analysis = new Set()
+ selectedPatterns.forEach((pattern) => {
+ switch (pattern) {
+ case 'featureless':
+ analysis.add('ammonia')
+ break
+ case 'turbulent':
+ analysis.add('ammonia')
+ analysis.add('ammoniumHydrosulfide')
+ if (altitude > 300) analysis.add('water')
+ break
+ case 'vortex':
+ analysis.add('ammoniumHydrosulfide')
+ analysis.add('water')
+ break
+ case 'bands':
+ analysis.add('ammonia')
+ analysis.add('ammoniumHydrosulfide')
+ break
+ }
+ })
+ return Array.from(analysis)
+ }
+
+ const getCurrentConfig = (): CloudConfiguration => ({
+ patterns: selectedPatterns,
+ altitude,
+ timestamp: new Date().toISOString()
+ })
+
+ const handleImport = (config: CloudConfiguration) => {
+ setSelectedPatterns(config.patterns)
+ setAltitude(config.altitude)
+ };
+
+ useEffect(() => {
+ if (classificationConfig) {
+ setSelectedPatterns(classificationConfig.patterns || [])
+ setAltitude(classificationConfig.altitude || 500)
+ };
+ }, [classificationConfig])
+
+ return (
+
+
+
+
+
+
Atmospheric Analysis Console
+
Cloud Pattern Classification System v1.0
+
+
+
+
+
+
+
+
+
Pattern Selection
+
+ {patterns.map(({ type, icon, label }) => (
+ togglePattern(type)}
+ className="flex items-center justify-center gap-2"
+ >
+ {icon}
+ {label}
+
+ ))}
+
+
+
+
+
Altitude Control
+
+
+ ALT: {altitude} km
+ DEPTH: {altitude > 300 ? 'DEEP' : altitude > 100 ? 'MED' : 'LOW'}
+
+
setAltitude(value[0])}
+ className="[&>span]:bg-cyan-400"
+ />
+
+
+
+
+
+ Cloud Formation Display
+
+
+
+
+
+
+
+
+ {selectedPatterns.length > 0 && (
+
+ Composition Analysis
+
+
+ {getCompositionAnalysis().map((compositionKey) => {
+ const composition = cloudCompositions[compositionKey]
+ return (
+
+ {composition.name}
+
+ )
+ })}
+
+
+ {selectedPatterns.map((pattern) => (
+
+ {pattern === 'featureless' && 'STATUS: Calm atmospheric region detected'}
+ {pattern === 'turbulent' && 'WARNING: Strong atmospheric activity present'}
+ {pattern === 'vortex' && 'ALERT: Deep storm system identified'}
+ {pattern === 'bands' && 'INFO: Zonal jet streams observed'}
+
+ ))}
+
+
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Meteorologists/JVH/cloud-pattern-generator.tsx b/components/Data/Generator/Meteorologists/JVH/cloud-pattern-generator.tsx
new file mode 100644
index 000000000..d21bf1cf1
--- /dev/null
+++ b/components/Data/Generator/Meteorologists/JVH/cloud-pattern-generator.tsx
@@ -0,0 +1,111 @@
+import { useState } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Slider } from '@/components/ui/slider';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { CloudPattern, AtmosphericLayer } from '../../../../../types/Generators/JVH/atmosphere';
+import { atmosphericLayers } from '../../../../../data/atmospheric-layers';
+import { Cloud, Wind, TornadoIcon as Hurricane, Waves } from 'lucide-react';
+
+export default function CloudPatternGenerator() {
+ const [selectedPattern, setSelectedPattern] = useState('featureless');
+ const [altitude, setAltitude] = useState(500);
+
+ const getCurrentLayer = (alt: number): AtmosphericLayer => {
+ return (
+ atmosphericLayers.find(
+ (layer) => alt >= layer.minAltitude && alt <= layer.maxAltitude
+ ) || atmosphericLayers[0]
+ );
+ };
+
+ const currentLayer = getCurrentLayer(altitude);
+
+ const patterns: { type: CloudPattern; icon: React.ReactNode; label: string }[] = [
+ { type: 'featureless', icon: , label: 'Featureless' },
+ { type: 'turbulent', icon: , label: 'Turbulent' },
+ { type: 'vortex', icon: , label: 'Vortex' },
+ { type: 'bands', icon: , label: 'Bands' },
+ ];
+
+ return (
+
+
+ Cloud Pattern Classifier
+
+
+ {/* Pattern Selection */}
+
+ {patterns.map(({ type, icon, label }) => (
+ setSelectedPattern(type)}
+ >
+ {icon}
+ {label}
+
+ ))}
+
+
+ {/* Cloud Visualization */}
+
+
+
+ {selectedPattern === 'featureless' && (
+
+ )}
+ {selectedPattern === 'turbulent' && (
+
+ )}
+ {selectedPattern === 'vortex' && (
+
+ )}
+ {selectedPattern === 'bands' && (
+
+ )}
+
+
+
+
+ {/* Altitude Slider */}
+
+
+ Altitude: {altitude} km
+ Layer: {currentLayer.name}
+
+
setAltitude(value[0])}
+ />
+
+
+ {/* Atmospheric Information */}
+
+
Atmospheric Composition
+
+ {currentLayer.primaryComposition.map((component) => (
+
+ {component.name}
+
+ ))}
+
+
+
+
Temperature:
+
{currentLayer.temperature}
+
+
+
Pressure:
+
{currentLayer.pressure}
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Meteorologists/JVH/configuration.tsx b/components/Data/Generator/Meteorologists/JVH/configuration.tsx
new file mode 100644
index 000000000..5f9a9b826
--- /dev/null
+++ b/components/Data/Generator/Meteorologists/JVH/configuration.tsx
@@ -0,0 +1,145 @@
+'use client'
+
+import { useState } from 'react'
+import { SciFiPanel } from '@/components/ui/styles/sci-fi/panel'
+import { SciFiButton } from '@/components/ui/styles/sci-fi/button'
+import { Textarea } from '@/components/ui/textarea'
+import { CloudConfiguration } from '@/types/Generators/JVH/atmosphere'
+import { Download, Upload, AlertCircle } from 'lucide-react'
+import { useSupabaseClient } from '@supabase/auth-helpers-react'
+
+interface ConfigIOPanelProps {
+ currentConfig: CloudConfiguration
+ classificationId: string
+ onImport: (config: CloudConfiguration) => void
+};
+
+export function ConfigIOPanel({ currentConfig, classificationId, onImport }: ConfigIOPanelProps) {
+ const supabase = useSupabaseClient()
+
+ const [importText, setImportText] = useState('')
+ const [error, setError] = useState(null)
+
+ const exportConfig = () => {
+ const configString = JSON.stringify(currentConfig, null, 2)
+ setImportText(configString)
+ setError(null)
+ }
+
+ const [exporting, setExporting] = useState(false)
+
+ const handleExport = async () => {
+ if (!classificationId) {
+ console.error('Classification ID not found.')
+ return
+ }
+
+ setExporting(true)
+ try {
+ const { data, error } = await supabase
+ .from('classifications')
+ .select('classificationConfiguration')
+ .eq('id', classificationId)
+ .single()
+
+ if (error) throw error
+
+ const currentConfiguration = data?.classificationConfiguration || {}
+ const cloudData = currentConfiguration.cloudData || {}
+
+ // Combine the new fields with cloudData without nested errors
+ const updatedConfig = {
+ ...cloudData, // Keep the original cloud data
+ patterns: currentConfig.patterns || [], // Add patterns
+ altitude: currentConfig.altitude || 500, // Add altitude
+ timestamp: currentConfig.timestamp || new Date().toISOString(), // Add timestamp
+ }
+
+ const { error: updateError } = await supabase
+ .from('classifications')
+ .update({
+ classificationConfiguration: updatedConfig
+ })
+ .eq('id', classificationId)
+
+ if (updateError) throw updateError
+
+ alert('Cloud data exported successfully!')
+ } catch (err) {
+ console.error('Error exporting cloud data:', err)
+ alert('Failed to export cloud data.')
+ } finally {
+ setExporting(false)
+ }
+
+ const configString = JSON.stringify(currentConfig, null, 2)
+ setImportText(configString)
+ setError(null)
+ }
+
+ const importConfig = () => {
+ try {
+ const config = JSON.parse(importText) as CloudConfiguration
+
+ if (!config.patterns || !Array.isArray(config.patterns)) {
+ throw new Error('Invalid patterns configuration')
+ }
+ if (typeof config.altitude !== 'number' || config.altitude < 0 || config.altitude > 1000) {
+ throw new Error('Invalid altitude value')
+ }
+ if (!config.timestamp) {
+ throw new Error('Missing timestamp')
+ }
+
+ onImport(config)
+ setError(null)
+ } catch (err) {
+ setError('Invalid configuration format')
+ }
+ }
+
+ return (
+
+
+
+
Configuration Transfer
+
+
+
+ Export
+
+
+
+ Import
+
+
+
+
+
+
Current Configuration
+
+ {JSON.stringify(currentConfig, null, 2)}
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Weather/EventsAcrossMyLocations.tsx b/components/Data/Generator/Weather/EventsAcrossMyLocations.tsx
new file mode 100644
index 000000000..5368a861e
--- /dev/null
+++ b/components/Data/Generator/Weather/EventsAcrossMyLocations.tsx
@@ -0,0 +1,540 @@
+'use client';
+
+import { useEffect, useState } from "react";
+import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
+import { startOfWeek, addWeeks, differenceInSeconds } from "date-fns";
+import { toZonedTime } from "date-fns-tz";
+import { Button } from "@/components/ui/button";
+
+const TIMEZONE = 'Australia/Melbourne';
+
+const biomeToStormMap: Record = {
+ "RockyHighlands": "Dust Storm",
+ "Barren (Pending)": "Dust Storm",
+ "Barren Wasteland": "Radiation Storm",
+ "Arid Dunes": "Sandstorm",
+ "Frigid Expanse": "Snowstorm",
+ "Volcanic Terrain": "Ashfall",
+ "Basalt Plains": "Windstorm",
+ "Sediment Flats": "Flash Flood",
+ "Cratered Terrain": "Seismic Shock",
+ "Tundra Basin": "Ice Storm",
+ "Temperate Highlands": "Thunderstorm",
+ "Oceanic World": "Cyclone",
+ "Tropical Jungle": "Monsoon",
+ "Flood Basin": "Deluge",
+ "Coral Reefs": "Supercell",
+ "Dune Fields": "Heatwave",
+};
+
+type EnvCategory = "biomass" | "temperature" | "humidity" | "density";
+
+function getPlanetType(density: number): "terrestrial" | "gaseous" | "ocean" {
+ if (density >= 3.5) return "terrestrial";
+ if (density < 1.5) return "gaseous";
+ return "ocean";
+};
+
+interface EnvironmentalEffect {
+ humidity: number;
+ temperature: number;
+ biomass: number;
+}
+
+const stormEffectsMap: Record = {
+ "Dust Storm": { humidity: -0.05, temperature: -1, biomass: -0.005 },
+ "lightning-kickoff": { humidity: +0.05, temperature: +2, biomass: +0.01 },
+ "Radiation Storm": { humidity: -0.10, temperature: +3, biomass: -0.015 },
+ "Sandstorm": { humidity: -0.04, temperature: +2, biomass: -0.01 },
+ "Snowstorm": { humidity: +0.10, temperature: -5, biomass: -0.002 },
+ "Ashfall": { humidity: -0.02, temperature: +4, biomass: -0.01 },
+ "Windstorm": { humidity: -0.03, temperature: +1, biomass: -0.002 },
+ "Flash Flood": { humidity: +0.15, temperature: -1, biomass: +0.005 },
+ "Seismic Shock": { humidity: 0, temperature: 0, biomass: -0.02 },
+ "Ice Storm": { humidity: +0.08, temperature: -6, biomass: -0.01 },
+ "Thunderstorm": { humidity: +0.12, temperature: +1, biomass: +0.01 },
+ "Cyclone": { humidity: +0.20, temperature: -2, biomass: +0.015 },
+ "Monsoon": { humidity: +0.25, temperature: -1, biomass: +0.02 },
+ "Deluge": { humidity: +0.30, temperature: -2, biomass: +0.015 },
+ "Supercell": { humidity: +0.18, temperature: +1, biomass: +0.01 },
+ "Heatwave": { humidity: -0.20, temperature: +6, biomass: -0.02 },
+};
+
+interface EventData {
+ classificationId: number;
+ eventCount: number;
+ hasEvent: boolean;
+ redeemed: boolean;
+ nextEventType: string | null;
+ anomalyId: number;
+ countdown: string;
+}
+
+function formatCountdown(seconds: number): string {
+ const h = Math.floor(seconds / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ const s = seconds % 60;
+ return `${h}h ${m}m ${s}s`;
+}
+
+const structureTheme = {
+ background: 'bg-transparent',
+ cardBackground: 'bg-white/20 backdrop-blur',
+ cardBorder: 'border border-white/10',
+ title: 'text-[#2E3440]',
+ subtitle: 'text-[#4C566A]',
+ redeemed: 'text-green-600',
+ notRedeemed: 'text-red-500',
+ countdown: 'text-[#D08770]',
+ nextEvent: 'text-[#5E81AC]',
+};
+
+export default function WeatherEventsOverview() {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+ const [eventsData, setEventsData] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ async function fetchData() {
+ if (!session) return;
+
+ const { data: classifications, error: classError } = await supabase
+ .from("classifications")
+ .select("id, anomaly") //, biome")
+ .eq("author", session.user.id)
+ .in("classificationtype", ["planet", "telescope-minorPlanet"]);
+
+ if (classError || !classifications) {
+ console.error("Error fetching classifications:", classError);
+ setLoading(false);
+ return;
+ }
+
+ const classificationIds = classifications.map(c => c.id);
+
+ const now = new Date();
+ const startOfWeekMelbourne = startOfWeek(toZonedTime(now, TIMEZONE), { weekStartsOn: 1 });
+ startOfWeekMelbourne.setHours(0, 1, 0, 0);
+ const endOfWeekMelbourne = addWeeks(startOfWeekMelbourne, 1);
+
+ const { data: events, error: eventsError } = await supabase
+ .from("events")
+ .select("*")
+ .in("classification_location", classificationIds)
+ .gte("time", startOfWeekMelbourne.toISOString());
+
+ if (eventsError) {
+ console.error("Error fetching events:", eventsError);
+ setLoading(false);
+ return;
+ }
+
+ const { data: comments, error: commentError } = await supabase
+ .from("comments")
+ .select("classification_id, category, value, created_at")
+ .in("classification_id", classificationIds)
+ .order("created_at", { ascending: false });
+
+ if (commentError) {
+ console.error("Error fetching comments:", commentError);
+ setLoading(false);
+ return;
+ }
+
+ const envValues: Record>> = {};
+
+ for (const comment of comments) {
+ const cid = comment.classification_id;
+ if (!envValues[cid]) envValues[cid] = {};
+
+ const rawCategory = comment.category?.toLowerCase();
+ if (["biomass", "temperature", "humidity", "density"].includes(rawCategory)) {
+ const category = rawCategory as EnvCategory;
+ if (!(category in envValues[cid])) {
+ envValues[cid][category] = parseFloat(comment.value);
+ }
+ }
+ }
+
+ const grouped: Record = {};
+
+ for (const info of classifications) {
+ const { id, anomaly } = info;// const { id, anomaly, biome } = info;
+ const env = envValues[id] || {};
+ const biomass = env.biomass ?? 0;
+ const humidity = env.humidity ?? 0;
+ const temperature = env.temperature ?? 0;
+ const density = env.density ?? 0;
+
+ const planetType = getPlanetType(density);
+
+ const eventsForLocation = events.filter(e => e.classification_location === id);
+ const hasEventThisWeek = eventsForLocation.length > 0;
+ const redeemed = eventsForLocation.some(e => e.status === "redeemed");
+ const lightningEventExists = eventsForLocation.some(e => e.type?.toLowerCase().includes("lightning"));
+
+ let newEventType: string | null = null;
+
+ if (!hasEventThisWeek) {
+ if (planetType === "gaseous") {
+ const stormTypes = Object.keys(stormEffectsMap);
+ newEventType = stormTypes[Math.floor(Math.random() * stormTypes.length)];
+ } else {
+ const eligibleForLightning = (
+ !lightningEventExists &&
+ biomass > 0.001 &&
+ temperature >= 10 &&
+ temperature <= 40 &&
+ humidity >= 0.10 &&
+ humidity <= 0.85
+ );
+
+ newEventType = eligibleForLightning
+ ? "lightning-kickoff"
+ : "Thunderstorm" // : biomeToStormMap[biome] || "Thunderstorm";
+ }
+ }
+
+ const secondsLeft = differenceInSeconds(endOfWeekMelbourne, toZonedTime(now, TIMEZONE));
+
+ grouped[id] = {
+ classificationId: id,
+ eventCount: eventsForLocation.length,
+ hasEvent: hasEventThisWeek,
+ redeemed,
+ nextEventType: newEventType,
+ anomalyId: anomaly,
+ countdown: formatCountdown(secondsLeft),
+ };
+ }
+
+ setEventsData(Object.values(grouped));
+ setLoading(false);
+ }
+
+ fetchData();
+ }, [session]);
+
+ const handleCreateEvent = async (classificationId: number, anomalyId: number, type: string) => {
+ if (!session) return;
+
+ const stormEffect = stormEffectsMap[type];
+ if (!stormEffect) {
+ console.warn(`No environmental effect defined for storm type: ${type}`);
+ return;
+ }
+
+ const { data: eventInsertData, error: eventError } = await supabase
+ .from("events")
+ .insert({
+ classification_location: classificationId,
+ location: anomalyId,
+ type,
+ configuration: stormEffect,
+ completed: false,
+ })
+ .select("id")
+ .single();
+
+ if (eventError || !eventInsertData) {
+ console.error("Error creating event:", eventError);
+ return;
+ }
+
+ const eventId = eventInsertData.id;
+
+ const commentPayloads = ["humidity", "temperature", "biomass"].map((category) => {
+ const value = stormEffect[category as keyof EnvironmentalEffect];
+
+ if (value === undefined) {
+ console.warn(`No value defined for category: ${category} in storm type: ${type}`);
+ return null;
+ }
+
+ const capitalizedCategory = category.charAt(0).toUpperCase() + category.slice(1);
+
+ return {
+ content: `Change in ${capitalizedCategory}: ${value}`,
+ author: session.user.id,
+ classification_id: classificationId,
+ category: capitalizedCategory,
+ value: value.toString(),
+ configuration: JSON.stringify({
+ type: "weather-effect",
+ effect: type,
+ delta: value,
+ }),
+ event: eventId,
+ };
+ }).filter(Boolean);
+
+ if (commentPayloads.length > 0) {
+ const { error: commentError } = await supabase
+ .from("comments")
+ .insert(commentPayloads);
+
+ if (commentError) {
+ console.error("Error inserting comments:", commentError);
+ }
+ }
+
+ window.location.reload();
+ };
+
+ if (loading) return Loading weather events...
;
+ if (eventsData.length === 0) return No planets found.
;
+
+ return (
+
+
+ {eventsData.map(event => (
+
+
+ Planet #{event.classificationId}
+
+
+ Events this week: {event.eventCount}
+
+
+ {event.redeemed ? 'Redeemed' : 'Not Redeemed'}
+
+ {event.hasEvent ? (
+
+ Next event in: {event.countdown}
+
+ ) : (
+ event.nextEventType && (
+ <>
+
+ Next Suggested Event: {event.nextEventType}
+
+
+ handleCreateEvent(event.classificationId, event.anomalyId, event.nextEventType!)
+ }
+ >
+ Create Event
+
+ >
+ )
+ )}
+
+ ))}
+
+
+ );
+};
+
+
+
+
+
+
+/*
+'use client';
+
+import { useEffect, useState } from "react";
+import { useSupabaseClient } from "@supabase/auth-helpers-react";
+import { Button } from "@/components/ui/button";
+import { differenceInSeconds, startOfWeek } from "date-fns";
+import { toZonedTime } from "date-fns-tz";
+import EventList from "./RecentEvents";
+
+const TIMEZONE = 'Australia/Melbourne';
+
+const biomeToStormMap: Record = {
+ "RockyHighlands": "Dust Storm",
+ "Barren (Pending)": "Dust Storm",
+ "Barren Wasteland": "Radiation Storm",
+ "Arid Dunes": "Sandstorm",
+ "Frigid Expanse": "Snowstorm",
+ "Volcanic Terrain": "Ashfall",
+ "Basalt Plains": "Windstorm",
+ "Sediment Flats": "Flash Flood",
+ "Cratered Terrain": "Seismic Shock",
+ "Tundra Basin": "Ice Storm",
+ "Temperate Highlands": "Thunderstorm",
+ "Oceanic World": "Cyclone",
+ "Tropical Jungle": "Monsoon",
+ "Flood Basin": "Deluge",
+ "Coral Reefs": "Supercell",
+ "Dune Fields": "Heatwave",
+};
+
+function getPlanetType(density: number): "terrestrial" | "gaseous" | "ocean" {
+ if (density >= 3.5) return "terrestrial";
+ if (density < 1.5) return "gaseous";
+ return "ocean";
+}
+
+type ClassificationGroup = {
+ classificationId: number;
+ biome?: string;
+ biomass?: number;
+ density?: number;
+};
+
+export default function MultiWeatherEventStatus({
+ classifications,
+}: {
+ classifications: ClassificationGroup[];
+}) {
+ const supabase = useSupabaseClient();
+ const [statuses, setStatuses] = useState>({});
+
+ useEffect(() => {
+ classifications.forEach((group) => {
+ const {
+ classificationId,
+ biome = "RockyHighlands",
+ biomass = 0.01,
+ density = 3.5,
+ } = group;
+
+ const planetType = getPlanetType(density);
+ const startOfWeekMelbourne = startOfWeek(toZonedTime(new Date(), TIMEZONE), { weekStartsOn: 1 });
+ startOfWeekMelbourne.setHours(0, 1, 0, 0);
+
+ const fetchAndSetStatus = async () => {
+ const { data: events, error } = await supabase
+ .from("events")
+ .select("*")
+ .eq("classification_location", classificationId)
+ .gte("time", startOfWeekMelbourne.toISOString());
+
+ if (error) return;
+
+ const hasEvent = events.length > 0;
+ const lightningEventExists = events.some(e =>
+ e.type?.toLowerCase().includes("lightning")
+ );
+ const pendingStorm = events.find(e =>
+ e.status === "pending" &&
+ (e.type?.toLowerCase().includes("storm") || e.type?.toLowerCase().includes("weather"))
+ );
+
+ let newEventType: string | null = null;
+ if (
+ planetType === "terrestrial" &&
+ biomass >= 0.000001 &&
+ biomass <= 0.02 &&
+ !lightningEventExists
+ ) {
+ newEventType = "lightning-kickoff";
+ } else if (!hasEvent) {
+ newEventType = "rain-general";
+ }
+
+ const bacteriumVisible =
+ classificationId === 40 || (lightningEventExists && biomass > 0.01 && biomass < 0.2);
+
+ const upcomingStormType = biomeToStormMap[biome] || null;
+
+ if (!hasEvent && (newEventType || upcomingStormType)) {
+ await supabase.from("events").insert({
+ type: newEventType ?? upcomingStormType,
+ classification_location: classificationId,
+ status: "pending",
+ time: new Date().toISOString(),
+ });
+ }
+
+ setStatuses(prev => ({
+ ...prev,
+ [classificationId]: {
+ hasEvent,
+ bacteriumVisible,
+ nextEventType: newEventType,
+ upcomingStormType,
+ timeLeft: hasEvent ? getSecondsUntilNextWeek() : 0,
+ }
+ }));
+ };
+
+ fetchAndSetStatus();
+ });
+ }, [classifications]);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setStatuses(prev => {
+ const now = toZonedTime(new Date(), TIMEZONE);
+ const nextWeek = startOfWeek(now, { weekStartsOn: 1 });
+ nextWeek.setDate(nextWeek.getDate() + 7);
+ nextWeek.setHours(0, 1, 0, 0);
+ const secondsLeft = differenceInSeconds(nextWeek, now);
+
+ const updated = { ...prev };
+ Object.keys(updated).forEach(key => {
+ const numericKey = Number(key);
+ if (updated[numericKey]?.hasEvent) {
+ updated[numericKey].timeLeft = secondsLeft;
+ }
+ });
+ return updated;
+ });
+ }, 1000);
+ return () => clearInterval(interval);
+ }, []);
+
+ function getSecondsUntilNextWeek() {
+ const now = toZonedTime(new Date(), TIMEZONE);
+ const nextWeek = startOfWeek(now, { weekStartsOn: 1 });
+ nextWeek.setDate(nextWeek.getDate() + 7);
+ nextWeek.setHours(0, 1, 0, 0);
+ return differenceInSeconds(nextWeek, now);
+ }
+
+ return (
+
+ {classifications.map(({ classificationId, biomass = 0.01 }) => {
+ const status = statuses[classificationId];
+ if (!status) return null;
+
+ const h = String(Math.floor(status.timeLeft / 3600)).padStart(2, "0");
+ const m = String(Math.floor((status.timeLeft % 3600) / 60)).padStart(2, "0");
+ const s = String(status.timeLeft % 60).padStart(2, "0");
+
+ return (
+
+
Classification #{classificationId}
+
+ {status.hasEvent ? (
+
Wait {h}:{m}:{s} for new weather event
+ ) : (
+ <>
+ {status.bacteriumVisible && (
+
+ Bacterial microcolonies are stirring...
+
+ )}
+
+ {status.nextEventType && (
+
+ Next event: {status.nextEventType}
+
+ )}
+
+ {status.upcomingStormType && (
+
+ Upcoming storm: {status.upcomingStormType}
+
+ )}
+ >
+ )}
+
+
Biomass: {biomass}
+
+
+ );
+ })}
+
+ );
+};
+*/
\ No newline at end of file
diff --git a/components/Data/Generator/Weather/EventsCounter.tsx b/components/Data/Generator/Weather/EventsCounter.tsx
new file mode 100644
index 000000000..ce3853614
--- /dev/null
+++ b/components/Data/Generator/Weather/EventsCounter.tsx
@@ -0,0 +1,197 @@
+'use client';
+
+import { useEffect, useState } from "react";
+import { useSupabaseClient } from "@supabase/auth-helpers-react";
+import { Button } from "@/components/ui/button";
+import { differenceInSeconds, startOfWeek } from "date-fns";
+import { toZonedTime } from "date-fns-tz";
+import EventList from "./RecentEvents";
+
+const TIMEZONE = 'Australia/Melbourne';
+
+const biomeToStormMap: Record = {
+ "RockyHighlands": "Dust Storm",
+ "Barren (Pending)": "Dust Storm",
+ "Barren Wasteland": "Radiation Storm",
+ "Arid Dunes": "Sandstorm",
+ "Frigid Expanse": "Snowstorm",
+ "Volcanic Terrain": "Ashfall",
+ "Basalt Plains": "Windstorm",
+ "Sediment Flats": "Flash Flood",
+ "Cratered Terrain": "Seismic Shock",
+ "Tundra Basin": "Ice Storm",
+ "Temperate Highlands": "Thunderstorm",
+ "Oceanic World": "Cyclone",
+ "Tropical Jungle": "Monsoon",
+ "Flood Basin": "Deluge",
+ "Coral Reefs": "Supercell",
+ "Dune Fields": "Heatwave",
+};
+
+function getPlanetType(density: number): "terrestrial" | "gaseous" | "ocean" {
+ if (density >= 3.5) return "terrestrial";
+ if (density < 1.5) return "gaseous";
+ return "ocean";
+};
+
+export default function WeatherEventStatus({
+ classificationId,
+ biome,
+ biomass,
+ density,
+}: {
+ classificationId: number;
+ density: number;
+ biome: string;
+ biomass: number;
+}) {
+ const supabase = useSupabaseClient();
+ const [hasEventThisWeek, setHasEventThisWeek] = useState(null);
+ const [timeLeft, setTimeLeft] = useState(0);
+ const [nextEventType, setNextEventType] = useState(null);
+ const [bacteriumVisible, setBacteriumVisible] = useState(false);
+ const [upcomingStormType, setUpcomingStormType] = useState(null);
+
+ useEffect(() => {
+ const checkWeatherEvent = async () => {
+ const startOfWeekMelbourne = startOfWeek(toZonedTime(new Date(), TIMEZONE), { weekStartsOn: 1 });
+ startOfWeekMelbourne.setHours(0, 1, 0, 0);
+
+ const { data: events, error: eventsError } = await supabase
+ .from("events")
+ .select("*")
+ .eq("classification_location", classificationId)
+ .gte("time", startOfWeekMelbourne.toISOString());
+
+ if (eventsError) {
+ console.error("Error fetching weather events:", eventsError);
+ return;
+ };
+
+ const hasEvent = events.length > 0;
+ setHasEventThisWeek(hasEvent);
+
+ const lightningEventExists = events.some(e =>
+ e.type?.toLowerCase().includes("lightning")
+ );
+
+ const pendingStorm = events.find(e =>
+ e.status === "pending" &&
+ (e.type?.toLowerCase().includes("storm") || e.type?.toLowerCase().includes("weather"))
+ );
+
+ const planetType = getPlanetType(density);
+
+ let newEventType: string | null = null;
+
+ if (
+ planetType === "terrestrial" &&
+ biomass >= 0.000001 &&
+ biomass <= 0.02 &&
+ !lightningEventExists
+ ) {
+ newEventType = "lightning-kickoff";
+ } else if (!hasEvent) {
+ newEventType = "rain-general";
+ }
+
+ setNextEventType(newEventType);
+
+ if (
+ classificationId === 40 ||
+ (lightningEventExists && biomass > 0.01 && biomass < 0.2)
+ ) {
+ setBacteriumVisible(true);
+ } else {
+ setBacteriumVisible(false);
+ }
+
+ if (pendingStorm && biomeToStormMap[biome]) {
+ setUpcomingStormType(biomeToStormMap[biome]);
+ } else if (!hasEvent && biomeToStormMap[biome]) {
+ setUpcomingStormType(biomeToStormMap[biome]);
+ } else {
+ setUpcomingStormType(null);
+ }
+
+ // Create new event if needed
+ if (!hasEvent && (newEventType || biomeToStormMap[biome])) {
+ const eventType = newEventType ?? biomeToStormMap[biome];
+ await supabase.from("events").insert({
+ type: eventType,
+ classification_location: classificationId,
+ status: "pending",
+ time: new Date().toISOString(),
+ });
+ setHasEventThisWeek(true);
+ }
+ };
+
+ checkWeatherEvent();
+ }, [classificationId, biome, biomass, density]);
+
+ useEffect(() => {
+ if (hasEventThisWeek !== true) return;
+
+ const interval = setInterval(() => {
+ const now = toZonedTime(new Date(), TIMEZONE);
+ const nextWeek = startOfWeek(now, { weekStartsOn: 1 });
+ nextWeek.setDate(nextWeek.getDate() + 7);
+ nextWeek.setHours(0, 1, 0, 0);
+
+ const secondsLeft = differenceInSeconds(nextWeek, now);
+ setTimeLeft(secondsLeft);
+ }, 1000);
+
+ return () => clearInterval(interval);
+ }, [hasEventThisWeek]);
+
+ if (hasEventThisWeek === null) return null;
+
+ if (hasEventThisWeek) {
+ const h = String(Math.floor(timeLeft / 3600)).padStart(2, "0");
+ const m = String(Math.floor((timeLeft % 3600) / 60)).padStart(2, "0");
+ const s = String(timeLeft % 60).padStart(2, "0");
+
+ return (
+
+ Wait {h}:{m}:{s} for new weather event
+
+ );
+ };
+
+ return (
+
+ {bacteriumVisible && (
+
+
+
+
+
+
+
+
+
+
+
Bacterial microcolonies are stirring...
+
+ )}
+
+ {nextEventType && (
+
+ Next event: {nextEventType}
+
+ )}
+
+ {upcomingStormType && (
+
+ Upcoming storm...view {upcomingStormType}
+
+ )}
+
+ {biomass}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Weather/RecentEvents.tsx b/components/Data/Generator/Weather/RecentEvents.tsx
new file mode 100644
index 000000000..61bb4a919
--- /dev/null
+++ b/components/Data/Generator/Weather/RecentEvents.tsx
@@ -0,0 +1,47 @@
+'use client';
+
+import { useEffect, useState } from "react";
+import { useSupabaseClient } from "@supabase/auth-helpers-react";
+
+export default function EventList({ classificationId }: { classificationId: number }) {
+ const supabase = useSupabaseClient();
+ const [events, setEvents] = useState([]);
+
+ useEffect(() => {
+ const fetchEvents = async () => {
+ const { data, error } = await supabase
+ .from("events")
+ .select("*")
+ .eq("classification_location", classificationId)
+ .order("time", { ascending: false });
+
+ if (!error) setEvents(data || []);
+ };
+
+ fetchEvents();
+ }, [classificationId]);
+
+ if (!events.length) return null;
+
+ return (
+
+
Recent Events
+
+ {events.map(event => (
+
+ {event.type}
+
+ {new Date(event.time).toLocaleString()}
+
+ {event.completed && (
+ ✓ Completed
+ )}
+
+ ))}
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/Generator/Weather/SimpleWeatherEvents.tsx b/components/Data/Generator/Weather/SimpleWeatherEvents.tsx
new file mode 100644
index 000000000..462f0f929
--- /dev/null
+++ b/components/Data/Generator/Weather/SimpleWeatherEvents.tsx
@@ -0,0 +1,718 @@
+"use client"
+
+import { useEffect, useRef, useState } from "react"
+import * as d3 from "d3"
+import { Button } from "@/components/ui/button"
+import { Switch } from "@/components/ui/switch"
+import { Label } from "@/components/ui/label"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { biomeData, BiomeData } from "@/utils/biome-data"
+
+interface WeatherSettings {
+ rain: boolean
+ snow: boolean
+ hail: boolean
+ clouds: boolean
+ sun: boolean
+ lightning: boolean
+};
+
+interface WeatherGeneratorProps {
+ biome?: string
+ settings?: WeatherSettings
+};
+
+const defaultSettings: WeatherSettings = {
+ rain: false,
+ snow: false,
+ hail: false,
+ clouds: true,
+ sun: true,
+ lightning: false,
+};
+
+// Convert kebab-case to Title Case
+function formatBiomeName(name: string): string {
+ if (!name) return ""
+ return name
+ .split("-")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(" ")
+};
+
+// Convert Title Case to kebab-case
+function kebabCase(name: string): string {
+ return name.toLowerCase().replace(/\s+/g, "-")
+};
+
+// Function to determine weather settings based on biome data
+function getWeatherSettingsForBiome(biomeName: string): WeatherSettings {
+ // Convert from kebab-case to Title Case if needed
+ const formattedName = formatBiomeName(biomeName)
+ const biome = biomeData[formattedName]
+ if (!biome) return defaultSettings
+
+ // Get average values for each parameter
+ const avgTemp = (biome.temperature[0] + biome.temperature[1]) / 2
+ const avgCloudCount = (biome.cloudCount[0] + biome.cloudCount[1]) / 2
+ const avgWaterLevel = (biome.waterLevel[0] + biome.waterLevel[1]) / 2
+ const avgVolcanicActivity = (biome.volcanicActivity[0] + biome.volcanicActivity[1]) / 2
+ const avgSurfaceRoughness = (biome.surfaceRoughness[0] + biome.surfaceRoughness[1]) / 2
+
+ // Determine weather settings based on biome parameters
+ return {
+ // Enable clouds if cloud count is above 30%
+ clouds: avgCloudCount > 30,
+
+ // Enable sun if temperature is above 260K (not too cold)
+ sun: avgTemp > 260,
+
+ // Enable rain if water level is above 0.3 (30%)
+ rain: avgWaterLevel > 0.3,
+
+ // Enable snow if temperature is below 240K (cold)
+ snow: avgTemp < 240,
+
+ // Enable lightning if volcanic activity is above 0.4 (40%)
+ lightning: avgVolcanicActivity > 0.4,
+
+ // Enable hail if surface roughness is above 0.6 (60%) - representing turbulent conditions
+ hail: avgSurfaceRoughness > 0.6,
+ }
+}
+
+// Calculate weather intensity based on biome data
+function getWeatherIntensity(biomeName: string) {
+ const formattedName = formatBiomeName(biomeName)
+ if (!biomeData[formattedName]) {
+ return {
+ rainIntensity: 0.5,
+ snowIntensity: 0.5,
+ hailIntensity: 0.5,
+ cloudIntensity: 0.5,
+ lightningIntensity: 0.5,
+ }
+ }
+
+ const data = biomeData[formattedName]
+
+ // Map biome parameters to weather intensities (0-1 scale)
+ return {
+ rainIntensity: data.waterLevel[1] * data.atmosphereStrength[1],
+ snowIntensity: (400 - data.temperature[0]) / 300, // Higher for colder temperatures
+ hailIntensity: data.atmosphereStrength[1] * data.plateTectonics[1],
+ cloudIntensity: data.cloudCount[1] / 100,
+ lightningIntensity: data.volcanicActivity[1] * data.atmosphereStrength[1],
+ }
+}
+
+export default function WeatherGenerator({ biome = "", settings }: WeatherGeneratorProps) {
+ const svgRef = useRef(null)
+ const [weatherSettings, setWeatherSettings] = useState(defaultSettings)
+ const [showControls, setShowControls] = useState(false)
+ const [selectedBiome, setSelectedBiome] = useState(formatBiomeName(biome))
+
+ // Update settings when biome prop changes
+ useEffect(() => {
+ if (biome) {
+ const formattedBiome = formatBiomeName(biome)
+ setSelectedBiome(formattedBiome)
+ if (!settings) {
+ setWeatherSettings(getWeatherSettingsForBiome(biome))
+ }
+ }
+ }, [biome, settings])
+
+ // Update settings when settings prop changes
+ useEffect(() => {
+ if (settings) {
+ setWeatherSettings(settings)
+ }
+ }, [settings])
+
+ const updateSetting = (key: keyof WeatherSettings, value: boolean) => {
+ setWeatherSettings((prev) => ({ ...prev, [key]: value }))
+ }
+
+ const handleBiomeChange = (biomeName: string) => {
+ setSelectedBiome(biomeName)
+ setWeatherSettings(getWeatherSettingsForBiome(kebabCase(biomeName)))
+ }
+
+ useEffect(() => {
+ if (!svgRef.current) return
+
+ const svg = d3.select(svgRef.current)
+ const width = window.innerWidth
+ const height = window.innerHeight
+
+ // Get weather intensity for the selected biome
+ const intensity = getWeatherIntensity(selectedBiome)
+
+ // Define alien world color palette
+ const colors = {
+ sky: "#0a0a1a",
+ clouds: ["#4a5fc1", "#5a6fc1", "#6a7fc1"],
+ sun: "#ff9e00",
+ sunRays: "#ff5400",
+ rain: "#00c8ff",
+ snow: "#a0e6ff",
+ hail: "#4cc9f0",
+ lightning: "#ffffff", // Changed to white to match the gas giant storms
+ }
+
+ // Add a subtle gradient background
+ const defs = svg.append("defs")
+
+ // Update the gradient background
+ const gradient = defs
+ .append("linearGradient")
+ .attr("id", "alien-sky-gradient")
+ .attr("x1", "0%")
+ .attr("y1", "0%")
+ .attr("x2", "100%")
+ .attr("y2", "100%")
+
+ gradient.append("stop").attr("offset", "0%").attr("stop-color", colors.sky).attr("stop-opacity", 0.7)
+ gradient.append("stop").attr("offset", "100%").attr("stop-color", "#1a2a4a").attr("stop-opacity", 0.3)
+
+ // Add filter for glow effects
+ const filter = defs
+ .append("filter")
+ .attr("id", "glow")
+ .attr("x", "-50%")
+ .attr("y", "-50%")
+ .attr("width", "200%")
+ .attr("height", "200%")
+
+ filter.append("feGaussianBlur").attr("stdDeviation", "3.5").attr("result", "coloredBlur")
+
+ const feMerge = filter.append("feMerge")
+ feMerge.append("feMergeNode").attr("in", "coloredBlur")
+ feMerge.append("feMergeNode").attr("in", "SourceGraphic")
+
+ // Clear any existing elements
+ svg.selectAll("*:not(defs)").remove()
+
+ // Add subtle background
+ svg
+ .append("rect")
+ .attr("width", width)
+ .attr("height", height)
+ .attr("fill", "url(#alien-sky-gradient)")
+ .attr("opacity", 0.3)
+
+ // Create alien clouds
+ if (weatherSettings.clouds) {
+ const cloudGroup = svg.append("g")
+
+ const createCloud = (x: number, y: number, scale: number) => {
+ const cloudColor = colors.clouds[Math.floor(Math.random() * colors.clouds.length)]
+
+ const cloud = cloudGroup
+ .append("g")
+ .attr("transform", `translate(${x}, ${y}) scale(${scale})`)
+ .style("filter", "url(#glow)")
+
+ // Create more alien-looking cloud shapes
+ for (let i = 0; i < 5; i++) {
+ const cx = 20 + i * 15 + Math.random() * 10
+ const cy = 25 + Math.random() * 10
+ const r = 10 + Math.random() * 20
+
+ cloud
+ .append("circle")
+ .attr("cx", cx)
+ .attr("cy", cy)
+ .attr("r", r)
+ .attr("fill", cloudColor)
+ .attr("opacity", 0.7 + Math.random() * 0.3)
+ }
+
+ // Add tendrils/wisps to make clouds look more alien
+ for (let i = 0; i < 3; i++) {
+ const startX = 30 + Math.random() * 40
+ const startY = 40 + Math.random() * 10
+ const controlX = startX + 20 - Math.random() * 40
+ const controlY = startY + 20 + Math.random() * 10
+ const endX = startX + 30 - Math.random() * 60
+ const endY = startY + 30 + Math.random() * 10
+
+ cloud
+ .append("path")
+ .attr("d", `M${startX},${startY} Q${controlX},${controlY} ${endX},${endY}`)
+ .attr("stroke", cloudColor)
+ .attr("stroke-width", 2 + Math.random() * 3)
+ .attr("fill", "none")
+ .attr("opacity", 0.6)
+ }
+
+ // Animate cloud with slight pulsing
+ cloud
+ .transition()
+ .duration(30000)
+ .ease(d3.easeLinear)
+ .attr("transform", `translate(${x + 300}, ${y}) scale(${scale})`)
+ .on("end", function () {
+ d3.select(this).remove()
+ createCloud(0, y, scale)
+ })
+
+ // Add subtle pulsing effect
+ function pulseCloud() {
+ cloud
+ .transition()
+ .duration(2000)
+ .ease(d3.easeSinInOut)
+ .attr("opacity", 0.7)
+ .transition()
+ .duration(2000)
+ .ease(d3.easeSinInOut)
+ .attr("opacity", 1)
+ .on("end", pulseCloud)
+ }
+
+ pulseCloud()
+ }
+
+ // Create alien clouds at different positions - number based on intensity
+ const cloudCount = Math.floor(3 + intensity.cloudIntensity * 5)
+ for (let i = 0; i < cloudCount; i++) {
+ createCloud(Math.random() * width, 50 + Math.random() * 150, 0.6 + Math.random() * 0.5)
+ }
+ }
+
+ // Create alien sun
+ if (weatherSettings.sun) {
+ const sunGroup = svg
+ .append("g")
+ .attr("transform", `translate(${width - 120}, 80)`)
+ .style("filter", "url(#glow)")
+
+ // Create binary star system (two suns)
+ sunGroup.append("circle").attr("r", 25).attr("fill", colors.sun).attr("opacity", 0.9)
+
+ sunGroup
+ .append("circle")
+ .attr("cx", 15)
+ .attr("cy", -15)
+ .attr("r", 15)
+ .attr("fill", colors.sunRays)
+ .attr("opacity", 0.8)
+
+ // Create corona/atmosphere around the suns
+ sunGroup
+ .append("circle")
+ .attr("r", 35)
+ .attr("fill", "none")
+ .attr("stroke", colors.sunRays)
+ .attr("stroke-width", 2)
+ .attr("stroke-dasharray", "3,3")
+ .attr("opacity", 0.6)
+
+ // Create unusual sun rays - more like energy tendrils
+ const rayData: number[] = [0, 60, 120, 180, 240, 300]
+
+ sunGroup
+ .selectAll(".ray")
+ .data(rayData)
+ .enter()
+ .append("path")
+ .attr("class", "ray")
+ .attr("d", (d: number) => {
+ const x1 = 0
+ const y1 = 0
+ const length = 45 + Math.random() * 15
+ const x2 = Math.cos((d * Math.PI) / 180) * length
+ const y2 = Math.sin((d * Math.PI) / 180) * length
+ const cpx = Math.cos(((d + 10) * Math.PI) / 180) * (length * 0.6)
+ const cpy = Math.sin(((d + 10) * Math.PI) / 180) * (length * 0.6)
+ return `M${x1},${y1} Q${cpx},${cpy} ${x2},${y2}`
+ })
+ .attr("stroke", colors.sunRays)
+ .attr("stroke-width", 2)
+ .attr("fill", "none")
+ .attr("opacity", 0.7)
+
+ // Animate sun rays
+ function animateSunRays() {
+ sunGroup
+ .selectAll(".ray")
+ .transition()
+ .duration(3000)
+ .attr("d", (d: number) => {
+ const x1 = 0
+ const y1 = 0
+ const length = 45 + Math.random() * 25
+ const x2 = Math.cos((d * Math.PI) / 180) * length
+ const y2 = Math.sin((d * Math.PI) / 180) * length
+ const cpx = Math.cos(((d + 20 * Math.random()) * Math.PI) / 180) * (length * 0.6)
+ const cpy = Math.sin(((d + 20 * Math.random()) * Math.PI) / 180) * (length * 0.6)
+ return `M${x1},${y1} Q${cpx},${cpy} ${x2},${y2}`
+ })
+ .transition()
+ .duration(3000)
+ .attr("d", (d: number) => {
+ const x1 = 0
+ const y1 = 0
+ const length = 45 + Math.random() * 15
+ const x2 = Math.cos((d * Math.PI) / 180) * length
+ const y2 = Math.sin((d * Math.PI) / 180) * length
+ const cpx = Math.cos(((d - 20 * Math.random()) * Math.PI) / 180) * (length * 0.6)
+ const cpy = Math.sin(((d - 20 * Math.random()) * Math.PI) / 180) * (length * 0.6)
+ return `M${x1},${y1} Q${cpx},${cpy} ${x2},${y2}`
+ })
+ .on("end", animateSunRays)
+ }
+
+ animateSunRays()
+ }
+
+ // Create alien rain - glowing droplets
+ if (weatherSettings.rain) {
+ const rainGroup = svg.append("g")
+
+ // Number of raindrops based on intensity
+ const rainCount = Math.floor(10 + intensity.rainIntensity * 30)
+
+ for (let i = 0; i < rainCount; i++) {
+ const x = Math.random() * width
+ const y = -20 - Math.random() * 100
+ const speed = 1.5 + Math.random() * 1.5
+ const length = 5 + Math.random() * 10
+
+ // Create glowing droplet
+ const raindrop = rainGroup.append("g").style("filter", "url(#glow)")
+
+ raindrop
+ .append("line")
+ .attr("x1", x)
+ .attr("y1", y)
+ .attr("x2", x)
+ .attr("y2", y + length)
+ .attr("stroke", colors.rain)
+ .attr("stroke-width", 2)
+ .attr("opacity", 0.8)
+
+ function animateRaindrop() {
+ raindrop
+ .transition()
+ .duration(2000 / speed)
+ .ease(d3.easeLinear)
+ .attr("transform", `translate(0, ${height + 20})`)
+ .on("end", function () {
+ d3.select(this).attr("transform", "translate(0, 0)").call(animateRaindrop)
+ })
+ }
+
+ animateRaindrop()
+ }
+ }
+
+ // Create enhanced alien lightning
+ let clearLightning: () => void = () => {}
+ if (weatherSettings.lightning) {
+ const createLightning = () => {
+ // Lightning frequency based on intensity (higher = more frequent)
+ const interval = Math.max(2000, 8000 - intensity.lightningIntensity * 6000)
+
+ const lightningInterval = setInterval(() => {
+ // Create multiple storm clusters instead of single bolts
+ const stormCount = 1 + Math.floor(Math.random() * 3)
+
+ for (let s = 0; s < stormCount; s++) {
+ const x = Math.random() * width * 0.8 + width * 0.1
+ const y = 100 + Math.random() * 150
+
+ // Create a storm cluster
+ const stormGroup = svg.append("g").attr("transform", `translate(${x}, ${y})`).style("filter", "url(#glow)")
+
+ // Generate swirling storm pattern inspired by gas giant storms
+ const generateStormPattern = () => {
+ const stormSize = 30 + Math.random() * 40
+ const spiralCount = 3 + Math.floor(Math.random() * 3)
+ const spiralTightness = 0.2 + Math.random() * 0.3
+
+ // Create the main storm body - a glowing ellipse
+ stormGroup
+ .append("ellipse")
+ .attr("rx", stormSize)
+ .attr("ry", stormSize * 0.7)
+ .attr("fill", "#ffffff")
+ .attr("opacity", 0.2)
+
+ // Create swirling spiral patterns
+ for (let i = 0; i < spiralCount; i++) {
+ const spiralPath = d3.path()
+ const startAngle = Math.random() * Math.PI * 2
+ const rotationDir = Math.random() > 0.5 ? 1 : -1
+
+ spiralPath.moveTo(0, 0)
+
+ for (let t = 0; t < 10; t++) {
+ const angle = startAngle + rotationDir * t * spiralTightness
+ const radius = t * (stormSize / 10)
+ const x = Math.cos(angle) * radius
+ const y = Math.sin(angle) * radius
+
+ spiralPath.lineTo(x, y)
+ }
+
+ stormGroup
+ .append("path")
+ .attr("d", spiralPath.toString())
+ .attr("stroke", "#ffffff")
+ .attr("stroke-width", 2)
+ .attr("fill", "none")
+ .attr("opacity", 0.7)
+ }
+
+ // Add bright spots within the storm
+ const spotCount = 3 + Math.floor(Math.random() * 5)
+ for (let i = 0; i < spotCount; i++) {
+ const angle = Math.random() * Math.PI * 2
+ const distance = Math.random() * stormSize * 0.7
+ const spotX = Math.cos(angle) * distance
+ const spotY = Math.sin(angle) * distance
+ const spotSize = 2 + Math.random() * 4
+
+ stormGroup
+ .append("circle")
+ .attr("cx", spotX)
+ .attr("cy", spotY)
+ .attr("r", spotSize)
+ .attr("fill", "#ffffff")
+ .attr("opacity", 0.9)
+ }
+ }
+
+ generateStormPattern()
+
+ // Animate storm flash and dissipation
+ stormGroup
+ .transition()
+ .duration(200)
+ .attr("opacity", 1)
+ .transition()
+ .duration(100)
+ .attr("opacity", 0.7)
+ .transition()
+ .duration(100)
+ .attr("opacity", 0.9)
+ .transition()
+ .duration(1500)
+ .attr("opacity", 0)
+ .remove()
+ }
+ }, interval)
+
+ return () => clearInterval(lightningInterval)
+ }
+
+ clearLightning = createLightning()
+ }
+
+ // Create alien snow - glowing spores/particles
+ if (weatherSettings.snow) {
+ const snowGroup = svg.append("g")
+
+ // Number of snowflakes based on intensity
+ const snowCount = Math.floor(5 + intensity.snowIntensity * 25)
+
+ for (let i = 0; i < snowCount; i++) {
+ const x = Math.random() * width
+ const y = -20 - Math.random() * 100
+ const size = 2 + Math.random() * 3
+ const speed = 0.5 + Math.random() * 0.5
+
+ // Create alien spore/particle
+ const snowflake = snowGroup.append("g").attr("transform", `translate(${x}, ${y})`).style("filter", "url(#glow)")
+
+ // Randomly choose between different alien particle shapes
+ const shapeType = Math.floor(Math.random() * 3)
+
+ if (shapeType === 0) {
+ // Glowing circle
+ snowflake.append("circle").attr("r", size).attr("fill", colors.snow).attr("opacity", 0.7)
+ } else if (shapeType === 1) {
+ // Star shape
+ const points = 5
+ const outerRadius = size
+ const innerRadius = size / 2
+
+ let pathData = ""
+ for (let i = 0; i < points * 2; i++) {
+ const radius = i % 2 === 0 ? outerRadius : innerRadius
+ const angle = (Math.PI / points) * i
+ const px = Math.sin(angle) * radius
+ const py = -Math.cos(angle) * radius
+ pathData += (i === 0 ? "M" : "L") + px + "," + py
+ }
+ pathData += "Z"
+
+ snowflake.append("path").attr("d", pathData).attr("fill", colors.snow).attr("opacity", 0.7)
+ } else {
+ // Hexagon
+ const hexPoints = []
+ for (let i = 0; i < 6; i++) {
+ const angle = (Math.PI / 3) * i
+ hexPoints.push([Math.sin(angle) * size, -Math.cos(angle) * size])
+ }
+
+ snowflake
+ .append("polygon")
+ .attr("points", hexPoints.map((p) => p.join(",")).join(" "))
+ .attr("fill", colors.snow)
+ .attr("opacity", 0.7)
+ }
+
+ // Add subtle rotation during animation
+ function animateSnowflake() {
+ snowflake
+ .transition()
+ .duration(6000 / speed)
+ .ease(d3.easeLinear)
+ .attr(
+ "transform",
+ `translate(${x + (Math.random() * 100 - 50)}, ${height + 20}) rotate(${Math.random() * 360})`,
+ )
+ .on("end", function () {
+ d3.select(this).attr("transform", `translate(${x}, ${y})`).call(animateSnowflake)
+ })
+ }
+
+ animateSnowflake()
+ }
+ }
+
+ // Create alien hail - crystalline structures
+ if (weatherSettings.hail) {
+ const hailGroup = svg.append("g")
+
+ // Number of hail pieces based on intensity
+ const hailCount = Math.floor(5 + intensity.hailIntensity * 15)
+
+ for (let i = 0; i < hailCount; i++) {
+ const x = Math.random() * width
+ const y = -20 - Math.random() * 100
+ const size = 4 + Math.random() * 3
+ const speed = 2 + Math.random() * 1
+
+ // Create crystalline hail
+ const hail = hailGroup.append("g").attr("transform", `translate(${x}, ${y})`).style("filter", "url(#glow)")
+
+ // Create diamond/crystal shape
+ const points = [
+ [0, -size],
+ [size, 0],
+ [0, size],
+ [-size, 0],
+ ]
+
+ hail
+ .append("polygon")
+ .attr("points", points.map((p) => p.join(",")).join(" "))
+ .attr("fill", colors.hail)
+ .attr("opacity", 0.8)
+
+ // Add inner glow
+ hail
+ .append("polygon")
+ .attr("points", points.map((p) => [p[0] * 0.5, p[1] * 0.5].join(",")).join(" "))
+ .attr("fill", "#ffffff")
+ .attr("opacity", 0.5)
+
+ function animateHail() {
+ hail
+ .transition()
+ .duration(1500 / speed)
+ .ease(d3.easeLinear)
+ .attr("transform", `translate(${x}, ${height + 20}) rotate(${Math.random() * 180})`)
+ .on("end", function () {
+ d3.select(this).attr("transform", `translate(${x}, ${y})`).call(animateHail)
+ })
+ }
+
+ animateHail()
+ }
+ }
+
+ // Cleanup function
+ return () => {
+ clearLightning()
+ }
+ }, [weatherSettings, selectedBiome])
+
+ return (
+ <>
+
+
+
+ {/* {biome}
*/}
+
+
+ {/* Update the button styling */}
+
setShowControls(!showControls)}
+ >
+ {showControls ? "×" : "☂"}
+
+
+ {showControls && (
+
+
+
Weather Controls
+
+
+
+
+ Biome
+
+
+
+
+
+
+ {Object.keys(biomeData).map((biome) => (
+
+ {biome}
+
+ ))}
+
+
+
+
+
+ {(Object.keys(weatherSettings) as Array
).map((key) => (
+
+
+ {key}
+
+ updateSetting(key, checked)}
+ className="data-[state=checked]:bg-blue-500"
+ />
+
+ ))}
+
+
+ {selectedBiome && biomeData[selectedBiome] && (
+
+
+ Based on biome parameters: temperature {biomeData[selectedBiome]?.temperature[0]}-
+ {biomeData[selectedBiome]?.temperature[1]}K, cloud count {biomeData[selectedBiome]?.cloudCount[0]}-
+ {biomeData[selectedBiome]?.cloudCount[1]}
+
+
+ )}
+
+ )}
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/components/Data/Generator/WeatherPlusMap.tsx b/components/Data/Generator/WeatherPlusMap.tsx
new file mode 100644
index 000000000..038774b1b
--- /dev/null
+++ b/components/Data/Generator/WeatherPlusMap.tsx
@@ -0,0 +1,81 @@
+import { useEffect, useRef } from 'react'
+
+interface MinimalMapProps {
+ width: number
+ height: number
+}
+
+export function MinimalMap({ width, height }: MinimalMapProps) {
+ const canvasRef = useRef(null)
+
+ useEffect(() => {
+ const canvas = canvasRef.current
+ if (!canvas) return
+
+ const ctx = canvas.getContext('2d')
+ if (!ctx) return
+
+ let animationFrameId: number
+
+ const drawContours = () => {
+ ctx.clearRect(0, 0, width, height)
+ ctx.strokeStyle = '#2C4F64'
+ ctx.lineWidth = 0.5
+
+ for (let i = 0; i < 5; i++) {
+ ctx.beginPath()
+ ctx.moveTo(0, (height / 5) * i)
+ ctx.bezierCurveTo(
+ width / 3,
+ (height / 5) * i + Math.random() * 20 - 10,
+ (2 * width) / 3,
+ (height / 5) * i + Math.random() * 20 - 10,
+ width,
+ (height / 5) * i
+ )
+ ctx.stroke()
+ }
+ }
+
+ const raindrops: { x: number; y: number; speed: number; size: number }[] = []
+
+ const createRaindrop = () => {
+ raindrops.push({
+ x: Math.random() * width,
+ y: 0,
+ speed: Math.random() * 2 + 1,
+ size: Math.random() * 2 + 1,
+ })
+ }
+
+ const drawRain = () => {
+ ctx.fillStyle = '#85DDA2'
+ raindrops.forEach((drop) => {
+ ctx.beginPath()
+ ctx.arc(drop.x, drop.y, drop.size, 0, Math.PI * 2)
+ ctx.fill()
+ drop.y += drop.speed
+ if (drop.y > height) {
+ drop.y = 0
+ drop.x = Math.random() * width
+ }
+ })
+ }
+
+ const animate = () => {
+ drawContours()
+ drawRain()
+ if (Math.random() < 0.1) createRaindrop()
+ animationFrameId = requestAnimationFrame(animate)
+ }
+
+ animate()
+
+ return () => {
+ cancelAnimationFrame(animationFrameId)
+ }
+ }, [width, height])
+
+ return
+}
+
diff --git a/components/Data/RelatedClassifications.tsx b/components/Data/RelatedClassifications.tsx
new file mode 100644
index 000000000..bdfed55f3
--- /dev/null
+++ b/components/Data/RelatedClassifications.tsx
@@ -0,0 +1,134 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useSupabaseClient } from '@supabase/auth-helpers-react';
+
+interface Comment {
+ id: number;
+ content: string;
+ created_at: string;
+ author: string;
+ classification_id: number;
+};
+
+interface Classification {
+ id: number;
+ content: string;
+ created_at: string;
+ classificationConfiguration: {
+ parentPlanetLocation?: number;
+ [key: string]: any;
+ };
+ anomaly: number | null;
+ media?: any;
+};
+
+interface RelatedClassificationsProps {
+ parentId: number;
+};
+
+export default function RelatedClassifications({ parentId }: RelatedClassificationsProps) {
+ const supabase = useSupabaseClient();
+ const [comments, setComments] = useState([]);
+ const [relatedClassifications, setRelatedClassifications] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setLoading(true);
+ setError(null);
+
+ // Step 1: Get the classification with parentId to find its anomaly
+ const { data: baseClassification, error: baseError } = await supabase
+ .from('classifications')
+ .select('id, anomaly, classificationConfiguration')
+ .eq('id', parentId)
+ .single();
+
+ if (baseError || !baseClassification) {
+ setError('Failed to fetch base classification.');
+ setLoading(false);
+ return;
+ }
+
+ const { anomaly } = baseClassification;
+ if (!anomaly) {
+ setError('No anomaly found in base classification.');
+ setLoading(false);
+ return;
+ }
+
+ // Step 2: Get all classifications with matching parentPlanetLocation = anomaly
+ const { data: matchingClassifications, error: matchError } = await supabase
+ .from('classifications')
+ .select('*')
+ .filter('classificationConfiguration->>parentPlanetLocation', 'eq', anomaly.toString());
+
+ if (matchError) {
+ setError('Failed to fetch related classifications.');
+ setLoading(false);
+ return;
+ }
+
+ setRelatedClassifications(matchingClassifications || []);
+
+ // Step 3: Get all comments where classification_id = parentId
+ const { data: classificationComments, error: commentsError } = await supabase
+ .from('comments')
+ .select('*')
+ .eq('classification_id', parentId);
+
+ if (commentsError) {
+ setError('Failed to fetch comments.');
+ setLoading(false);
+ return;
+ }
+
+ setComments(classificationComments || []);
+ setLoading(false);
+ };
+
+ fetchData();
+ }, [parentId, supabase]);
+
+ if (loading) return Loading related classifications and comments...
;
+ if (error) return {error}
;
+
+ return (
+
+
+
Comments on This Classification
+ {comments.length === 0 ? (
+
No comments found.
+ ) : (
+
+ {comments.map((comment) => (
+
+
{comment.content}
+
Posted on {new Date(comment.created_at).toLocaleString()}
+
+ ))}
+
+ )}
+
+
+
+
Classifications with Same Anomaly Location
+ {relatedClassifications.length === 0 ? (
+
No related classifications found.
+ ) : (
+
+ {relatedClassifications.map((rc) => (
+
+
+ {JSON.stringify(rc, null, 2)}
+
+
+ ))}
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Data/ZoodexDataSources.tsx b/components/Data/ZoodexDataSources.tsx
new file mode 100644
index 000000000..a85136c39
--- /dev/null
+++ b/components/Data/ZoodexDataSources.tsx
@@ -0,0 +1,366 @@
+'use client';
+
+import { CameraIcon, CloudDrizzleIcon, DogIcon, MicroscopeIcon, SunDimIcon, TelescopeIcon, TestTube } from 'lucide-react';
+import { BurrowingOwlIcon } from '../../content/Icons/BurrowingOwl';
+import { FishIcon } from '../../content/Icons/FishIcon';
+import { StarterTelescopeTess } from '../Projects/Telescopes/Transiting';
+import { DailyMinorPlanet } from '../Projects/Telescopes/DailyMinorPlanet';
+import { PlanetFourProject } from '../Projects/Satellite/PlanetFour';
+import { StarterSunspot } from '../Projects/Telescopes/Sunspots';
+import { TelescopeDiskDetector } from '../Projects/Telescopes/DiskDetector';
+// import CameraComponent from '../Projects/Zoodex/Upload/Camera';
+import { DataSourcesModal } from './unlockNewDataSources';
+import { BurrowingOwl } from '../Projects/Zoodex/burrowingOwls';
+import { PenguinWatch } from '../Projects/Zoodex/penguinWatch';
+import { ZoodexIguanas } from '../Projects/Zoodex/iguanasFromAbove';
+
+export const zoodexDataSources = [
+ {
+ category: 'Animals',
+ items: [
+ {
+ name: 'Burrowing Owls',
+ description: 'Wildwatch Burrowing Owl seeks to document the behaviors and developmental milestones of burrowing owl families “holed up” in Otay Mesa, CA. Motion-activated cameras are strategically positioned at burrow entrances to collect candid data.',
+ identifier: 'zoodex-burrowingOwl',
+ researchId: 'wildwatch-burrowing-owl',
+ researcher: 'zooniverse/sandiego-zoo-global',
+ // icon: BurrowingOwlIcon,
+ unlocked: false,
+ compatiblePlanetTypes: ['Lush'],
+ bestPlanetType: ['Lush'],
+ tutorialMission: 3000002,
+ activeStructure: 3104,
+ techId: 5,
+ },
+ {
+ name: "Penguin Watch",
+ description: "Count penguin adults, chicks and eggs in far away lands to help us understand their lives and environment",
+ identifier: 'zoodex-penguinWatch',
+ researchId: 'penguin-watch',
+ researcher: 'zooniverse/penguintom79',
+ unlocked: false,
+ compatiblePlanetTypes: ['Frozen', 'Lush'],
+ bestPlanetType: ['Lush'],
+ tutorialMission: 200000010,
+ activeStructure: 3104,
+ techId: 5,
+ },
+ {
+ name: 'Iguanas from Above',
+ description: 'Help us count Galapagos Marine Iguanas from aerial photographs',
+ identifier: 'zoodex-iguanasFromAbove',
+ researchId: 'iguanas-from-above',
+ researcher: 'zooniverse/andreavarela89',
+ unlocked: false,
+ compatiblePlanetTypes: ['Lush'],
+ bestPlanetType: ['Lush'],
+ tutorialMission: 3000004,
+ activeStructure: 3104,
+ techId: 5,
+ },
+ // {
+ // name: 'South Coast Fauna Recovery',
+ // description: 'Help us protect Western Australia's rarest mammal and bird: Gilbert's Potoroo and the Western Ground Parrot',
+ // identifier: 'zoodex-southCoastFaunaRecovery',
+ // researchId: 'south-coast-threatened-fauna-recovery-project',
+ // researcher: 'zooniverse/abbsta',
+ // unlocked: false,
+ // compatiblePlanetTypes: ['Lush'],
+ // bestPlanetType: ['Lush'],
+ // tutorialMission: 3000006,
+ // activeStructure: 3104,
+ // },
+ {
+ name: 'Nest Quest Go',
+ description: 'From avocet to vireos, help transcribe historical nest record cards for an exciting variety of species',
+ identifier: 'zoodex-nestQuestGo',
+ researchId: 'nest-quest-go-bird-medley',
+ researcher: 'zooniverse/brbcornell',
+ unlocked: false,
+ compatiblePlanetTypes: ['Lush'],
+ bestPlanetType: ['Lush'],
+ tutorialMission: 3000005,
+ activeStructure: 3104,
+ techId: 5,
+ },
+ {
+ name: "Plankton Portal",
+ description: "No plankton = No life in the ocean. Help us better understand the health of our oceans through plankton.",
+ identifier: 'zoodex-planktonPortal',
+ researchId: 'plankton-portal',
+ researcher: 'zooniverse/kelseyswieca',
+ unlocked: false,
+ compatiblePlanetTypes: ['Lush'],
+ bestPlanetType: ['Lush'],
+ tutorialMission: 200000012,
+ activeStructure: 3104,
+ techId: 5,
+ },
+ ],
+ },
+];
+
+export const telescopeDataSources = [
+ {
+ category: 'Planets',
+ items: [
+ {
+ name: 'Transiting Exoplanet Survey Satellite Search',
+ description: 'Hunt for exoplanet candidates using lightcurve data',
+ identifier: 'telescope-tess',
+ researchId: 'planet-hunters-tess',
+ researcher: 'zooniverse',
+ unlocked: false,
+ compatiblePlanetTypes: ['Lush', 'Frozen', 'Hellhole', 'Arid'],
+ bestPlanetType: ['Lush', 'Frozen', 'Hellhole', 'Arid'],
+ tutorialMission: 3000001,
+ activeStructure: 3103,
+ researched: 'telescope-tess',
+ techId: 1,
+ dynamicComponent:
+ }
+ ]
+ },
+ {
+ category: 'Solar System Objects',
+ items: [
+ {
+ name: 'The Daily Minor Planet',
+ description: 'Discover new asteroids every day by searching through your telescope data',
+ identifier: 'telescope-minorPlanet',
+ researchId: 'the-daily-minor-planet',
+ researcher: 'zooniverse/fulsdavid',
+ unlocked: false,
+ compatiblePlanetTypes: ['Arid', 'Hellhole', 'Asteroid', 'Frozen'],
+ bestPlanetType: ['Asteroid'],
+ tutorialMission: 20000004,
+ activeStructure: 3103,
+ researched: 'telescope-minorPlanets',
+ techId: 1,
+ dynamicComponent:
+ },
+ {
+ name: 'Planet Four',
+ description: "Help explore the surface and weather of Mars' south polar region",
+ identifier: 'satellite-planetFour',
+ researchId: 'planet-four',
+ researcher: 'zooniverse/mschwamb',
+ unlocked: false,
+ compatiblePlanetTypes: ['Frozen', 'Lush', 'Asteroid'],
+ bestPlanetType: ['Frozen'],
+ tutorialMission: 20000005,
+ activeStructure: 3103,
+ researched: 'satellite-planetFour',
+ techId: 1,
+ dynamicComponent:
+ }
+ ]
+ },
+ {
+ category: 'Solar observations',
+ items: [
+ {
+ name: 'Sun-spot observations',
+ description: 'Observe and classify sunspots',
+ identifier: 'telescope-sunspots',
+ researchId: 'sunspot-detectives',
+ researcher: 'zooniverse/teolixx',
+ unlocked: false,
+ compatiblePlanetTypes: ['Lush', 'Frozen', 'Hellhole', 'Arid'],
+ bestPlanetType: ['Lush', 'Frozen', 'Hellhole', 'Arid'],
+ tutorialMission: 3000003,
+ activeStructure: 3103,
+ researched: 'telescope-sunspots',
+ techId: 5,
+ dynamicComponent:
+ }
+ ]
+ },
+ {
+ category: 'exo-Solar System Observations',
+ items: [
+ {
+ name: 'Disk Detective',
+ description: 'Find the birthplace of solar systems',
+ identifier: 'telescope-diskDetective',
+ researchId: 'disk-detective',
+ researcher: 'zooniverse/ssilverberg',
+ unlocked: false,
+ compatiblePlanetTypes: ['Lush', 'Frozen', 'Hellhole', 'Arid'],
+ bestPlanetType: ['Lush', 'Frozen', 'Hellhole', 'Arid'],
+ tutorialMission: 3000009,
+ activeStructure: 3103,
+ researched: 'telescope-diskDetective',
+ techId: 5,
+ dynamicComponent:
+ },
+ ],
+ },
+];
+
+
+export const lidarDataSources = [
+ {
+ category: 'Clouds',
+ items: [
+ {
+ name: 'Martian Cloud Survey',
+ description: 'Survey and classify clouds on Mars and similar planets',
+ identifier: 'lidar-martianClouds',
+ researchId: 'cloudspotting-on-mars',
+ researcher: 'zooniverse',
+ // icon: CloudDrizzleIcon,
+ unlocked: false,
+ compatiblePlanetTypes: ['Frozen', 'Arid'],
+ bestPlanetType: ['Frozen'],
+ tutorialMission: 3000010,
+ activeStructure: 3105,
+ techId: 5,
+ },
+ {
+ name: 'Jovian Vortex Hunter',
+ description: "Help find interesting features & discover fluid dynamics in Jupiter's atmosphere",
+ identifier: 'lidar-jovianVortexHunter',
+ researchId: 'jovian-vortex-hunter',
+ researcher: 'zooniverse/ramanakumars',
+ unlocked: false,
+ compatiblePlanetTypes: ['GasGiant', 'IceGiant', 'Asteroid', 'Frozen'],
+ bestPlanetType: ['GasGiant'],
+ tutorialMission: 20000007,
+ activeStructure: 3105,
+ techId: 5,
+ },
+ ],
+ },
+];
+
+export const physicsLabDataSources = [
+ {
+ category: 'Particle Accelerator',
+ items: [
+ {
+ name: 'Cosmic Muon Images',
+ description: 'Using Muon Tomography we can probe the internal structure of massive objects, like volcanoes, with particles from stars and galaxies far far away... help us identify these particles inside our detectors',
+ identifier: 'physicslab-cosmicMuons',
+ researchId: 'muon-cosmic-muon-images',
+ researcher: 'zooniverse/reinforce',
+ unlocked: false,
+ compatiblePlanetTypes: ['Frozen', 'Hellhole', 'Arid', 'Lush', 'IceGiant', 'GasGiant'],
+ bestPlanetType: ['Hellhole'],
+ tutorialMission: 20000002,
+ activeStructure: 31010,
+ techId: 6,
+ },
+ {
+ name: 'Name that Neutrino',
+ description: 'When a neutrino interacts in ice, a signal or light pattern is produced. Classify the shape of the signals to help us understand the pattern and movement of these particles',
+ identifier: 'physicslab-neutrinoName',
+ researchId: 'name-that-neutrino',
+ researcher: 'zooniverse/icecubeobservatory',
+ unlocked: false,
+ compatiblePlanetTypes: ['Frozen', 'Hellhole', 'Arid', 'Lush', 'IceGiant', 'GasGiant'],
+ bestPlanetType: ['Frozen'],
+ tutorialMission: 20000001,
+ activeStructure: 31010,
+ techId: 6,
+ },
+ ],
+ },
+];
+
+export const roverDataSources = [
+ {
+ category: 'Terrain',
+ items: [
+ {
+ name: 'Rover photo analysis',
+ description: 'Classify photos from your rover to interpret the terrain formation',
+ identifier: 'automaton-roverPhotos',
+ researchId: 'null',
+ researcher: 'star-sailors',
+ unlocked: false,
+ compatiblePlanetTypes: ['Frozen', 'Hellhole', 'Arid', 'Lush'],
+ bestPlanetType: ['Frozen'],
+ tutorialMission: 3000011,
+ activeStructure: 3102,
+ techId: 5,
+ },
+ {
+ name: 'AI For Mars',
+ description: 'Help improve the rovers’ ability to identify different, sometimes dangerous terrain - an essential skill for autonomous exploration',
+ identifier: 'automaton-aiForMars',
+ researchId: 'ai4mars',
+ researcher: 'zooniverse/hiro-ono',
+ unlocked: false,
+ compatiblePlanetTypes: ['Frozen', 'Hellhole', 'Arid', 'Lush', 'Asteroid',],
+ bestPlanetType: ['Frozen'],
+ tutorialMission: 20000006,
+ activeStructure: 3102,
+ techId: 2, // Add satellite
+ },
+ ],
+ },
+ {
+ category: 'Clouds',
+ items: [
+ {
+ name: 'Martian Cloud Survey',
+ description: 'Survey and classify clouds on Mars and similar planets',
+ identifier: 'lidar-martianClouds',
+ researchId: 'cloudspotting-on-mars',
+ researcher: 'zooniverse',
+ // icon: CloudDrizzleIcon,
+ unlocked: false,
+ compatiblePlanetTypes: ['Frozen', 'Arid'],
+ bestPlanetType: ['Frozen'],
+ tutorialMission: 3000010,
+ activeStructure: 3105,
+ techId: 5,
+ },
+ {
+ name: 'Jovian Vortex Hunter',
+ description: "Help find interesting features & discover fluid dynamics in Jupiter's atmosphere",
+ identifier: 'lidar-jovianVortexHunter',
+ researchId: 'jovian-vortex-hunter',
+ researcher: 'zooniverse/ramanakumars',
+ unlocked: false,
+ compatiblePlanetTypes: ['GasGiant', 'IceGiant', 'Asteroid', 'Frozen'],
+ bestPlanetType: ['GasGiant'],
+ tutorialMission: 20000007,
+ activeStructure: 3105,
+ techId: 5,
+ },
+ ],
+ },
+];
+
+// Extra (#zoodex):
+ // {
+ // name: 'Fish Research',
+ // description: 'Hello there',
+ // identifier: 'zoodex-fishResearch',
+ // researchId: 'ghigrh',
+ // researcher: 'zooniverse',
+ // // icon: FishIcon,
+ // unlocked: false,
+ // compatiblePlanetTypes: ['Lush'],
+ // bestPlanetType: ['Lush'],
+ // },
+ // ],
+ // },
+ // {
+ // category: 'Plants',
+ // items: [
+ // {
+ // name: 'Test Tube Plant',
+ // description: 'Description for test tube plant',
+ // identifier: 'zoodex-testTubePlant',
+ // researchId: 'testtube-plant',
+ // researcher: 'botanical-society',
+ // // icon: TestTube,
+ // unlocked: false,
+ // compatiblePlanetTypes: ['Arid', 'Frozen'],
+ // // bestPlanetType: ['Lush'],
+ // },
+ // ],
+ // },
\ No newline at end of file
diff --git a/components/Data/unlockNewDataSources.tsx b/components/Data/unlockNewDataSources.tsx
new file mode 100644
index 000000000..4c1e13505
--- /dev/null
+++ b/components/Data/unlockNewDataSources.tsx
@@ -0,0 +1,449 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { ChevronDown, ChevronUp, Unlock } from "lucide-react";
+import { lidarDataSources, telescopeDataSources, zoodexDataSources, roverDataSources } from "./ZoodexDataSources";
+import { useActivePlanet } from "@/context/ActivePlanet";
+import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
+import { InventoryIdFetcher } from "@/components/Inventory/fetchId";
+import Link from "next/link";
+
+export function DataSourcesModal({ structureId, structure }: DataSourcesModalProps) {
+ const supabase = useSupabaseClient();
+ const session = useSession();
+
+ const { activePlanet } = useActivePlanet();
+
+ const [expandedCategories, setExpandedCategories] = useState>(
+ Object.fromEntries([
+ ...zoodexDataSources,
+ ...telescopeDataSources,
+ ...lidarDataSources,
+ ...roverDataSources,
+ ].map((category) => [category.category, true]))
+ );
+
+ const [unlockedZoodexDataSources, setUnlockedZoodexDataSources] = useState>(
+ Object.fromEntries(
+ zoodexDataSources.flatMap((category) =>
+ category.items.map((item: ZoodexItem) => [item.name, item.unlocked])
+ ),
+ ),
+ );
+
+ const [unlockedTelescopeDataSources, setUnlockedTelescopeDataSources] = useState>(
+ Object.fromEntries(
+ telescopeDataSources.flatMap((category) =>
+ category.items.map((item: TelescopeItem) => [item.name, item.unlocked])
+ ),
+ )
+ );
+
+ const [unlockedLidarDataSources, setUnlockedLidarDataSources] = useState>(
+ Object.fromEntries(
+ lidarDataSources.flatMap((category) =>
+ category.items.map((item: LidarItem) => [item.name, item.unlocked])
+ ),
+ )
+ );
+
+ const [unlockedRoverDataSources, setUnlockedRoverDataSources] = useState>(
+ Object.fromEntries(
+ roverDataSources.flatMap((category) =>
+ category.items.map((item: RoverItem) => [item.name, item.unlocked])
+ ),
+ )
+ );
+
+ useEffect(() => {
+ const fetchConfiguration = async () => {
+ if (!session?.user?.id || !activePlanet?.id) return;
+
+ try {
+ const { data, error } = await supabase
+ .from("inventory")
+ .select("configuration")
+ .eq("owner", session.user.id)
+ .eq("anomaly", activePlanet.id)
+ .eq("item", structureId)
+ .single();
+
+ if (error) throw error;
+
+ const configuration = data?.configuration || {};
+ const missionsUnlocked = configuration["missions unlocked"] || [];
+
+ // For Zoodex
+ setUnlockedZoodexDataSources(
+ Object.fromEntries(
+ zoodexDataSources.flatMap((category) =>
+ category.items.map((item: ZoodexItem) => [item.name, missionsUnlocked.includes(item.identifier)])
+ ),
+ ),
+ );
+
+ // For Telescope
+ setUnlockedTelescopeDataSources(
+ Object.fromEntries(
+ telescopeDataSources.flatMap((category) =>
+ category.items.map((item: TelescopeItem) => [item.name, missionsUnlocked.includes(item.identifier)])
+ ),
+ ),
+ );
+
+ // For LIDAR
+ setUnlockedLidarDataSources(
+ Object.fromEntries(
+ lidarDataSources.flatMap((category) =>
+ category.items.map((item: LidarItem) => [item.name, missionsUnlocked.includes(item.identifier)])
+ ),
+ ),
+ );
+
+ // For Rover
+ setUnlockedRoverDataSources(
+ Object.fromEntries(
+ roverDataSources.flatMap((category) =>
+ category.items.map((item: RoverItem) => [item.name, missionsUnlocked.includes(item.identifier)])
+ ),
+ ),
+ );
+
+ } catch (err) {
+ console.error("Error fetching configuration:", err);
+ }
+ };
+
+ fetchConfiguration();
+ }, [session, activePlanet, structureId, supabase]);
+
+ const toggleCategory = (category: string) => {
+ setExpandedCategories((prev) => ({ ...prev, [category]: !prev[category] }));
+ };
+
+ const toggleFeature = async (featureName: string, dataSourceType: "Zoodex" | "Telescope" | "LIDAR" | "Rover") => {
+ const dataSource = dataSourceType === "Zoodex"
+ ? zoodexDataSources
+ : dataSourceType === "Telescope"
+ ? telescopeDataSources
+ : dataSourceType === "LIDAR"
+ ? lidarDataSources
+ : roverDataSources; // Add rover logic
+
+ const item = dataSource
+ .flatMap(category => category.items)
+ .find(item => item.name === featureName);
+
+ if (!item) return;
+
+ if (dataSourceType === "Telescope") {
+ setUnlockedTelescopeDataSources(prev => ({
+ ...prev,
+ [featureName]: !prev[featureName],
+ }));
+ } else if (dataSourceType === "LIDAR") {
+ setUnlockedLidarDataSources(prev => ({
+ ...prev,
+ [featureName]: !prev[featureName],
+ }));
+ } else if (dataSourceType === "Rover") {
+ setUnlockedRoverDataSources(prev => ({
+ ...prev,
+ [featureName]: !prev[featureName],
+ }));
+ } else {
+ setUnlockedZoodexDataSources(prev => ({
+ ...prev,
+ [featureName]: !prev[featureName],
+ }));
+ }
+
+ if (session?.user?.id && activePlanet?.id) {
+ try {
+ const { data, error } = await supabase
+ .from("inventory")
+ .select("id, configuration")
+ .eq("owner", session.user.id)
+ .eq("anomaly", activePlanet.id)
+ .eq("item", structureId)
+ .single();
+
+ const { data: updateDataO } = await supabase
+ .from("missions")
+ .insert([{ user: session.user.id, time_of_completion: new Date().toISOString(), mission: 200000015 }]);
+
+ if (error) throw error;
+
+ const newConfiguration: { "missions unlocked"?: string[] } = data?.configuration || {};
+
+ if (!newConfiguration["missions unlocked"]) {
+ newConfiguration["missions unlocked"] = [];
+ };
+
+ const missionsUnlocked = newConfiguration["missions unlocked"];
+ if (!missionsUnlocked.includes(item.identifier)) {
+ missionsUnlocked.push(item.identifier);
+ };
+
+ const { error: updateError } = await supabase
+ .from("inventory")
+ .update({ configuration: newConfiguration })
+ .eq("id", data?.id);
+
+ if (updateError) throw updateError;
+
+ console.log("Updated configuration:", newConfiguration);
+ } catch (err) {
+ console.error("Error updating inventory configuration:", err);
+ };
+ };
+ };
+
+ return (
+
+
+
+
console.log("Fetched Inventory ID:", id)}
+ />
+ {structure === "Telescope" && telescopeDataSources.map((category) => (
+
+
toggleCategory(category.category)}
+ >
+ {category.category}
+ {expandedCategories[category.category] ? (
+
+ ) : (
+
+ )}
+
+ {expandedCategories[category.category] && (
+
+ {category.items
+ .sort(
+ (a, b) =>
+ (unlockedTelescopeDataSources[b.name] ? 1 : 0) -
+ (unlockedTelescopeDataSources[a.name] ? 1 : 0)
+ )
+ .map((feature) => (
+
+