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 }) {
Application UI 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 */} +
+
+
+
+
+ +
+ Star Sailors +
+ + +
+
+
+ + {/* Hero */} +
+
+
+
+
+
+
+
+
+
+ + New content every week + +
+

+ Actively contribute to science +

+

+ Discover the world around you and contribute to real scientific research in Star Sailors +

+
+ + + + +
+
+
+
+ Game Screenshot +
+
+
+
+ + {/* 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.name}

+

{member.role}

+

{member.bio}

+
+ {member.social.map((link, i) => ( + + {link.icon} + + ))} +
+
+ ))} +
+
+ +
+

Our Partners

+
+ {partners.map((partner, index) => ( + + {partner.name} +

{partner.name}

+

{partner.description}

+ + Learn More + +
+ ))} +
+
+ + + + {/* Roadmap Section */} + + {/*
+
+

Game Screenshots

+
+ {[1, 2, 3].map((i) => ( +
+ {`Screenshot +
+ ))} +
+
+
*/} + + {/* Footer */} +
+
+ ); +}; + + +{/* News Section */} +{/*
+
+
+

Latest News

+

+ Stay updated with our latest discoveries and community achievements +

+
+
+ {[1, 2, 3].map((i) => ( + +
+ News Image +
+
+ News +

New Galaxy Cluster Discovered by Players

+

+ Our community has identified a previously unknown galaxy cluster... +

+ +
+
+ ))} +
+
+
*/} \ 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 ( +
+
+
+ {/* Social Links */} +
+

Connect with us

+
+ + + + + + + + + + + {/* GitHub */} + + + + + + {/* Threads */} + {/* + + + + + + {/* Bluesky + + + + + + */} +
+
+ + {/* Newsletter Signup */} +
+ {/*

Stay Updated

+
+ + +
*/} +
+ + {/* Data Submission */} +
+ {/*

Submit Data

+

+ Contribute datasets and help expand our research. +

+ */} +
+ + {/* Signup/Login */} +
+

Account

+
    +
  • Sign Up
  • +
  • Login
  • +
+
+
+ + {/* Top Section */} + +
+ {/* Branding & About */} +
+

Star Sailors

+

+ Making scientific discovery & contribution accessible to everyone. +

+
+ + {/* Sitemap */} +
+

Resources

+
    +
  • + Documentation +
  • +
  • + API Reference +
  • +
  • + Data Sources +
  • +
+
+ +
+

Community

+
    +
  • + Forums +
  • +
  • + GitHub +
  • +
  • + Contributors +
  • +
+
+ +
+

Meta

+
    +
  • + Release history +
  • +
  • + Development log +
  • +
  • + Suggestions +
  • +
+
+
+ + {/* Copyright */} +
+

© 2021 - {new Date().getFullYear()}. Star Sailors - built in Melbourne

+
+
+
+ ); +}; \ 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}

+
+ {/*
+ {point.title} +
*/} +
+
+
+ ))} +
+
+
+ ); +}; \ 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 ( +
+ Earth Background + +
+ +
+ +
+ { + if (!open) { + router.push("/") + } + }} + > + + {/* Header */} +
+
+ {/* <-- CHANGE THIS */} +

Kickoff

+
+
+ +
+ Icon +

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.

*/} + + {/* */} + + {/* setMessage(e.target.value)} + className="w-full px-3 py-2 border border-gray-500 rounded-md bg-transparent placeholder-gray-400 text-white" + /> */} + + {/* */} + + ) : ( + <> + {/*

You are not subscribed to push notifications.

*/} + + + )} +
+ ); +}; + +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

+ + {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? + + + + + + + + + )} +
+ +
+
+
+
+
+
+ + + +
+
+
+
+
+ + +
+
+ {!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 ( +
+ +
+
+ background +
+
+ + {/* {dominantBiome && ( +
+ +
+ )} */} + +
+ {currentView !== "planet" && ( +
+ +
+ )} + +
+
+
Biomass:
+ {biomassScore} +
+
+
Density:
+ {density} + g/cm^3 +
+
+
Main biome:
+ {dominantBiome} +
+ {/*
+
Period:
+ {surveyorPeriod} Days {/* // or {period} +
*/} +
+ +
+ + + {/* */} + + +
+ +
+ {renderFocusComponent()} +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + + +
+
+
+
+ ); +}; \ 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 ( +
+ + Barren (default planet type) background + + + {/* 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

+ {/* */} + + + {/* 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

+ {/* */} + + + {/* 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...

+
+
+
+ + + +
+
+
+
+

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 = () => ( +
+

Edit Planet Data

+
+
+
+ + +
+
+ + +
+
+ + {/* */} + {/* */} + {biomassScore} +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ ) + + 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 ( +
+ + +
+ Space background +
+
+ + {dominantBiome && ( +
+ +
+ )} + +
+ {/* Back button */} + {currentView !== "planet" && ( +
+ +
+ )} + + {/* Action buttons - Desktop */} +
+ + + + +
+ + {/* 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 */} +
+ + + + +
+
+
+ ); +}; \ 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 ( +
+ Backdrop +
+

Loading classification...

+
+
+ ); + + if (error) return

