From b321b82799fd026403b8deec206b33b24faef9b2 Mon Sep 17 00:00:00 2001 From: nicolasito1411 <60229704+Marchand-Nicolas@users.noreply.github.com> Date: Sat, 16 Sep 2023 15:54:11 +0200 Subject: [PATCH 01/14] feat: new quest page --- components/UI/backButton.tsx | 26 ++ .../iconsComponents/icons/arrowRightIcon.tsx | 23 ++ components/pages/home/homeControls.tsx | 13 + components/pages/home/howToParticipate.tsx | 7 +- components/pages/home/questCategories.tsx | 62 ++++ components/pages/home/trending.tsx | 42 +++ components/quests/featuredQuest.tsx | 6 +- components/quests/nftDisplay.tsx | 16 +- components/quests/nfts.tsx | 23 ++ components/quests/questCategory.tsx | 35 ++ components/quests/questCategoryDetails.tsx | 69 ++++ components/quests/questDetails.tsx | 40 ++ components/quests/questMenu.tsx | 336 +++++++++++++++++ components/quests/screenLayout.tsx | 32 ++ components/quests/task.tsx | 13 +- components/quiz/quizControls.tsx | 17 +- components/shapes/blur.tsx | 12 + pages/index.tsx | 35 +- pages/quest/[questPage].tsx | 344 ++---------------- styles/Home.module.css | 182 ++++++++- styles/components/backButton.module.css | 10 + styles/components/footer.module.css | 10 +- styles/components/quests/quiz.module.css | 5 - styles/components/shapes.module.css | 16 + styles/globals.css | 3 +- styles/profile.module.css | 19 +- styles/quests.module.css | 38 +- types/frontTypes.d.ts | 6 + 28 files changed, 1018 insertions(+), 422 deletions(-) create mode 100644 components/UI/backButton.tsx create mode 100644 components/UI/iconsComponents/icons/arrowRightIcon.tsx create mode 100644 components/pages/home/homeControls.tsx create mode 100644 components/pages/home/questCategories.tsx create mode 100644 components/pages/home/trending.tsx create mode 100644 components/quests/nfts.tsx create mode 100644 components/quests/questCategory.tsx create mode 100644 components/quests/questCategoryDetails.tsx create mode 100644 components/quests/questDetails.tsx create mode 100644 components/quests/questMenu.tsx create mode 100644 components/quests/screenLayout.tsx create mode 100644 components/shapes/blur.tsx create mode 100644 styles/components/backButton.module.css diff --git a/components/UI/backButton.tsx b/components/UI/backButton.tsx new file mode 100644 index 00000000..05b5a618 --- /dev/null +++ b/components/UI/backButton.tsx @@ -0,0 +1,26 @@ +import React, { FunctionComponent } from "react"; +import styles from "../../styles/components/backButton.module.css"; + +type BackButtonProps = { + onClick: () => void; +}; + +const BackButton: FunctionComponent = ({ onClick }) => ( + +); + +export default BackButton; diff --git a/components/UI/iconsComponents/icons/arrowRightIcon.tsx b/components/UI/iconsComponents/icons/arrowRightIcon.tsx new file mode 100644 index 00000000..8ec3d6ed --- /dev/null +++ b/components/UI/iconsComponents/icons/arrowRightIcon.tsx @@ -0,0 +1,23 @@ +import { FunctionComponent } from "react"; + +const ArrowRightIcon: FunctionComponent = ({ + width = 24, + color, +}) => { + return ( + + + + ); +}; + +export default ArrowRightIcon; diff --git a/components/pages/home/homeControls.tsx b/components/pages/home/homeControls.tsx new file mode 100644 index 00000000..02985ed2 --- /dev/null +++ b/components/pages/home/homeControls.tsx @@ -0,0 +1,13 @@ +import { FunctionComponent } from "react"; +import styles from "../../../styles/Home.module.css"; + +const HomeControls: FunctionComponent = () => { + return ( +
+ + +
+ ); +}; + +export default HomeControls; diff --git a/components/pages/home/howToParticipate.tsx b/components/pages/home/howToParticipate.tsx index d623b51a..ee0993d3 100644 --- a/components/pages/home/howToParticipate.tsx +++ b/components/pages/home/howToParticipate.tsx @@ -1,16 +1,14 @@ -import React from "react"; +import React, { FunctionComponent } from "react"; import Steps from "../../UI/steps/steps"; import CategoryTitle from "../../UI/titles/categoryTitle"; -import Crosses from "../../shapes/crosses"; import styles from "../../../styles/components/pages/home/howToParticipate.module.css"; -const HowToParticipate = () => { +const HowToParticipate: FunctionComponent = () => { return (
{ }, ]} /> -
); diff --git a/components/pages/home/questCategories.tsx b/components/pages/home/questCategories.tsx new file mode 100644 index 00000000..15f46962 --- /dev/null +++ b/components/pages/home/questCategories.tsx @@ -0,0 +1,62 @@ +import React, { FunctionComponent, useEffect, useState } from "react"; +import styles from "../../../styles/Home.module.css"; +import { QuestDocument } from "../../../types/backTypes"; +import QuestCategory from "../../quests/questCategory"; +import QuestsSkeleton from "../../skeletons/questsSkeleton"; + +type QuestCategoriesProps = { + quests: QuestDocument[]; +}; + +const QuestCategories: FunctionComponent = ({ + quests, +}) => { + const [categories, setCategories] = useState([]); + + useEffect(() => { + const res: { + [key: string]: QuestCategory; + } = {}; + for (let quest of quests) { + const key = quest.category as string; + const value = res[key]; + if (!value) { + res[key] = { + name: quest.category, + img: quest.img_card, + questNumber: 1, + }; + } else { + res[key].questNumber += 1; + } + } + setCategories(Object.values(res)); + }, [quests]); + + return ( + <> +

Accomplish your Starknet Quests

+
+
+ {categories ? ( + categories.map((category) => { + return ( + quest.category === category.name + )} + /> + ); + }) + ) : ( + + )} +
+
+ + ); +}; + +export default QuestCategories; diff --git a/components/pages/home/trending.tsx b/components/pages/home/trending.tsx new file mode 100644 index 00000000..4c9b5b70 --- /dev/null +++ b/components/pages/home/trending.tsx @@ -0,0 +1,42 @@ +import React, { FunctionComponent } from "react"; +import styles from "../../../styles/Home.module.css"; +import Quest from "../../quests/quest"; +import QuestsSkeleton from "../../skeletons/questsSkeleton"; +import { useRouter } from "next/router"; +import { QuestDocument } from "../../../types/backTypes"; + +type TrendingQuestsProps = { + quests: QuestDocument[]; +}; + +const TrendingQuests: FunctionComponent = ({ quests }) => { + const router = useRouter(); + return ( + <> +

Trending quests

+
+ {quests ? ( + quests.slice(0, 6).map((quest) => { + return ( + router.push(`/quest/${quest.id}`)} + imgSrc={quest.img_card} + issuer={{ + name: quest.issuer, + logoFavicon: quest.logo, + }} + reward={quest.rewards_title} + /> + ); + }) + ) : ( + + )} +
+ + ); +}; + +export default TrendingQuests; diff --git a/components/quests/featuredQuest.tsx b/components/quests/featuredQuest.tsx index e8695ebd..a0f69e3c 100644 --- a/components/quests/featuredQuest.tsx +++ b/components/quests/featuredQuest.tsx @@ -26,17 +26,17 @@ const FeaturedQuest: FunctionComponent = ({ }) => { const isSmallScreen = useMediaQuery("(max-width: 1024px)"); - return onClick && !isSmallScreen ? ( + return onClick ? (
-

Featured

+

Featured

{title}

{desc}

{reward}

-
+
diff --git a/components/quests/nftDisplay.tsx b/components/quests/nftDisplay.tsx index 73128654..c569afa3 100644 --- a/components/quests/nftDisplay.tsx +++ b/components/quests/nftDisplay.tsx @@ -1,6 +1,6 @@ import React, { FunctionComponent } from "react"; -import styles from "../../styles/quests.module.css"; import NftIssuer from "./nftIssuer"; +import Nfts from "./nfts"; type NftDisplayProps = { nfts: Nft[]; @@ -11,19 +11,7 @@ const NftDisplay: FunctionComponent = ({ nfts, issuer }) => { return (
-
- {nfts.map((nft, index) => ( -
- - {nft.level && nfts.length > 1 ? ( -

Level {nft.level}

- ) : null} -
- ))} -
+
); }; diff --git a/components/quests/nfts.tsx b/components/quests/nfts.tsx new file mode 100644 index 00000000..e4045107 --- /dev/null +++ b/components/quests/nfts.tsx @@ -0,0 +1,23 @@ +import { FunctionComponent } from "react"; +import styles from "../../styles/quests.module.css"; + +type NftsProps = { + nfts: Nft[]; +}; + +const Nfts: FunctionComponent = ({ nfts }) => { + return ( +
+ {nfts.map((nft, index) => ( +
+ + {nft.level && nfts.length > 1 ? ( +

Level {nft.level}

+ ) : null} +
+ ))} +
+ ); +}; + +export default Nfts; diff --git a/components/quests/questCategory.tsx b/components/quests/questCategory.tsx new file mode 100644 index 00000000..ea4d934d --- /dev/null +++ b/components/quests/questCategory.tsx @@ -0,0 +1,35 @@ +import React, { FunctionComponent, useState } from "react"; +import styles from "../../styles/Home.module.css"; +import QuestCategoryDetails from "./questCategoryDetails"; +import { QuestDocument } from "../../types/backTypes"; + +type QuestCategoryProps = { + category: QuestCategory; + quests: QuestDocument[]; +}; + +const QuestCategory: FunctionComponent = ({ + category, + quests, +}) => { + const [showMenu, setShowMenu] = useState(false); + + return ( + <> +
setShowMenu(true)}> +
+

{category.name} Quest

+

+ {category.questNumber} quest{category.questNumber > 1 ? "s" : ""} +

+
+ +
+ {showMenu && ( + + )} + + ); +}; + +export default QuestCategory; diff --git a/components/quests/questCategoryDetails.tsx b/components/quests/questCategoryDetails.tsx new file mode 100644 index 00000000..5166aa18 --- /dev/null +++ b/components/quests/questCategoryDetails.tsx @@ -0,0 +1,69 @@ +import React, { + FunctionComponent, + ReactNode, + useEffect, + useState, +} from "react"; +import styles from "../../styles/Home.module.css"; +import { QuestDocument } from "../../types/backTypes"; +import ScreenLayout from "./screenLayout"; +import Quest from "./quest"; +import QuestDetails from "./questDetails"; + +type QuestCategoryDetailsProps = { + quests: QuestDocument[]; + setShowMenu: (showMenu: boolean) => void; +}; + +const QuestCategoryDetails: FunctionComponent = ({ + quests, + setShowMenu, +}) => { + const [menu, setMenu] = useState(null); + + useEffect(() => { + const documentBody = document.querySelector("body"); + if (!documentBody) return; + // Mount + documentBody.style.overflow = "hidden"; + // Scroll to top + window.scrollTo(0, 0); + // Unmount + return () => { + documentBody.style.removeProperty("overflow"); + }; + }, []); + + return ( + + <> +

Onboarding quests

+
+ {quests.map((quest) => ( + + setMenu( + setMenu(null)} + /> + ) + } + imgSrc={quest.img_card} + issuer={{ + name: quest.issuer, + logoFavicon: quest.logo, + }} + reward={quest.rewards_title} + /> + ))} +
+ {menu} + +
+ ); +}; + +export default QuestCategoryDetails; diff --git a/components/quests/questDetails.tsx b/components/quests/questDetails.tsx new file mode 100644 index 00000000..64792450 --- /dev/null +++ b/components/quests/questDetails.tsx @@ -0,0 +1,40 @@ +import React, { FunctionComponent, useEffect } from "react"; +import { QuestDocument } from "../../types/backTypes"; +import ScreenLayout from "./screenLayout"; +import { useRouter } from "next/router"; +import QuestMenu from "./questMenu"; + +type QuestDetailsProps = { + quest: QuestDocument; + setShowMenu: (showMenu: boolean) => void; +}; + +const QuestDetails: FunctionComponent = ({ + quest, + setShowMenu, +}) => { + const router = useRouter(); + + useEffect(() => { + const documentBody = document.querySelector("body"); + if (!documentBody) return; + // Mount + documentBody.style.overflow = "hidden"; + // Scroll to top + window.scrollTo(0, 0); + // Unmount + return () => { + documentBody.style.removeProperty("overflow"); + }; + }, []); + + return ( + + <> + + + + ); +}; + +export default QuestDetails; diff --git a/components/quests/questMenu.tsx b/components/quests/questMenu.tsx new file mode 100644 index 00000000..d86fd200 --- /dev/null +++ b/components/quests/questMenu.tsx @@ -0,0 +1,336 @@ +import React, { + ReactNode, + useEffect, + useState, + FunctionComponent, +} from "react"; +import styles from "../../styles/quests.module.css"; +import Task from "../../components/quests/task"; +import Reward from "../../components/quests/reward"; +import quests_nft_abi from "../../abi/quests_nft_abi.json"; +import { useAccount, useProvider } from "@starknet-react/core"; +import { useRouter } from "next/router"; +import { hexToDecimal } from "../../utils/feltService"; +import { + NFTItem, + QueryError, + QuestDocument, + UserTask, +} from "../../types/backTypes"; +import { Call, Contract } from "starknet"; +import { Skeleton } from "@mui/material"; +import TasksSkeleton from "../../components/skeletons/tasksSkeleton"; +import { generateCodeChallenge } from "../../utils/codeChallenge"; +import Timer from "../../components/quests/timer"; +import Nfts from "./nfts"; + +const splitByNftContract = ( + rewards: EligibleReward[] +): Record => { + return rewards.reduce( + (acc: Record, reward: EligibleReward) => { + if (!acc[reward.nft_contract]) { + acc[reward.nft_contract] = []; + } + + acc[reward.nft_contract].push(reward); + return acc; + }, + {} + ); +}; + +type QuestMenuProps = { + quest: QuestDocument; + taskId?: string; + res?: string; + errorMsg?: string; +}; + +const QuestMenu: FunctionComponent = ({ + quest, + taskId, + res, + errorMsg, +}) => { + const router = useRouter(); + const { address } = useAccount(); + const { provider } = useProvider(); + const [tasks, setTasks] = useState([]); + const [rewardsEnabled, setRewardsEnabled] = useState(false); + const [eligibleRewards, setEligibleRewards] = useState< + Record + >({}); + const [unclaimedRewards, setUnclaimedRewards] = useState< + EligibleReward[] | undefined + >(); + const [mintCalldata, setMintCalldata] = useState(); + const [taskError, setTaskError] = useState(); + const [showQuiz, setShowQuiz] = useState(); + const questId = quest.id.toString(); + + // this fetches all tasks of this quest from db + useEffect(() => { + if (questId) { + // If a call was made with an address in the first second, the call with 0 address should be cancelled + let shouldFetchWithZeroAddress = true; + + // Set a 1-second timer to allow time for address loading + const timer = setTimeout(() => { + // If address isn't loaded after 1 second, make the API call with the zero address + if (shouldFetchWithZeroAddress) { + fetch( + `${process.env.NEXT_PUBLIC_API_LINK}/get_tasks?quest_id=${questId}&addr=0` + ) + .then((response) => response.json()) + .then((data: UserTask[] | QueryError) => { + if ((data as UserTask[]).length) setTasks(data as UserTask[]); + }); + } + }, 1000); + + // If the address is loaded before the 1-second timer, make the API call with the loaded address + if (address) { + shouldFetchWithZeroAddress = false; + clearTimeout(timer); + fetch( + `${ + process.env.NEXT_PUBLIC_API_LINK + }/get_tasks?quest_id=${questId}&addr=${hexToDecimal(address)}` + ) + .then((response) => response.json()) + .then((data: UserTask[] | QueryError) => { + if ((data as UserTask[]).length) setTasks(data as UserTask[]); + }); + } + + // Clear the timer when component unmounts or dependencies change to prevent memory leaks + return () => { + clearTimeout(timer); + }; + } + }, [questId, address]); + + const refreshRewards = ( + quest: QuestDocument, + address: string | undefined + ) => { + if (address && quest.rewards_endpoint) { + fetch(`${quest.rewards_endpoint}?addr=${hexToDecimal(address)}`) + .then((response) => response.json()) + .then((data) => { + if (data.rewards) { + setEligibleRewards(splitByNftContract(data.rewards)); + } + }); + } + }; + + // this fetches all rewards claimable by the user + useEffect(() => { + refreshRewards(quest, address); + }, [quest, address]); + + // this filters the claimable rewards to find only the unclaimed ones (on chain) + useEffect(() => { + (async () => { + let unclaimed: EligibleReward[] = []; + for (const contractAddr in eligibleRewards) { + const perContractRewards = eligibleRewards[contractAddr]; + const calldata = []; + for (const reward of perContractRewards) { + calldata.push({ + quest_id: questId as string, + task_id: reward.task_id.toString(), + user_addr: address as string, + }); + } + const contract = new Contract(quests_nft_abi, contractAddr, provider); + + const response = await contract.call("get_tasks_status", [calldata]); + if ( + typeof response === "object" && + response !== null && + !Array.isArray(response) + ) { + const status = response["status"]; + + if (Array.isArray(status)) { + const result = status.map((x: any) => { + if (typeof x === "bigint") { + return Number(x); + } + }); + const unclaimedPerContractRewards = perContractRewards.filter( + (_, index) => result[index] === 0 + ); + unclaimed = unclaimed.concat(unclaimedPerContractRewards); + } + } + } + setUnclaimedRewards(unclaimed); + })(); + }, [questId, eligibleRewards]); + + // this builds multicall for minting rewards + useEffect(() => { + const calldata: Call[] = []; + + // if the sequencer query failed, let's consider the eligible as unclaimed + const to_claim = + unclaimedRewards === undefined + ? ([] as EligibleReward[]).concat(...Object.values(eligibleRewards)) + : unclaimedRewards; + + to_claim.forEach((reward) => { + calldata.push({ + contractAddress: reward.nft_contract, + entrypoint: "mint", + calldata: [ + reward.token_id, + 0, + questId as string, + reward.task_id, + reward.sig[0], + reward.sig[1], + ], + }); + }); + + if (to_claim?.length > 0) { + setRewardsEnabled(true); + } else setRewardsEnabled(false); + setMintCalldata(calldata); + }, [questId, unclaimedRewards, eligibleRewards]); + + useEffect(() => { + if (!taskId || res === "true") return; + if (taskId && res === "false") { + setTaskError({ + taskId: parseInt(taskId.toString()), + res: false, + error: errorMsg?.toString(), + }); + } + }, [taskId, res, errorMsg]); + + const generateOAuthUrl = (task: UserTask): string => { + if (task.verify_endpoint_type === "oauth_discord") { + const rootUrl = "https://discord.com/api/oauth2/authorize"; + const options = { + redirect_uri: `${task.verify_endpoint}`, + client_id: process.env.NEXT_PUBLIC_DISCORD_CLIENT_ID as string, + response_type: "code", + scope: ["identify", "guilds"].join(" "), + state: hexToDecimal(address), + }; + const qs = new URLSearchParams(options).toString(); + return `${rootUrl}?${qs}`; + } else { + const codeChallenge = generateCodeChallenge( + process.env.NEXT_PUBLIC_TWITTER_CODE_VERIFIER as string + ); + const rootUrl = "https://twitter.com/i/oauth2/authorize"; + const options = { + redirect_uri: `${task.verify_endpoint}?addr=${hexToDecimal(address)}`, + client_id: process.env.NEXT_PUBLIC_TWITTER_CLIENT_ID as string, + state: "state", + response_type: "code", + code_challenge: codeChallenge, + code_challenge_method: "S256", + scope: ["follows.read", "tweet.read", "users.read"].join(" "), + }; + const qs = new URLSearchParams(options).toString(); + return `${rootUrl}?${qs}`; + } + }; + + return ( + <> + { + return { imgSrc: nft.img, level: nft.level }; + })} + /> + +
+ {quest.name === "loading" ? ( + + ) : ( +

{quest.name}

+ )} + {quest.desc === "loading" ? ( + + ) : ( +

{quest.desc}

+ )} +
+ {quest?.expiry_timestamp && quest?.expiry_timestamp !== "loading" ? ( + + ) : null} +
+ {tasks.length === 0 || quest.rewards_title === "loading" ? ( + + ) : ( + <> + {tasks.map((task) => { + return ( + refreshRewards(quest, address)} + wasVerified={task.completed} + hasError={Boolean(taskError && taskError.taskId === task.id)} + verifyError={ + taskError && taskError.taskId === task.id + ? taskError.error + : "" + } + setShowQuiz={setShowQuiz} + quizName={task.quiz_name || undefined} + issuer={{ + name: quest.issuer, + logoFavicon: quest.logo, + }} + /> + ); + })} + { + setRewardsEnabled(false); + }} + disabled={!rewardsEnabled} + mintCalldata={mintCalldata} + /> + + )} +
+ {showQuiz} + + ); +}; + +export default QuestMenu; diff --git a/components/quests/screenLayout.tsx b/components/quests/screenLayout.tsx new file mode 100644 index 00000000..dd16fdbc --- /dev/null +++ b/components/quests/screenLayout.tsx @@ -0,0 +1,32 @@ +import styles from "../../styles/Home.module.css"; +import { FunctionComponent, ReactNode } from "react"; +import BackButton from "../UI/backButton"; +import Footer from "../UI/footer"; +import Blur from "../shapes/blur"; + +type ScreenLayoutProps = { + children: ReactNode; + setShowMenu: (showMenu: boolean) => void; +}; + +const ScreenLayout: FunctionComponent = ({ + children, + setShowMenu, +}) => { + return ( +
+
+ +
+
+ setShowMenu(false)} /> +
+
{children}
+
+
+
+
+ ); +}; + +export default ScreenLayout; diff --git a/components/quests/task.tsx b/components/quests/task.tsx index 09c7e368..633ba0a2 100644 --- a/components/quests/task.tsx +++ b/components/quests/task.tsx @@ -4,12 +4,11 @@ import { CheckCircle as CheckCircleIcon, ErrorRounded as ErrorRoundedIcon, } from "@mui/icons-material"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight"; import Button from "../UI/button"; import { CircularProgress } from "@mui/material"; import { useAccount } from "@starknet-react/core"; import Quiz from "../quiz/quiz"; +import ArrowRightIcon from "../UI/iconsComponents/icons/arrowRightIcon"; const Task: FunctionComponent = ({ name, @@ -131,12 +130,10 @@ const Task: FunctionComponent = ({ className={styles.taskTitle} onClick={() => setIsClicked(!isClicked)} > -
- {isClicked ? ( - - ) : ( - - )} +
+
+ +

