diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..509ee77e --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,67 @@ +name: "CodeQL" + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + schedule: + - cron: '0 21 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['javascript'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 \ No newline at end of file diff --git a/app/scenes/globe/page.tsx b/app/scenes/globe/page.tsx index 38292a82..9dbefe59 100644 --- a/app/scenes/globe/page.tsx +++ b/app/scenes/globe/page.tsx @@ -1,31 +1,128 @@ "use client"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { ReactNode, useCallback, useEffect, useState } from "react"; import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react"; import { useActivePlanet } from "@/context/ActivePlanet"; import { InventoryStructureItem } from "@/types/Items"; import { PlanetarySystem } from "@/components/(scenes)/planetScene/orbitals/system"; import StructuresOnPlanet from "./Structures"; import { EarthViewLayout } from "@/components/(scenes)/planetScene/layout"; +import { StructuresConfigForSandbox } from "@/constants/Structures/SandboxProperties"; -const GlobeView: React.FC = () => { - const { activePlanet, updatePlanetLocation } = useActivePlanet(); - const handleUpdatetToGlobeAnomalyLocation = () => { - updatePlanetLocation(35); +export default function GlobeView() { + const [activeComponent, setActiveComponent] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + + // Create a list of all the buttons and actions from the structures config + const buttonsList = Object.values(StructuresConfigForSandbox).flatMap( + (structure) => [...(structure.actions || []), ...structure.buttons] + ); + + // Handle button click and open the modal + const handleClick = (component: React.ReactNode) => { + setActiveComponent(component); + setIsModalOpen(true); // Open modal on button click }; - - if (activePlanet?.id !== 35) { - updatePlanetLocation(35); + + // Close the modal + const closeModal = () => { + setIsModalOpen(false); + setActiveComponent(null); }; - + return ( -
- + +
+
+
+ +
+
+
+
+
+ <> + +
+
+
+
+
+ {buttonsList.map((button, index) => ( +
+ {button.dynamicComponent ? ( + + ) : ( + + )} +
+ ))} +
+
+ + {isModalOpen && ( + +
+ {activeComponent} +
+
+ )} +
+ ); + } + + // Modal component for the popup + interface ModalProps { + onClose: () => void; + children: ReactNode; + } + + const Modal: React.FC = ({ onClose, children }) => { + return ( +
+
+ +
{children}
+
+
); -}; + }; + +// const GlobeView: React.FC = () => { +// const { activePlanet, updatePlanetLocation } = useActivePlanet(); +// const handleUpdatetToGlobeAnomalyLocation = () => { +// updatePlanetLocation(35); +// }; + +// if (activePlanet?.id !== 35) { +// updatePlanetLocation(35); +// }; + +// return ( +//
+// +//
+// ); +// }; -export default GlobeView; +// export default GlobeView; const GlobeStructures: React.FC = () => { const supabase = useSupabaseClient(); diff --git a/app/tests/page.tsx b/app/tests/page.tsx index 7039517d..e07e8121 100644 --- a/app/tests/page.tsx +++ b/app/tests/page.tsx @@ -2,11 +2,12 @@ import React, { useEffect, useState, useCallback } from "react"; import Camera from "@/components/Projects/Zoodex/Upload/Camera"; +import CloudUploadEarthCameraComponent from "@/components/Projects/Lidar/Upload/CloudCamera"; export default function TestPage() { return (
- +
); }; diff --git a/citizen b/citizen index 8999f34e..6c397b25 160000 --- a/citizen +++ b/citizen @@ -1 +1 @@ -Subproject commit 8999f34e67dce3b9c252e7a773ac8a2fcec1f0e7 +Subproject commit 6c397b25305f89055713701a1d59b28739f2e05a diff --git a/components/(classifications)/PostForm.tsx b/components/(classifications)/PostForm.tsx index f3ee70b9..cdda6251 100644 --- a/components/(classifications)/PostForm.tsx +++ b/components/(classifications)/PostForm.tsx @@ -226,6 +226,100 @@ const initialCloudClassificationOptions: ClassificationOption[] = [ }, ]; +const lidarEarthCloudsReadClassificationOptions: ClassificationOption[] = [ + { + id: 1, + text: "Nimbostratus", + }, + { + id: 2, + text: 'Cumulonimbus', + }, + { + id: 3, + text: 'Stratocumulus', + }, + { + id: 4, + text: 'Stratus' + }, + { + id: 5, + text: "Cumulus", + }, + { + id: 6, + text: "Altostratus", + }, + { + id: 7, + text: "Altocumulus", + }, + { + id: 8, + text: "Cirrostratus", + }, + { + id: 9, + text: "Cirrocumulus", + }, + { + id: 10, + text: "Cirrus", + }, + { + id: 11, + text: "No clouds", + }, +]; + +const lidarEarthCloudsUploadClassificationOptions: ClassificationOption[] = [ + { + id: 1, + text: "Nimbostratus", + }, + { + id: 2, + text: 'Cumulonimbus', + }, + { + id: 3, + text: 'Stratocumulus', + }, + { + id: 4, + text: 'Stratus' + }, + { + id: 5, + text: "Cumulus", + }, + { + id: 6, + text: "Altostratus", + }, + { + id: 7, + text: "Altocumulus", + }, + { + id: 8, + text: "Cirrostratus", + }, + { + id: 9, + text: "Cirrocumulus", + }, + { + id: 10, + text: "Cirrus", + }, + { + id: 11, + text: "No clouds", + }, +]; + const zoodexBurrowingOwlClassificationOptions: ClassificationOption[] = [ { id: 1, @@ -343,6 +437,8 @@ const ClassificationForm: React.FC = ({ anomalyType, an return 'Describe the number and behaviour of the penguins...' case 'zoodex-planktonPortal': return 'Describe the plankton you see and their behaviour...' + case 'lidar-earthCloudRead': + return 'Describe the type of cloud you see...' default: return "Enter your classification details..."; }; @@ -368,6 +464,8 @@ const ClassificationForm: React.FC = ({ anomalyType, an return diskDetectorClassificationOptions; case 'zoodex-planktonPortal': return planktonPortalClassificationOptions; + case 'lidar-earthCloudRead': + return lidarEarthCloudsReadClassificationOptions; case 'sunspot': // return sunspotsConfigurationTemporary; return []; diff --git a/components/Data/unlockNewDataSources.tsx b/components/Data/unlockNewDataSources.tsx index 9a6c575e..2bdf0b93 100644 --- a/components/Data/unlockNewDataSources.tsx +++ b/components/Data/unlockNewDataSources.tsx @@ -198,9 +198,9 @@ export function DataSourcesModal({ structureId, structure }: DataSourcesModalPro return (
-
+ {/*
-
+
*/}
(true); - useEffect(() => { + useEffect(() => { async function fetchAnomaly() { if (!session) { setLoading(false); diff --git a/components/Projects/Lidar/EarthCloudsRead.tsx b/components/Projects/Lidar/EarthCloudsRead.tsx new file mode 100644 index 00000000..451ced49 --- /dev/null +++ b/components/Projects/Lidar/EarthCloudsRead.tsx @@ -0,0 +1,30 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useSupabaseClient, useSession } from "@supabase/auth-helpers-react"; +import { useActivePlanet } from "@/context/ActivePlanet"; +import ClassificationForm from "@/components/(classifications)/PostForm"; +import { Anomaly } from "../Telescopes/Transiting"; + +export function EarthCloudRead() { + const supabase = useSupabaseClient(); + const session = useSession(); + + const { activePlanet } = useActivePlanet(); + + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + // const imageUrl = `${supabaseUrl}/storage/v1/object/public/clouds/${anomalyId}.png`; + + const [part, setPart] = useState(1); + const [line, setLine] = useState(1); + + const nextLine = () => setLine((prevLine) => prevLine + 1); + const nextPart = () => { + setPart(2); + setLine(1); + }; + + return ( + <>Test + ) +} \ No newline at end of file diff --git a/components/Projects/Lidar/Upload/CloudCamera.tsx b/components/Projects/Lidar/Upload/CloudCamera.tsx new file mode 100644 index 00000000..6465aeb1 --- /dev/null +++ b/components/Projects/Lidar/Upload/CloudCamera.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { useEffect, useRef, useState } from 'react'; +import useSound from 'use-sound'; +import Webcam from "react-webcam"; +import { useSupabaseClient, useSession } from '@supabase/auth-helpers-react'; +import { useActivePlanet } from '@/context/ActivePlanet'; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Upload, ChevronRight } from "lucide-react"; + +const CloudUploadEarthCameraComponent = () => { + const supabase = useSupabaseClient(); + const session = useSession(); + const { activePlanet } = useActivePlanet(); + + const webcamRef = useRef(null); + const [loadingContent, setLoadingContent] = useState(false); + const [buttonPressed, setButtonPressed] = useState(false); + const [captureImage, setCaptureImage] = useState(null); + const [uploadData, setUploadData] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const [comment, setComment] = useState(""); + const [cloudName, setCloudName] = useState(""); + const [location, setLocation] = useState(""); + const [uploads, setUploads] = useState([]); + const [showCommentBox, setShowCommentBox] = useState(false); + + const takeScreenshot = async () => { + if (loadingContent || buttonPressed) return; + setButtonPressed(true); + setTimeout(() => { + setButtonPressed(false); + setLoadingContent(true); + }, 200); + + const imageSrc = webcamRef.current?.getScreenshot(); + if (imageSrc) { + const resizedImage = await resizeImage(imageSrc); + const file = await convertBase64ToFile(resizedImage, 'screenshot.jpg'); + await uploadImageToSupabase(file); + } + }; + + const uploadImage = async (e: React.ChangeEvent) => { + if (loadingContent || buttonPressed) return; + setButtonPressed(true); + setTimeout(() => { + setButtonPressed(false); + setLoadingContent(true); + }, 200); + const files = e.target.files; + if (files && files.length > 0) { + const fileArray = Array.from(files); + for (const file of fileArray) { + await uploadImageToSupabase(file); + } + } + }; + + const uploadImageToSupabase = async (file: File) => { + if (!session || !activePlanet) return; + setIsUploading(true); + const fileName = `${Date.now()}-${session.user.id}-${file.name}`; + try { + const { data: uploadData, error: uploadError } = await supabase.storage + .from('media') + .upload(fileName, file); + + if (uploadError) { + console.error('Upload error:', uploadError.message); + } else if (uploadData) { + const url = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/media/${uploadData.path}`; + const { data: uploadEntry, error: entryError } = await supabase + .from('uploads') + .insert({ + author: session.user.id, + location: activePlanet.id, + source: 'webcam', + file_url: url, + configuration: JSON.stringify({ filename: fileName }), + }) + .single(); + + if (!entryError) { + setUploadData(uploadEntry); + setUploads(prev => [...prev, url]); + setCaptureImage(url); + setShowCommentBox(true); + } + } + } catch (err) { + console.error('Unexpected error during file upload:', err); + } finally { + setIsUploading(false); + setLoadingContent(false); + } + }; + + const handleSubmitClassification = async (event: React.FormEvent) => { + event.preventDefault(); + if (uploads.length === 0) return; + const media = { uploadUrl: uploads[0] }; + const classificationConfiguration = { + media: media, + comment: comment, + cloudName: cloudName, + location: location, + }; + try { + const { data, error } = await supabase + .from('classifications') + .insert({ + content: comment || null, + author: session?.user?.id, + anomaly: activePlanet?.id, + media: JSON.stringify(media), + classificationtype: "userUpload-lidar-EarthCloud", + classificationConfiguration: classificationConfiguration, + }); + if (!error) console.log("Classification added successfully", data); + } catch (err) { + console.error("Unexpected error during classification insert:", err); + } + }; + + return ( +
+
+ {!showCommentBox ? ( + <> +
+ {captureImage ? ( + Captured + ) : ( +
+ +
+ )} +
+
+ + +
+ + ) : ( +
+ Captured + setCloudName(e.target.value)} + className="bg-white text-[#2C4F64] placeholder:text-[#2C4F64]/70" + /> + setLocation(e.target.value)} + className="bg-white text-[#2C4F64] placeholder:text-[#2C4F64]/70" + /> + setComment(e.target.value)} + className="bg-white text-[#2C4F64] placeholder:text-[#2C4F64]/70" + /> + +
+ )} +
+
+ ); +}; + +export default CloudUploadEarthCameraComponent; + +function resizeImage(base64Str: string, maxWidth = 512, maxHeight = 512): Promise { + return new Promise((resolve) => { + const img = new Image(); + img.src = base64Str; + img.onload = () => { + const canvas = document.createElement('canvas'); + let width = img.width; + let height = img.height; + if (width > height && width > maxWidth) { + height *= maxWidth / width; + width = maxWidth; + } else if (height > maxHeight) { + width *= maxHeight / height; + height = maxHeight; + } + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + ctx?.drawImage(img, 0, 0, width, height); + resolve(canvas.toDataURL("image/jpeg", 0.8)); + }; + }); +} + +function convertBase64ToFile(base64Str: string, fileName: string): Promise { + return fetch(base64Str) + .then((res) => res.arrayBuffer()) + .then((buffer) => new File([buffer], fileName, { type: 'image/jpeg' })); +}; diff --git a/components/Projects/Lidar/cloudspottingOnMars.tsx b/components/Projects/Lidar/cloudspottingOnMars.tsx index 104f2a73..6c6bb4b0 100644 --- a/components/Projects/Lidar/cloudspottingOnMars.tsx +++ b/components/Projects/Lidar/cloudspottingOnMars.tsx @@ -15,7 +15,7 @@ export const CloudspottingOnMars: React.FC = ({ }) => { const supabase = useSupabaseClient(); const session = useSession(); - + const { activePlanet } = useActivePlanet(); const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; diff --git a/components/Projects/Telescopes/Sunspots.tsx b/components/Projects/Telescopes/Sunspots.tsx index 4f80d659..5baa85d0 100644 --- a/components/Projects/Telescopes/Sunspots.tsx +++ b/components/Projects/Telescopes/Sunspots.tsx @@ -142,14 +142,14 @@ const SunspotDetectorTutorial: React.FC = ({ )} {part === 2 && ( <> -
+ {/*
telescope -
+
*/}
diff --git a/components/Projects/Telescopes/Transiting.tsx b/components/Projects/Telescopes/Transiting.tsx index 36602358..8968ea1f 100644 --- a/components/Projects/Telescopes/Transiting.tsx +++ b/components/Projects/Telescopes/Transiting.tsx @@ -159,7 +159,7 @@ export function StarterTelescope() { if (!userChoice) { return (
- + {/* */}

Choose a target to observe using your Telescope:

{configuration["missions unlocked"] && Array.isArray(configuration["missions unlocked"]) && configuration["missions unlocked"].length > 0 ? ( configuration["missions unlocked"].map((missionId: string) => ( diff --git a/components/Projects/Zoodex/ClassifyOthersAnimals.tsx b/components/Projects/Zoodex/ClassifyOthersAnimals.tsx index 5a7e0b92..25be2a27 100644 --- a/components/Projects/Zoodex/ClassifyOthersAnimals.tsx +++ b/components/Projects/Zoodex/ClassifyOthersAnimals.tsx @@ -157,7 +157,7 @@ export function StarterZoodex() { if (!userChoice) { return (
- + {/* */}

You've been given some animals to observe and compare to their mannerisms on Earth. As you progress, more species will become available.

Choose a data source:

{configuration["missions unlocked"] && Array.isArray(configuration["missions unlocked"]) && configuration["missions unlocked"].length > 0 ? ( @@ -204,7 +204,7 @@ export function StarterZoodex() { return (
- + {/* */}

{anomaly.content}

{anomaly.avatar_url && ( @@ -363,7 +363,7 @@ export function StarterZoodexGallery() { if (!userChoice) { return (
- + {/* */}

You've been given some animals to observe and compare to their mannerisms on Earth. As you progress, more species will become available.

Choose a data source:

{configuration["missions unlocked"] && Array.isArray(configuration["missions unlocked"]) && configuration["missions unlocked"].length > 0 ? ( diff --git a/constants/Structures/SandboxProperties.tsx b/constants/Structures/SandboxProperties.tsx index afd711ca..b4fdca26 100644 --- a/constants/Structures/SandboxProperties.tsx +++ b/constants/Structures/SandboxProperties.tsx @@ -6,7 +6,7 @@ import { StarterTelescope } from "@/components/Projects/Telescopes/Transiting"; import { StarterLidar } from "@/components/Projects/Lidar/Clouds"; import ClassificationViewer from "@/components/(classifications)/YourClassifications"; import { StarterZoodexGallery } from "@/components/Projects/Zoodex/ClassifyOthersAnimals"; -import { BeanIcon, BirdIcon, BookAIcon, BookAudioIcon, BookCopy, BookDashedIcon, BriefcaseMedical, CameraIcon, CaravanIcon, CloudCogIcon, CogIcon, ConstructionIcon, DogIcon, DotSquare, FishIcon, GemIcon, HeartIcon, LockIcon, MehIcon, MicroscopeIcon, PenBox, PhoneIcon, PickaxeIcon, PowerIcon, RssIcon, SaladIcon, StarIcon, SunIcon, SwitchCamera, TelescopeIcon, TestTubeDiagonal, TestTubeDiagonalIcon, TreePalmIcon, WebcamIcon } from "lucide-react"; +import { BeanIcon, BirdIcon, BookAIcon, BookAudioIcon, BookCopy, BookDashedIcon, BriefcaseMedical, CameraIcon, CaravanIcon, CloudCogIcon, CloudDrizzleIcon, CogIcon, ConstructionIcon, DogIcon, DotSquare, FishIcon, GemIcon, HeartIcon, LockIcon, MehIcon, MicroscopeIcon, PenBox, PhoneIcon, PickaxeIcon, PowerIcon, RssIcon, SaladIcon, StarIcon, SunIcon, SwitchCamera, TelescopeIcon, TestTubeDiagonal, TestTubeDiagonalIcon, TreePalmIcon, WebcamIcon } from "lucide-react"; import StructureRepair from "@/components/(structures)/Config/RepairStructure"; import { RoverPhoto } from "@/components/(anomalies)/(data)/Mars-Photos"; import { AnomalyRoverPhoto } from "@/components/(structures)/Auto/AutomatonClassificationShell"; @@ -22,6 +22,7 @@ import CameraComponent from "@/components/Projects/Zoodex/Upload/Camera"; import { PenguinWatch } from "@/components/Projects/Zoodex/penguinWatch"; import { PlanktonPortal, PlanktonPortalTutorial } from "@/components/Projects/Zoodex/planktonPortal"; import { BurrowingOwl } from "@/components/Projects/Zoodex/burrowingOwls"; +import CloudUploadEarthCameraComponent from "@/components/Projects/Lidar/Upload/CloudCamera"; interface IndividualStructureProps { name?: string; @@ -98,12 +99,12 @@ export const StructuresConfigForSandbox: StructureConfig = { ], imageSrc: '/assets/Items/Zoodex.png', buttons: [ - { - icon: , - text: "Classify animals", - dynamicComponent: , - sizePercentage: 60, - }, + // { + // icon: , + // text: "Classify animals", + // dynamicComponent: , + // sizePercentage: 60, + // }, { icon: , text: "Capture animals", @@ -145,10 +146,16 @@ export const StructuresConfigForSandbox: StructureConfig = { buttons: [ { icon: , - text: "Search your clouds", + text: "Classify Martian clouds", dynamicComponent: , sizePercentage: 60, }, + { + icon: , + text: "Upload clouds you see", + dynamicComponent: , + sizePercentage: 70, + }, ], }, }; \ No newline at end of file