-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
️🗣️🥻 ↝ [SSC-73 SSC-71 SSC-70 SSC-33]: Integrating plankton portal pro…
…ject back into flow
- Loading branch information
1 parent
b3dc425
commit 2cd94bc
Showing
9 changed files
with
369 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
components/Structures/Missions/Biologists/Plankton/PPVote.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
153
components/Structures/Missions/Biologists/Plankton/PlanktonPortal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
67 changes: 67 additions & 0 deletions
67
components/Structures/Missions/Biologists/Plankton/PlanktonScore.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.