diff --git a/.DS_Store b/.DS_Store index c04ddc6b7..bc10459aa 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app/activity/deploy/page.tsx b/app/activity/deploy/page.tsx new file mode 100644 index 000000000..906e14aae --- /dev/null +++ b/app/activity/deploy/page.tsx @@ -0,0 +1,52 @@ +'use client' + +import Image from "next/image" +import { useRouter } from "next/navigation" +import { AvatarGenerator } from "@/components/Account/Avatar" +import DeployTelescopeViewport from "@/components/(scenes)/deploy/TelescopeViewportRange" +import { Button } from "@/components/ui/button" +import { useSession } from "@supabase/auth-helpers-react" + +export default function NewDeployPage() { + const router = useRouter() + const session = useSession() + + return ( +
+ {/* Header */} +
+ {/* Background image */} + Cover + {/* Foreground content */} +
+
+ +
+
+

Deploy Telescope

+

TESS Mission Interface

+
+
+
+ +
+
+ + {/* Main Content */} +
+ +
+
+ ) +}; \ No newline at end of file diff --git a/app/alpha/page.tsx b/app/alpha/page.tsx new file mode 100644 index 000000000..2109bc1ee --- /dev/null +++ b/app/alpha/page.tsx @@ -0,0 +1,482 @@ +"use client"; + +import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react"; +import { useEffect, useState } from "react"; +import LoginPage from "../auth/LoginModal"; +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 { 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"; +import NPSPopup from "@/lib/helper/nps-popup"; +import Structures from "@/components/Structures/Structures"; + +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); + const [showNpsModal, setShowNpsModal] = useState(false); + const [hasCheckedNps, setHasCheckedNps] = 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("id") + .eq("author", session.user.id); + + const hasClassified = !error && Array.isArray(data) && data.length > 0; + setUserClassifications(hasClassified); + + if (hasClassified) { + await checkLinkedAnomaliesLastWeek(); + scheduleNpsCheck(); + } + } catch (error: any) { + console.error("Error checking classifications:", error); + setUserClassifications(false); + } + }; + + 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); + } + }; + + const scheduleNpsCheck = () => { + if (hasCheckedNps) return; + + const timer = setTimeout(async () => { + try { + const { data, error } = await supabase + .from("nps_surveys") + .select("id") + .eq("user_id", session.user.id); + + if (!error && Array.isArray(data) && data.length === 0) { + setShowNpsModal(true); + } + setHasCheckedNps(true); + } catch (err: any) { + console.error("Error checking NPS Survey Status: ", err); + } + }, 15000); + + return () => clearTimeout(timer); + }; + + checkInventory(); + checkClassifications(); + }, [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 ; + } + + 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 && } +
+ {showNpsModal && ( + setShowNpsModal(false)} + /> + )} + {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 diff --git a/app/api/gameplay/inventory/route.ts b/app/api/gameplay/inventory/route.ts index 93cba6678..fa324b2b0 100644 --- a/app/api/gameplay/inventory/route.ts +++ b/app/api/gameplay/inventory/route.ts @@ -75,6 +75,14 @@ const inventoryItems: InventoryItem[] = [ ItemCategory: "Structure", locationType: 'Atmosphere', }, + { + id: 3107, + name: "SeisCAM", + description: "Explore the surfaces of exoplanets", + icon_url: '/assets/Items/miningstation.png', + ItemCategory: "Structure", + locationType: "Surface", + }, { id: 3106, name: "Research Station", diff --git a/app/api/gameplay/milestones/route.ts b/app/api/gameplay/milestones/route.ts index 76cd04646..cefa2f9f3 100644 --- a/app/api/gameplay/milestones/route.ts +++ b/app/api/gameplay/milestones/route.ts @@ -2,13 +2,144 @@ import { Telescope } from "lucide-react"; import { NextRequest, NextResponse } from "next/server"; const milestones = [ + { + weekStart: '2025-07-13', + data: [ + { + name: "Verify other users' discoveries and get some of the earnings in return", + structure: "Telescope", + group: "Astronomy", + icon: 'Telescope', + extendedDescription: ',', + xp: 1, + completionInfo: '', + rewardInfo: '', + table: 'votes', + field: 'vote_type', + value: 'up', + requiredCount: 2, + }, + { + name: "Contribute to someone else's discovery by commenting", + structure: "Telescope", + group: "Astronomy", + icon: "Telescope", + extendedDescription: '', + xp: 1, + completionInfo: '', + rewardInfo: '', + table: 'comments', + field: 'surveyor', + value: 'TRUE', + requiredCount: 1, + }, + ], + }, + { + weekStart: '2025-07-06', + data: [ + { + name: "Verify other users' discoveries and get some of the earnings in return", + structure: "Telescope", + group: "Astronomy", + icon: 'Telescope', + extendedDescription: ',', + xp: 1, + completionInfo: '', + rewardInfo: '', + table: 'votes', + field: 'vote_type', + value: 'up', + requiredCount: 2, + }, + { + name: "Contribute to someone else's discovery by commenting", + structure: "Telescope", + group: "Astronomy", + icon: "Telescope", + extendedDescription: '', + xp: 1, + completionInfo: '', + rewardInfo: '', + table: 'comments', + field: 'surveyor', + value: 'TRUE', + requiredCount: 1, + }, + ], + }, + { + weekStart: '2025-06-29', + data: [ + { + name: "Contribute to someone else's discovery by commenting", + structure: "Telescope", + group: "Astronomy", + icon: "Telescope", + extendedDescription: '', + xp: 1, + completionInfo: '', + rewardInfo: '', + table: 'comments', + field: 'surveyor', + value: 'TRUE', + requiredCount: 1, + }, + { + name: "Train our automatons - discover 3 obstacles", + structure: "WeatherBalloon", + group: "Meteorology", + icon: "SnowflakeIcon", + extendedDescription: '', + xp: 3, + completionInfo: '', + rewardInfo: '', + table: 'classifications', + field: 'classificationtype', + value: 'automaton-aiForMars', + requiredCount: 3, + }, + ], + }, + { + weekStart: '2025-06-15', + data: [ + { + name: 'Propose 3 planet candidates', + structure: "Telescope", + group: 'Astronomy', + icon: "Telescope", + extendedDescription: '', + xp: 3, + completionInfo: '', + rewardInfo: '', + table: "classifications", + field: "classificationtype", + value: "planet", + requiredCount: 3, + }, + { + name: 'Review satellite data', + structure: "WeatherBalloon", + group: 'Meteorology', + icon: "SnowflakeIcon", + extendedDescription: '', + xp: 2, + completionInfo: '', + rewardInfo: '', + table: "classifications", + field: "classificationtype", + value: "satellite-planetFour", + requiredCount: 2, + }, + ], + }, { - weekStart: '2025-05-25', + weekStart: '2025-06-06', data: [ { name: 'Propose 5 planet candidates', structure: "Telescope", - // group: 'Meteorology', group: 'Astronomy', icon: "Telescope", extendedDescription: '', @@ -339,8 +470,18 @@ const communityMilestones = [ ]; export async function GET(req: NextRequest) { + const today = new Date(); + + // Sort milestones descending by weekStart + const sortedMilestones = milestones + .map(m => ({ ...m, weekStartDate: new Date(m.weekStart) })) + .filter(m => m.weekStartDate <= today) + .sort((a, b) => b.weekStartDate.getTime() - a.weekStartDate.getTime()); + + const mostRecentMilestone = sortedMilestones[0]; + return NextResponse.json({ - playerMilestones: milestones, - communityMilestones: communityMilestones, + playerMilestones: mostRecentMilestone ? [mostRecentMilestone] : [], + communityMilestones: communityMilestones, // or filter this too if needed }); }; \ No newline at end of file diff --git a/app/api/send-push/route.ts.rb b/app/api/send-push/route.ts.rb new file mode 100644 index 000000000..9c15dd602 --- /dev/null +++ b/app/api/send-push/route.ts.rb @@ -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/auth/landing.tsx b/app/auth/landing.tsx index 29cbe2790..522b5043b 100644 --- a/app/auth/landing.tsx +++ b/app/auth/landing.tsx @@ -168,7 +168,7 @@ export default function LandingSS() { size="lg" className="bg-blue-300 text-white hover:bg-aqua-400 hover:translate-y-[-2px] hover:translate-x-[2px] text-lg px-8 py-6 rounded-md border-4 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] transition-all hover:shadow-none" > - Login + Sign Up/Log In */} - - {/* 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.

*/} - - - )} - - ); -}; + const handleScroll = () => { + setScrolled(window.scrollY > 10); + }; -function InstallPrompt() { - const [isIOS, setIsIOS] = useState(false); - const [isStandalone, setIsStandalone] = useState(false); - const [deferredPrompt, setDeferredPrompt] = useState(null); + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, [session]); 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; + if (!session) return; - return ( -
-

Install App

- - {isIOS && ( -

- On iOS, tap the Share icon ⎋ and then - "Add to Home Screen" ➕. -

- )} -
- ); -}; + const fetchData = async () => { + const userId = session.user.id; -export default function Home() { - const session = useSession(); - const supabase = useSupabaseClient(); - const { activePlanet } = useActivePlanet(); - const router = useRouter(); + const { data: profileData } = await supabase + .from("profiles") + .select("id, username, full_name, classificationPoints") + .eq("id", userId) + .maybeSingle(); + setProfile(profileData); - const [hasRequiredItems, setHasRequiredItems] = useState(null); - const [userClassifications, setUserClassifications] = useState(false); - const [planetData, setPlanetData] = useState(null); + const { data: myClassifications } = await supabase + .from("classifications") + .select("id, classificationtype, content, created_at, anomaly") + .eq("author", userId) + .order("created_at", { ascending: false }) + .limit(4); + setClassifications(myClassifications ?? []); - const [showDeployModal, setShowDeployModal] = useState(false); + const classifiedAnomalyIds = new Set(myClassifications?.map((c) => c.anomaly)); - useEffect(() => { - if (!session) return; + const { data: rawLinked } = await supabase + .from("linked_anomalies") + .select(` + id, + anomaly_id, + date, + anomaly:anomaly_id( + content, + anomalytype, + anomalySet + ) + `) + .eq("author", userId) + .order("date", { ascending: false }); + + const linked = (rawLinked ?? []) as unknown as LinkedAnomaly[]; + const filteredLinked = linked.filter((a) => !classifiedAnomalyIds.has(a.anomaly_id)); + setLinkedAnomalies(filteredLinked); + + const oneWeekAgo = subDays(new Date(), 7).toISOString(); + + const { data: comments } = await supabase + .from("comments") + .select("id") + .eq("author", userId); - 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 { data: votes } = await supabase + .from("votes") + .select("id") + .eq("user_id", userId); + + const hasInteracted = (comments?.length ?? 0) > 0 || (votes?.length ?? 0) > 0; + setShowTipsPanel(!hasInteracted); + + const { data: commentDetails } = await supabase + .from("comments") + .select("created_at, content, classification_id") + .in("classification_id", myClassifications?.map((c) => c.id) ?? []) + .gte("created_at", oneWeekAgo); + + const { data: voteDetails } = await supabase + .from("votes") + .select("created_at, vote_type, classification_id") + .in("classification_id", myClassifications?.map((c) => c.id) ?? []) + .gte("created_at", oneWeekAgo); + + const allActivity: CommentVote[] = []; + if (commentDetails) allActivity.push(...commentDetails.map((c) => ({ type: "comment" as const, ...c }))); + if (voteDetails) allActivity.push(...voteDetails.map((v) => ({ type: "vote" as const, ...v }))); + + setActivityFeed( + allActivity.sort( + (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ) + ); + + const { data: others } = await supabase + .from("classifications") + .select("id, classificationtype, content, author, created_at") + .neq("author", userId) + .order("created_at", { ascending: false }) + .limit(20); + setOtherClassifications(others ?? []); }; - const checkClassifications = async () => { - try { + const scheduleNpsCheck = () => { + if (hasCheckedNps) return; + const timer = setTimeout(async () => { 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(); + .from("nps_surveys") + .select("id") + .eq("user_id", session.user.id); - 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); - } + if (!error && Array.isArray(data) && data.length === 0) { + setShowNpsModal(true); + } + setHasCheckedNps(true); + }, 15000); + return () => clearTimeout(timer); }; - checkClassifications(); - checkInventory(); - checkLinkedAnomaliesLastWeek(); + fetchData(); + scheduleNpsCheck(); }, [session, supabase]); - useEffect(() => { - if (!activePlanet) return; + if (!session) 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 ; - } - - 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); - } - } + const needsProfileSetup = !profile?.username || !profile?.full_name; return ( - -
- -
-
-
-

Temperature:

-

{planetData?.temperatureEq} K

-

Humidity:

-

{planetData?.humidity} K

- {planetData?.id === 30 && } -
- {showDeployModal && ( - - +
+ setLandmarksExpanded((prev) => !prev)} + /> +
+ + {scrolled && } + +
+
+
+ + + {needsProfileSetup ? ( +
- - 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? - - - - - - - -
- )} -
- -
-
-
-
-
-
- - - -
+

+ Finish setting up your account +

+

+ Unlock Structures, Research, and referral features by completing your profile. +

+ + + ) : ( + <> + + +
+

+ + Structures & Research +

+
+
+
+
+

Telescope Array

+

+ Enhances classification of distant light curves and planet candidates. +

+
+ + Deploy + +
+
+ + {["Satellites", "Rovers", "Balloons"].map((label) => ( +
+

{label} (Coming Soon)

+

+ {label === "Satellites" && "Will scan magnetic & atmospheric fields."} + {label === "Rovers" && "Explore surface geology and send rich data back."} + {label === "Balloons" && "Float above atmospheres for weather anomaly mapping."} +

+
+ ))} +
+
+ + )} + + {showNpsModal && ( + setShowNpsModal(false)} + /> + )} + + + {showTipsPanel && }
-
-
- - -
-
- {!userClassifications && ( -
- -
- )} -
+ + + + + + Complete Your Profile + + + setShowProfileModal(false)} /> + + + + + ); -}; \ 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 index b0a46f962..164ab1287 100644 --- a/app/planets/[id]/page.tsx +++ b/app/planets/[id]/page.tsx @@ -46,15 +46,15 @@ type FocusView = "planet" | "overview" | "Climate" | "atmosphere" | "exploration 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; + 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 { @@ -419,14 +419,14 @@ export default function TestPlanetWrapper() { const AtmosphereComponent = () => (
- {classification?.id !== undefined && ( + {/* {classification?.id !== undefined && ( - )} + )} */}
); diff --git a/app/planets/paint/[id]/page.tsx b/app/planets/paint/[id]/page.tsx new file mode 100644 index 000000000..f31d7190b --- /dev/null +++ b/app/planets/paint/[id]/page.tsx @@ -0,0 +1,148 @@ +"use client" + +import { useState, useEffect } from "react" +import { useParams, useRouter } from "next/navigation" +import PlanetViewer from "@/components/Data/Generator/Astronomers/PlanetHunters/planetViewer" +import { type PlanetConfig, defaultPlanetConfig } from "./planet-config" +import SettingsPanel from "@/components/Data/Generator/Astronomers/PlanetHunters/SettingsPanel" +import { useSupabaseClient } from "@supabase/auth-helpers-react" +import { Button } from "@/components/ui/button" +import GameNavbar from "@/components/Layout/Tes" + +export default function PlanetGeneratorPage() { + const router = useRouter() + const supabase = useSupabaseClient() + const params = useParams() + const planetId = params.id as string; + + const [planetConfig, setPlanetConfig] = useState(defaultPlanetConfig) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const handleConfigChange = (newConfig: Partial) => { + setPlanetConfig((prev) => ({ ...prev, ...newConfig })) + } + + useEffect(() => { + const fetchPlanetData = async () => { + if (!planetId) { + setError("No planet ID provided.") + setLoading(false) + return + } + + try { + const idAsNumber = Number.parseInt(planetId) + if (isNaN(idAsNumber)) { + throw new Error("Invalid planet ID") + } + + const { data, error: fetchError } = await supabase + .from("classifications") + .select("*") + .eq("id", idAsNumber) + .single() + + if (fetchError) { + throw fetchError + } + + if (!data) { + throw new Error("Planet not found") + } + + let config = data.classificationConfiguration + + // Ensure it's parsed if Supabase returned it as a string + if (typeof config === "string") { + try { + config = JSON.parse(config) + } catch (err) { + console.warn("Failed to parse classificationConfiguration JSON string:", err) + config = {} + } + } + + if (config?.planetConfiguration) { + setPlanetConfig(config.planetConfiguration) + } else { + console.log("No planetConfiguration found, using default.") + } + + setError(null) + } catch (err) { + console.error("Error fetching planet data:", err) + setError(err instanceof Error ? err.message : "Failed to load planet data") + } finally { + setLoading(false) + } + } + + fetchPlanetData() + }, [planetId, supabase]) + + if (loading) { + return ( +
+
+
+

Loading planet data...

+
+
+ ) + } + + if (error) { + return ( +
+
+

Error

+

{error}

+ +
+
+ ) + } + + return ( +
+ +
+ {/* Planet Viewer */} +
+ +
+ + {/* Settings Panel */} +
+
+
+

Planet Settings

+ ID: {planetId} +
+ +
+
+ + +
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/planets/paint/[id]/planet-config.ts b/app/planets/paint/[id]/planet-config.ts new file mode 100644 index 000000000..6f7e28513 --- /dev/null +++ b/app/planets/paint/[id]/planet-config.ts @@ -0,0 +1,451 @@ +export interface PlanetConfig { + type: "terrestrial" | "gaseous" + seed: number + radius: number + temperature: number // Now in Kelvin + biomass: number + mass: number + terrainRoughness: number + liquidHeight: number + volcanicActivity: number + continentSize: number + continentCount: number + noiseScale: number + debugMode?: boolean + visibleTerrains: { + ocean: boolean + beach: boolean + lowland: boolean + midland: boolean + highland: boolean + mountain: boolean + snow: boolean + } + colors: { + atmosphere: string + ocean: string + oceanPattern: string + beach: string + lowland: string + midland: string + highland: string + mountain: string + snow: string + } +} + +export const defaultPlanetConfig: PlanetConfig = { + type: "terrestrial", + seed: Math.floor(Math.random() * 10000), + radius: 1.0, + temperature: 288, // Earth average in Kelvin (15°C) + biomass: 0.7, + mass: 1.0, + terrainRoughness: 0.6, + liquidHeight: 0.55, + volcanicActivity: 0.2, + continentSize: 0.5, + continentCount: 5, + noiseScale: 1.0, + debugMode: false, + visibleTerrains: { + ocean: true, + beach: true, + lowland: true, + midland: true, + highland: true, + mountain: true, + snow: true, + }, + colors: { + atmosphere: "#87CEEB", + ocean: "#1E90FF", + oceanPattern: "#1E7FFF", + beach: "#F0E68C", + lowland: "#32CD32", + midland: "#228B22", + highland: "#8B4513", + mountain: "#A0A0A0", + snow: "#FFFFFF", + }, +} + +// Simple planet presets using the full PlanetConfig +export const simplePlanetPresets: Record = { + earth: { + type: "terrestrial", + seed: 1234, + radius: 1.0, + temperature: 288, + biomass: 0.7, + mass: 1.0, + terrainRoughness: 0.6, + liquidHeight: 0.55, + volcanicActivity: 0.2, + continentSize: 0.5, + continentCount: 5, + noiseScale: 1.0, + debugMode: false, + visibleTerrains: { + ocean: true, + beach: true, + lowland: true, + midland: true, + highland: true, + mountain: true, + snow: true, + }, + colors: { + atmosphere: "#87CEEB", + ocean: "#1E90FF", + oceanPattern: "#1E7FFF", + beach: "#F0E68C", + lowland: "#32CD32", + midland: "#228B22", + highland: "#8B4513", + mountain: "#A0A0A0", + snow: "#FFFFFF", + }, + }, + mars: { + type: "terrestrial", + seed: 5678, + radius: 0.5, + temperature: 210, // Mars average + biomass: 0.0, + mass: 0.1, + terrainRoughness: 0.8, + liquidHeight: 0.3, + volcanicActivity: 0.1, + continentSize: 0.8, + continentCount: 2, + noiseScale: 1.2, + debugMode: false, + visibleTerrains: { + ocean: true, + beach: true, + lowland: true, + midland: true, + highland: true, + mountain: true, + snow: true, + }, + colors: { + atmosphere: "#CD853F", + ocean: "#8B4513", + oceanPattern: "#A0522D", + beach: "#CD853F", + lowland: "#A0522D", + midland: "#8B4513", + highland: "#8B0000", + mountain: "#696969", + snow: "#F5F5DC", + }, + }, + jupiter: { + type: "gaseous", + seed: 9012, + radius: 2.5, + temperature: 165, // Jupiter average + biomass: 0.0, + mass: 8.0, + terrainRoughness: 0.3, + liquidHeight: 0.0, + volcanicActivity: 0.0, + continentSize: 0.0, + continentCount: 0, + noiseScale: 0.5, + debugMode: false, + visibleTerrains: { + ocean: true, + beach: true, + lowland: true, + midland: true, + highland: true, + mountain: true, + snow: true, + }, + colors: { + atmosphere: "#DAA520", + ocean: "#B8860B", + oceanPattern: "#8B7355", + beach: "#DAA520", + lowland: "#B8860B", + midland: "#8B7355", + highland: "#8B4513", + mountain: "#654321", + snow: "#F5DEB3", + }, + }, + neptune: { + type: "gaseous", + seed: 3456, + radius: 2.0, + temperature: 72, // Neptune average + biomass: 0.0, + mass: 5.0, + terrainRoughness: 0.2, + liquidHeight: 0.0, + volcanicActivity: 0.0, + continentSize: 0.0, + continentCount: 0, + noiseScale: 0.3, + debugMode: false, + visibleTerrains: { + ocean: true, + beach: true, + lowland: true, + midland: true, + highland: true, + mountain: true, + snow: true, + }, + colors: { + atmosphere: "#4169E1", + ocean: "#0000CD", + oceanPattern: "#191970", + beach: "#4169E1", + lowland: "#0000CD", + midland: "#191970", + highland: "#000080", + mountain: "#483D8B", + snow: "#E6E6FA", + }, + }, + venus: { + type: "terrestrial", + seed: 7890, + radius: 0.9, + temperature: 737, // Venus average + biomass: 0.0, + mass: 0.8, + terrainRoughness: 0.4, + liquidHeight: 0.0, + volcanicActivity: 0.8, + continentSize: 0.6, + continentCount: 3, + noiseScale: 0.8, + debugMode: false, + visibleTerrains: { + ocean: true, + beach: true, + lowland: true, + midland: true, + highland: true, + mountain: true, + snow: true, + }, + colors: { + atmosphere: "#FFA500", + ocean: "#FF4500", + oceanPattern: "#FF6347", + beach: "#FFA500", + lowland: "#FF8C00", + midland: "#FF4500", + highland: "#8B0000", + mountain: "#2F4F4F", + snow: "#D3D3D3", + }, + }, +} + +// Function to determine liquid type based on temperature +export function getLiquidType(temperature: number): { + name: string + color: string + patternColor: string +} { + // Temperature ranges in Kelvin + if (temperature < 90) { + // Liquid nitrogen (63K to 77K) + return { + name: "Liquid Nitrogen", + color: "#D6E7FF", + patternColor: "#C0D6FF", + } + } else if (temperature < 120) { + // Liquid methane (90K to 112K) + return { + name: "Liquid Methane", + color: "#A2CDB0", + patternColor: "#8EBDA0", + } + } else if (temperature < 373) { + // Water (273K to 373K) + return { + name: "Water", + color: "#1E90FF", + patternColor: "#1E7FFF", + } + } else if (temperature < 600) { + // Sulfuric acid (283K to 610K) + return { + name: "Sulfuric Acid", + color: "#D6C562", + patternColor: "#C4B250", + } + } else { + // Molten silicates/lava (>1000K) + return { + name: "Molten Rock", + color: "#FF4500", + patternColor: "#FF2400", + } + } +} + +// Function to adjust terrain colors based on temperature +export function getTemperatureAdjustedColors( + temperature: number, + biomass: number, +): { + beach: string + lowland: string + midland: string + highland: string + mountain: string + snow: string +} { + // Base colors + let beach = "#F0E68C" // Default sandy beach + let lowland = "#32CD32" // Default green lowland + let midland = "#228B22" // Default forest green + let highland = "#8B4513" // Default brown + let mountain = "#A0A0A0" // Default gray + let snow = "#FFFFFF" // Default white + + // Cold planet (< 240K) + if (temperature < 240) { + beach = "#E0E0E0" // Icy shore + lowland = "#E8E8F0" // Light ice + midland = "#C8D8E0" // Bluish ice + highland = "#A8B8C0" // Darker ice + mountain = "#889098" // Dark gray + snow = "#FFFFFF" // Pure white + } + // Cool planet (240K to 280K) + else if (temperature < 280) { + beach = "#D8D8C0" // Cool sand + lowland = "#C0D8C0" // Tundra + midland = "#A0C0A0" // Sparse vegetation + highland = "#909080" // Rocky tundra + mountain = "#808080" // Gray rock + snow = "#FFFFFF" // White snow + } + // Temperate planet (280K to 310K) + else if (temperature < 310) { + // Adjust based on biomass + const bioFactor = Math.min(biomass * 1.5, 1.0) + + beach = "#F0E68C" // Sandy beach + lowland = bioFactor > 0.3 ? "#32CD32" : "#C2CC70" // Green if biomass, otherwise yellowish + midland = bioFactor > 0.3 ? "#228B22" : "#A0A060" // Forest if biomass, otherwise scrubland + highland = "#8B4513" // Brown + mountain = "#A0A0A0" // Gray + snow = "#FFFFFF" // White + } + // Hot planet (310K to 400K) + else if (temperature < 400) { + beach = "#F8E0A0" // Light sand + lowland = biomass > 0.4 ? "#74A662" : "#D8C878" // Either vegetation or desert + midland = biomass > 0.4 ? "#567D46" : "#C0A060" // Either vegetation or desert + highland = "#B89060" // Light brown + mountain = "#A89080" // Reddish rock + snow = temperature > 350 ? "#F0F0F0" : "#FFFFFF" // Slightly off-white at higher temps + } + // Very hot planet (400K to 600K) + else if (temperature < 600) { + beach = "#F8D080" // Orange sand + lowland = "#E0B060" // Desert + midland = "#C09050" // Dark desert + highland = "#A07040" // Reddish brown + mountain = "#805030" // Dark red rock + snow = "#E8E8E8" // Off-white (salt flats) + } + // Extremely hot planet (>600K) + else { + beach = "#FF9060" // Orange-red + lowland = "#E07040" // Reddish + midland = "#C05020" // Dark red + highland = "#A03010" // Very dark red + mountain = "#802000" // Almost black-red + snow = "#D0D0D0" // Light gray (ash) + } + + return { beach, lowland, midland, highland, mountain, snow } +} + +// Auto-determine planet type based on mass and radius +export function getAutoType(mass: number, radius: number): "terrestrial" | "gaseous" { + return mass > 7.5 || radius > 2.5 ? "gaseous" : "terrestrial" +} + +// Get simplified colors for simple planet interface (maps to primary terrain colors) +export function getSimplifiedColors(config: PlanetConfig): { + primary: string + secondary: string + accent: string +} { + if (config.type === "gaseous") { + return { + primary: config.colors.atmosphere, + secondary: config.colors.ocean, + accent: config.colors.oceanPattern, + } + } else { + return { + primary: config.colors.ocean, // Water/ocean + secondary: config.colors.lowland, // Land + accent: config.colors.highland, // Mountains + } + } +} + +// Update full config from simplified colors +export function updateFromSimplifiedColors( + config: PlanetConfig, + colors: { primary: string; secondary: string; accent: string }, +): Partial { + if (config.type === "gaseous") { + return { + colors: { + ...config.colors, + atmosphere: colors.primary, + ocean: colors.secondary, + oceanPattern: colors.accent, + beach: colors.primary, + lowland: colors.secondary, + midland: colors.accent, + highland: colors.primary, + mountain: colors.secondary, + snow: colors.accent, + }, + } + } else { + return { + colors: { + ...config.colors, + ocean: colors.primary, + oceanPattern: colors.primary, + lowland: colors.secondary, + midland: colors.secondary, + highland: colors.accent, + mountain: colors.accent, + }, + } + } +} + +// Create a simplified config interface for the simple planet viewer +export function createSimplifiedConfig(config: PlanetConfig) { + return { + type: config.type, + radius: config.radius, + mass: config.mass, + density: config.mass / ((config.radius ** 3 * Math.PI * 4) / 3), + seed: config.seed, + colors: getSimplifiedColors(config), + // Include full config for the mesh to use + fullConfig: config, + } +}; \ No newline at end of file diff --git a/app/planets/tests/[id]/page.tsx b/app/planets/tests/[id]/page.tsx index 71d0628d5..ef2dd860e 100644 --- a/app/planets/tests/[id]/page.tsx +++ b/app/planets/tests/[id]/page.tsx @@ -9,7 +9,7 @@ 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 StructuresOnTerrarium from "@/components/Structures/StructuresOnTerrarium"; import { Badge } from "@/components/ui/badge"; import { Camera, Clock } from "lucide-react"; import Image from "next/image" diff --git a/app/posts/[id]/page.tsx b/app/posts/[id]/page.tsx index a857345fd..271cba587 100644 --- a/app/posts/[id]/page.tsx +++ b/app/posts/[id]/page.tsx @@ -4,8 +4,10 @@ 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"; +import GameNavbar from "@/components/Layout/Tes"; +import { Button } from "@/components/ui/button"; +import { useRouter } from "next/navigation"; interface Classification { id: number; @@ -20,7 +22,9 @@ interface Classification { export default function SinglePostPage({ params }: { params: { id: string } }) { const supabase = useSupabaseClient(); - + + const router = useRouter(); + const [classification, setClassification] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -32,7 +36,7 @@ export default function SinglePostPage({ params }: { params: { id: string } }) { setError("Invalid classification ID."); setLoading(false); return; - }; + } try { const { data, error } = await supabase @@ -46,9 +50,7 @@ export default function SinglePostPage({ params }: { params: { id: string } }) { } else { const flattenedMedia = (data.media || []) .flat() - .filter( - (url: string) => typeof url === "string" && url.startsWith("http") - ); + .filter((url: string) => typeof url === "string" && url.startsWith("http")); setClassification({ ...data, @@ -68,32 +70,35 @@ export default function SinglePostPage({ params }: { params: { id: string } }) { if (loading) return ( -
+
Backdrop -
-

Loading classification...

-
+
Loading classification...
); - if (error) return

{error}

; + if (error) return

{error}

; if (!classification) return

No classification found.

; return ( -
- +
+ Backdrop -
+ +
+ +
+ +
{classification.author && ( -
+
- + {/*
+ +
*/}
)}
diff --git a/app/posts/surveyor/[id]/page.tsx b/app/posts/surveyor/[id]/page.tsx index 464684c4b..4e6579cde 100644 --- a/app/posts/surveyor/[id]/page.tsx +++ b/app/posts/surveyor/[id]/page.tsx @@ -100,7 +100,7 @@ export default function SurveyorPostPage({ params }: { params: { id: string } }) content={classification.content || "No content available"} category={classification.classificationtype || "Unknown"} images={classification.media || []} classificationId={0} anomalyId={classification.id.toString()} votes={0} classificationType={classification.classificationtype || ''} /> - + {/* */}
)}
diff --git a/app/research/page.tsx b/app/research/page.tsx index 28aead86c..8dcdcd28e 100644 --- a/app/research/page.tsx +++ b/app/research/page.tsx @@ -1,83 +1,201 @@ "use client"; -import { useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import GameNavbar from "@/components/Layout/Tes"; +import { Button } from "@/components/ui/button"; +import { useRouter } from "next/navigation"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { ChevronDown } from "lucide-react"; +import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react"; +import TotalPoints from "@/components/Structures/Missions/Stardust/Total"; +import BiologyResearch from "@/components/Research/BiologyItems"; 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"; +import ReferralCodePanel from "@/components/Account/Referrals"; +import AutomatonSurfaceRoverResearch from "@/components/Research/Automatons/Rovers"; type CapacityKey = | "probeCount" | "probeDistance" | "stationCount" | "balloonCount" + | "Seiscam" + | "Wheels" | "cameraCount" | "stationSize"; type UserCapacities = Record; -export default function TechTreeResearchPage() { +export default function ResearchPage() { + const router = useRouter(); + const supabase = useSupabaseClient(); + const session = useSession(); + 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 totalPointsRef = useRef(null); + + // Referral related states + const [userHasReferral, setUserHasReferral] = useState(null); + const [referralCodeInput, setReferralCodeInput] = useState(""); + const [referralError, setReferralError] = useState(null); + const [isSubmittingReferral, setIsSubmittingReferral] = useState(false); + const [referralSuccess, setReferralSuccess] = useState(null); + + // Check if user already has a referral code + useEffect(() => { + if (!session?.user?.id) return; + + const checkReferral = async () => { + const { data, error } = await supabase + .from("referrals") + .select("id") + .eq("referree_id", session.user.id) + .limit(1) + .maybeSingle(); + + if (error) { + console.error("Error checking referral:", error.message); + setUserHasReferral(false); + } else { + setUserHasReferral(!!data); + } + }; + + checkReferral(); + }, [session, supabase]); + + const handleDivClick = () => { + setTimeout(() => { + if (totalPointsRef.current?.refreshPoints) { + totalPointsRef.current.refreshPoints(); + } + }, 3000); }; - const handleUpgrade = (capacity: CapacityKey, currentLevel: number) => { - const cost = getUpgradeCost(currentLevel); - if (availablePoints >= cost) { - setAvailablePoints((prev) => prev - cost); - setUserCapacities((prev) => ({ - ...prev, - [capacity]: prev[capacity] + 1, - })); + // Submit referral code for user + const handleReferralSubmit = async () => { + setReferralError(null); + setReferralSuccess(null); + + if (!session?.user?.id) { + setReferralError("You must be logged in to submit a referral code."); + return; + }; + + if (!referralCodeInput.trim()) { + setReferralError("Please enter a valid referral code."); + return; + }; + + setIsSubmittingReferral(true); + + try { + const { error } = await supabase.from("referrals").insert({ + referree_id: session.user.id, + referral_code: referralCodeInput.trim(), + }); + + if (error) { + setReferralError("Failed to submit referral code. Please try again."); + setIsSubmittingReferral(false); + return; + } + + setReferralSuccess("Referral code added successfully!"); + setUserHasReferral(true); + setReferralCodeInput(""); + } catch (e) { + setReferralError("An unexpected error occurred."); + } finally { + setIsSubmittingReferral(false); } }; return ( -
-
+
+ Earth background + +
-
-
-
-

RESEARCH LAB

-
-
- STARDUST: - +
+ { + if (!open) router.push("/"); + }} + > + +
+

+ RESEARCH LAB +

+
+ Stardust: + + +
-
-
- -
- {/* setAvailablePoints(total)} /> */} - -
- - - -
- -
+ +
+
+ + + + +
+ +
+ +
+ + {/* NEW referral input section if user has no referral yet */} + {userHasReferral === false && ( +
+

+ Add Your Referral Code +

+

+ If someone referred you to Star Sailors, add their code to get some bonus stardust! +

+ setReferralCodeInput(e.target.value)} + placeholder="Enter your referral code" + disabled={isSubmittingReferral} + className="w-full mb-3 px-3 py-2 rounded border border-[#81A1C1] bg-[#3B4252] text-[#ECEFF4] focus:outline-none focus:ring-2 focus:ring-[#88C0D0]" + /> + {referralError && ( +

{referralError}

+ )} + {referralSuccess && ( +

{referralSuccess}

+ )} + +
+ )} +
+ +
); diff --git a/app/scenes/earth/page.tsx b/app/scenes/earth/page.tsx index 6bbc10d28..1458af7f3 100644 --- a/app/scenes/earth/page.tsx +++ b/app/scenes/earth/page.tsx @@ -6,12 +6,13 @@ import { useActivePlanet } from "@/context/ActivePlanet"; import { EarthViewLayout } from "@/components/(scenes)/planetScene/layout"; import { InventoryStructureItem } from "@/types/Items"; import { PlanetarySystem } from "@/components/(scenes)/planetScene/orbitals/system"; -import StructuresOnPlanet, { - AtmosphereStructuresOnPlanet, - OrbitalStructuresOnPlanet, -} from "@/components/Structures/Structures"; +// import StructuresOnPlanet, { +// AtmosphereStructuresOnPlanet, +// OrbitalStructuresOnPlanet, +// } from "@/components/Structures/Structures"; import AllAutomatonsOnActivePlanet from "@/components/Structures/Auto/AllAutomatons"; import InventoryPage from "@/components/Inventory/Grid/Grid"; +import Structures from "@/components/Structures/Structures"; // import EnhancedWeatherEvents from '@/components/(scenes)/mining/enhanced-weather-events'; // import TopographicMap from "@/components/topographic-map"; @@ -53,14 +54,11 @@ const EarthStructures: React.FC = () => { {/* */}
-
- -
- +
diff --git a/app/structures/balloon/[project]/page.tsx b/app/structures/balloon/[project]/page.tsx new file mode 100644 index 000000000..59aa12240 --- /dev/null +++ b/app/structures/balloon/[project]/page.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { useRouter, useParams } from 'next/navigation'; +import { useSession } from '@supabase/auth-helpers-react'; +import Home from '@/app/page'; +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 { DataSourcesModal } from '@/components/Data/unlockNewDataSources'; +import React from 'react'; + +export default function WeatherBalloonProjectPage() { + const session = useSession(); + + const router = useRouter(); + const params = useParams(); + + if (!session) return ; + + const project = params?.project; + + const componentMap: { [key: string]: React.ReactNode } = { + // research: , + clouds: , + storms: , + landmarks: , + surface: , + }; + + const SelectedComponent = componentMap[project as string]; + + if (!SelectedComponent) { + return ( +
+

Unknown Project

+

The selected weather balloon project does not exist.

+
+ ); + }; + + return ( +
+ Earth Background +
+ +
+
+
+
+ +
+ {SelectedComponent} +
+
+
+ ); +}; \ No newline at end of file diff --git a/app/structures/balloon/page.tsx b/app/structures/balloon/page.tsx index 746bf596c..c8230651c 100644 --- a/app/structures/balloon/page.tsx +++ b/app/structures/balloon/page.tsx @@ -1,47 +1,34 @@ -'use client'; +'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 { useSession } from "@supabase/auth-helpers-react"; import { BuildingIcon, CameraOffIcon, CloudCogIcon, CloudDrizzleIcon, MicroscopeIcon, - RssIcon, - UmbrellaIcon + RssIcon } from "lucide-react"; import { useRouter } from "next/navigation"; -import React, { useState } from "react"; +import React from "react"; +import GameNavbar from "@/components/Layout/Tes"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; export default function WeatherBalloonOnEarthPage() { - const supabase = useSupabaseClient(); const session = useSession(); - - const router = useRouter(); - const [activeComponent, setActiveComponent] = useState(null); + const router = useRouter(); - if (!session) return ; + if (!session) { + router.push("/"); + }; const actions = [ { icon: , text: "Research", - dynamicComponent: , - }, - { - icon: , - text: "Upcoming weather events", - dynamicComponent: , + route: "research", }, ]; @@ -49,33 +36,25 @@ export default function WeatherBalloonOnEarthPage() { { icon: , text: "Search your clouds", - dynamicComponent: , + route: "clouds", }, { icon: , text: "Map storms on gas planets", - dynamicComponent: , + route: "storms", }, { icon: , text: "Identify landmarks on terrestrial planets", - dynamicComponent: , + route: "landmarks", }, { icon: , text: "Map the surface of planets", - dynamicComponent: , + route: "surface", }, ]; - const handleComponentChange = (component: React.ReactNode) => { - setActiveComponent(component); - }; - - const handleBack = () => { - setActiveComponent(null); - }; - return (
- {/* Back button when showing dynamic content */} - {activeComponent && ( -
- +
+
+
+ +

Weather Balloon

+
- )} - {/* Content switcher */} -
- {activeComponent ? ( -
{activeComponent}
- ) : ( - <> -
-
- -

Weather Balloon

-
-
+
+ Weather Balloon +
-
- Weather Balloon +
+ {actions.map((action, index) => ( +
router.push(`/structures/balloon/${action.route}`)} + > + {action.icon} +

{action.text}

+ ))} +
-
- {actions.map((action, index) => ( -
handleComponentChange(action.dynamicComponent)} - > - {action.icon} -

{action.text}

+
+
+ {buttons.map((button, index) => ( +
router.push(`/structures/balloon/${button.route}`)} + style={{ width: "100%", maxWidth: "240px" }} + > +
+
{button.icon}
+

{button.text}

- ))} -
- -
-
- {buttons.map((button, index) => ( -
handleComponentChange(button.dynamicComponent)} - style={{ width: "100%", maxWidth: "240px" }} - > -
-
{button.icon}
-

{button.text}

-
-
- ))}
-
- - )} + ))} +
+
diff --git a/app/structures/greenhouse/[project]/page.tsx b/app/structures/greenhouse/[project]/page.tsx new file mode 100644 index 000000000..74651d35c --- /dev/null +++ b/app/structures/greenhouse/[project]/page.tsx @@ -0,0 +1,71 @@ +'use client' + +import React, { useEffect, useState } from "react" +import { useParams, useRouter } from "next/navigation" +import { useSessionContext } from "@supabase/auth-helpers-react" +import GameNavbar from "@/components/Layout/Tes" +import { GreenhouseResearchStations } from "@/components/Structures/Missions/Biologists/ResearchStations" +import ChatGPTImageClassifier from "@/app/tests/pleaseWork" + +export default function GreenhouseProjectRoute() { + const { project } = useParams(); + const router = useRouter(); + + const { session, isLoading } = useSessionContext(); + + const [ProjectComponent, setProjectComponent] = useState(null); + + useEffect(() => { + if (!isLoading && !session) { + router.push('/'); + }; + }, [isLoading, session, router]); + + useEffect(() => { + if (!isLoading && session) { + switch (project) { + case 'stations': + setProjectComponent(); + break; + case 'scan': + setProjectComponent(); + break; + default: + router.push('/structures/greenhouse'); + break; + }; + }; + }, [project, session, isLoading, router]); + + if (isLoading) { + return null; + }; + + return ( +
+ Earth Background + +
+ +
+ +
+
+
+ +
+ {ProjectComponent} +
+
+
+ ); +}; \ No newline at end of file diff --git a/app/structures/greenhouse/page.tsx b/app/structures/greenhouse/page.tsx index cd53bb97d..978bd29ee 100644 --- a/app/structures/greenhouse/page.tsx +++ b/app/structures/greenhouse/page.tsx @@ -1,45 +1,33 @@ '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"; +import React 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: , + route: "stations", }, { icon: , text: "Scan animals around you", - dynamicComponent: , + route: "scan", }, ]; - const handleComponentChange = (component: React.ReactNode) => { - setActiveComponent(component); - }; - - const handleBack = () => { - setActiveComponent(null); - }; - return (
- {/* Back button when showing dynamic content */} - {activeComponent && ( -
- -
- )} - - {/* Content switcher */}
- {activeComponent ? ( -
{activeComponent}
- ) : ( - <> -
-
- -

Greenhouse

-
-
+
+
+ +

Greenhouse

+
+
-
- Greenhouse Biodome -
+
+ Greenhouse Biodome +
-
-
- {buttons.map((button, index) => ( -
handleComponentChange(button.dynamicComponent)} - style={{ width: "100%", maxWidth: "240px" }} - > -
-
{button.icon}
-

{button.text}

-
-
- ))} +
+
+ {buttons.map((button, index) => ( +
router.push(`/structures/greenhouse/${button.route}`)} + style={{ width: "100%", maxWidth: "240px" }} + > +
+
{button.icon}
+

{button.text}

+
-
- - )} + ))} +
+
diff --git a/app/structures/seiscam/[project]/page.tsx b/app/structures/seiscam/[project]/page.tsx new file mode 100644 index 000000000..7cb1d13e1 --- /dev/null +++ b/app/structures/seiscam/[project]/page.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useRouter, useParams } from "next/navigation"; +import { useSession } from "@supabase/auth-helpers-react"; +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"; + +export default function SeisCamProjectpage() { + const params = useParams(); + const router = useRouter(); + const project = params?.project; + + const session = useSession(); + + if (!session) { + router.push("/"); + }; + + const componentMap: { [ key: string ]: React.ReactNode } = { + landmarks: , + surface: , + }; + + const SelectedComponent = componentMap[ project as string ]; + + if (!SelectedComponent) { + router.push("/structures/seiscam"); + }; + + return ( +
+ Earth Background + +
+ +
+ +
+
+
+ +
+ {SelectedComponent} +
+
+ +
+ + ) +} \ No newline at end of file diff --git a/app/structures/seiscam/page.tsx b/app/structures/seiscam/page.tsx new file mode 100644 index 000000000..97410588a --- /dev/null +++ b/app/structures/seiscam/page.tsx @@ -0,0 +1,133 @@ +'use client' + +import React, { useEffect } from "react" +import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react" +import { useRouter } from "next/navigation" +import GameNavbar from "@/components/Layout/Tes" +import { Dialog, DialogContent } from "@/components/ui/dialog" +import { + BuildingIcon, + CameraOffIcon, + MicroscopeIcon, + RssIcon, +} from "lucide-react" + +export default function SeiscamOnEarthPage() { + const router = useRouter() + const session = useSession() + + useEffect(() => { + if (!session) { + router.push("/") + } + }, [session, router]) + + const actions = [ + { + icon: , + text: "Research", + route: "research", + }, + ] + + const buttons = [ + { + icon: , + text: "Identify landmarks on terrestrial planets", + route: "landmarks", + }, + { + icon: , + text: "Map the surface of planets", + route: "surface", + }, + ] + + return ( +
+ Earth Background + +
+ +
+ +
+ { + if (!open) { + router.push("/") + } + }} + > + +
+ {/* Header */} +
+
+ +

