diff --git a/components/Structures/Missions/Meteorologists/Cloudspotting/CloudspottingOnMars.tsx b/components/Structures/Missions/Meteorologists/Cloudspotting/CloudspottingOnMars.tsx
index c187d976c..71ef01aaf 100644
--- a/components/Structures/Missions/Meteorologists/Cloudspotting/CloudspottingOnMars.tsx
+++ b/components/Structures/Missions/Meteorologists/Cloudspotting/CloudspottingOnMars.tsx
@@ -2,11 +2,12 @@ import { useEffect, useState } from "react";
import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
import MissionShell from "@/components/Structures/Missions/BasePlate";
import { CloudCogIcon, FolderCog, HelpCircle, Paintbrush2Icon, PaintBucket, Vote } from "lucide-react";
-import { CloudspottingWrapper, StarterLidar } from "@/components/Projects/Lidar/Clouds";
+import { CloudspottingOnMarsWithId, StarterLidar } from "@/components/Projects/Lidar/Clouds";
import VoteCoMClassifications from "./CoMVote";
import CloudClassificationGenerator from "./CloudMaker";
import { CloudspottingOnMarsTutorial } from "@/components/Projects/Lidar/cloudspottingOnMars";
-import { CloudspottingShapesWrapper } from "@/components/Projects/Lidar/CloudspottingOnMarsShapes";
+import { StarterCoMShapes } from "@/components/Projects/Lidar/CloudspottingOnMarsShapes";
+// import { CloudspottingShapesWrapper } from "@/components/Projects/Lidar/CloudspottingOnMarsShapes";
export interface Mission {
id: number;
@@ -44,23 +45,12 @@ const CloudspottingOnMars = () => {
icon: CloudCogIcon,
points: 2,
completedCount: 0,
- internalComponent: () =>
,
+ internalComponent: () =>
,
color: "text-blue-500",
},
{
id: 2,
chapter: 1,
- title: "Propose a cloud in your classifications",
- description: "Make a classification indicating a positive cloud candidate",
- icon: FolderCog,
- points: 1,
- completedCount: 0,
- internalComponent: () =>
,
- color: "text-cyan-300",
- },
- {
- id: 3,
- chapter: 1,
title: "Comment or vote on a cloud classification",
description:
"Collaborate with other players to rate proposed cloud candidates and behaviour",
@@ -71,7 +61,7 @@ const CloudspottingOnMars = () => {
color: "text-green-700",
},
{
- id: 4,
+ id: 3,
chapter: 2,
title: "Create a cloud representation",
description:
@@ -83,14 +73,14 @@ const CloudspottingOnMars = () => {
color: 'text-green-300',
},
{
- id: 5,
+ id: 4,
chapter: 2,
title: "Identify shapes in cloud classifications",
description: 'Use your LIDAR to identify shapes in the cloud classification',
icon: Paintbrush2Icon,
points: 2,
completedCount: 0,
- internalComponent: () =>
,
+ internalComponent: () =>
,
color: 'text-green-200',
}
];
diff --git a/components/Structures/Missions/Meteorologists/Cloudspotting/Shapes/ShapesOnMars.tsx b/components/Structures/Missions/Meteorologists/Cloudspotting/Shapes/ShapesOnMars.tsx
index 0b7a982b0..59fe197cf 100644
--- a/components/Structures/Missions/Meteorologists/Cloudspotting/Shapes/ShapesOnMars.tsx
+++ b/components/Structures/Missions/Meteorologists/Cloudspotting/Shapes/ShapesOnMars.tsx
@@ -2,8 +2,9 @@ import React, { useEffect, useState } from "react";
import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
import MissionShell from "../../../BasePlate";
import { CloudCogIcon, FolderCog, HelpCircle, PaintBucket, Vote } from "lucide-react";
-import { CloudspottingShapesWrapper } from "@/components/Projects/Lidar/CloudspottingOnMarsShapes";
+// import { CloudspottingShapesWrapper } from "@/components/Projects/Lidar/CloudspottingOnMarsShapes";
import { Mission } from "../CloudspottingOnMars";
+import { StarterCoMShapes } from "@/components/Projects/Lidar/CloudspottingOnMarsShapes";
interface MissionPoints {
[key: number]: number;
@@ -29,7 +30,7 @@ const CloudspottingOnMarsShapes = () => {
icon: CloudCogIcon,
points: 2,
completedCount: 0,
- internalComponent: () =>
,
+ internalComponent: () =>
,
color: 'text-blue-500',
},
{
diff --git a/components/Structures/Missions/Meteorologists/JVH/JovianVortexHunters.tsx b/components/Structures/Missions/Meteorologists/JVH/JovianVortexHunters.tsx
index 444febdbb..eb6285a66 100644
--- a/components/Structures/Missions/Meteorologists/JVH/JovianVortexHunters.tsx
+++ b/components/Structures/Missions/Meteorologists/JVH/JovianVortexHunters.tsx
@@ -1,14 +1,14 @@
import React, { useEffect, useState } from "react";
import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
import MissionShell from "../../BasePlate";
-import { JVHWrapper } from "@/components/Projects/Lidar/JovianVortexHunter";
+import { LidarJVHSatelliteWithId } from "@/components/Projects/Lidar/JovianVortexHunter";
import { CloudCogIcon, CloudHail, HelpCircle, ShovelIcon } from "lucide-react";
-import { LidarJVHSatellite, StarterJovianVortexHunter } from "@/components/Projects/Lidar/JovianVortexHunter";
+import { StarterJovianVortexHunter } from "@/components/Projects/Lidar/JovianVortexHunter";
import VoteJVH from "./JVHVote";
import CloudClassifier from "@/components/Data/Generator/Meteorologists/JVH/cloud-classifier";
import JVHCloudClassificationGenerator from "./GaseousPlanetCloudMaker";
import ClassificationOptionsCounter from "@/content/Posts/ClassificationOptionsCounter";
-import { PreferredGaseousClassifications } from "../../PickPlanet";
+// import { PreferredGaseousClassifications } from "../../PickPlanet";
interface Mission {
id: number;
@@ -45,7 +45,7 @@ const JovianVortexHunters = () => {
points: 2,
completedCount: 0,
// internalComponent: () =>
, //
- internalComponent: () =>
,
+ internalComponent: () =>
,
color: "text-blue-500",
},
{
diff --git a/components/Structures/Missions/Milestones/Completed.tsx b/components/Structures/Missions/Milestones/Completed.tsx
index 8e4363e81..9d4dc90c4 100644
--- a/components/Structures/Missions/Milestones/Completed.tsx
+++ b/components/Structures/Missions/Milestones/Completed.tsx
@@ -61,7 +61,7 @@ export default function MilestoneHistoryList() {
.from(table)
.select("*", { count: "exact" })
.eq(field, value)
- .eq("author", session.user.id)
+ .or(`author.eq.${session.user.id},user_id.eq.${session.user.id}`)
.gte("created_at", startDate.toISOString())
.lte("created_at", endDate.toISOString());
diff --git a/components/Structures/Missions/Milestones/NextStepsTips.tsx b/components/Structures/Missions/Milestones/NextStepsTips.tsx
new file mode 100644
index 000000000..4cadcc2e2
--- /dev/null
+++ b/components/Structures/Missions/Milestones/NextStepsTips.tsx
@@ -0,0 +1,88 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react"
+
+export default function TipsPanel() {
+ const supabase = useSupabaseClient()
+ const session = useSession()
+
+ const [showTips, setShowTips] = useState(false)
+ const [countdown, setCountdown] = useState("")
+
+ useEffect(() => {
+ if (!session) return
+
+ const checkHasCommentsOrVotes = async () => {
+ const userId = session.user.id
+
+ const { data: comments } = await supabase
+ .from("comments")
+ .select("id")
+ .eq("author", userId)
+ .limit(1)
+
+ const { data: votes } = await supabase
+ .from("votes")
+ .select("id")
+ .eq("user_id", userId)
+ .limit(1)
+
+ const hasInteracted = (comments?.length ?? 0) > 0 || (votes?.length ?? 0) > 0
+ setShowTips(!hasInteracted)
+ }
+
+ const updateCountdown = () => {
+ const now = new Date()
+
+ // Convert to Melbourne time (UTC+10 or UTC+11)
+ const utcOffset = now.getTimezoneOffset() / -60 // e.g. +10 for AEST
+ const melbourneOffset = 10 // Standard (no DST support needed unless you want it)
+
+ const melbourneNow = new Date(now.getTime() + (melbourneOffset - utcOffset) * 60 * 60 * 1000)
+
+ const melbourneDay = melbourneNow.getUTCDay() // 0 = Sun, 1 = Mon, ..., 6 = Sat
+ const melbourneHour = melbourneNow.getUTCHours()
+ const melbourneMinute = melbourneNow.getUTCMinutes()
+
+ let daysUntilMonday = (8 - melbourneDay) % 7
+ if (daysUntilMonday === 0 && (melbourneHour < 0 || (melbourneHour === 0 && melbourneMinute < 1))) {
+ daysUntilMonday = 7
+ }
+
+ const nextReset = new Date(melbourneNow)
+ nextReset.setUTCDate(melbourneNow.getUTCDate() + daysUntilMonday)
+ nextReset.setUTCHours(14) // Melbourne 00:01 = UTC 14:01
+ nextReset.setUTCMinutes(1)
+ nextReset.setUTCSeconds(0)
+ nextReset.setUTCMilliseconds(0)
+
+ const msRemaining = nextReset.getTime() - melbourneNow.getTime()
+ const hours = Math.floor(msRemaining / (1000 * 60 * 60))
+ const minutes = Math.floor((msRemaining % (1000 * 60 * 60)) / (1000 * 60))
+
+ setCountdown(`${hours}h ${minutes}m`)
+ }
+
+ checkHasCommentsOrVotes()
+ updateCountdown()
+ const interval = setInterval(updateCountdown, 60000)
+ return () => clearInterval(interval)
+ }, [session, supabase])
+
+ if (!showTips) return null
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/components/Structures/Missions/MissionProgress.tsx b/components/Structures/Missions/MissionProgress.tsx
new file mode 100644
index 000000000..104788419
--- /dev/null
+++ b/components/Structures/Missions/MissionProgress.tsx
@@ -0,0 +1,171 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { Sheet, SheetContent } from "@/components/ui/sheet";
+import { Button } from "@/components/ui/button";
+import {
+ Telescope,
+ SnowflakeIcon,
+ PawPrintIcon,
+ MountainIcon,
+ Star,
+ ExternalLink,
+} from "lucide-react";
+import Link from "next/link";
+
+type Milestone = {
+ name: string;
+ structure: string;
+ icon: string;
+ group: string;
+ extendedDescription?: string;
+};
+
+type WeekMilestones = {
+ weekStart: string;
+ data: Milestone[];
+};
+
+const GROUPS = ["Astronomy", "Meteorology", "Geology", "Biology"];
+
+const groupIcons: Record
= {
+ Astronomy: ,
+ Meteorology: ,
+ Biology: ,
+ Geology: ,
+};
+
+export default function UserMilestoneMissionSummary() {
+ const [milestones, setMilestones] = useState(null);
+ const [selectedGroup, setSelectedGroup] = useState(null);
+
+ useEffect(() => {
+ const fetchMilestones = async () => {
+ const res = await fetch("/api/gameplay/milestones");
+ const data = await res.json();
+ setMilestones(data.playerMilestones?.[0] ?? null);
+ };
+
+ fetchMilestones();
+ }, []);
+
+ if (!milestones) return null;
+
+ const grouped: Record = {};
+ for (const m of milestones.data) {
+ if (!grouped[m.group]) grouped[m.group] = [];
+ grouped[m.group].push(m);
+ }
+
+ // Your layered background texture style converted from CSS:
+ const backgroundStyle: React.CSSProperties = {
+ width: "100%",
+ height: "100%",
+ position: "absolute",
+ inset: 0,
+ backgroundImage: `
+ radial-gradient(circle at 100% 50%, #ff00cc 0% 2%, #00ffcc 3% 5%, transparent 6%),
+ radial-gradient(circle at 0% 50%, #ff00cc 0% 2%, #00ffcc 3% 5%, transparent 6%),
+ radial-gradient(ellipse at 50% 0%, #3300ff 0% 3%, transparent 4%) 10px 10px,
+ radial-gradient(circle at 50% 50%, #00ffcc 0% 1%, #ff00cc 2% 3%, #3300ff 4% 5%, transparent 6%) 20px 20px,
+ repeating-linear-gradient(45deg, #1a1a1a, #1a1a1a 10px, #242424 10px, #242424 20px)
+ `,
+ backgroundSize: "50px 50px, 50px 50px, 40px 40px, 60px 60px, 100% 100%",
+ animation: "shift 15s linear infinite",
+ opacity: 0.3,
+ pointerEvents: "none", // so clicks go through
+ zIndex: 0,
+ };
+
+ return (
+
+ {/* Inject keyframes scoped inside component */}
+
+
+ {/* Background texture layer */}
+
+
+ {/* Foreground content */}
+
+ {GROUPS.map((group) => {
+ const hasMissions = grouped[group] && grouped[group].length > 0;
+ const icon = groupIcons[group] ||
;
+
+ return hasMissions ? (
+
+ ) : (
+
+ {icon}
+ {group}
+
+ );
+ })}
+
+
+
setSelectedGroup(null)}>
+
+
+ {selectedGroup} Missions –{" "}
+ {new Date(milestones.weekStart).toLocaleDateString()}
+
+
+ {grouped[selectedGroup ?? ""]?.map((m, idx) => (
+
+
+ {groupIcons[m.group] || }
+
+
+
{m.name}
+ {m.extendedDescription && (
+
{m.extendedDescription}
+ )}
+
+ Structure: {m.structure}
+
+
+
+ View structure
+
+
+
+ ))}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/Structures/Missions/Stardust/Total.tsx b/components/Structures/Missions/Stardust/Total.tsx
index 4fb15b11b..10a79b0e0 100644
--- a/components/Structures/Missions/Stardust/Total.tsx
+++ b/components/Structures/Missions/Stardust/Total.tsx
@@ -35,6 +35,7 @@ interface WeekMilestones {
const TotalPoints = forwardRef((props: TotalPointsProps, ref,) => {
const { onPointsUpdate, type } = props;
+
const supabase = useSupabaseClient();
const session = useSession();
@@ -48,10 +49,13 @@ const TotalPoints = forwardRef((props: TotalPointsProps, ref,) => {
const [milestonePoints, setMilestonePoints] = useState(0);
const [researchedPenalty, setResearchedPenalty] = useState(0);
const [loading, setLoading] = useState(true);
+ const [referralPoints, setReferralPoints] = useState(0);
+ const [showBreakdown, setShowBreakdown] = useState(false);
const userId = session?.user?.id;
useImperativeHandle(ref, () => ({
+ refreshPoints: fetchAllPoints,
planetHuntersPoints,
dailyMinorPlanetPoints,
ai4mPoints,
@@ -73,66 +77,43 @@ const TotalPoints = forwardRef((props: TotalPointsProps, ref,) => {
researchedPenalty,
}));
+ const fetchReferralPoints = async () => {
+ // First, get the user's referral code from profiles
+ const { data: profileData, error: profileError } = await supabase
+ .from('profiles')
+ .select('referral_code')
+ .eq('id', userId)
+ .single();
+
+ if (profileError || !profileData?.referral_code) {
+ setReferralPoints(0);
+ return;
+ }
+
+ const referralCode = profileData.referral_code;
+
+ // Count rows in referrals where referral_code = user's referral_code
+ const { count: referralsCount, error: referralCountError } = await supabase
+ .from('referrals')
+ .select('id', { count: 'exact', head: true })
+ .eq('referral_code', referralCode);
+
+ if (referralCountError) {
+ setReferralPoints(0);
+ return;
+ }
+
+ // Calculate points: 5 per referral
+ const points = (referralsCount || 0) * 5;
+ setReferralPoints(points);
+};
+
const [bonusPoints, setBonusPoints] = useState({
bonusBiology: 0,
bonusAstronomy: 0,
bonusMeteorology: 0,
});
- const [milestones, setMilestones] = useState([]);
- const [userProgress, setUserProgress] = useState<{
- [weekKey: string]: { [milestoneName: string]: number };
- }>({});
-
- useEffect(() => {
- const fetchData = async () => {
- const res = await fetch("/api/gameplay/milestones");
- const data = await res.json();
-
- const sorted = [...data.playerMilestones].sort(
- (a: WeekMilestones, b: WeekMilestones) =>
- new Date(b.weekStart).getTime() - new Date(a.weekStart).getTime()
- );
-
- setMilestones(sorted);
-
- if (!session?.user?.id) return;
-
- const progressMap: {
- [weekKey: string]: { [milestoneName: string]: number };
- } = {};
-
- for (const week of sorted) {
- const startDate = new Date(week.weekStart);
- const endDate = new Date(startDate);
- endDate.setDate(startDate.getDate() + 6);
-
- const weekKey = week.weekStart;
- progressMap[weekKey] = {};
-
- for (const milestone of week.data) {
- const { table, field, value } = milestone;
-
- const { count } = await supabase
- .from(table)
- .select("*", { count: "exact" })
- .eq(field, value)
- .eq("author", session.user.id)
- .gte("created_at", startDate.toISOString())
- .lte("created_at", endDate.toISOString());
-
- progressMap[weekKey][milestone.name] = count || 0;
- }
- }
-
- setUserProgress(progressMap);
- };
-
- fetchData();
- }, [session]);
-
- useEffect(() => {
- if (!userId) return;
const fetchAllPoints = async () => {
setLoading(true);
@@ -317,6 +298,7 @@ const TotalPoints = forwardRef((props: TotalPointsProps, ref,) => {
fetchPlanetFourPoints(),
fetchJvhPoints(),
fetchCloudspottingPoints(),
+ fetchReferralPoints(),
]);
const totalPoints =
@@ -327,6 +309,7 @@ const TotalPoints = forwardRef((props: TotalPointsProps, ref,) => {
jvhPoints +
cloudspottingPoints +
planktonPoints +
+ referralPoints +
milestonePoints -
researchedPenalty;
@@ -334,6 +317,61 @@ const TotalPoints = forwardRef((props: TotalPointsProps, ref,) => {
if (onPointsUpdate) onPointsUpdate(totalPoints);
};
+ const [milestones, setMilestones] = useState([]);
+ const [userProgress, setUserProgress] = useState<{
+ [weekKey: string]: { [milestoneName: string]: number };
+ }>({});
+
+ useEffect(() => {
+ const fetchData = async () => {
+ const res = await fetch("/api/gameplay/milestones");
+ const data = await res.json();
+
+ const sorted = [...data.playerMilestones].sort(
+ (a: WeekMilestones, b: WeekMilestones) =>
+ new Date(b.weekStart).getTime() - new Date(a.weekStart).getTime()
+ );
+
+ setMilestones(sorted);
+
+ if (!session?.user?.id) return;
+
+ const progressMap: {
+ [weekKey: string]: { [milestoneName: string]: number };
+ } = {};
+
+ for (const week of sorted) {
+ const startDate = new Date(week.weekStart);
+ const endDate = new Date(startDate);
+ endDate.setDate(startDate.getDate() + 6);
+
+ const weekKey = week.weekStart;
+ progressMap[weekKey] = {};
+
+ for (const milestone of week.data) {
+ const { table, field, value } = milestone;
+
+ const { count } = await supabase
+ .from(table)
+ .select("*", { count: "exact" })
+ .eq(field, value)
+ .eq("author", session.user.id)
+ .gte("created_at", startDate.toISOString())
+ .lte("created_at", endDate.toISOString());
+
+ progressMap[weekKey][milestone.name] = count || 0;
+ }
+ }
+
+ setUserProgress(progressMap);
+ };
+
+ fetchData();
+ }, [session]);
+
+ useEffect(() => {
+ if (!userId) return;
+
fetchAllPoints();
}, [userId, supabase]);
@@ -348,6 +386,7 @@ const TotalPoints = forwardRef((props: TotalPointsProps, ref,) => {
planetFourPoints +
jvhPoints +
cloudspottingPoints +
+ referralPoints +
planktonPoints +
milestonePoints -
researchedPenalty;
@@ -388,15 +427,30 @@ const TotalPoints = forwardRef((props: TotalPointsProps, ref,) => {
return {researchedPenalty};
};
- // if (type === "groups") {
+ // if (type === "groups") {
// return (
-
// )
// }
+ const breakdown = (
+
+
🪐 Planet Hunters: {planetHuntersPoints}
+
📷 Daily Minor Planets: {dailyMinorPlanetPoints}
+
🤖 AI4Mars: {ai4mPoints}
+
🛰️ Planet Four: {planetFourPoints}
+
🌪️ Jovian Vortex Hunter: {jvhPoints}
+
☁️ Cloudspotting: {cloudspottingPoints}
+
🌊 Plankton: {planktonPoints}
+
🎯 Milestones: {milestonePoints}
+
🎁 Referrals: {referralPoints}
+
❌ Researched Penalty: -{researchedPenalty}
+
+ );
+
return (
-
- {totalPoints}
+
setShowBreakdown(!showBreakdown)}>
+
{totalPoints}
+ {showBreakdown && breakdown}
);
});
diff --git a/components/Structures/Structures.tsx b/components/Structures/Structures.tsx
index 374253f8a..e8539ecfe 100644
--- a/components/Structures/Structures.tsx
+++ b/components/Structures/Structures.tsx
@@ -4,40 +4,35 @@ import React, { useCallback, useEffect, useState } from "react";
import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
import { useActivePlanet } from "@/context/ActivePlanet";
import { InventoryStructureItem, StructureItemDetail } from "@/types/Items";
-import IndividualStructure, { IndividualStructureProps } from "./IndividualStructure";
-import { StructuresConfig } from "@/constants/Structures/Properties";
+// import IndividualStructure, { IndividualStructureProps } from "./IndividualStructure";
+// import { StructuresConfig } from "@/constants/Structures/Properties";
import "../../styles/Anims/StarterStructureAnimations.css";
-
-interface StructuresOnPlanetProps {
- onStructuresFetch: (
- orbitalStructures: InventoryStructureItem[],
- atmosphereStructures: InventoryStructureItem[],
- surfaceStructures: InventoryStructureItem[]
- ) => void;
-};
-import { UnownedSurfaceStructures } from "./Build/EditMode";
+// import { UnownedSurfaceStructures } from "./Build/EditMode";
+import { useRouter } from "next/navigation";
interface Props {
author?: string;
};
-export default function StructuresOnPlanet({ author }: Props) {
- const supabase = useSupabaseClient();
+export default function Structures() {
+ const supabase = useSupabaseClient();
const session = useSession();
-
+ const router = useRouter();
const { activePlanet } = useActivePlanet();
- const [userStructuresOnPlanet, setUserStructuresOnPlanet] = useState
([]);
- const [itemDetails, setItemDetails] = useState