{error}

; + if (!classification) return

No classification found.

; + + return ( +
+ + Backdrop +
+ {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 ( +
+ Backdrop +
+

Loading classification...

+
+
+ ); + + if (error) return

{error}

; + if (!classification) return

No classification found.

; + + return ( +
+ + Backdrop +
+ {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 ( +
+
+ +
+ +
+
+
+

RESEARCH LAB

+
+
+ STARDUST: + +
+
+
+
+ +
+ {/* setAvailablePoints(total)} /> */} + +
+ + + +
+ +
+
+
+ ); +}; \ 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 ( +
+ +
+ Earth Background + + {/* 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 + + )} +
+ +
+
+ + +
+ {project.images?.map((img: string, idx: number) => ( +
+ {`Capture +
+ ))} +
+
+ + +
+ + {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) => ( +
+ {`Capture +
+ ))} +
+
+
+ + + + Station Metrics + + + +
+ + + + +
+
+
*/} + + {/* + + Biome: {project.biome} + + + +
+ + + + +
+
+
*/} + {/* Archive Section */} + {/*
+
+

+ + Archive +

+ + 142 Items + +
+ + + + + + + Studied + + + Community + + + + {["studied", "community"].map((tab: string) => ( + +
+ {archiveImages.map((img: string, idx: number) => ( +
+ {`${tab} +
+ ))} +
+
+ ))} +
+
+
+
*/} + + {/* 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.

+ +
+
); - }; +}; 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) => ( -
- {structure.itemDetail?.name} -
- ))} -
+ {/*
+ {/*
-
+
-
-
-
-
-
-
-
-
-
-
- -
- - ); -}; \ 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 ( +
+ Earth Background + +
+ {topSection && ( +
+ {topSection} +
+ )} + + {middleSection && ( +
+
+ {middleSection} +
+ + {toolbar && ( +
+ {toolbar} +
+ )} +
+ )} + + {middleSectionTwo && ( +
+
+ {middleSectionTwo} +
+
+ )} + + {bottomSection && ( +
+
+ {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 ( +
+ +
+ Ocean Background + + {/* 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 + +

+ {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 ( +
+ Earth Background + +
+ +
+ +
+ { + if (!open) { + router.push("/"); + } + }} + > + + {/* Back button when showing dynamic content */} + {activeComponent && ( +
+ +
+ )} + + {/* Content switcher */} +
+ {activeComponent ? ( +
{activeComponent}
+ ) : ( + <> +
+
+ +

Weather Balloon

+
+
+ +
+ 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 ( +
+ Earth Background + +
+ +
+ +
+ { + if (!open) { + router.push("/"); + } + }} + > + + {activeComponent ? ( +
+ {activeComponent} +
+ ) : ( + <> +
+
+ +

+ Your cameras (across all structures) +

+
+
+ +
+ Your cameras +
+ +
+
+ {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 ( +
+ Earth Background + +
+ +
+ +
+ { + if (!open) { + router.push("/"); + } + }} + > + + {/* Back button when showing dynamic content */} + {activeComponent && ( +
+ +
+ )} + + {/* Content switcher */} +
+ {activeComponent ? ( +
{activeComponent}
+ ) : ( + <> +
+
+ +

Greenhouse

+
+
+ +
+ Greenhouse Biodome +
+ +
+
+ {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 ( +
+ Earth Background + +
+ +
+ +
+ { + if (!open) { + router.push("/"); + } + }} + > + + {/* Back button when showing dynamic content */} + {activeComponent && ( +
+ +
+ )} + + {/* Content switcher */} +
+ {activeComponent ? ( +
{activeComponent}
+ ) : ( + <> +
+
+ +

Telescope

+
+
+ +
+ Telescope +
+ +
+ {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 */} +
+
+
+

Scanner

+
+ + {/* 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 ? ( + Preview + ) : ( +
+
+ + + + +

Upload an image or switch to camera

+
+
+ )} + + {/* Scanning animation when loading */} + {loading && ( +
+
+
+ )} +
+
+ + {/* Scan button with mechanical housing */} +
+
+ {useWebcam ? ( + + ) : preview ? ( + + ) : ( + + )} + +
+ + {/* Camera toggle with mechanical look */} +
+
+ +
+
+ + {/* 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} +

+
+ +
+ setLocation(e.target.value)} + className="w-full p-2 bg-gray-800 border border-gray-700 rounded-md text-white" + /> + +