SEISCAM

+
+ + Weather Balloon +
+ + {/* Actions */} +
+ {actions.map((action, index) => ( +
+ router.push(`/structures/seiscam/${action.route}`) + } + > + {action.icon} +

{action.text}

+
+ ))} +
+ + {/* Buttons */} +
+ {buttons.map((button, index) => ( +
+ router.push(`/structures/seiscam/${button.route}`) + } + style={{ width: "100%", maxWidth: "240px" }} + > +
+
{button.icon}
+

{button.text}

+
+
+ ))} +
+
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/app/structures/telescope/[project]/[id]/[mission]/page.tsx b/app/structures/telescope/[project]/[id]/[mission]/page.tsx new file mode 100644 index 000000000..935b0ba72 --- /dev/null +++ b/app/structures/telescope/[project]/[id]/[mission]/page.tsx @@ -0,0 +1,120 @@ +'use client' + +import { useEffect, useState } from "react" +import { useParams, useRouter } from "next/navigation" +import { useSessionContext } from "@supabase/auth-helpers-react" + +// Mission imports +import PlanetHuntersSteps from "@/components/Structures/Missions/Astronomers/PlanetHunters/PlanetHunters" +import { StarterSunspot } from "@/components/Projects/Telescopes/Sunspots" +import { StarterTelescopeTess } from "@/components/Projects/Telescopes/Transiting" +import GameNavbar from "@/components/Layout/Tes" +import PlanetTypeCommentForm from "@/components/Structures/Missions/Astronomers/PlanetHunters/PlanetType" +import VotePlanetClassifications from "@/components/Structures/Missions/Astronomers/PlanetHunters/PHVote" +import PlanetGenerator from "@/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator" +import { DailyMinorPlanetWithId } from "@/components/Projects/Telescopes/DailyMinorPlanet" + +export default function TelescopeClassifyPage() { + const params = useParams() + const router = useRouter() + + const { session, isLoading } = useSessionContext() + + const [MissionComponent, setMissionComponent] = useState(null) + + useEffect(() => { + if (!isLoading && !session) { + router.push('/') + } + }, [session, isLoading, router]) + + useEffect(() => { + if (isLoading || !session) return + + const project = String(params.project) + const mission = String(params.mission) + const idParam = String(params.id || "") + + let classificationId: string | undefined + if (idParam.startsWith("cl-")) { + classificationId = idParam.replace("cl-", "") + } else if (!isNaN(Number(idParam))) { + classificationId = idParam + } + + let component: React.ReactNode = null + + switch (project) { + case "planet-hunters": + switch (mission) { + case "one": + case "classify": + component = + break + case "comment": + component = ( + + ) + break + case "survey": + component = ( + + ) + break + case "paint": + component = ( + + ) + break + default: + component = + break + } + break + + case "sunspots": + component = + break + + case "daily-minor-planet": + switch (mission) { + case "classify": + component = + break + } + break + + default: + component =
Unknown mission or project.
+ break + } + + setMissionComponent(component) + }, [params, session, isLoading]) + + return ( +
+ {/* Background image behind everything */} +
+ Earth Background +
+ + {/* Sticky/relative nav and content */} +
+ +
+ +
+
+ {MissionComponent} +
+
+
+ ); +}; \ No newline at end of file diff --git a/app/structures/telescope/[project]/page.tsx b/app/structures/telescope/[project]/page.tsx new file mode 100644 index 000000000..1a50d5309 --- /dev/null +++ b/app/structures/telescope/[project]/page.tsx @@ -0,0 +1,80 @@ +'use client' + +import { useEffect, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { useSessionContext } from "@supabase/auth-helpers-react"; + +import GameNavbar from "@/components/Layout/Tes"; +import PlanetHuntersSteps from "@/components/Structures/Missions/Astronomers/PlanetHunters/PlanetHunters"; +import { TelescopeDiskDetector } from "@/components/Projects/Telescopes/DiskDetector"; +import DailyMinorPlanetMissions from "@/components/Structures/Missions/Astronomers/DailyMinorPlanet/DailyMinorPlanet"; +import SunspotSteps from "@/components/Projects/Telescopes/Sunspots/SunspotShell"; + +export default function TelescopeProjectRoute() { + const { project } = useParams(); + const router = useRouter(); + + const { session, isLoading } = useSessionContext(); + + const [ProjectComponent, setProjectComponent] = useState(null); + + useEffect(() => { + if (!isLoading && !session) { + router.push('/'); + } + }, [isLoading, session, router]); + + useEffect(() => { + if (!isLoading && session) { + switch (project) { + case 'planet-hunters': + setProjectComponent(); + break; + case 'sunspots': + setProjectComponent(); + break; + case 'daily-minor-planet': + setProjectComponent(); + break; + case 'disk-detective': + setProjectComponent(); + break; + default: + router.push('/structures/telescope'); + break; + }; + }; + }, [project, session, isLoading, router]); + + if (isLoading) { + return null; + }; + + return ( +
+ Earth Background + +
+ +
+ +
+
+
+ +
+ {ProjectComponent} +
+
+
+ ); +}; \ No newline at end of file diff --git a/app/structures/telescope/page.tsx b/app/structures/telescope/page.tsx index 80f528695..f587e816e 100644 --- a/app/structures/telescope/page.tsx +++ b/app/structures/telescope/page.tsx @@ -1,168 +1,68 @@ -'use client'; +'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 { useSession } from "@supabase/auth-helpers-react"; +import TelescopeViewport from "@/components/Structures/Telescope/telescope-viewport"; import { useState } from "react"; export default function TelescopeOnEarthPage() { - const supabase = useSupabaseClient(); - const session = useSession(); const router = useRouter(); - - const [activeComponent, setActiveComponent] = useState(null); + const session = useSession(); + const [simpleMode, setSimpleMode] = useState(false); // default: viewport mode 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 (
+ {/* Background Image */} Earth Background -
+ {/* Navbar */} +
-
+ {/* Dialog Wrapper */} +
{ - if (!open) { - router.push("/"); - } + if (!open) router.push("/"); }} > - {/* Back button when showing dynamic content */} - {activeComponent && ( -
- -
- )} + {/* Mode Toggle */} +
+ +
- {/* Content switcher */} -
- {activeComponent ? ( -
{activeComponent}
+ {/* Content */} +
+ {!simpleMode ? ( +
+ +
) : ( - <> -
-
- -

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}

-
-
- ))} -
-
- + )}
@@ -170,4 +70,49 @@ export default function TelescopeOnEarthPage() {
); +} + +function SimpleTelescopePanel({ router }: { router: any }) { + const actions = [ + { + icon: "🔧", + text: "Upgrades", + onClick: () => router.push('/research'), + }, + ]; + + const buttons = [ + { icon: "🔭", text: "Discover planets", route: "planet-hunters" }, + { icon: "☀️", text: "Sunspot data", route: "sunspots" }, + { icon: "🧪", text: "Find early solar systems", route: "disk-detective" }, + { icon: "🌑", text: "Discover asteroids", route: "daily-minor-planet" }, + ]; + + return ( +
+
+ {actions.map((action, i) => ( + + ))} +
+ +
+ {buttons.map((btn, i) => ( + + ))} +
+
+ ); }; \ No newline at end of file diff --git a/app/tests/page.tsx b/app/tests/page.tsx index 3b4bf0c80..19e7ba682 100644 --- a/app/tests/page.tsx +++ b/app/tests/page.tsx @@ -1,12 +1,16 @@ "use client"; +import PlanetGenerator from "@/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator"; // 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 MilestoneCard from "@/components/Structures/Missions/Milestones/MilestoneCard"; import TotalPoints from "@/components/Structures/Missions/Stardust/Total"; import MyLocationIds from "@/content/Classifications/UserLocationPK"; import MySettlementsLocations from "@/content/Classifications/UserLocations"; +import NPSPopup from "@/lib/helper/nps-popup"; +// import TelescopeViewport from "@/telescope-viewport"; import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react"; import { useEffect, useState } from "react"; // import { PlanetGenerator } from "starsailors-planet-generator"; @@ -37,13 +41,14 @@ export default function TestPage() { setLocationIds(parsedData); } setLoading(false); - } + }; fetchLocationIds(); }, [session]); return ( -
+
+ {/* {}} isOpen={true} /> */} {/* */} {/* @@ -55,7 +60,8 @@ export default function TestPage() { /> */} {/* */} - + {/* */} + {/* */} {/* */}
); diff --git a/components/(scenes)/deploy/ActivityHeader.tsx b/components/(scenes)/deploy/ActivityHeader.tsx new file mode 100644 index 000000000..aefa2b9a6 --- /dev/null +++ b/components/(scenes)/deploy/ActivityHeader.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react"; +import { AvatarGenerator } from "@/components/Account/Avatar"; +import ProfileDetailsPanel from "@/components/Account/ProfileDetailsPanel"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import UserMilestoneMissionSummary from "@/components/Structures/Missions/MissionProgress"; + +export default function ActivityHeader({ + landmarksExpanded, + onToggleLandmarks, + scrolled, +}: { + landmarksExpanded: boolean; + onToggleLandmarks: () => void; + scrolled: boolean; +}) { + const supabase = useSupabaseClient(); + const session = useSession(); + + const [profile, setProfile] = useState<{ classificationPoints: number | null } | null>(null); + const [showDetailsModal, setShowDetailsModal] = useState(false); + + useEffect(() => { + if (!session?.user?.id) return; + + const fetchData = async () => { + const { data: profileData } = await supabase + .from("profiles") + .select("classificationPoints") + .eq("id", session.user.id) + .maybeSingle(); + setProfile(profileData); + }; + + fetchData(); + }, [session, supabase]); + + return ( +
+
+ {!scrolled && ( +
+ +
+ )} +
+

