Skip to content

Commit

Permalink
️🗣️🥻 ↝ [SSC-73 SSC-71 SSC-70 SSC-33]: Integrating plankton portal pro…
Browse files Browse the repository at this point in the history
…ject back into flow
  • Loading branch information
Gizmotronn committed Jan 29, 2025
1 parent b3dc425 commit 2cd94bc
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 74 deletions.
2 changes: 1 addition & 1 deletion components/Projects/Lidar/Clouds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export function StarterLidar({ anomalyid }: SelectedAnomProps) {
anomalyType="cloud"
missionNumber={100000034}
assetMentioned={imageUrl}
structureItemId={3015}
structureItemId={3105}
parentPlanetLocation={anomalyid?.toString() || ''}
annotationType="CoM"
/>
Expand Down
1 change: 1 addition & 0 deletions components/Projects/Telescopes/Transiting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export function StarterTelescopeTess({ anomalyid }: SelectedAnomProps) {
otherAssets={imageUrls}
anomalyType='planet'
missionNumber={1372001}
structureItemId={3103}
assetMentioned={selectedAnomaly.id.toString()}
annotationType='PH'
initialImageUrl={imageUrls[1]}
Expand Down
4 changes: 2 additions & 2 deletions components/Projects/Zoodex/planktonPortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ClassificationForm from "@/components/Projects/(classifications)/PostForm
import { Anomaly } from "../Telescopes/Transiting";

interface ZoodexProps {
anomalyId: string;
anomalyId: string;
};

export const PlanktonPortalTutorial: React.FC<ZoodexProps> = ({ anomalyId }) => {
Expand Down Expand Up @@ -128,7 +128,7 @@ export const PlanktonPortalTutorial: React.FC<ZoodexProps> = ({ anomalyId }) =>
);
};

export function PlanktonPortal() {
export function PlanktonPortalFrame() {
const supabase = useSupabaseClient();
const session = useSession();

Expand Down
130 changes: 130 additions & 0 deletions components/Structures/Missions/Biologists/Plankton/PPVote.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client';

import React, { useEffect, useState } from "react";
import { PostCardSingle } from "@/content/Posts/PostSingle";
import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";
import StarnetLayout from "@/components/Layout/Starnet";

interface Classification {
id: number;
created_at: string;
content: string | null;
author: string | null;
anomaly: number | null;
media: any | null;
classificationtype: string | null;
classificationConfiguration: any | null;
};

export default function VotePPClassifications() {
const supabase = useSupabaseClient();
const session = useSession();

const [classifications, setClassifications] = useState<any[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);

const fetchClassifications = async () => {
if (!session?.user) {
setError("User session not found.");
setLoading(false);
return;
};

setLoading(true);
setError(null);
try {
const { data, error } = await supabase
.from('classifications')
.select('*')
.eq('classificationtype', 'zoodex-planktonPortal')
.order('created_at', { ascending: false }) as { data: Classification[]; error: any };

if (error) throw error;

const processedData = data.map((classification) => {
const media = classification.media;
let images: string[] = [];

if (Array.isArray(media) && media.length === 2 && typeof media[1] === "string") {
images.push(media[1]);
} else if (media && media.uploadUrl) {
images.push(media.uploadUrl);
};

const votes = classification.classificationConfiguration?.votes || 0;

return { ...classification, images, votes };
});

setClassifications(processedData);
} catch (error) {
console.error("Error fetching classifications:", error);
setError("Failed to load classifications.");
} finally {
setLoading(false);
};
};

useEffect(() => {
fetchClassifications();
}, [session]);

const handleVote = async (classificationId: number, currentConfig: any) => {
try {
const currentVotes = currentConfig?.votes || 0;

const updatedConfig = {
...currentConfig,
votes: currentVotes + 1,
};

const { error } = await supabase
.from("classifications")
.update({ classificationConfiguration: updatedConfig })
.eq("id", classificationId);

if (error) {
console.error("Error updating classificationConfiguration:", error);
} else {
setClassifications((prevClassifications) =>
prevClassifications.map((classification) =>
classification.id === classificationId
? { ...classification, votes: updatedConfig.votes }
: classification
)
);
}
} catch (error) {
console.error("Error voting:", error);
};
};

return (
<div className="space-y-8">
{loading ? (
<p>Loading classifications...</p>
) : error ? (
<p>{error}</p>
) : (
classifications.map((classification) => (
<PostCardSingle
key={classification.id}
classificationId={classification.id}
title={classification.title}
author={classification.author}
content={classification.content}
votes={classification.votes || 0}
category={classification.category}
tags={classification.tags || []}
images={classification.images || []}
anomalyId={classification.anomaly}
classificationConfig={classification.classificationConfiguration}
classificationType={classification.classificationtype}
onVote={() => handleVote(classification.id, classification.classificationConfiguration)}
/>
))
)}
</div>
);
};
153 changes: 153 additions & 0 deletions components/Structures/Missions/Biologists/Plankton/PlanktonPortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React, { useEffect, useState, useCallback } from "react";
import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
import MissionShell from "../../BasePlate";
import { FishIcon, PartyPopper, VoicemailIcon } from "lucide-react";
import { PlanktonPortalFrame } from "@/components/Projects/Zoodex/planktonPortal";
import VotePPClassifications from "./PPVote";
import PlanktonDiscoveryStats from "./PlanktonScore";

interface Mission {
id: number;
chapter: number;
title: string;
description: string;
icon: React.ElementType;
completedCount: number;
points: number;
internalComponent: React.ElementType | (() => JSX.Element);
color: string;
}

interface MissionPoints {
[key: number]: number;
}

const PlanktonPortal = () => {
const supabase = useSupabaseClient();
const session = useSession();

const [missions, setMissions] = useState<Mission[]>([]);
const [experiencePoints, setExperiencePoints] = useState(0);
const [level, setLevel] = useState(1);
const [currentChapter, setCurrentChapter] = useState(1);

const fetchMissions = useCallback((): Mission[] => {
return [
{
id: 1,
chapter: 1,
title: "Make a plankton classification",
description:
"Observe and mark plankton in your habitats and track their behaviour",
icon: FishIcon,
points: 2,
completedCount: 0,
internalComponent: PlanktonPortalFrame,
color: "text-blue-500",
},
{
id: 2,
chapter: 1,
title: "Comment & vote on Plankton discoveries",
description:
"Work with other players to drill down on specific plankter points",
icon: VoicemailIcon,
points: 1,
completedCount: 0,
internalComponent: VotePPClassifications,
color: "text-green-400",
},

// Mission for unidentifiable traits

{
id: 3,
chapter: 1,
title: "Plankton stats",
description:
"View the health score for Earth's oceans and how your discoveries have improved our understanding of plankton behaviour",
icon: PartyPopper,
points: 0,
completedCount: 0,
internalComponent: PlanktonDiscoveryStats,
color: 'text-red-600',
},
];
}, []);

const fetchMissionPoints = useCallback(
async (session: any, supabase: any): Promise<MissionPoints> => {
const { data: classifications } = await supabase
.from("classifications")
.select("id, classificationtype, classificationConfiguration")
.eq("author", session.user.id)
.eq("classificationtype", "zoodex-planktonPortal");

const mission1Points = classifications?.length || 0;

const mission2Points =
classifications?.filter((classification: any) => {
const config = classification.classificationConfiguration || {};
const options = config?.classificationOptions?.[""] || {};
return Object.values(options).some((value) => value === true);
}).length || 0;

const { data: comments } = await supabase
.from("comments")
.select("id, classification_id")
.eq("author", session.user.id);

const classificationIds = classifications?.map((c: any) => c.id) || [];
const mission3Points =
comments?.filter((comment: any) =>
classificationIds.includes(comment.classification_id)
).length || 0;

return {
1: mission1Points,
2: mission2Points,
3: mission3Points,
};
},
[]
);

useEffect(() => {
if (!session) return;

const updateMissionData = async () => {
const points = await fetchMissionPoints(session, supabase);

const updatedMissions = fetchMissions().map((mission) => ({
...mission,
completedCount: points[mission.id] || 0,
}));

setExperiencePoints(points[1] + points[2] + points[3]);
setMissions(updatedMissions);
};

updateMissionData();
}, [session, supabase, fetchMissions, fetchMissionPoints]);

const maxUnlockedChapter = Math.max(
Math.floor(experiencePoints / 9) + 1,
Math.max(...missions.map((mission) => mission.chapter), 1)
);

return (
<MissionShell
missions={missions.filter((mission) => mission.chapter === currentChapter)}
experiencePoints={experiencePoints}
level={level}
currentChapter={currentChapter}
maxUnlockedChapter={maxUnlockedChapter}
onPreviousChapter={() => setCurrentChapter((prev) => Math.max(prev - 1, 1))}
onNextChapter={() =>
setCurrentChapter((prev) => Math.min(prev + 1, maxUnlockedChapter))
}
/>
);
};

export default PlanktonPortal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useEffect, useState } from "react";
import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react";
import { Card, CardContent } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { Progress } from "@/components/ui/progress";
import { FishIcon } from "lucide-react";

const PlanktonDiscoveryStats = () => {
const supabase = useSupabaseClient();
const session = useSession();
const [totalPlankton, setTotalPlankton] = useState(0);
const [recentPlankton, setRecentPlankton] = useState(0);
const [onlyMine, setOnlyMine] = useState(false);

useEffect(() => {
if (!session) return;

const fetchPlanktonCount = async () => {
const query = supabase
.from("classifications")
.select("id, created_at")
.eq("classificationtype", "zoodex-planktonPortal");

if (onlyMine) {
query.eq("author", session.user.id);
};

const { data, error } = await query;
if (error) return;

setTotalPlankton(data.length);

const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
const recentCount = data.filter((entry) => new Date(entry.created_at) > oneYearAgo).length;
setRecentPlankton(recentCount);
};

fetchPlanktonCount();
}, [supabase, session, onlyMine]);

const healthScore = Math.min(100, (recentPlankton / 50) * 100);
const healthLabel = healthScore > 75 ? "Excellent" : healthScore > 50 ? "Good" : healthScore > 25 ? "Moderate" : "Low";

return (
<Card className="p-4 w-full max-w-md bg-card border shadow-md rounded-lg">
<CardContent className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-blue-500 flex items-center gap-2">
<FishIcon className="w-5 h-5" /> Total Plankton Discovered
</h2>
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-600">Only Mine</span>
<Switch checked={onlyMine} onCheckedChange={setOnlyMine} />
</div>
</div>
<p className="text-xl font-bold text-green-400">{totalPlankton}</p>
<div>
<p className="text-sm text-gray-500">Health Score: {healthLabel}</p>
<Progress value={healthScore} className="h-2 bg-green-400" />
</div>
</CardContent>
</Card>
);
};

export default PlanktonDiscoveryStats;
Loading

0 comments on commit 2cd94bc

Please sign in to comment.