diff --git a/backend/index.ts b/backend/index.ts index 0f6153a2..0460cacb 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -6,7 +6,6 @@ import profile from "routes/profile.js"; import auth from "routes/auth.js"; import support from "routes/support.js"; import taxonomy from "routes/taxonomy.js"; -import quiz from "routes/quiz.js"; import piper from "routes/piper.js"; import region from "routes/region.js"; import ebirdProxy from "routes/ebird-proxy.js"; @@ -28,7 +27,6 @@ app.route("/v1/trips", trips); app.route("/v1/auth", auth); app.route("/v1/support", support); app.route("/v1/taxonomy", taxonomy); -app.route("/v1/quiz", quiz); app.route("/v1/piper", piper); app.route("/v1/region", region); app.route("/v1/ebird-proxy", ebirdProxy); diff --git a/backend/lib/db.ts b/backend/lib/db.ts index d9ead702..ef28c887 100644 --- a/backend/lib/db.ts +++ b/backend/lib/db.ts @@ -2,7 +2,6 @@ import Trip from "models/Trip.js"; import Profile from "models/Profile.js"; import TargetList from "models/TargetList.js"; import Invite from "models/Invite.js"; -import QuizImages from "models/QuizImages.js"; import Vault from "models/Vault.js"; import mongoose from "mongoose"; @@ -45,4 +44,4 @@ export async function connect() { } } -export { Trip, Profile, TargetList, Invite, QuizImages, Vault }; +export { Trip, Profile, TargetList, Invite, Vault }; diff --git a/backend/models/QuizImages.ts b/backend/models/QuizImages.ts deleted file mode 100644 index 945da23c..00000000 --- a/backend/models/QuizImages.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { QuizImages } from "@birdplan/shared"; -import mongoose, { Schema, model, Model } from "mongoose"; -import { nanoId } from "lib/utils.js"; - -const fields: Record, any> = { - _id: { type: String, default: () => nanoId() }, - code: { type: String, required: true }, - ids: { type: [String], required: true }, - name: { type: String, required: true }, -}; - -const QuizImagesSchema = new Schema(fields, { - timestamps: true, -}); - -const QuizImagesModel = - (mongoose.models.QuizImages as Model) || model("QuizImages", QuizImagesSchema); - -export default QuizImagesModel; diff --git a/backend/routes/quiz.ts b/backend/routes/quiz.ts deleted file mode 100644 index 6a9b49d6..00000000 --- a/backend/routes/quiz.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Hono } from "hono"; -import { connect, QuizImages } from "lib/db.js"; -import type { QuizImages as QuizImagesT } from "@birdplan/shared"; - -const quiz = new Hono(); - -type eBirdResponse = { - results: { - content: { - assetId: string; - commonName: string; - }[]; - }; -}; - -quiz.post("/generate", async (c) => { - const { codes } = await c.req.json<{ codes: string[] }>(); - - await connect(); - - const savedImages = await QuizImages.find({ code: { $in: codes } }).lean(); - - const steps = await Promise.all( - codes.map(async (code: string) => { - try { - let savedItem: QuizImagesT | null = savedImages.find((it) => it.code === code) || null; - if (!savedItem) { - const res = await fetch( - `https://search.macaulaylibrary.org/api/v1/search?count=20&sort=rating_rank_desc&mediaType=p®ionCode=&taxonCode=${code}&taxaLocale=en` - ); - const data: eBirdResponse = await res.json(); - const results = data.results.content; - const ids = results.map((it) => it.assetId); - const name = results[0].commonName; - const newItem = { code, ids, name }; - const item = await QuizImages.create(newItem); - savedItem = item.toObject(); - } - const mlId = savedItem?.ids[Math.floor(Math.random() * savedItem?.ids.length)]; - return { - name: savedItem?.name, - code: savedItem?.code, - mlId, - guessName: "", - isCorrect: false, - }; - } catch (error) { - return null; - } - }) - ); - - const filteredSteps = steps.filter((it) => it !== null); - - return c.json(filteredSteps); -}); - -export default quiz; diff --git a/frontend/components/TripOptionsDropdown.tsx b/frontend/components/TripOptionsDropdown.tsx index c5e407c2..5d940319 100644 --- a/frontend/components/TripOptionsDropdown.tsx +++ b/frontend/components/TripOptionsDropdown.tsx @@ -27,11 +27,6 @@ export default function TripOptionsDropdown({ className }: Props) { icon: "bullseye", hidden: !canEdit, }, - { - name: "Bird Quiz", - href: `/${trip?._id}/quiz`, - icon: "questionMark", - }, { name: "Share Trip", onClick: () => open("share"), diff --git a/frontend/pages/[tripId]/quiz.tsx b/frontend/pages/[tripId]/quiz.tsx deleted file mode 100644 index 7ab64896..00000000 --- a/frontend/pages/[tripId]/quiz.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import React from "react"; -import Header from "components/Header"; -import Head from "next/head"; -import { useTrip } from "providers/trip"; -import toast from "react-hot-toast"; -import Button from "components/Button"; -import { useProfile } from "providers/profile"; -import LoginModal from "components/LoginModal"; -import Footer from "components/Footer"; -import Select from "components/ReactSelectStyled"; -import { getRandomItemsFromArray } from "lib/helpers"; -import Link from "next/link"; -import NotFound from "components/NotFound"; - -type StepT = { - code: string; - name: string; - mlId: number; - guessName: string; - isCorrect: boolean; -}; - -const quizLength = 10; - -export default function Quiz() { - const [index, setIndex] = React.useState(0); - const [steps, setSteps] = React.useState([]); - const [isInitialized, setIsInitialized] = React.useState(false); - const { is404, trip, targets } = useTrip(); - const { lifelist } = useProfile(); - const selectRef = React.useRef(null); - - const liferTargets = targets?.items.filter((target) => !lifelist.includes(target.code)) || []; - const options = liferTargets.map(({ code, name }) => ({ value: code, label: name })); - const step = steps[index]; - - const initQuiz = React.useCallback(async () => { - console.log("Initializing quiz..."); - setSteps([]); - setIndex(0); - const targetCodes = - targets?.items.filter((target) => !lifelist.includes(target.code)).map((target) => target.code) || []; - - const randomCodes = getRandomItemsFromArray(targetCodes, quizLength); - try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/quiz/generate`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ codes: randomCodes }), - }); - const data = await res.json(); - setSteps(data); - setTimeout(() => { - selectRef.current?.focus(); - }, 100); - } catch (error) { - toast.error("Error generating quiz"); - return; - } - }, [targets, lifelist]); - - React.useEffect(() => { - if (!trip || !lifelist?.length || isInitialized) return; - initQuiz(); - setIsInitialized(true); - }, [trip, lifelist, initQuiz, isInitialized]); - - const isComplete = index === steps.length && steps.length > 0; - - const handleNext = () => { - setIndex(index + 1); - setTimeout(() => { - selectRef.current?.focus(); - }, 100); - }; - - const handleGuess = (code: string, name: string) => { - setSteps((prevSteps) => { - return prevSteps.map((step, i) => { - if (i === index) { - const isCorrect = step.code === code; - if (!isCorrect) { - toast.error(`Incorrect`); - } else { - toast.success("Correct!"); - } - return { ...step, guessName: name, isCorrect }; - } - return step; - }); - }); - }; - - React.useEffect(() => { - const handleLeft = (e: KeyboardEvent) => { - if (e.key === "ArrowLeft") { - setIndex((prevIndex) => prevIndex - 1); - } - }; - const handleRight = (e: KeyboardEvent) => { - if (e.key === "ArrowRight") { - setIndex((prevIndex) => prevIndex + 1); - setTimeout(() => { - selectRef.current?.focus(); - }, 500); - } - }; - window.addEventListener("keydown", handleLeft); - window.addEventListener("keydown", handleRight); - return () => { - window.removeEventListener("keydown", handleLeft); - window.removeEventListener("keydown", handleRight); - }; - }, []); - - if (is404) return ; - - return ( -
- - Quiz | BirdPlan.app - - -
-
-
- - ← Back to trip - -

🤔 Bird Quiz

-
- {isComplete ? ( -
-

Quiz complete!

-

- You answered {steps.filter((step) => step.isCorrect).length} out of {steps.length} correctly. -

- -
- ) : !!step ? ( -
-

- {step.guessName ? step.name : "What species is this?"} -

- - {step.guessName && ( -
-

{step.isCorrect ? "✅ Correct!" : "❌ Incorrect"}

-

- - {step.name} - -   •   - - ML{step.mlId} - -

-
- )} - {!step.guessName && ( -
-