{session?.user?.email || "User"}

+ + {!scrolled && ( +
+ + + +
+ )} +
+
+ + {!scrolled && ( +
+
+ {profile?.classificationPoints ?? 0} Stardust +
+ +
+ )} + + + + + Your Profile + + setShowDetailsModal(false)} /> + + +
+ ); +}; \ No newline at end of file diff --git a/components/(scenes)/deploy/AutomatonItems.tsx b/components/(scenes)/deploy/AutomatonItems.tsx index e11d2b323..05e8e854d 100644 --- a/components/(scenes)/deploy/AutomatonItems.tsx +++ b/components/(scenes)/deploy/AutomatonItems.tsx @@ -24,7 +24,8 @@ const automatons: Vehicle[] = [ { id: "1", name: "Probe", - description: "Simple and versatile satellite used to obtain data of nearby exoplanets", + // description: "Simple and versatile satellite used to obtain data of nearby exoplanets", + description: "Send probes to your discovered planets to find clouds and study the surface", image: "/assets/Automatons/Sat.png", stats: { speed: 100, armor: 100, capacity: 100 }, cost: 1, @@ -33,7 +34,8 @@ const automatons: Vehicle[] = [ { id: "2", name: "Scout Roover", - description: "Train roover", + // description: "Train roover", + description: "Send rovers alongside your probes to planets to train the rover AI and find cool sights (and alien artifacts) on the planets discovered by you and the community!", image: "/assets/Automatons/ExploreRover1.png", stats: { speed: 100, armor: 100, capacity: 100 }, cost: 1, @@ -94,7 +96,7 @@ export default function AutomatonDeploySection() { if (!data || data.length === 0) { setDeployDisabled(false); return; - } + }; const now = new Date(); const currentWeek = getISOWeekYear(now); @@ -152,8 +154,8 @@ export default function AutomatonDeploySection() { await fetch(`api/gameplay/research/upgrades?techType=proberange&count=${probeDistance}`); } catch (error) { console.error("Error fetching research data:", error); - } - } + }; + }; fetchResearchData(); }, [session, supabase]); @@ -193,7 +195,7 @@ export default function AutomatonDeploySection() { if (fetchErr) { console.error("Error fetching existing linked anomalies:", fetchErr); return; - } + }; const now = new Date(); const currentWeek = getISOWeekYear(now); @@ -239,19 +241,22 @@ export default function AutomatonDeploySection() { const neededSets: Record = {}; - rovers.forEach(() => { - getRandomTypes(["AI4M", "JVH"]).forEach((type) => { - const key = anomalySetMap[type]; - neededSets[key] = (neededSets[key] || 0) + 1; - }); - }); +const roverCount = rovers.length; +const probeCount = probes.length; - probes.forEach(() => { - getRandomTypes(["P4", "Clouds"]).forEach((type) => { - const key = anomalySetMap[type]; - neededSets[key] = (neededSets[key] || 0) + 1; - }); - }); +// Ensure 3 of each anomaly set for every rover +neededSets["automaton-aiForMars"] = roverCount * 3; +neededSets["lidar-jovianVortexHunter"] = roverCount * 3; +neededSets["satellite-planetFour"] = roverCount * 3; +neededSets["cloudspottingOnMars"] = roverCount * 3; + +// For probes, keep the randomized behavior (optional: adjust to fixed 2 each as well if needed) +probes.forEach(() => { + getRandomTypes(["P4", "Clouds"]).forEach((type) => { + const key = anomalySetMap[type]; + neededSets[key] = (neededSets[key] || 0) + 1; + }); +}); const anomalyIdPool: Record = {}; @@ -300,7 +305,22 @@ export default function AutomatonDeploySection() { }); }; - assignAnomalies(rovers, ["AI4M", "JVH"], "Rover"); + // Assign anomalies for rovers: 3 each of all 4 types per rover +rovers.forEach((loc) => { + ["automaton-aiForMars", "lidar-jovianVortexHunter", "satellite-planetFour", "cloudspottingOnMars"].forEach((set) => { + for (let i = 0; i < 3; i++) { + const ids = anomalyIdPool[set] || []; + const assignedId = ids.length ? ids.shift()! : null; + + assignments.push({ + classification: loc, + anomalySet: set, + anomalyId: assignedId, + automaton: "Rover", + }); + } + }); +}); assignAnomalies(probes, ["P4", "Clouds"], "Probe"); const entriesToInsert = assignments diff --git a/components/(scenes)/deploy/BiodomeStationCard.tsx b/components/(scenes)/deploy/BiodomeStationCard.tsx index ce8815729..0835cbb0e 100644 --- a/components/(scenes)/deploy/BiodomeStationCard.tsx +++ b/components/(scenes)/deploy/BiodomeStationCard.tsx @@ -67,10 +67,10 @@ const stationConfig: Station[] = [ mass: 0.25, }, ], - location: { - coordinates: "23.4162° N, 75.2397° W", - depth: "+2m", - }, + // location: { + // coordinates: "23.4162° N, 75.2397° W", + // depth: "+2m", + // }, built: false, }, { @@ -91,10 +91,10 @@ const stationConfig: Station[] = [ mass: 0.0000001, }, ], - location: { - coordinates: "2.4162° S, 54.2397° W", - altitude: "-10m", - }, + // location: { + // coordinates: "2.4162° S, 54.2397° W", + // altitude: "-10m", + // }, built: false, }, ]; diff --git a/components/(scenes)/deploy/TelescopeRange.tsx b/components/(scenes)/deploy/TelescopeRange.tsx index 142da0c2e..8ab7cdd49 100644 --- a/components/(scenes)/deploy/TelescopeRange.tsx +++ b/components/(scenes)/deploy/TelescopeRange.tsx @@ -28,6 +28,7 @@ export default function TelescopeRangeSlider() { const [dropRates, setDropRates] = useState>({}); const [fetchedAnomalies, setFetchedAnomalies] = useState([]); const [alreadyDeployed, setAlreadyDeployed] = useState(false); + const [deploymentMessage, setDeploymentMessage] = useState(null); // Calculate weighted anomaly drop rates whenever the slider changes useEffect(() => { @@ -91,6 +92,7 @@ export default function TelescopeRangeSlider() { .from("linked_anomalies") .select("*") .eq("automaton", "Telescope") + .eq("author", session?.user.id) .gte("date", oneWeekAgo.toISOString()); if (error) { @@ -104,7 +106,7 @@ export default function TelescopeRangeSlider() { }; checkDeployment(); - }, [supabase]); + }, [supabase, session?.user?.id]); const activeDropTypes = [ { @@ -140,8 +142,6 @@ export default function TelescopeRangeSlider() { }; const handleDeploy = async () => { - // This code block should never run if alreadyDeployed is true, - // but is retained for logic completeness. const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); @@ -198,7 +198,10 @@ export default function TelescopeRangeSlider() { } if (data && data.length > 0) { - const anomaly = data[0]; + // 🔧 FIX: Pick a random anomaly from the list instead of always using data[0] + const randomIndex = Math.floor(Math.random() * data.length); + const anomaly = data[randomIndex]; + anomalies.push(anomaly); const { error: insertError } = await supabase @@ -222,12 +225,20 @@ export default function TelescopeRangeSlider() { console.log("Fetched anomalies:", anomalies); setFetchedAnomalies(anomalies); + if (anomalies.length > 0) { + const anomalyNames = anomalies.map((a) => a.anomalySet).join(", "); + setDeploymentMessage(`Telescope successfully deployed. New anomalies detected: ${anomalyNames}. Open the telescope interface to begin your observations.`); + }; }; - // If already deployed this week, show only a message if (alreadyDeployed) { return (
+ {deploymentMessage && ( +
+ {deploymentMessage} +
+ )}

The Telescope has already been deployed this week. You’ll be able to recalibrate and search again next week.

@@ -235,9 +246,13 @@ export default function TelescopeRangeSlider() { ); } - // Main UI if not yet deployed return (
+ {deploymentMessage && ( +
+ {deploymentMessage} +
+ )} @@ -294,14 +309,14 @@ export default function TelescopeRangeSlider() {
); -} +}; interface DropRateItemProps { icon: React.ReactNode; label: string; rate: number; colorClass: string; -} +}; function DropRateItem({ icon, diff --git a/components/(scenes)/deploy/TelescopeViewportRange.tsx b/components/(scenes)/deploy/TelescopeViewportRange.tsx new file mode 100644 index 000000000..961bd0735 --- /dev/null +++ b/components/(scenes)/deploy/TelescopeViewportRange.tsx @@ -0,0 +1,281 @@ +'use client' + +import { useState, useEffect, useCallback } from "react" +import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react" +import { TelescopeView } from "@/components/Structures/Telescope/telescope-view" +import type { Anomaly, Star } from "@/types/Structures/telescope" +import { generateSectorName, generateStars } from "@/utils/Structures/Telescope/sector-utils" +import DeployTelescopeSidebar from "./sub/TelescopeSidebar" +import DeployTelescopeMobileHeader from "./sub/TelescopeMobileHeader" +import DeployTelescopeOverlay from "./sub/TelescopeOverlay" + +export interface DatabaseAnomaly { + id: number + content: string | null + anomalytype: string | null + avatar_url: string | null + created_at: string + configuration: any + parentAnomaly: number | null + anomalySet: string | null +} +type LocalAnomaly = Anomaly & { dbData: DatabaseAnomaly } + +function seededRandom1(seed: number, salt: number = 0) { + let x = Math.sin(seed + salt) * 10000 + return x - Math.floor(x) +} + +function generateAnomalyFromDB(dbAnomaly: DatabaseAnomaly, sectorX: number, sectorY: number): LocalAnomaly { + const seed = dbAnomaly.id + sectorX * 1000 + sectorY + return { + id: `db-${dbAnomaly.id}`, + name: dbAnomaly.content || `TESS-${String(dbAnomaly.id).padStart(3, "0")}`, + x: seededRandom1(seed, 1) * 80 + 10, + y: seededRandom1(seed, 2) * 80 + 10, + brightness: seededRandom1(seed, 3) * 0.7 + 0.5, + size: seededRandom1(seed, 4) * 0.8 + 0.6, + pulseSpeed: seededRandom1(seed, 5) * 2 + 1, + glowIntensity: seededRandom1(seed, 6) * 0.5 + 0.3, + color: "#78cce2", + shape: "circle", + sector: generateSectorName(sectorX, sectorY), + discoveryDate: new Date().toLocaleDateString(), + type: "exoplanet", + project: "planet-hunters", + dbData: dbAnomaly, + } +} + +export default function DeployTelescopeViewport() { + const supabase = useSupabaseClient() + const session = useSession() + + const [currentSector, setCurrentSector] = useState({ x: 0, y: 0 }) + const [tessAnomalies, setTessAnomalies] = useState([]) + const [sectorAnomalies, setSectorAnomalies] = useState([]) + const [stars, setStars] = useState([]) + const [selectedSector, setSelectedSector] = useState<{ x: number; y: number } | null>(null) + const [alreadyDeployed, setAlreadyDeployed] = useState(false) + const [deploymentMessage, setDeploymentMessage] = useState(null) + const [loading, setLoading] = useState(true) + const [deploying, setDeploying] = useState(false) + const [isDragging, setIsDragging] = useState(false) + const [dragStart, setDragStart] = useState({ x: 0, y: 0 }) + const [focusedAnomaly, setFocusedAnomaly] = useState(null) + + const fetchTessAnomalies = async () => { + const { data, error } = await supabase + .from("anomalies") + .select("*") + .eq("anomalySet", "telescope-tess") + + if (error) { + console.error("Error fetching anomalies:", error) + return + } + if (data) { + setTessAnomalies(data) + } + } + + const checkDeployment = async () => { + if (!session?.user?.id) return + const userId = session.user.id + const oneWeekAgo = new Date() + oneWeekAgo.setDate(oneWeekAgo.getDate() - 7) + + const { data: linkedAnomalies, error: linkedAnomaliesError } = await supabase + .from("linked_anomalies") + .select("*") + .eq("author", userId) + .gte("date", oneWeekAgo.toISOString()) + + if (linkedAnomaliesError) { + console.error("Error fetching linked anomalies:", linkedAnomaliesError) + return + } + + const { data: comments, error: commentsError } = await supabase + .from("comments") + .select("id, classification_id, classification:classifications(author)") + .eq("author", userId) + .gte("created_at", oneWeekAgo.toISOString()) + + if (commentsError) { + console.error("Error fetching comments:", commentsError) + return + } + + const validComments = (comments || []).filter( + (c: any) => c.classification?.author && c.classification.author !== userId + ) + + const { data: votes, error: votesError } = await supabase + .from("votes") + .select("id, classification_id, classification:classifications(author)") + .eq("user_id", userId) + .eq("vote_type", "up") + .gte("created_at", oneWeekAgo.toISOString()) + + if (votesError) { + console.error("Error fetching votes:", votesError) + return + } + + const validVotes = (votes || []).filter( + (v: any) => v.classification?.author && v.classification.author !== userId + ) + + const linkedCount = linkedAnomalies?.length ?? 0 + const commentsCount = validComments.length + const votesCount = validVotes.length + const additionalDeploys = Math.floor(votesCount / 3) + commentsCount + const totalAllowedDeploys = linkedCount + additionalDeploys + const userCanRedeploy = totalAllowedDeploys > linkedCount + + console.log("Linked anomalies deployed this week:", linkedCount) + console.log("Valid comments on others' classifications this week:", commentsCount) + console.log(`Valid votes on others' classifications this week: ${votesCount} (counts as ${Math.floor(votesCount / 3)} deploys)`) + console.log("Total allowed deploys:", totalAllowedDeploys) + console.log("User can redeploy?", userCanRedeploy ? "YES" : "NO") + + if (linkedCount === 0) { + setAlreadyDeployed(false) + setDeploymentMessage(null) + } else if (userCanRedeploy) { + setAlreadyDeployed(false) + setDeploymentMessage("You have earned additional deploys by interacting with the community this week!") + } else { + setAlreadyDeployed(true) + setDeploymentMessage("Telescope has already been deployed this week. Recalibrate & search again next week.") + } + } + + const loadSector = useCallback((x: number, y: number) => { + setStars(generateStars(x, y)) + const seed = x * 1000 + y + const candidates = tessAnomalies + .filter((_, i) => Math.floor(seededRandom1(seed + i) * 10) < 3) + .slice(0, 8) + .map(a => generateAnomalyFromDB(a, x, y)) + setSectorAnomalies(candidates) + }, [tessAnomalies]) + + const handleDeploy = async () => { + if (!session?.user?.id || !selectedSector || alreadyDeployed) return + setDeploying(true) + const seed = selectedSector.x * 1000 + selectedSector.y + const selectedAnomalies = tessAnomalies + .filter((_, i) => Math.floor(seededRandom1(seed, i) * 10) < 3) + .slice(0, 4) + if (selectedAnomalies.length === 0) { + setDeploymentMessage("No TESS anomalies found in selected sector.") + setDeploying(false) + return + } + const inserts = selectedAnomalies.map(anomaly => + supabase.from("linked_anomalies").insert({ + author: session.user.id, + anomaly_id: anomaly.id, + classification_id: null, + automaton: "Telescope", + }) + ) + const results = await Promise.all(inserts) + const hasError = results.some(r => r.error) + setDeploymentMessage( + hasError + ? "Error deploying telescope. Please try again." + : `Telescope successfully deployed! ${selectedAnomalies.length} TESS exoplanet candidates are now being monitored.` + ) + setAlreadyDeployed(!hasError) + setDeploying(false) + } + + const handleAnomalyClick = (a: Anomaly) => setFocusedAnomaly(a) + const handleMouseDown = (e: React.MouseEvent) => { + setDragStart({ x: e.clientX, y: e.clientY }) + setIsDragging(true) + } + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDragging) return + const deltaX = e.clientX - dragStart.x + const deltaY = e.clientY - dragStart.y + if (Math.abs(deltaX) > 50 || Math.abs(deltaY) > 50) { + setCurrentSector({ + x: currentSector.x - Math.sign(deltaX), + y: currentSector.y - Math.sign(deltaY), + }) + setDragStart({ x: e.clientX, y: e.clientY }) + } + } + const handleMouseUp = () => setIsDragging(false) + + useEffect(() => { + const load = async () => { + setLoading(true) + await fetchTessAnomalies() + await checkDeployment() + setLoading(false) + } + load() + }, [session]) + + useEffect(() => { + if (!loading && tessAnomalies.length > 0) { + loadSector(currentSector.x, currentSector.y) + } + }, [currentSector, loadSector, loading, tessAnomalies]) + + if (loading) { + return ( +
+
Loading telescope data....
+
+ ) + } + + return ( +
+ + +
+ + +
+
+ ) +} \ No newline at end of file diff --git a/components/(scenes)/deploy/sub/TelescopeMobileHeader.tsx b/components/(scenes)/deploy/sub/TelescopeMobileHeader.tsx new file mode 100644 index 000000000..c57ac6b10 --- /dev/null +++ b/components/(scenes)/deploy/sub/TelescopeMobileHeader.tsx @@ -0,0 +1,47 @@ +import { Button } from "@/components/ui/button" +import { Crosshair } from "lucide-react" +import type { Anomaly } from "@/types/Structures/telescope" + +interface Props { + sectorAnomalies: Anomaly[] + selectedSector: { x: number; y: number } | null + alreadyDeployed: boolean + deploying: boolean + onDeploy: () => void +} + +export default function DeployTelescopeMobileHeader({ + sectorAnomalies, + selectedSector, + alreadyDeployed, + deploying, + onDeploy, +}: Props) { + return ( +
+
+
+ +
+

Deploy telescope

+

+ {sectorAnomalies.length} targets + {selectedSector && " • Sector selected"} +

+
+
+ + {selectedSector && ( + + )} +
+
+ ); +}; \ No newline at end of file diff --git a/components/(scenes)/deploy/sub/TelescopeNavControls.tsx b/components/(scenes)/deploy/sub/TelescopeNavControls.tsx new file mode 100644 index 000000000..f5e9eb0ee --- /dev/null +++ b/components/(scenes)/deploy/sub/TelescopeNavControls.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + ChevronDown, + ChevronLeft, + ChevronRight, + ChevronUp, + Crosshair, + Target, +} from "lucide-react"; + +interface Sector { + x: number; + y: number; +}; + +interface Props { + currentSector: Sector; + setCurrentSector: (sector: Sector) => void; + setSelectedSector: (sector: Sector) => void; +}; + +export default function DeployTelescopeViewportNavigationControls({ + currentSector, + setCurrentSector, + setSelectedSector, +}: Props) { + const handleNavigate = (direction: "up" | "down" | "left" | "right") => { + const newSector = { ...currentSector }; + switch (direction) { + case "up": + newSector.y -= 1; + break; + case "down": + newSector.y += 1; + break; + case "left": + newSector.x -= 1; + break; + case "right": + newSector.x += 1; + break; + }; + setCurrentSector(newSector); + }; + + const handleSectorSelect = () => { + setSelectedSector({ ...currentSector }); + }; + + return ( + + + + Navigation + + + +
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+ ); +}; \ No newline at end of file diff --git a/components/(scenes)/deploy/sub/TelescopeOverlay.tsx b/components/(scenes)/deploy/sub/TelescopeOverlay.tsx new file mode 100644 index 000000000..5a6c22e15 --- /dev/null +++ b/components/(scenes)/deploy/sub/TelescopeOverlay.tsx @@ -0,0 +1,53 @@ +import type { Anomaly } from "@/types/Structures/telescope"; + +interface Props { + sectorAnomalies: Anomaly[] + currentSector: { x: number; y: number } + selectedSector: { x: number; y: number } | null +} + +export default function DeployTelescopeOverlay({ + sectorAnomalies, + currentSector, + selectedSector, +}: Props) { + const isSelected = selectedSector && + selectedSector.x === currentSector.x && + selectedSector.y === currentSector.y + + return ( +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
TESS TARGETS: {sectorAnomalies.length}
+
STATUS: {selectedSector ? "SELECTED" : "SCANNING"}
+
+ + {isSelected && ( +
+
+
+ SECTOR SELECTED +
+
+ )} +
+ ) +}; \ No newline at end of file diff --git a/components/(scenes)/deploy/sub/TelescopeSidebar.tsx b/components/(scenes)/deploy/sub/TelescopeSidebar.tsx new file mode 100644 index 000000000..7af870d18 --- /dev/null +++ b/components/(scenes)/deploy/sub/TelescopeSidebar.tsx @@ -0,0 +1,147 @@ +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Zap, Target, Crosshair } from "lucide-react" +import type { Anomaly } from "@/types/Structures/telescope" +import DeployTelescopeViewportNavigationControls from "./TelescopeNavControls" + +interface DatabaseAnomaly { + id: number + content: string | null + anomalytype: string | null + avatar_url: string | null + created_at: string + configuration: any + parentAnomaly: number | null + anomalySet: string | null +} + +// ✅ Locally extend Anomaly to include dbData +type LocalAnomaly = Anomaly & { + dbData: DatabaseAnomaly +} + +interface Props { + sectorAnomalies: Anomaly[] + tessAnomalies: DatabaseAnomaly[] + selectedSector: { x: number; y: number } | null + currentSector: { x: number; y: number } + setCurrentSector: (val: { x: number; y: number }) => void + setSelectedSector: (val: { x: number; y: number } | null) => void + onDeploy: () => void + alreadyDeployed: boolean + deploying: boolean + deploymentMessage: string | null +} + +export default function DeployTelescopeSidebar({ + sectorAnomalies, + tessAnomalies, + selectedSector, + currentSector, + setCurrentSector, + setSelectedSector, + onDeploy, + alreadyDeployed, + deploying, + deploymentMessage, +}: Props) { + return ( +
+
+
+
+ +
+
+