{name}

{isVerified ? ( diff --git a/components/quiz/quizControls.tsx b/components/quiz/quizControls.tsx index fa7293ee..f4214dc9 100644 --- a/components/quiz/quizControls.tsx +++ b/components/quiz/quizControls.tsx @@ -1,5 +1,6 @@ import React, { FunctionComponent } from "react"; import styles from "../../styles/components/quests/quiz.module.css"; +import BackButton from "../UI/backButton"; type QuizControlsProps = { setStep: (s: number) => void; @@ -12,21 +13,7 @@ const QuizControls: FunctionComponent = ({ }) => { return (
- + setStep(step - 1)} />
); diff --git a/components/shapes/blur.tsx b/components/shapes/blur.tsx new file mode 100644 index 00000000..a3268e66 --- /dev/null +++ b/components/shapes/blur.tsx @@ -0,0 +1,12 @@ +import React, { FunctionComponent } from "react"; +import styles from "../../styles/components/shapes.module.css"; + +type BlurProps = { + green?: boolean; +}; + +const Blur: FunctionComponent = ({ green = false }) => { + return
; +}; + +export default Blur; diff --git a/pages/index.tsx b/pages/index.tsx index 6266d669..245eca43 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,13 +1,15 @@ import React, { useEffect, useState } from "react"; import type { NextPage } from "next"; import styles from "../styles/Home.module.css"; -import Quest from "../components/quests/quest"; import FeaturedQuest from "../components/quests/featuredQuest"; -import QuestsSkeleton from "../components/skeletons/questsSkeleton"; import { useRouter } from "next/router"; import { QueryError, QuestDocument } from "../types/backTypes"; import HowToParticipate from "../components/pages/home/howToParticipate"; +import QuestCategories from "../components/pages/home/questCategories"; +import TrendingQuests from "../components/pages/home/trending"; +import Blur from "../components/shapes/blur"; +import HomeControls from "../components/pages/home/homeControls"; const Quests: NextPage = () => { const router = useRouter(); @@ -36,6 +38,10 @@ const Quests: NextPage = () => { return (
+ +
+ +
{ desc={featuredQuest?.desc} expiry={featuredQuest?.expiry_timestamp} /> -

Accomplish your Starknet Quests

-
- {quests ? ( - quests.map((quest) => { - return ( - router.push(`/quest/${quest.id}`)} - imgSrc={quest.img_card} - issuer={{ - name: quest.issuer, - logoFavicon: quest.logo, - }} - reward={quest.rewards_title} - /> - ); - }) - ) : ( - - )} + +
+
+
diff --git a/pages/quest/[questPage].tsx b/pages/quest/[questPage].tsx index d476802f..226a7857 100644 --- a/pages/quest/[questPage].tsx +++ b/pages/quest/[questPage].tsx @@ -1,43 +1,13 @@ import { NextPage } from "next"; -import React, { ReactNode, useEffect, useState } from "react"; +import QuestMenu from "../../components/quests/questMenu"; +import React, { useEffect, useState } from "react"; import homeStyles from "../../styles/Home.module.css"; import styles from "../../styles/quests.module.css"; -import NftDisplay from "../../components/quests/nftDisplay"; -import Task from "../../components/quests/task"; -import Reward from "../../components/quests/reward"; -import quests_nft_abi from "../../abi/quests_nft_abi.json"; -import { useAccount, useProvider } from "@starknet-react/core"; import { useRouter } from "next/router"; -import { hexToDecimal } from "../../utils/feltService"; -import { - NFTItem, - QueryError, - QuestDocument, - UserTask, -} from "../../types/backTypes"; -import { Call, Contract } from "starknet"; -import { Skeleton } from "@mui/material"; -import TasksSkeleton from "../../components/skeletons/tasksSkeleton"; +import { QueryError, QuestDocument } from "../../types/backTypes"; import RewardSkeleton from "../../components/skeletons/rewardSkeleton"; -import { generateCodeChallenge } from "../../utils/codeChallenge"; import ErrorScreen from "../../components/UI/screens/errorScreen"; -import Timer from "../../components/quests/timer"; - -const splitByNftContract = ( - rewards: EligibleReward[] -): Record => { - return rewards.reduce( - (acc: Record, reward: EligibleReward) => { - if (!acc[reward.nft_contract]) { - acc[reward.nft_contract] = []; - } - - acc[reward.nft_contract].push(reward); - return acc; - }, - {} - ); -}; +import NftIssuer from "../../components/quests/nftIssuer"; const QuestPage: NextPage = () => { const router = useRouter(); @@ -47,8 +17,6 @@ const QuestPage: NextPage = () => { res, error_msg: errorMsg, } = router.query; - const { address } = useAccount(); - const { provider } = useProvider(); const [quest, setQuest] = useState({ id: 0, name: "loading", @@ -66,18 +34,7 @@ const QuestPage: NextPage = () => { disabled: false, expiry_timestamp: "loading", }); - const [tasks, setTasks] = useState([]); - const [rewardsEnabled, setRewardsEnabled] = useState(false); - const [eligibleRewards, setEligibleRewards] = useState< - Record - >({}); - const [unclaimedRewards, setUnclaimedRewards] = useState< - EligibleReward[] | undefined - >(); - const [mintCalldata, setMintCalldata] = useState(); - const [taskError, setTaskError] = useState(); const [errorPageDisplay, setErrorPageDisplay] = useState(false); - const [showQuiz, setShowQuiz] = useState(); // this fetches quest data useEffect(() => { @@ -96,182 +53,6 @@ const QuestPage: NextPage = () => { }); }, [questId]); - // this fetches all tasks of this quest from db - useEffect(() => { - if (questId) { - // If a call was made with an address in the first second, the call with 0 address should be cancelled - let shouldFetchWithZeroAddress = true; - - // Set a 1-second timer to allow time for address loading - const timer = setTimeout(() => { - // If address isn't loaded after 1 second, make the API call with the zero address - if (shouldFetchWithZeroAddress) { - fetch( - `${process.env.NEXT_PUBLIC_API_LINK}/get_tasks?quest_id=${questId}&addr=0` - ) - .then((response) => response.json()) - .then((data: UserTask[] | QueryError) => { - if ((data as UserTask[]).length) setTasks(data as UserTask[]); - }); - } - }, 1000); - - // If the address is loaded before the 1-second timer, make the API call with the loaded address - if (address) { - shouldFetchWithZeroAddress = false; - clearTimeout(timer); - fetch( - `${ - process.env.NEXT_PUBLIC_API_LINK - }/get_tasks?quest_id=${questId}&addr=${hexToDecimal(address)}` - ) - .then((response) => response.json()) - .then((data: UserTask[] | QueryError) => { - if ((data as UserTask[]).length) setTasks(data as UserTask[]); - }); - } - - // Clear the timer when component unmounts or dependencies change to prevent memory leaks - return () => { - clearTimeout(timer); - }; - } - }, [questId, address]); - - const refreshRewards = ( - quest: QuestDocument, - address: string | undefined - ) => { - if (address && quest.rewards_endpoint) { - fetch(`${quest.rewards_endpoint}?addr=${hexToDecimal(address)}`) - .then((response) => response.json()) - .then((data) => { - if (data.rewards) { - setEligibleRewards(splitByNftContract(data.rewards)); - } - }); - } - }; - - // this fetches all rewards claimable by the user - useEffect(() => { - refreshRewards(quest, address); - }, [quest, address]); - - // this filters the claimable rewards to find only the unclaimed ones (on chain) - useEffect(() => { - (async () => { - let unclaimed: EligibleReward[] = []; - for (const contractAddr in eligibleRewards) { - const perContractRewards = eligibleRewards[contractAddr]; - const calldata = []; - for (const reward of perContractRewards) { - calldata.push({ - quest_id: questId as string, - task_id: reward.task_id.toString(), - user_addr: address as string, - }); - } - const contract = new Contract(quests_nft_abi, contractAddr, provider); - - const response = await contract.call("get_tasks_status", [calldata]); - if ( - typeof response === "object" && - response !== null && - !Array.isArray(response) - ) { - const status = response["status"]; - - if (Array.isArray(status)) { - const result = status.map((x: any) => { - if (typeof x === "bigint") { - return Number(x); - } - }); - const unclaimedPerContractRewards = perContractRewards.filter( - (_, index) => result[index] === 0 - ); - unclaimed = unclaimed.concat(unclaimedPerContractRewards); - } - } - } - setUnclaimedRewards(unclaimed); - })(); - }, [questId, eligibleRewards]); - - // this builds multicall for minting rewards - useEffect(() => { - const calldata: Call[] = []; - - // if the sequencer query failed, let's consider the eligible as unclaimed - const to_claim = - unclaimedRewards === undefined - ? ([] as EligibleReward[]).concat(...Object.values(eligibleRewards)) - : unclaimedRewards; - - to_claim.forEach((reward) => { - calldata.push({ - contractAddress: reward.nft_contract, - entrypoint: "mint", - calldata: [ - reward.token_id, - 0, - questId as string, - reward.task_id, - reward.sig[0], - reward.sig[1], - ], - }); - }); - - if (to_claim?.length > 0) { - setRewardsEnabled(true); - } else setRewardsEnabled(false); - setMintCalldata(calldata); - }, [questId, unclaimedRewards, eligibleRewards]); - - useEffect(() => { - if (!taskId || res === "true") return; - if (taskId && res === "false") { - setTaskError({ - taskId: parseInt(taskId.toString()), - res: false, - error: errorMsg?.toString(), - }); - } - }, [taskId, res, errorMsg]); - - const generateOAuthUrl = (task: UserTask): string => { - if (task.verify_endpoint_type === "oauth_discord") { - const rootUrl = "https://discord.com/api/oauth2/authorize"; - const options = { - redirect_uri: `${task.verify_endpoint}`, - client_id: process.env.NEXT_PUBLIC_DISCORD_CLIENT_ID as string, - response_type: "code", - scope: ["identify", "guilds"].join(" "), - state: hexToDecimal(address), - }; - const qs = new URLSearchParams(options).toString(); - return `${rootUrl}?${qs}`; - } else { - const codeChallenge = generateCodeChallenge( - process.env.NEXT_PUBLIC_TWITTER_CODE_VERIFIER as string - ); - const rootUrl = "https://twitter.com/i/oauth2/authorize"; - const options = { - redirect_uri: `${task.verify_endpoint}?addr=${hexToDecimal(address)}`, - client_id: process.env.NEXT_PUBLIC_TWITTER_CLIENT_ID as string, - state: "state", - response_type: "code", - code_challenge: codeChallenge, - code_challenge_method: "S256", - scope: ["follows.read", "tweet.read", "users.read"].join(" "), - }; - const qs = new URLSearchParams(options).toString(); - return `${rootUrl}?${qs}`; - } - }; - return errorPageDisplay ? ( { onClick={() => router.push("/")} /> ) : ( - <> -
-
- {quest.issuer === "loading" ? ( - - ) : ( - { - return { imgSrc: nft.img, level: nft.level }; - })} - /> - )} -
-
- {quest.name === "loading" ? ( - - ) : ( -

{quest.name}

- )} - {quest.desc === "loading" ? ( - - ) : ( -

{quest.desc}

- )} -
- {quest?.expiry_timestamp && quest?.expiry_timestamp !== "loading" ? ( - - ) : null} -
- {tasks.length === 0 || quest.rewards_title === "loading" ? ( - - ) : ( - <> - {tasks.map((task) => { - return ( - refreshRewards(quest, address)} - wasVerified={task.completed} - hasError={Boolean( - taskError && taskError.taskId === task.id - )} - verifyError={ - taskError && taskError.taskId === task.id - ? taskError.error - : "" - } - setShowQuiz={setShowQuiz} - quizName={task.quiz_name || undefined} - issuer={{ - name: quest.issuer, - logoFavicon: quest.logo, - }} - /> - ); - })} - { - setRewardsEnabled(false); - }} - disabled={!rewardsEnabled} - mintCalldata={mintCalldata} - /> - - )} -
+
+
+ {quest.issuer === "loading" ? ( + + ) : ( + + )}
- {showQuiz} - + +
); }; diff --git a/styles/Home.module.css b/styles/Home.module.css index dfff96c2..114cb5bf 100644 --- a/styles/Home.module.css +++ b/styles/Home.module.css @@ -18,17 +18,19 @@ align-items: center; flex-direction: column; text-align: center; - margin-top: 3rem; overflow: hidden; + width: 100%; + margin-bottom: 3rem; } .questContainer { flex-direction: row; flex-wrap: wrap; - padding: 2rem; gap: 2rem; max-width: 950px; margin-bottom: 3rem; + margin-top: 1rem; + margin-top: 3rem; } .title { @@ -42,18 +44,192 @@ color: transparent; } +.questCategories { + width: 100%; + max-width: 950px; + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +} + +.questCategory { + display: flex; + padding: 28px 40px; + align-items: center; + border-radius: 8px; + background: var(--menu-background); + width: 100%; + + /* Small Shadow */ + box-shadow: 0px 2px 30px 0px rgba(16, 16, 18, 0.06); + border: solid 1px transparent; + cursor: pointer; +} + +.questCategory:hover { + border: solid 1px var(--secondary500); +} + +.questCategory img { + width: 218px; + height: 218px; + border-radius: 8px; +} + +.categoryInfos { + width: 100%; + height: 218px; + text-transform: capitalize; + display: flex; + flex-direction: column; + text-align: left; +} + +.categoryInfos h2 { + font-family: "Sora-Bold"; + font-size: 1.8rem; + color: var(--secondary600); + margin-bottom: auto; +} + @media (min-width: 768px) and (max-width: 1024px) { .title { font-size: 2rem; } } +.blur1 { + position: absolute; + left: 0; + top: 0; +} + +.blur2 { + position: absolute; + right: 0; + z-index: -1; +} + +.categoryDetails { + position: fixed; + left: 0; + top: 0; + bottom: 0; + background-color: var(--background); + z-index: 10; + width: 100%; + padding-top: calc(12vh + 2em); + overflow-y: auto; + overflow-x: hidden; +} + +.categoryDetails .content { + width: 100%; + min-height: calc(100% - 13vh - 4em); + max-width: calc(100% - 126px); + display: block; + margin: 0 auto; + padding: 25px; +} + +.categoryDetails .footer { + width: 100%; +} + +.categoryDetails .backButton { + margin-left: 63px; +} + +.categoryDetails .blur { + position: absolute; + left: 0; + top: 0; + z-index: -1; +} + +.categoryDetails .questList { + display: flex; + gap: 1rem; + margin-top: 2rem; + flex-wrap: wrap; + display: flex; + justify-content: center; +} + +.controls { + display: none; + gap: 1rem; + margin: 1rem 0; +} + +.controls button[aria-selected="true"] { + background-color: white; + color: var(--background); + border-radius: 10px; + display: flex; + height: 32px; + padding: 0px 12px; + align-items: center; + gap: 8px; +} + @media (max-width: 768px) { .title { font-size: 2rem; } - .cardTitle { font-size: 1.5rem; } + .controls { + display: flex; + } + .questCategory { + width: calc(100% - 2rem); + flex-direction: column; + padding: 1rem; + position: relative; + margin-top: 1px; + } + .questCategory::before { + content: ""; + position: absolute; + top: -2px; + right: -2px; + left: -1px; + bottom: -1px; + background: linear-gradient( + 90deg, + #437aba 0%, + #59c2e8 45%, + #00ff77 60%, + #59c2e8 70%, + #437aba50 100% + ); + border-radius: 8px; + z-index: -1; + mask: linear-gradient(4deg, transparent 92%, black); + } + .categoryInfos { + width: 100%; + height: auto; + text-transform: capitalize; + display: flex; + flex-direction: column; + text-align: left; + } + .questCategory img { + width: 100%; + height: auto; + margin-top: 1rem; + } + .categoryInfos h2 { + margin-bottom: 0.8rem; + } + .categoryDetails .content { + max-width: calc(100% - 24px); + } + .categoryDetails .backButton { + margin-left: 24px; + } } diff --git a/styles/components/backButton.module.css b/styles/components/backButton.module.css new file mode 100644 index 00000000..456a4222 --- /dev/null +++ b/styles/components/backButton.module.css @@ -0,0 +1,10 @@ +.backButton { + display: flex; + align-items: center; + cursor: pointer; +} + +.backButton svg { + width: 16px; + margin-right: 8px; +} diff --git a/styles/components/footer.module.css b/styles/components/footer.module.css index 3b9a22b8..35ba981d 100644 --- a/styles/components/footer.module.css +++ b/styles/components/footer.module.css @@ -4,10 +4,10 @@ padding: 2rem 0; justify-content: center; align-items: center; - border-top: 1px solid rgba(225, 220, 234, 0.40); + border-top: 1px solid rgba(225, 220, 234, 0.4); max-width: calc(100% - 126px); padding: 48px 0; - margin: 0 auto; + margin: 2em auto 0 auto; color: var(--secondary); font-family: Sora; @@ -25,12 +25,14 @@ width: 100%; } -.content, .social { +.content, +.social { transition: opacity 0.3s; opacity: 0.7; } -.content:hover, .social:hover { +.content:hover, +.social:hover { transition: opacity 0.3s; opacity: 1; } diff --git a/styles/components/quests/quiz.module.css b/styles/components/quests/quiz.module.css index 7aaf4464..70a61937 100644 --- a/styles/components/quests/quiz.module.css +++ b/styles/components/quests/quiz.module.css @@ -50,11 +50,6 @@ align-items: center; } -.controls button svg { - width: 16px; - margin-right: 8px; -} - .menu { max-width: 600px; height: 100%; diff --git a/styles/components/shapes.module.css b/styles/components/shapes.module.css index b4b45c10..e1bd456d 100644 --- a/styles/components/shapes.module.css +++ b/styles/components/shapes.module.css @@ -57,3 +57,19 @@ .dot:first-child { margin-top: 0; } + +.blur { + width: 376px; + height: 220px; + flex-shrink: 0; + border-radius: 376px; + opacity: 0.18; + background: var(--primary-green, #5ce3fe); + filter: blur(100px); + z-index: -1; +} + +.blur.green { + background: var(--primary-green, #6affaf); + opacity: 0.14; +} diff --git a/styles/globals.css b/styles/globals.css index f1c356a0..52c11905 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -25,9 +25,10 @@ --tertiary: #5ce3fe; --background: #101012; --background600: #29282b; - --background500: #E1DCEA; + --background500: #e1dcea; --nimiq-ease: cubic-bezier(0.25, 0, 0, 1); --shapes: #66666f; + --menu-background: #1f1f25; } html, diff --git a/styles/profile.module.css b/styles/profile.module.css index 4854a88b..e8c549df 100644 --- a/styles/profile.module.css +++ b/styles/profile.module.css @@ -27,13 +27,13 @@ align-items: center; gap: 16px; border-radius: 8px; - background: var(--background600, #29282B); + background: var(--background600, #29282b); width: 250px; } .title { align-self: stretch; - color: var(--secondary, #F4FAFF); + color: var(--secondary, #f4faff); text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); font-family: Sora; font-size: 16px; @@ -47,7 +47,7 @@ height: 0.5px; align-self: stretch; opacity: 0.3; - background: var(--background500, #E1DCEA); + background: var(--background500, #e1dcea); } .nameCard { @@ -58,8 +58,9 @@ .profilePicture { border-radius: 36px; - border: 1px solid var(--secondary, #F4FAFF); - box-shadow: 0px 2.1599998474121094px 28.799999237060547px 0px rgba(191, 158, 123, 0.06); + border: 1px solid var(--secondary, #f4faff); + box-shadow: 0px 2.1599998474121094px 28.799999237060547px 0px + rgba(191, 158, 123, 0.06); width: 72px; height: 72px; overflow: hidden; @@ -77,7 +78,7 @@ .address { margin-left: 22px; - color: var(--secondary, #F4FAFF); + color: var(--secondary, #f4faff); font-family: Sora; font-size: 8px; font-style: normal; @@ -92,12 +93,12 @@ align-items: center; gap: 10px; border-radius: 8px; - background: #1F1F25; + background: var(--menu-background); background-blend-mode: overlay; } .memberSince > p { - color: var(--secondary, #F4FAFF); + color: var(--secondary, #f4faff); text-align: center; font-family: Sora; font-size: 8px; @@ -117,7 +118,7 @@ .trophy { border-radius: 3.429px; - background: #E6CD84; + background: #e6cd84; display: flex; width: 24px; height: 24px; diff --git a/styles/quests.module.css b/styles/quests.module.css index 7942b429..2790fc6e 100644 --- a/styles/quests.module.css +++ b/styles/quests.module.css @@ -1,15 +1,20 @@ /* Component quest on the /quests page */ .questCard { - width: 250px; - background-color: #fff; - border: 4px solid var(--background600); + width: 290px; border-radius: 8px; - padding: 0px; + padding: 24px; display: flex; flex-direction: column; - background-color: var(--background600); + background-color: var(--menu-background); cursor: pointer; + + align-self: flex-start; + border: solid 1px transparent; +} + +.questCard:hover { + border: solid 1px var(--secondary500); } .questImage { @@ -22,7 +27,7 @@ flex-direction: column; justify-content: flex-start; align-items: flex-start; - padding: 1rem; + margin-top: 1rem; } .questTitle { @@ -193,13 +198,13 @@ /* Auto layout */ display: flex; flex-direction: row; + flex-wrap: wrap; justify-content: space-between; align-items: center; padding: 24px; gap: 48px; width: 728px; - height: 104px; max-width: 90%; margin-top: 1.5rem; @@ -218,15 +223,15 @@ .featuredQuest { width: 950px; - height: 350px; + height: 448px; margin-bottom: 5rem; - background-color: var(--background600); + background: linear-gradient(var(--background600), transparent); border-radius: 8px; display: flex; flex-direction: row; justify-content: space-between; align-items: center; - padding: 0.4rem; + padding: 24px; gap: 0.5rem; position: relative; } @@ -238,7 +243,6 @@ align-items: flex-start; padding: 0.1rem; width: 100%; - margin-left: 3rem; } .featuredQuestImage { @@ -262,4 +266,16 @@ width: 100px; height: 38px; } + .featuredQuest { + flex-direction: column; + width: calc(100% - 2rem); + text-align: left; + margin: 3em 0; + height: fit-content; + } + .reward { + flex-direction: column; + gap: 1rem; + align-items: center; + } } diff --git a/types/frontTypes.d.ts b/types/frontTypes.d.ts index 45910c93..63087111 100644 --- a/types/frontTypes.d.ts +++ b/types/frontTypes.d.ts @@ -169,3 +169,9 @@ type BuildingsInfo = { level: number; pos?: THREE.Vector2; }; + +type QuestCategory = { + name: string; + img: string; + questNumber: number; +}; From 76949d63629953c2045900a76e94508044bbea68 Mon Sep 17 00:00:00 2001 From: nicolasito1411 <60229704+Marchand-Nicolas@users.noreply.github.com> Date: Sat, 16 Sep 2023 16:00:09 +0200 Subject: [PATCH 02/14] softer blurs --- styles/components/shapes.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/components/shapes.module.css b/styles/components/shapes.module.css index e1bd456d..b2b3c1d3 100644 --- a/styles/components/shapes.module.css +++ b/styles/components/shapes.module.css @@ -63,7 +63,7 @@ height: 220px; flex-shrink: 0; border-radius: 376px; - opacity: 0.18; + opacity: 0.16; background: var(--primary-green, #5ce3fe); filter: blur(100px); z-index: -1; @@ -71,5 +71,5 @@ .blur.green { background: var(--primary-green, #6affaf); - opacity: 0.14; + opacity: 0.1; } From 887bc2c1aad15d8b94d486a965dcbcee2bc8e3a2 Mon Sep 17 00:00:00 2001 From: nicolasito1411 <60229704+Marchand-Nicolas@users.noreply.github.com> Date: Sat, 16 Sep 2023 16:02:52 +0200 Subject: [PATCH 03/14] fixing type error --- components/quests/task.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/quests/task.tsx b/components/quests/task.tsx index 633ba0a2..8eda08a3 100644 --- a/components/quests/task.tsx +++ b/components/quests/task.tsx @@ -131,7 +131,7 @@ const Task: FunctionComponent = ({ onClick={() => setIsClicked(!isClicked)} >
-
+

{name}

From 0d67a7a02eda4d7252eaa16c007d96de11a78aeb Mon Sep 17 00:00:00 2001 From: nicolasito1411 <60229704+Marchand-Nicolas@users.noreply.github.com> Date: Sat, 16 Sep 2023 16:06:00 +0200 Subject: [PATCH 04/14] cleaning the code and fixing build --- components/UI/iconsComponents/icons/arrowRightIcon.tsx | 2 +- components/pages/home/homeControls.tsx | 2 +- components/pages/home/questCategories.tsx | 2 +- components/quests/nfts.tsx | 2 +- components/quests/questCategoryDetails.tsx | 2 +- components/quests/questDetails.tsx | 3 --- components/quests/questMenu.tsx | 2 -- 7 files changed, 5 insertions(+), 10 deletions(-) diff --git a/components/UI/iconsComponents/icons/arrowRightIcon.tsx b/components/UI/iconsComponents/icons/arrowRightIcon.tsx index 8ec3d6ed..d8d2ee67 100644 --- a/components/UI/iconsComponents/icons/arrowRightIcon.tsx +++ b/components/UI/iconsComponents/icons/arrowRightIcon.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent } from "react"; +import React, { FunctionComponent } from "react"; const ArrowRightIcon: FunctionComponent = ({ width = 24, diff --git a/components/pages/home/homeControls.tsx b/components/pages/home/homeControls.tsx index 02985ed2..de9686e8 100644 --- a/components/pages/home/homeControls.tsx +++ b/components/pages/home/homeControls.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent } from "react"; +import React, { FunctionComponent } from "react"; import styles from "../../../styles/Home.module.css"; const HomeControls: FunctionComponent = () => { diff --git a/components/pages/home/questCategories.tsx b/components/pages/home/questCategories.tsx index 15f46962..378affdc 100644 --- a/components/pages/home/questCategories.tsx +++ b/components/pages/home/questCategories.tsx @@ -17,7 +17,7 @@ const QuestCategories: FunctionComponent = ({ const res: { [key: string]: QuestCategory; } = {}; - for (let quest of quests) { + for (const quest of quests) { const key = quest.category as string; const value = res[key]; if (!value) { diff --git a/components/quests/nfts.tsx b/components/quests/nfts.tsx index e4045107..41c3f613 100644 --- a/components/quests/nfts.tsx +++ b/components/quests/nfts.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent } from "react"; +import React, { FunctionComponent } from "react"; import styles from "../../styles/quests.module.css"; type NftsProps = { diff --git a/components/quests/questCategoryDetails.tsx b/components/quests/questCategoryDetails.tsx index 5166aa18..c7245a75 100644 --- a/components/quests/questCategoryDetails.tsx +++ b/components/quests/questCategoryDetails.tsx @@ -47,7 +47,7 @@ const QuestCategoryDetails: FunctionComponent = ({ setMenu( setMenu(null)} + setShowMenu={() => setMenu(null)} /> ) } diff --git a/components/quests/questDetails.tsx b/components/quests/questDetails.tsx index 64792450..d7964f35 100644 --- a/components/quests/questDetails.tsx +++ b/components/quests/questDetails.tsx @@ -1,7 +1,6 @@ import React, { FunctionComponent, useEffect } from "react"; import { QuestDocument } from "../../types/backTypes"; import ScreenLayout from "./screenLayout"; -import { useRouter } from "next/router"; import QuestMenu from "./questMenu"; type QuestDetailsProps = { @@ -13,8 +12,6 @@ const QuestDetails: FunctionComponent = ({ quest, setShowMenu, }) => { - const router = useRouter(); - useEffect(() => { const documentBody = document.querySelector("body"); if (!documentBody) return; diff --git a/components/quests/questMenu.tsx b/components/quests/questMenu.tsx index d86fd200..52d0cd21 100644 --- a/components/quests/questMenu.tsx +++ b/components/quests/questMenu.tsx @@ -9,7 +9,6 @@ import Task from "../../components/quests/task"; import Reward from "../../components/quests/reward"; import quests_nft_abi from "../../abi/quests_nft_abi.json"; import { useAccount, useProvider } from "@starknet-react/core"; -import { useRouter } from "next/router"; import { hexToDecimal } from "../../utils/feltService"; import { NFTItem, @@ -53,7 +52,6 @@ const QuestMenu: FunctionComponent = ({ res, errorMsg, }) => { - const router = useRouter(); const { address } = useAccount(); const { provider } = useProvider(); const [tasks, setTasks] = useState([]); From dce50cff766c5da9bb417b176f629d16a91e750b Mon Sep 17 00:00:00 2001 From: nicolasito1411 <60229704+Marchand-Nicolas@users.noreply.github.com> Date: Sat, 16 Sep 2023 16:07:03 +0200 Subject: [PATCH 05/14] Fixing build --- components/quests/screenLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/quests/screenLayout.tsx b/components/quests/screenLayout.tsx index dd16fdbc..20b44104 100644 --- a/components/quests/screenLayout.tsx +++ b/components/quests/screenLayout.tsx @@ -1,5 +1,5 @@ import styles from "../../styles/Home.module.css"; -import { FunctionComponent, ReactNode } from "react"; +import React, { FunctionComponent, ReactNode } from "react"; import BackButton from "../UI/backButton"; import Footer from "../UI/footer"; import Blur from "../shapes/blur"; From 0cc007f65e6acd3aafa470dd8c1be1ac89c9dbec Mon Sep 17 00:00:00 2001 From: nicolasito1411 <60229704+Marchand-Nicolas@users.noreply.github.com> Date: Sun, 17 Sep 2023 10:25:52 +0200 Subject: [PATCH 06/14] cleaning the code --- components/quests/questCategory.tsx | 2 +- components/quests/questCategoryDetails.tsx | 52 ++++++++++------------ components/quests/questDetails.tsx | 10 ++--- components/quests/questMenu.tsx | 17 +------ components/quests/screenLayout.tsx | 6 +-- utils/rewards.ts | 15 +++++++ 6 files changed, 48 insertions(+), 54 deletions(-) create mode 100644 utils/rewards.ts diff --git a/components/quests/questCategory.tsx b/components/quests/questCategory.tsx index ea4d934d..c8c8f306 100644 --- a/components/quests/questCategory.tsx +++ b/components/quests/questCategory.tsx @@ -20,7 +20,7 @@ const QuestCategory: FunctionComponent = ({

{category.name} Quest

- {category.questNumber} quest{category.questNumber > 1 ? "s" : ""} + {category.questNumber} quest{category.questNumber > 1 ? "s" : null}

diff --git a/components/quests/questCategoryDetails.tsx b/components/quests/questCategoryDetails.tsx index c7245a75..f6a0dec2 100644 --- a/components/quests/questCategoryDetails.tsx +++ b/components/quests/questCategoryDetails.tsx @@ -19,7 +19,7 @@ const QuestCategoryDetails: FunctionComponent = ({ quests, setShowMenu, }) => { - const [menu, setMenu] = useState(null); + const [selectedIndex, setSelectedIndex] = useState(null); useEffect(() => { const documentBody = document.querySelector("body"); @@ -35,33 +35,29 @@ const QuestCategoryDetails: FunctionComponent = ({ }, []); return ( - - <> -

Onboarding quests

-
- {quests.map((quest) => ( - - setMenu( - setMenu(null)} - /> - ) - } - imgSrc={quest.img_card} - issuer={{ - name: quest.issuer, - logoFavicon: quest.logo, - }} - reward={quest.rewards_title} - /> - ))} -
- {menu} - + setShowMenu(false)}> +

Onboarding quests

+
+ {quests.map((quest, index) => ( + setSelectedIndex(index)} + imgSrc={quest.img_card} + issuer={{ + name: quest.issuer, + logoFavicon: quest.logo, + }} + reward={quest.rewards_title} + /> + ))} +
+ {selectedIndex !== null && ( + setSelectedIndex(null)} + /> + )}
); }; diff --git a/components/quests/questDetails.tsx b/components/quests/questDetails.tsx index d7964f35..47e6023e 100644 --- a/components/quests/questDetails.tsx +++ b/components/quests/questDetails.tsx @@ -5,12 +5,12 @@ import QuestMenu from "./questMenu"; type QuestDetailsProps = { quest: QuestDocument; - setShowMenu: (showMenu: boolean) => void; + close: () => void; }; const QuestDetails: FunctionComponent = ({ quest, - setShowMenu, + close, }) => { useEffect(() => { const documentBody = document.querySelector("body"); @@ -26,10 +26,8 @@ const QuestDetails: FunctionComponent = ({ }, []); return ( - - <> - - + + ); }; diff --git a/components/quests/questMenu.tsx b/components/quests/questMenu.tsx index 52d0cd21..e382f128 100644 --- a/components/quests/questMenu.tsx +++ b/components/quests/questMenu.tsx @@ -22,22 +22,7 @@ import TasksSkeleton from "../../components/skeletons/tasksSkeleton"; import { generateCodeChallenge } from "../../utils/codeChallenge"; import Timer from "../../components/quests/timer"; import Nfts from "./nfts"; - -const splitByNftContract = ( - rewards: EligibleReward[] -): Record => { - return rewards.reduce( - (acc: Record, reward: EligibleReward) => { - if (!acc[reward.nft_contract]) { - acc[reward.nft_contract] = []; - } - - acc[reward.nft_contract].push(reward); - return acc; - }, - {} - ); -}; +import { splitByNftContract } from "../../utils/rewards"; type QuestMenuProps = { quest: QuestDocument; diff --git a/components/quests/screenLayout.tsx b/components/quests/screenLayout.tsx index 20b44104..7ed01686 100644 --- a/components/quests/screenLayout.tsx +++ b/components/quests/screenLayout.tsx @@ -6,12 +6,12 @@ import Blur from "../shapes/blur"; type ScreenLayoutProps = { children: ReactNode; - setShowMenu: (showMenu: boolean) => void; + close: () => void; }; const ScreenLayout: FunctionComponent = ({ children, - setShowMenu, + close, }) => { return (
@@ -19,7 +19,7 @@ const ScreenLayout: FunctionComponent = ({
- setShowMenu(false)} /> + close()} />
{children}
diff --git a/utils/rewards.ts b/utils/rewards.ts new file mode 100644 index 00000000..84000469 --- /dev/null +++ b/utils/rewards.ts @@ -0,0 +1,15 @@ +export const splitByNftContract = ( + rewards: EligibleReward[] +): Record => { + return rewards.reduce( + (acc: Record, reward: EligibleReward) => { + if (!acc[reward.nft_contract]) { + acc[reward.nft_contract] = []; + } + + acc[reward.nft_contract].push(reward); + return acc; + }, + {} + ); +}; From b4b43b0434cc8167cd05296af764a28965e1456b Mon Sep 17 00:00:00 2001 From: nicolasito1411 <60229704+Marchand-Nicolas@users.noreply.github.com> Date: Sun, 17 Sep 2023 10:32:25 +0200 Subject: [PATCH 07/14] fixing footer on quizzes --- components/quiz/quiz.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components/quiz/quiz.tsx b/components/quiz/quiz.tsx index a85b1ae5..2e4d8cbf 100644 --- a/components/quiz/quiz.tsx +++ b/components/quiz/quiz.tsx @@ -37,12 +37,14 @@ const Quiz: FunctionComponent = ({ useEffect(() => { const documentBody = document.querySelector("body"); - if (!documentBody) return; + const footer = document.querySelector("footer"); // Mount - documentBody.style.overflow = "hidden"; + if (documentBody) documentBody.style.overflow = "hidden"; + if (footer) footer.style.display = "none"; // Unmount return () => { - documentBody.style.removeProperty("overflow"); + if (documentBody) documentBody.style.removeProperty("overflow"); + if (footer) footer.style.removeProperty("display"); }; }, []); From 633331b544f2aea56bd756c8d32057c192d83904 Mon Sep 17 00:00:00 2001 From: nicolasito1411 <60229704+Marchand-Nicolas@users.noreply.github.com> Date: Sun, 17 Sep 2023 10:58:32 +0200 Subject: [PATCH 08/14] renaming NftImage component --- components/quests/nftDisplay.tsx | 4 ++-- components/quests/{nfts.tsx => nftImage.tsx} | 6 +++--- components/quests/questMenu.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename components/quests/{nfts.tsx => nftImage.tsx} (82%) diff --git a/components/quests/nftDisplay.tsx b/components/quests/nftDisplay.tsx index c569afa3..878701bf 100644 --- a/components/quests/nftDisplay.tsx +++ b/components/quests/nftDisplay.tsx @@ -1,6 +1,6 @@ import React, { FunctionComponent } from "react"; import NftIssuer from "./nftIssuer"; -import Nfts from "./nfts"; +import NftImage from "./nftImage"; type NftDisplayProps = { nfts: Nft[]; @@ -11,7 +11,7 @@ const NftDisplay: FunctionComponent = ({ nfts, issuer }) => { return (
- +
); }; diff --git a/components/quests/nfts.tsx b/components/quests/nftImage.tsx similarity index 82% rename from components/quests/nfts.tsx rename to components/quests/nftImage.tsx index 41c3f613..c4006b4d 100644 --- a/components/quests/nfts.tsx +++ b/components/quests/nftImage.tsx @@ -1,11 +1,11 @@ import React, { FunctionComponent } from "react"; import styles from "../../styles/quests.module.css"; -type NftsProps = { +type NftImageProps = { nfts: Nft[]; }; -const Nfts: FunctionComponent = ({ nfts }) => { +const NftImage: FunctionComponent = ({ nfts }) => { return (
{nfts.map((nft, index) => ( @@ -20,4 +20,4 @@ const Nfts: FunctionComponent = ({ nfts }) => { ); }; -export default Nfts; +export default NftImage; diff --git a/components/quests/questMenu.tsx b/components/quests/questMenu.tsx index e382f128..57c6ed65 100644 --- a/components/quests/questMenu.tsx +++ b/components/quests/questMenu.tsx @@ -21,7 +21,7 @@ import { Skeleton } from "@mui/material"; import TasksSkeleton from "../../components/skeletons/tasksSkeleton"; import { generateCodeChallenge } from "../../utils/codeChallenge"; import Timer from "../../components/quests/timer"; -import Nfts from "./nfts"; +import NftImage from "./nftImage"; import { splitByNftContract } from "../../utils/rewards"; type QuestMenuProps = { @@ -230,7 +230,7 @@ const QuestMenu: FunctionComponent = ({ return ( <> - { return { imgSrc: nft.img, level: nft.level }; })} From 7dbaafc212f0f948744b4ec48c6ab1d57de83ca6 Mon Sep 17 00:00:00 2001 From: nicolasito1411 <60229704+Marchand-Nicolas@users.noreply.github.com> Date: Sun, 17 Sep 2023 11:32:11 +0200 Subject: [PATCH 09/14] using a context to store quests --- components/quests/questCategoryDetails.tsx | 21 +++------ context/QuestsProvider.tsx | 52 ++++++++++++++++++++++ pages/_app.tsx | 9 ++-- pages/index.tsx | 26 ++--------- pages/quest/[questPage].tsx | 4 ++ styles/Home.module.css | 4 +- 6 files changed, 73 insertions(+), 43 deletions(-) create mode 100644 context/QuestsProvider.tsx diff --git a/components/quests/questCategoryDetails.tsx b/components/quests/questCategoryDetails.tsx index f6a0dec2..b1916dae 100644 --- a/components/quests/questCategoryDetails.tsx +++ b/components/quests/questCategoryDetails.tsx @@ -1,14 +1,9 @@ -import React, { - FunctionComponent, - ReactNode, - useEffect, - useState, -} from "react"; +import React, { FunctionComponent, useEffect } from "react"; import styles from "../../styles/Home.module.css"; import { QuestDocument } from "../../types/backTypes"; import ScreenLayout from "./screenLayout"; import Quest from "./quest"; -import QuestDetails from "./questDetails"; +import { useRouter } from "next/router"; type QuestCategoryDetailsProps = { quests: QuestDocument[]; @@ -19,7 +14,7 @@ const QuestCategoryDetails: FunctionComponent = ({ quests, setShowMenu, }) => { - const [selectedIndex, setSelectedIndex] = useState(null); + const router = useRouter(); useEffect(() => { const documentBody = document.querySelector("body"); @@ -38,11 +33,11 @@ const QuestCategoryDetails: FunctionComponent = ({ setShowMenu(false)}>

Onboarding quests

- {quests.map((quest, index) => ( + {quests.map((quest) => ( setSelectedIndex(index)} + onClick={() => router.push(`/quest/${quest.id}`)} imgSrc={quest.img_card} issuer={{ name: quest.issuer, @@ -52,12 +47,6 @@ const QuestCategoryDetails: FunctionComponent = ({ /> ))}
- {selectedIndex !== null && ( - setSelectedIndex(null)} - /> - )}
); }; diff --git a/context/QuestsProvider.tsx b/context/QuestsProvider.tsx new file mode 100644 index 00000000..88572ca1 --- /dev/null +++ b/context/QuestsProvider.tsx @@ -0,0 +1,52 @@ +import { ReactNode, createContext, useMemo, useState } from "react"; +import { QueryError, QuestDocument } from "../types/backTypes"; + +interface QuestsConfig { + quests: QuestDocument[]; + featuredQuest?: QuestDocument; +} + +export const QuestsContext = createContext({ + quests: [], + featuredQuest: undefined, +}); + +export const QuestsContextProvider = ({ + children, +}: { + children: ReactNode; +}) => { + const [quests, setQuests] = useState([]); + const [featuredQuest, setFeaturedQuest] = useState< + QuestDocument | undefined + >(); + + useMemo(() => { + fetch(`${process.env.NEXT_PUBLIC_API_LINK}/get_quests`) + .then((response) => response.json()) + .then((data: QuestDocument[] | QueryError) => { + if (!(data as QueryError).error) { + setQuests(data as QuestDocument[]); + const activeQuests = data as QuestDocument[]; + setFeaturedQuest( + activeQuests.length >= 1 + ? activeQuests[activeQuests.length - 1] + : undefined + ); + } + }); + }, []); + + const contextValues = useMemo(() => { + return { + quests, + featuredQuest, + }; + }, [quests, featuredQuest]); + + return ( + + {children} + + ); +}; diff --git a/pages/_app.tsx b/pages/_app.tsx index 67ca0b14..701e6be5 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -9,6 +9,7 @@ import { Analytics } from "@vercel/analytics/react"; import { StarknetIdJsProvider } from "../context/StarknetIdJsProvider"; import { createTheme } from "@mui/material/styles"; import Footer from "../components/UI/footer"; +import { QuestsContextProvider } from "../context/QuestsProvider"; function MyApp({ Component, pageProps }: AppProps) { const connectors = [ @@ -44,9 +45,11 @@ function MyApp({ Component, pageProps }: AppProps) { content="width=device-width, initial-scale=1" /> - - -