Telescope Deploy

+

TESS Exoplanet Search

+
+
+
+ +
+
Targets in view: {sectorAnomalies.length}
+ {selectedSector && ( + Sector selected + )} +
+ + + + + + + + Deploy + + + Focus telescope on selected sector + + + + {selectedSector ? ( +
+
+
+ {sectorAnomalies.length} TESS candidates detected +
+
+ + +
+ ) : ( +
+ +
No sector selected
+
Navigate and select a sector first
+
+ )} +
+
+ + {deploymentMessage && ( +
+
{deploymentMessage}
+
+ )} + +
+
TESS Mission
+
Transiting Exoplanet Survey Satellite
+
+
+
{tessAnomalies.length}
+
Total
+
+
+
{sectorAnomalies.length}
+
In View
+
+
+
+
+ ) +}; \ No newline at end of file diff --git a/components/Account/FinishProfile.tsx b/components/Account/FinishProfile.tsx new file mode 100644 index 000000000..e8509fbb6 --- /dev/null +++ b/components/Account/FinishProfile.tsx @@ -0,0 +1,179 @@ +"use client"; + +import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react"; +import { useEffect, useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +function generateReferralCode(length = 8) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let code = ""; + for (let i = 0; i < length; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return code; +} + +export default function CompleteProfileForm({ onSuccess }: { onSuccess: () => void }) { + const supabase = useSupabaseClient(); + const session = useSession(); + + const [username, setUsername] = useState(""); + const [fullName, setFullName] = useState(""); + const [ownReferralCode, setOwnReferralCode] = useState(""); + const [referrerCodeInput, setReferrerCodeInput] = useState(""); + const [loading, setLoading] = useState(false); + const [errorMsg, setErrorMsg] = useState(""); + + useEffect(() => { + setOwnReferralCode(generateReferralCode()); + }, []); + + const handleSubmit = async () => { + if (!session) return; + + if (username.trim().length < 3) { + setErrorMsg("Username must be at least 3 characters."); + return; + } + + setLoading(true); + setErrorMsg(""); + + const userId = session.user.id; + + const updates = { + id: userId, + username: username.trim(), + full_name: fullName.trim(), + referral_code: ownReferralCode, + updated_at: new Date(), + }; + + // Step 1: Update Profile + const { error: updateError } = await supabase.from("profiles").upsert(updates); + + if (updateError) { + if (updateError.message.includes("profiles_username_key")) { + setErrorMsg("That username is already taken. Please choose another one."); + } else if (updateError.message.includes("profiles_referral_code_key")) { + setErrorMsg("Referral code already exists. Please try again."); + } else { + setErrorMsg(updateError.message || "Failed to update profile."); + } + setLoading(false); + return; + } + + // Step 2: Handle referral (if input provided) + if (referrerCodeInput.trim()) { + // Check if user already has a referral + const { data: existingReferral, error: referralCheckError } = await supabase + .from("referrals") + .select("id") + .eq("referree_id", userId) + .maybeSingle(); + + if (!referralCheckError && !existingReferral) { + // Find user with referral_code + const { data: referrerProfile } = await supabase + .from("profiles") + .select("id, referral_code") + .eq("referral_code", referrerCodeInput.trim()) + .maybeSingle(); + + if (referrerProfile) { + // Create referral from current user → referrer + await supabase.from("referrals").insert({ + referree_id: userId, + referral_code: referrerCodeInput.trim(), + }); + + // Check if the referrer was also referred by someone else + const { data: referrersReferral } = await supabase + .from("referrals") + .select("referral_code") + .eq("referree_id", referrerProfile.id) + .maybeSingle(); + + if (referrersReferral) { + // Create an extended referral from current user → referrer's referrer + await supabase.from("referrals").insert({ + referree_id: userId, + referral_code: referrersReferral.referral_code, + }); + } + } else { + // Referral code not found — soft fail, but don't stop profile completion + console.warn("Invalid referral code provided"); + } + } + } + + if (typeof onSuccess === "function") { + onSuccess(); // Close the modal + } + }; + + return ( +
+

Complete Your Profile

+

+ To unlock full gameplay access and referral features, please set your display name and username. +

+ +
+
+ + setUsername(e.target.value)} + placeholder="e.g. stargazer_42" + className="bg-white" + /> +
+ +
+ + setFullName(e.target.value)} + placeholder="e.g. Ada Lovelace" + className="bg-white" + /> +
+ +
+ + +
+ +
+ + setReferrerCodeInput(e.target.value)} + placeholder="Enter your friend's code" + className="bg-white" + /> +
+ + {errorMsg &&

{errorMsg}

} +
+ + +
+ ); +}; \ No newline at end of file diff --git a/components/Account/ProfileDetailsPanel.tsx b/components/Account/ProfileDetailsPanel.tsx new file mode 100644 index 000000000..b42fcdaee --- /dev/null +++ b/components/Account/ProfileDetailsPanel.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +interface ProfileDetailsPanelProps { + onClose: () => void; +} + +export default function ProfileDetailsPanel({ onClose }: ProfileDetailsPanelProps) { + const supabase = useSupabaseClient(); + const session = useSession(); + + const [username, setUsername] = useState(""); + const [referralCode, setReferralCode] = useState(""); + const [fullName, setFullName] = useState(""); + const [referredUsers, setReferredUsers] = useState<{ username: string | null }[]>([]); + const [referredBy, setReferredBy] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [errorMsg, setErrorMsg] = useState(""); + + useEffect(() => { + const fetchData = async () => { + if (!session) return; + + const userId = session.user.id; + + const { data: profile, error: profileError } = await supabase + .from("profiles") + .select("username, referral_code, full_name") + .eq("id", userId) + .maybeSingle(); + + if (profile && !profileError) { + setUsername(profile.username ?? ""); + setReferralCode(profile.referral_code ?? ""); + setFullName(profile.full_name ?? ""); + } + + const { data: referrals } = await supabase + .from("referrals") + .select("referree_id") + .eq("referral_code", profile?.referral_code ?? ""); + + if (referrals && referrals.length > 0) { + const ids = referrals.map((r) => r.referree_id); + const { data: referredProfiles } = await supabase + .from("profiles") + .select("username") + .in("id", ids); + setReferredUsers(referredProfiles ?? []); + } + + const { data: referrerReferral } = await supabase + .from("referrals") + .select("referral_code") + .eq("referree_id", userId) + .maybeSingle(); + + if (referrerReferral?.referral_code) { + const { data: referrerProfile } = await supabase + .from("profiles") + .select("username") + .eq("referral_code", referrerReferral.referral_code) + .maybeSingle(); + + if (referrerProfile?.username) { + setReferredBy(referrerProfile.username); + } + } + + setLoading(false); + }; + + fetchData(); + }, [session, supabase]); + + const handleSave = async () => { + if (!session) return; + setSaving(true); + setErrorMsg(""); + + const { error } = await supabase + .from("profiles") + .update({ full_name: fullName.trim(), updated_at: new Date() }) + .eq("id", session.user.id); + + if (error) { + setErrorMsg(error.message || "Failed to update profile."); + setSaving(false); + return; + } + + onClose(); + }; + + if (loading) { + return ( +
+ Loading profile... +
+ ); + } + + return ( +
+
+
+ + +
+ +
+ + +
+ +
+ + setFullName(e.target.value)} + placeholder="e.g. Ada Lovelace" + className="bg-white" + /> +
+
+ +
+
+

Referred By

+

+ {referredBy ? referredBy : "—"} +

+
+ +
+

Users You've Referred

+ {referredUsers.length > 0 ? ( +
    + {referredUsers.map((user, idx) => ( +
  • {user.username ?? "Unnamed User"}
  • + ))} +
+ ) : ( +

—

+ )} +
+
+ + {errorMsg &&

{errorMsg}

} + +
+ +
+
+ ); +}; \ No newline at end of file diff --git a/components/Account/ProfileSetup.tsx b/components/Account/ProfileSetup.tsx index 42e47c8b4..8c2bdce00 100644 --- a/components/Account/ProfileSetup.tsx +++ b/components/Account/ProfileSetup.tsx @@ -6,7 +6,7 @@ import { useActivePlanet } from "@/context/ActivePlanet"; interface ProfileSetupFormProps { onProfileUpdate: () => void | null; -} +}; export default function ProfileSetupForm({ onProfileUpdate }: ProfileSetupFormProps) { const supabase = useSupabaseClient(); diff --git a/components/Account/Referrals.tsx b/components/Account/Referrals.tsx new file mode 100644 index 000000000..edcc489b0 --- /dev/null +++ b/components/Account/Referrals.tsx @@ -0,0 +1,195 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react"; + +interface ReferredUser { + id: string; + email?: string | null; + username?: string | null; +} + +export default function ReferralCodePanel() { + const supabase = useSupabaseClient(); + const session = useSession(); + + const [referralCode, setReferralCode] = useState(null); + const [referredUsers, setReferredUsers] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (!session) return; + + const fetchReferralData = async () => { + setLoading(true); + setError(null); + + try { + // 1. Get the referral code from profiles table for current user + const { data: profileData, error: profileError } = await supabase + .from("profiles") + .select("referral_code") + .eq("id", session.user.id) + .maybeSingle(); + + if (profileError) { + console.error("Error fetching referral code:", profileError); + setError("Failed to load your referral code."); + setLoading(false); + return; + } + + if (!profileData?.referral_code) { + setError("No referral code found for your account."); + setLoading(false); + return; + } + + const userReferralCode = profileData.referral_code; + setReferralCode(userReferralCode); + + // 2. Get all referrals where referral_code matches current user's referral code + const { data: referralsData, error: referralsError } = await supabase + .from("referrals") + .select("referree_id") + .eq("referral_code", userReferralCode); + + if (referralsError) { + console.error("Error fetching referrals:", referralsError); + setError("Failed to load referred users."); + setLoading(false); + return; + } + + const referredUserIds = referralsData?.map((r) => r.referree_id) || []; + + if (referredUserIds.length === 0) { + setReferredUsers([]); + setLoading(false); + return; + } + + // 3. Fetch profiles of all referred users + const { data: usersData, error: usersError } = await supabase + .from("profiles") + .select("id, username") + .in("id", referredUserIds); + + if (usersError) { + console.error("Error fetching referred user profiles:", usersError); + setError("Failed to load referred users' profiles."); + setReferredUsers([]); + } else { + // Optional: Merge in emails from auth.users + const { data: authUsers, error: authError } = await supabase + .from("users") + .select("id, email") + .in("id", referredUserIds); + + if (authError) { + console.warn("Could not fetch emails from auth.users:", authError); + } + + const merged = usersData.map((user) => ({ + ...user, + email: authUsers?.find((u) => u.id === user.id)?.email ?? null, + })); + + setReferredUsers(merged); + } + } catch (err) { + console.error("Unexpected error:", err); + setError("An unexpected error occurred."); + } + + setLoading(false); + }; + + fetchReferralData(); + }, [session, supabase]); + + const handleCopy = () => { + if (!referralCode) return; + navigator.clipboard.writeText(referralCode); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const stardustEarned = referredUsers.length * 5; + + return ( +
+

+ Your Referral Code +

+ +
+ + {loading ? "Loading..." : referralCode ?? "N/A"} + + +
+ +

+ {stardustEarned} Stardust earned for referrals +

+ +

+ Users You Referred +

+ +
+ {loading &&

Loading...

} + {error &&

{error}

} + + {!loading && !error && referredUsers.length === 0 && ( +

No users referred yet.

+ )} + +
    + {referredUsers.map((user) => ( +
  • + {user.username || user.email || user.id} +
  • + ))} +
+
+
+ ); +}; \ No newline at end of file diff --git a/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator.tsx b/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator.tsx index ff2416b5a..bc7d1ee68 100644 --- a/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator.tsx +++ b/components/Data/Generator/Astronomers/PlanetHunters/PlanetGenerator.tsx @@ -1,448 +1,169 @@ -"use client"; +'use client'; import { useState, useEffect } from "react"; import PlanetViewer from "./planetViewer"; import { type PlanetConfig, defaultPlanetConfig } from "@/utils/planet-physics"; +import SettingsPanel from "./SettingsPanel"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { useSupabaseClient } from "@supabase/auth-helpers-react"; interface PlanetGeneratorProps { - classificationId: string; -}; + classificationId: string; + editMode?: boolean; +} interface PlanetStats { mass?: number; radius?: number; [key: string]: any; -}; - -export default function PlanetGenerator({ - classificationId, -}: PlanetGeneratorProps) { - const supabase = useSupabaseClient(); - - const [planetConfig, setPlanetConfig] = useState(defaultPlanetConfig); - const [importDialogOpen, setImportDialogOpen] = useState(false); - const [exportDialogOpen, setExportDialogOpen] = useState(false); - const [importText, setImportText] = useState(""); - const [importError, setImportError] = useState(""); - - const [planetStats, setPlanetStats] = useState({}); - - const handleConfigChange = ( newConfig: Partial ) => { - setPlanetConfig(( prev ) => ({ - ...prev, - ...newConfig - })) +} + +export default function PlanetGenerator({ classificationId, editMode }: PlanetGeneratorProps) { + const supabase = useSupabaseClient(); + const [planetConfig, setPlanetConfig] = useState(defaultPlanetConfig); + const [importDialogOpen, setImportDialogOpen] = useState(false); + const [exportDialogOpen, setExportDialogOpen] = useState(false); + const [importText, setImportText] = useState(""); + const [importError, setImportError] = useState(""); + const [planetStats, setPlanetStats] = useState({}); + + const handleConfigChange = (newConfig: Partial) => { + setPlanetConfig((prev) => ({ ...prev, ...newConfig })); + }; + + const handleImport = () => { + try { + const config = JSON.parse(importText); + setPlanetConfig(config); + setImportDialogOpen(false); + setImportError(""); + } catch { + setImportError("Invalid JSON configuration"); + } + }; + + const handleExport = async () => { + const idAsNumber = Number.parseInt(classificationId); + if (isNaN(idAsNumber)) return; + + const { data, error: fetchError } = await supabase + .from("classifications") + .select("classificationConfiguration") + .eq("id", idAsNumber) + .single(); + + if (fetchError || !data) return; + + const updatedConfig = { + ...data.classificationConfiguration, + planetConfiguration: planetConfig, + }; + + await supabase + .from("classifications") + .update({ classificationConfiguration: updatedConfig }) + .eq("id", idAsNumber); + }; + + useEffect(() => { + const fetchExportedValues = async () => { + const idAsNumber = Number.parseInt(classificationId); + if (isNaN(idAsNumber)) return; + + const { data, error } = await supabase + .from("classifications") + .select("classificationConfiguration") + .eq("id", idAsNumber) + .single(); + + if (error || !data?.classificationConfiguration?.exportedValue) return; + + const { exportedValue } = data.classificationConfiguration; + setPlanetStats((prev) => ({ + ...prev, + ...(exportedValue.mass && { mass: exportedValue.mass }), + ...(exportedValue.radius && { radius: exportedValue.radius }), + })); }; - const handleImport = () => { - try { - const config = JSON.parse(importText); - setPlanetConfig(config); - setImportDialogOpen(false); - setImportError(""); - } catch (error: any) { - setImportError("Invalid JSON configuration"); - }; - }; - - const handleExport = async () => { - const idAsNumber = Number.parseInt(classificationId); - if (isNaN(idAsNumber)) return; - - const { data, error: fetchError } = await supabase - .from("classifications") - .select("classificationConfiguration") - .eq("id", idAsNumber) - .single(); - - if (fetchError || !data) { - console.error("Failed to fetch planet configuration: ", fetchError); - return; - }; - - const currentConfig = data.classificationConfiguration || {}; - const updatedConfig = { - ...currentConfig, - planetConfiguration: planetConfig, - }; - - const { error: updateError } = await supabase - .from("classifications") - .update({ classificationConfiguration: updatedConfig }) - .eq("id", idAsNumber); - - if (updateError) { - console.error("Failed to update planet configuration, ", updateError); - }; - }; - - // Fetch planet data - useEffect(() => { - const fetchExportedValues = async () => { - const idAsNumber = Number.parseInt(classificationId) - if (isNaN(idAsNumber)) return - - const { data, error } = await supabase - .from("classifications") - .select("classificationConfiguration") - .eq("id", idAsNumber) - .single() - - if (error || !data?.classificationConfiguration?.exportedValue) return - - const { exportedValue } = data.classificationConfiguration - - setPlanetStats((prev) => ({ - ...prev, - ...(exportedValue.mass && { mass: exportedValue.mass }), - ...(exportedValue.radius && { radius: exportedValue.radius }), - })) - } - - fetchExportedValues() - }, [classificationId, supabase]) - - // Listen for custom events from the settings panel - useEffect(() => { - const handleOpenImport = () => setImportDialogOpen(true); - const handleOpenExport = () => setExportDialogOpen(true); - - window.addEventListener("open-import-dialog", handleOpenImport); - window.addEventListener("open-export-dialog", handleOpenExport); - - return () => { - window.removeEventListener("open-import-dialog", handleOpenImport); - window.removeEventListener("open-export-dialog", handleOpenExport); - }; - }, []); - - const exportConfig = JSON.stringify(planetConfig, null, 2); - - return ( -
-
- -
- -

{JSON.stringify(planetStats, null, 2)}

- - - - - Import Planet Configuration - Paste a previously exported planet configuration JSON below. - -
-