From 214b06a262f6275bef7cb44eb02d2fb99f6277ae Mon Sep 17 00:00:00 2001 From: ayushtom Date: Tue, 7 Nov 2023 06:34:04 +0530 Subject: [PATCH 01/10] dev: add base --- components/leaderboard/searchbar.tsx | 92 +++++++++++++++++--- hooks/useDebounce.tsx | 15 ++++ pages/leaderboard.tsx | 121 +++++++++++++++++++-------- public/icons/cross.svg | 4 + services/apiService.ts | 2 +- styles/leaderboard.module.css | 50 ++++++++++- 6 files changed, 235 insertions(+), 49 deletions(-) create mode 100644 hooks/useDebounce.tsx create mode 100644 public/icons/cross.svg diff --git a/components/leaderboard/searchbar.tsx b/components/leaderboard/searchbar.tsx index 66ce0269..f89fab5b 100644 --- a/components/leaderboard/searchbar.tsx +++ b/components/leaderboard/searchbar.tsx @@ -1,19 +1,87 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import SearchIcon from "../../public/icons/searchIcon.svg"; import Image from "next/image"; +import styles from "../../styles/leaderboard.module.css"; +import CrossIcon from "../../public/icons/cross.svg"; -export default function Searchbar({ handleSearch, value, onKeyDown }) { +export default function Searchbar(props: { + handleChange: (_: string) => void; + value: string; + onKeyDown: (e: React.KeyboardEvent) => void; + suggestions: string[]; + handleSuggestionClick: (_: string) => void; +}) { + const { handleChange, value, onKeyDown, suggestions, handleSuggestionClick } = + props; + const [showSuggestions, setShowSuggestions] = useState( + suggestions?.length > 0 + ); + + useEffect(() => { + const handleOutsideClick = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if ( + target.className !== styles.search_bar_container && + target.className !== styles.search_bar && + target.className !== styles.search_bar_suggestions + ) { + setShowSuggestions(false); + } else { + setShowSuggestions(true); + } + }; + + document.addEventListener("click", handleOutsideClick); + + return () => { + document.removeEventListener("click", handleOutsideClick); + }; + }, []); + + useEffect(() => { + console.log({ suggestions, showSuggestions }); + }, [suggestions, showSuggestions]); return ( -
- - handleSearch(e.target.value)} - className="bg-transparent outline-none ml-2 w-full" - placeholder="Search" - style={{ fontSize: 14 }} - onKeyDown={onKeyDown} - /> +
+
+ + handleChange(e.target.value)} + className={styles.search_bar} + placeholder="Search" + style={{ fontSize: 14 }} + onKeyDown={onKeyDown} + /> + {value.length > 0 ? ( +
{ + handleChange(""); + handleSuggestionClick(""); + }} + > + +
+ ) : null} +
+ {showSuggestions && suggestions?.length > 0 ? ( +
+ {suggestions.map((suggestion, index) => ( +
+ handleSuggestionClick( + "804388756904569972460955916013815525033312120440152538849502850576260523679" + ) + } + > +

{suggestion}

+
+ ))} +
+ ) : null}
); } diff --git a/hooks/useDebounce.tsx b/hooks/useDebounce.tsx new file mode 100644 index 00000000..2b32a749 --- /dev/null +++ b/hooks/useDebounce.tsx @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; + +export function useDebounce(value: T, delay?: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebouncedValue(value), delay || 500); + + return () => { + clearTimeout(timer); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx index 40af0eba..b5c409eb 100644 --- a/pages/leaderboard.tsx +++ b/pages/leaderboard.tsx @@ -25,6 +25,7 @@ import RankingSkeleton from "../components/skeletons/rankingSkeleton"; import { minifyAddress } from "../utils/stringService"; import { getDomainFromAddress } from "../utils/domainService"; import { decimalToHex } from "../utils/feltService"; +import { useDebounce } from "../hooks/useDebounce"; // declare types type RankingData = { @@ -57,6 +58,18 @@ type FormattedRankingProps = { completedQuests?: number; }[]; +const sampleDomains = ["ayush.stark", "ayushtom.stark"]; + +// Simulated API function +function verifyDomain(domain: string, delay = 1000) { + return new Promise((resolve) => { + setTimeout(() => { + const res = sampleDomains.includes(domain); + resolve(res); + }, delay); + }); +} + // used to map the time frame to the api call const timeFrameMap = { "Last 7 Days": "weekly", @@ -64,6 +77,9 @@ const timeFrameMap = { "All time": "all_time", }; +// page size limit +const PAGE_SIZE = [10, 15, 20]; + // show leaderboard ranking table const Rankings = (props: { data: { @@ -72,6 +88,8 @@ const Rankings = (props: { }; paginationLoading: boolean; setPaginationLoading: (_: boolean) => void; + userAddress: string; + searchedAddress: string; }) => { // used to format the data to be displayed const [displayData, setDisplayData] = useState([]); @@ -81,7 +99,13 @@ const Rankings = (props: { return num > 9 ? num : `0${num}`; }; - const { data, setPaginationLoading, paginationLoading } = props; + const { + data, + setPaginationLoading, + paginationLoading, + userAddress, + searchedAddress, + } = props; // this will run whenever the rankings are fetched and the data is updated useEffect(() => { @@ -98,13 +122,13 @@ const Rankings = (props: { item.completedQuests = completedQuestsResponse?.length; // get the domain name from the address - const hexAddress = decimalToHex(item.address); - const domainName = await getDomainFromAddress(hexAddress); - if (domainName.length > 0) { - item.address = domainName; - } else { - item.address = minifyAddress(hexAddress); - } + // const hexAddress = decimalToHex(item.address); + // const domainName = await getDomainFromAddress(hexAddress); + // if (domainName.length > 0) { + // item.address = domainName; + // } else { + // item.address = minifyAddress(hexAddress); + // } }) ); setDisplayData(res); @@ -121,7 +145,13 @@ const Rankings = (props: { displayData?.map((item, index) => (
{showMenu ? (
- - - + {PAGE_SIZE.map((item, index) => ( + + ))}
) : null}
@@ -254,10 +275,13 @@ export default function Leaderboard() { const [duration, setDuration] = useState("Last 7 Days"); const [userPercentile, setUserPercentile] = useState(100); const [searchQuery, setSearchQuery] = useState(""); - const [searchAddress, setSearchAddress] = useState(""); + const searchAddress = useDebounce(searchQuery, 200); + const [currentSearchedAddress, setCurrentSearchedAddress] = + useState(""); const [rowsPerPage, setRowsPerPage] = useState(10); const [currentPage, setCurrentPage] = useState(0); const [loading, setLoading] = useState(false); + const [searchResults, setSearchResults] = useState([]); const [paginationLoading, setPaginationLoading] = useState(false); const [ranking, setRanking] = useState({ first_elt_position: 0, @@ -281,19 +305,36 @@ export default function Leaderboard() { const address = userAddress; + // on user selecting duration const handleChangeSelection = (title: string) => { setDuration(title); }; // on user typing - const handleSearch = (query: string) => { + const handleChange = (query: string) => { setSearchQuery(query); }; + useEffect(() => { + const checkIfValidAddress = async (address: string) => { + const res = await verifyDomain(address); + const suggestions = []; + if (res) { + suggestions.push(address); + setSearchResults(suggestions); + } + }; + + if (searchAddress.length > 0) { + checkIfValidAddress(searchAddress); + } + }, [searchAddress]); + // on user Press enter const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { - setSearchAddress(searchQuery); + setSearchQuery(searchQuery); + setCurrentSearchedAddress(searchQuery); } }; @@ -355,7 +396,8 @@ export default function Leaderboard() { useEffect(() => { if (!address) return; const requestBody = { - addr: searchAddress.length > 0 ? searchAddress : address, + addr: + currentSearchedAddress.length > 0 ? currentSearchedAddress : address, page_size: rowsPerPage, shift: currentPage, ...getTimeRange(), @@ -368,7 +410,7 @@ export default function Leaderboard() { }; fetchRankings(); - }, [rowsPerPage, currentPage, duration, searchAddress]); + }, [rowsPerPage, currentPage, duration, currentSearchedAddress]); // handle pagination with forward and backward direction as params const handlePagination = (type: string) => { @@ -434,7 +476,7 @@ export default function Leaderboard() { expiry={featuredQuest?.expiry_timestamp} />
-
+

Leaderboard

@@ -452,8 +494,13 @@ export default function Leaderboard() {
{ + setCurrentSearchedAddress(address); + setSearchResults([]); + }} />
@@ -473,12 +520,16 @@ export default function Leaderboard() {
) : null} + + {/* this will be if searched user is not present in leaderboard or server returns 500 */} {ranking ? ( <> + ) : (
@@ -495,7 +547,6 @@ export default function Leaderboard() {
)} -
{leaderboardToppers ? leaderboardToppers[ diff --git a/public/icons/cross.svg b/public/icons/cross.svg new file mode 100644 index 00000000..b4167182 --- /dev/null +++ b/public/icons/cross.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/services/apiService.ts b/services/apiService.ts index 57bc56ff..83d11f99 100644 --- a/services/apiService.ts +++ b/services/apiService.ts @@ -10,7 +10,7 @@ type LeaderboardRankingParams = { end_timestamp: number; }; -const baseurl = process.env.NEXT_PUBLIC_API_LINK; +const baseurl = "http://0.0.0.0:8080"; export const fetchLeaderboardToppers = async ( params: LeaderboardTopperParams diff --git a/styles/leaderboard.module.css b/styles/leaderboard.module.css index b2aff60a..1e375efd 100644 --- a/styles/leaderboard.module.css +++ b/styles/leaderboard.module.css @@ -87,7 +87,7 @@ .quests_text { color: var(--background-400, #E1DCEA); text-align: right; - font-size: 12px; + font-size: 10px; font-weight: 400; line-height: 16px; } @@ -97,4 +97,52 @@ .percentile_text_container { justify-content: center; } +} + + +.search_bar { + background-color: transparent; + outline: none; + margin-left: 0.5rem; + width: 100%; +} + +.search_bar:focus { + outline: red; +} + +.search_bar_container { + border: 1px solid transparent; + padding: 10px; + transition: border-color 0.3s; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: 0.5rem; + background-color: #101012; + width: 100%; + border-radius: 0.5rem; + z-index: 100; +} + +.search_bar_container:focus-within { + border-color: white; + /* Change the border color when an input is focused */ +} + +.search_bar_suggestions { + position: absolute; + height: auto; + bottom: 0.4rem; + transform: translateY(100%); + width: 100%; + background-color: #101012; + border-bottom-left-radius: 0.75rem; + border-bottom-right-radius: 0.75rem; + padding: 0.5rem 0.5rem; + display: flex; + flex-direction: column; + gap: 1rem; + border: 1px solid white; } \ No newline at end of file From caaf55d2b8ad3eb01601796f8a4f1d9e8342ade0 Mon Sep 17 00:00:00 2001 From: ayushtom Date: Tue, 7 Nov 2023 06:36:41 +0530 Subject: [PATCH 02/10] dev: add naming convention --- components/UI/RankCards.tsx | 22 +++++++++++++++++++--- pages/leaderboard.tsx | 14 +++++++------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/components/UI/RankCards.tsx b/components/UI/RankCards.tsx index 72a1d923..b22a33ad 100644 --- a/components/UI/RankCards.tsx +++ b/components/UI/RankCards.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import Divider from "./Divider"; import AchievementSilver from "../../public/icons/achievementSilver.svg"; import Image from "next/image"; @@ -7,8 +7,9 @@ import AchievementGold from "../../public/icons/achievementGold.svg"; import AchievementBronze from "../../public/icons/achievementBronze.svg"; import Trophy from "../../public/icons/trophy.svg"; import XpBadge from "../../public/icons/xpBadge.svg"; -import { useDisplayName } from "../../hooks/displayName.tsx"; import Avatar from "./avatar"; +import { decimalToHex } from "../../utils/feltService"; +import { getDomainFromAddress } from "../../utils/domainService"; type RankCardsProps = { name: string; @@ -25,6 +26,21 @@ const iconMap = { export default function RankCards(props: RankCardsProps) { const { name, experience, trophy, position } = props; + const [displayName, setDisplayName] = useState(""); + + useEffect(() => { + const getDisplayName = async () => { + const hexAddress = decimalToHex(name); + const domainName = await getDomainFromAddress(hexAddress); + if (domainName.length > 0) { + setDisplayName(domainName); + } else { + setDisplayName(minifyAddress(hexAddress)); + } + }; + + getDisplayName(); + }, [name]); return (
@@ -34,7 +50,7 @@ export default function RankCards(props: RankCardsProps) {
-
{minifyAddress(name)}
+
{displayName}
diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx index b5c409eb..877b7474 100644 --- a/pages/leaderboard.tsx +++ b/pages/leaderboard.tsx @@ -122,13 +122,13 @@ const Rankings = (props: { item.completedQuests = completedQuestsResponse?.length; // get the domain name from the address - // const hexAddress = decimalToHex(item.address); - // const domainName = await getDomainFromAddress(hexAddress); - // if (domainName.length > 0) { - // item.address = domainName; - // } else { - // item.address = minifyAddress(hexAddress); - // } + const hexAddress = decimalToHex(item.address); + const domainName = await getDomainFromAddress(hexAddress); + if (domainName.length > 0) { + item.address = domainName; + } else { + item.address = minifyAddress(hexAddress); + } }) ); setDisplayData(res); From f64a387b97c875e847fe0ab3c3d21d8cb5cd6f25 Mon Sep 17 00:00:00 2001 From: ayushtom Date: Tue, 7 Nov 2023 14:07:34 +0530 Subject: [PATCH 03/10] feat: add search bar functionality --- components/leaderboard/searchbar.tsx | 25 ++++--- pages/leaderboard.tsx | 100 ++++++++++++++++++--------- services/apiService.ts | 2 +- 3 files changed, 85 insertions(+), 42 deletions(-) diff --git a/components/leaderboard/searchbar.tsx b/components/leaderboard/searchbar.tsx index f89fab5b..816c1a67 100644 --- a/components/leaderboard/searchbar.tsx +++ b/components/leaderboard/searchbar.tsx @@ -1,8 +1,10 @@ -import React, { useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import SearchIcon from "../../public/icons/searchIcon.svg"; import Image from "next/image"; import styles from "../../styles/leaderboard.module.css"; import CrossIcon from "../../public/icons/cross.svg"; +import { StarknetIdJsContext } from "../../context/StarknetIdJsProvider"; +import { hexToDecimal } from "../../utils/feltService"; export default function Searchbar(props: { handleChange: (_: string) => void; @@ -17,6 +19,8 @@ export default function Searchbar(props: { suggestions?.length > 0 ); + const { starknetIdNavigator } = useContext(StarknetIdJsContext); + useEffect(() => { const handleOutsideClick = (e: MouseEvent) => { const target = e.target as HTMLElement; @@ -38,9 +42,16 @@ export default function Searchbar(props: { }; }, []); - useEffect(() => { - console.log({ suggestions, showSuggestions }); - }, [suggestions, showSuggestions]); + const handleOptionClick = async (option: string) => { + const addr = await starknetIdNavigator + ?.getAddressFromStarkName(option) + .catch((err) => { + return ""; + }); + if (!addr) return; + handleSuggestionClick(hexToDecimal(addr)); + }; + return (
@@ -71,11 +82,7 @@ export default function Searchbar(props: {
- handleSuggestionClick( - "804388756904569972460955916013815525033312120440152538849502850576260523679" - ) - } + onClick={() => handleOptionClick(suggestion)} >

{suggestion}

diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx index 877b7474..0064ca98 100644 --- a/pages/leaderboard.tsx +++ b/pages/leaderboard.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useContext, useEffect, useMemo, useState } from "react"; import ChipList from "../components/UI/ChipList"; import Divider from "../components/UI/Divider"; import RankCards from "../components/UI/RankCards"; @@ -22,10 +22,15 @@ import { QuestsContext } from "../context/QuestsProvider"; import { useRouter } from "next/router"; import Searchbar from "../components/leaderboard/searchbar"; import RankingSkeleton from "../components/skeletons/rankingSkeleton"; -import { minifyAddress } from "../utils/stringService"; +import { getDomainWithoutStark, minifyAddress } from "../utils/stringService"; import { getDomainFromAddress } from "../utils/domainService"; -import { decimalToHex } from "../utils/feltService"; +import { decimalToHex, hexToDecimal } from "../utils/feltService"; import { useDebounce } from "../hooks/useDebounce"; +import { Abi, Contract, Provider, starknetId } from "starknet"; +import naming_abi from "../abi/naming_abi.json"; +import { StarknetIdJsContext } from "../context/StarknetIdJsProvider"; +import { utils } from "starknetid.js"; +import { isStarkDomain } from "starknetid.js/packages/core/dist/utils"; // declare types type RankingData = { @@ -58,18 +63,6 @@ type FormattedRankingProps = { completedQuests?: number; }[]; -const sampleDomains = ["ayush.stark", "ayushtom.stark"]; - -// Simulated API function -function verifyDomain(domain: string, delay = 1000) { - return new Promise((resolve) => { - setTimeout(() => { - const res = sampleDomains.includes(domain); - resolve(res); - }, delay); - }); -} - // used to map the time frame to the api call const timeFrameMap = { "Last 7 Days": "weekly", @@ -282,7 +275,9 @@ export default function Leaderboard() { const [currentPage, setCurrentPage] = useState(0); const [loading, setLoading] = useState(false); const [searchResults, setSearchResults] = useState([]); + const { starknetIdNavigator } = useContext(StarknetIdJsContext); const [paginationLoading, setPaginationLoading] = useState(false); + const [showNoResultsFound, setShowNoResultsFound] = useState(true); const [ranking, setRanking] = useState({ first_elt_position: 0, ranking: [], @@ -303,7 +298,30 @@ export default function Leaderboard() { }, }); - const address = userAddress; + const contract = useMemo(() => { + return new Contract( + naming_abi as Abi, + process.env.NEXT_PUBLIC_NAMING_CONTRACT as string, + starknetIdNavigator?.provider as Provider + ); + }, [starknetIdNavigator?.provider]); + + // contract call to check if typed domain is taken + async function verifyDomain(domain: string): Promise<{ message: boolean }> { + if (!domain) return { message: false }; + const currentTimeStamp = new Date().getTime() / 1000; + const encoded = utils.encodeDomain(domain).map((elem) => elem.toString()); + const res = await contract?.call("domain_to_expiry", [encoded]); + if (Number(res?.["expiry" as keyof typeof res]) < currentTimeStamp) { + return { + message: false, + }; + } else { + return { + message: true, + }; + } + } // on user selecting duration const handleChangeSelection = (title: string) => { @@ -317,11 +335,20 @@ export default function Leaderboard() { useEffect(() => { const checkIfValidAddress = async (address: string) => { - const res = await verifyDomain(address); - const suggestions = []; - if (res) { - suggestions.push(address); - setSearchResults(suggestions); + try { + let domain = address; + if (isStarkDomain(address)) { + domain = getDomainWithoutStark(address); + } + const res: { message: boolean } = await verifyDomain(domain); + if (res.message) { + setSearchResults([domain.concat(".stark")]); + } else { + setSearchResults([]); + } + } catch (err) { + console.log(err); + setSearchResults([]); } }; @@ -342,17 +369,23 @@ export default function Leaderboard() { useEffect(() => { const makeCall = async () => { setLoading(true); + const address = + currentSearchedAddress.length > 0 + ? hexToDecimal(currentSearchedAddress) + : userAddress + ? hexToDecimal(userAddress) + : ""; const requestBody = { - addr: address ? address : "", - page_size: 10, - shift: 0, + addr: address, + page_size: rowsPerPage, + shift: currentPage, start_timestamp: new Date().setDate(new Date().getDate() - 7), end_timestamp: new Date().getTime(), }; const rankingData = await fetchLeaderboardRankings(requestBody); const topperData = await fetchLeaderboardToppers({ - addr: address ? address : "", + addr: address, }); setRanking(rankingData); @@ -361,7 +394,7 @@ export default function Leaderboard() { }; makeCall(); - }, []); + }, [currentSearchedAddress]); const getTimeRange = () => { switch (duration) { @@ -394,10 +427,13 @@ export default function Leaderboard() { duration changes, search address changes */ useEffect(() => { - if (!address) return; const requestBody = { addr: - currentSearchedAddress.length > 0 ? currentSearchedAddress : address, + currentSearchedAddress.length > 0 + ? hexToDecimal(currentSearchedAddress) + : userAddress + ? hexToDecimal(userAddress) + : "", page_size: rowsPerPage, shift: currentPage, ...getTimeRange(), @@ -410,7 +446,7 @@ export default function Leaderboard() { }; fetchRankings(); - }, [rowsPerPage, currentPage, duration, currentSearchedAddress]); + }, [rowsPerPage, currentPage, duration]); // handle pagination with forward and backward direction as params const handlePagination = (type: string) => { @@ -509,7 +545,7 @@ export default function Leaderboard() { {userPercentile >= 0 ? (

- {address === userAddress ? "You are" : "He is"} + {currentSearchedAddress.length > 0 ? "He is" : "You are "}

 better than {userPercentile}%  @@ -522,13 +558,13 @@ export default function Leaderboard() { {/* this will be if searched user is not present in leaderboard or server returns 500 */} - {ranking ? ( + {ranking || showNoResultsFound ? ( <> Date: Wed, 8 Nov 2023 05:48:59 +0530 Subject: [PATCH 04/10] chore: add css styling --- components/UI/ChipList.tsx | 5 +- .../{RankCards.tsx => RankCard.tsx} | 19 +- pages/leaderboard.tsx | 134 +++++---- styles/components/chiplist.module.css | 23 ++ styles/globals.css | 9 +- styles/leaderboard.module.css | 267 +++++++++++++++++- 6 files changed, 363 insertions(+), 94 deletions(-) rename components/leaderboard/{RankCards.tsx => RankCard.tsx} (80%) create mode 100644 styles/components/chiplist.module.css diff --git a/components/UI/ChipList.tsx b/components/UI/ChipList.tsx index f45b002a..53dceff9 100644 --- a/components/UI/ChipList.tsx +++ b/components/UI/ChipList.tsx @@ -1,4 +1,5 @@ import React, { FunctionComponent } from "react"; +import styles from "../../styles/components/chiplist.module.css"; type ChipProps = { selected: string; @@ -12,12 +13,12 @@ const ChipList: FunctionComponent = ({ tags, }) => { return ( -
+
{tags.map((tag, index) => (
handleChangeSelection(tag)} key={index} - className={"px-1 md:px-3 py-2 text-center cursor-pointer rounded"} + className={styles.each_chip} style={{ backgroundColor: tag === selected ? "white" : "inherit", }} diff --git a/components/leaderboard/RankCards.tsx b/components/leaderboard/RankCard.tsx similarity index 80% rename from components/leaderboard/RankCards.tsx rename to components/leaderboard/RankCard.tsx index b66e93dd..e3fc200a 100644 --- a/components/leaderboard/RankCards.tsx +++ b/components/leaderboard/RankCard.tsx @@ -1,5 +1,6 @@ import React, { FunctionComponent, useEffect, useState } from "react"; import AchievementSilver from "../../public/icons/achievementSilver.svg"; +import styles from "../../styles/leaderboard.module.css"; import Image from "next/image"; import { minifyAddress } from "../../utils/stringService"; import AchievementGold from "../../public/icons/achievementGold.svg"; @@ -11,7 +12,7 @@ import { decimalToHex } from "../../utils/feltService"; import { getDomainFromAddress } from "../../utils/domainService"; import Divider from "@mui/material/Divider"; -type RankCardsProps = { +type RankCardProps = { name: string; experience: number; trophy: number; @@ -24,7 +25,7 @@ const iconMap = { 3: AchievementBronze, }; -const RankCards: FunctionComponent = ({ +const RankCard: FunctionComponent = ({ name, experience, trophy, @@ -47,12 +48,12 @@ const RankCards: FunctionComponent = ({ }, [name]); return ( -
-
+
+
-
+
{displayName}
@@ -67,12 +68,12 @@ const RankCards: FunctionComponent = ({ }} /> -
-
+
+

{experience}

-
+

{trophy}

@@ -81,4 +82,4 @@ const RankCards: FunctionComponent = ({ ); }; -export default RankCards; +export default RankCard; diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx index 1d885aef..eb95f48c 100644 --- a/pages/leaderboard.tsx +++ b/pages/leaderboard.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useMemo, useState } from "react"; import ChipList from "../components/UI/ChipList"; -import RankCards from "../components/leaderboard/RankCards"; +import RankCard from "../components/leaderboard/RankCard"; import ChevronRight from "../public/icons/ChevronRightIcon.svg"; import ChevronLeft from "../public/icons/ChevronLeftIcon.svg"; import BottomArrow from "../public/icons/dropdownArrow.svg"; @@ -31,6 +31,7 @@ import { StarknetIdJsContext } from "../context/StarknetIdJsProvider"; import { utils } from "starknetid.js"; import { isStarkDomain } from "starknetid.js/packages/core/dist/utils"; import Divider from "@mui/material/Divider"; +import Blur from "../components/shapes/blur"; // declare types type RankingData = { @@ -61,6 +62,7 @@ type FormattedRankingProps = { xp: number; achievements: number; completedQuests?: number; + displayName?: string; }[]; // used to map the time frame to the api call @@ -118,9 +120,9 @@ const Rankings = (props: { const hexAddress = decimalToHex(item.address); const domainName = await getDomainFromAddress(hexAddress); if (domainName.length > 0) { - item.address = domainName; + item.displayName = domainName; } else { - item.address = minifyAddress(hexAddress); + item.displayName = minifyAddress(hexAddress); } }) ); @@ -131,14 +133,14 @@ const Rankings = (props: { }, [data]); return ( -
+
{paginationLoading ? ( ) : ( displayData?.map((item, index) => (
-
-
+
+

{addNumberPadding(data.first_elt_position + index)}

-
+
-

{item.address}

+

{item.displayName}

-
-
+
+

{item.xp}

@@ -195,20 +194,20 @@ const ControlsDashboard = (props: { } = props; const [showMenu, setShowMenu] = useState(false); return ( -
-
+
+

Rows per page

setShowMenu((prev) => !prev)} > -
+

{rowsPerPage}

{showMenu ? ( -
+
{PAGE_SIZE.map((item, index) => (
-
-
-
{ - if (ranking.first_elt_position == 1) return; - handlePagination("prev"); - }} - > - -
-
{ - if ( - ranking.first_elt_position + ranking.ranking.length >= - leaderboardToppers[ - timeFrameMap[ - duration as keyof typeof timeFrameMap - ] as keyof typeof leaderboardToppers - ]?.length - ) - return; - handlePagination("next"); - }} - > - -
+ +
+
{ + if (ranking.first_elt_position == 1) return; + handlePagination("prev"); + }} + > + +
+
{ + if ( + ranking.first_elt_position + ranking.ranking.length >= + leaderboardToppers[ + timeFrameMap[ + duration as keyof typeof timeFrameMap + ] as keyof typeof leaderboardToppers + ]?.length + ) + return; + handlePagination("next"); + }} + > +
@@ -277,7 +275,6 @@ export default function Leaderboard() { const [searchResults, setSearchResults] = useState([]); const { starknetIdNavigator } = useContext(StarknetIdJsContext); const [paginationLoading, setPaginationLoading] = useState(false); - const [showNoResultsFound, setShowNoResultsFound] = useState(true); const [ranking, setRanking] = useState({ first_elt_position: 0, ranking: [], @@ -333,6 +330,7 @@ export default function Leaderboard() { setSearchQuery(query); }; + // this will be called when the search address is debounced and updated and suggestions will be loaded useEffect(() => { const checkIfValidAddress = async (address: string) => { try { @@ -489,14 +487,21 @@ export default function Leaderboard() { }, [leaderboardToppers]); return ( -
+
{loading ? ( -
+
) : ( <> -
+
+
+ +
+
+ +
+
-
-
+
+

Leaderboard

-
+
- ) : ( -
- No results found! -
- )} + ) : null} -
+
{leaderboardToppers ? leaderboardToppers[ timeFrameMap[ duration as keyof typeof timeFrameMap ] as keyof typeof leaderboardToppers ]?.best_users?.map((item, index) => ( - Date: Sat, 11 Nov 2023 04:14:44 +0530 Subject: [PATCH 05/10] fix: search bar working --- components/leaderboard/searchbar.tsx | 19 +++++++---- pages/leaderboard.tsx | 50 +++++++++++++++++++--------- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/components/leaderboard/searchbar.tsx b/components/leaderboard/searchbar.tsx index ae43eae7..b0a8fbf8 100644 --- a/components/leaderboard/searchbar.tsx +++ b/components/leaderboard/searchbar.tsx @@ -1,4 +1,9 @@ -import React, { useContext, useEffect, useState,FunctionComponent } from "react"; +import React, { + useContext, + useEffect, + useState, + FunctionComponent, +} from "react"; import SearchIcon from "../../public/icons/searchIcon.svg"; import Image from "next/image"; import styles from "../../styles/leaderboard.module.css"; @@ -7,7 +12,7 @@ import { StarknetIdJsContext } from "../../context/StarknetIdJsProvider"; import { hexToDecimal } from "../../utils/feltService"; type SearchbarProps = { - handleChange: (_: string) => void; + handleChange: (_: string) => void; value: string; onKeyDown: (e: React.KeyboardEvent) => void; suggestions: string[]; @@ -15,10 +20,13 @@ type SearchbarProps = { }; const Searchbar: FunctionComponent = ({ - handleChange, value, onKeyDown, suggestions, handleSuggestionClick + handleChange, + value, + onKeyDown, + suggestions, + handleSuggestionClick, }) => { - - const [showSuggestions, setShowSuggestions] = useState( + const [showSuggestions, setShowSuggestions] = useState( suggestions?.length > 0 ); @@ -55,7 +63,6 @@ const Searchbar: FunctionComponent = ({ handleSuggestionClick(hexToDecimal(addr)); }; - return (
diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx index 0b650969..ca64a105 100644 --- a/pages/leaderboard.tsx +++ b/pages/leaderboard.tsx @@ -1,10 +1,6 @@ import React, { useContext, useEffect, useMemo, useState } from "react"; import ChipList from "../components/UI/ChipList"; import RankCard from "../components/leaderboard/RankCard"; -import ChevronRight from "../public/icons/ChevronRightIcon.svg"; -import ChevronLeft from "../public/icons/ChevronLeftIcon.svg"; -import BottomArrow from "../public/icons/dropdownArrow.svg"; -import Image from "next/image"; import { fetchLeaderboardRankings, fetchLeaderboardToppers, @@ -17,9 +13,7 @@ import FeaturedQuest from "../components/UI/featured_banner/featuredQuest"; import { QuestsContext } from "../context/QuestsProvider"; import { useRouter } from "next/router"; import Searchbar from "../components/leaderboard/searchbar"; -import { getDomainWithoutStark, minifyAddress } from "../utils/stringService"; -import { getDomainFromAddress } from "../utils/domainService"; -import { decimalToHex, hexToDecimal } from "../utils/feltService"; +import { getDomainWithoutStark } from "../utils/stringService"; import { useDebounce } from "../hooks/useDebounce"; import { Abi, Contract, Provider } from "starknet"; import naming_abi from "../abi/naming_abi.json"; @@ -44,8 +38,6 @@ export default function Leaderboard() { const searchAddress = useDebounce(searchQuery, 200); const [currentSearchedAddress, setCurrentSearchedAddress] = useState(""); - const [isCustomResult, setCustomResult] = useState(false); - const [rowsPerPage, setRowsPerPage] = useState(10); const [currentPage, setCurrentPage] = useState(0); const [loading, setLoading] = useState(false); @@ -58,6 +50,10 @@ export default function Leaderboard() { ranking: [], }); + // to check if current view is the default view or a user requested view(to prevent multiple api calls) + const [isCustomResult, setCustomResult] = useState(false); + + // set user address on wallet connect and disconnect useEffect(() => { if (address === "") return; if (address) setUserAddress(address); @@ -78,7 +74,7 @@ export default function Leaderboard() { const fetchLeaderboardToppersResult = async () => { const topperData = await fetchLeaderboardToppers({ - addr: status === "connected" ? hexToDecimal(address) : "", + addr: requestBody.addr, }); setLeaderboardToppers(topperData); }; @@ -135,6 +131,11 @@ export default function Leaderboard() { } } + // to reset the page shift when duration is changes or new address is searched + useEffect(() => { + setCurrentPage(0); + }, [duration, currentSearchedAddress]); + // on user selecting duration const handleChangeSelection = (title: string) => { setDuration(title); @@ -215,7 +216,7 @@ export default function Leaderboard() { const requestBody = { addr: currentSearchedAddress.length > 0 - ? hexToDecimal(currentSearchedAddress) + ? currentSearchedAddress : userAddress ? hexToDecimal(userAddress) : "", @@ -230,8 +231,22 @@ export default function Leaderboard() { setRanking(rankingData); }; + const fetchLeaderboard = async () => { + const topperData = await fetchLeaderboardToppers({ + addr: requestBody.addr, + }); + setLeaderboardToppers(topperData); + }; + + if (searchAddress.length > 0) fetchLeaderboard(); fetchRankings(); - }, [rowsPerPage, currentPage, duration, searchAddress, isCustomResult]); + }, [ + rowsPerPage, + currentPage, + duration, + currentSearchedAddress, + isCustomResult, + ]); // handle pagination with forward and backward direction as params const handlePagination = (type: string) => { @@ -272,7 +287,7 @@ export default function Leaderboard() { ]?.length ?? 0 ); setUserPercentile(res); - }, [leaderboardToppers]); + }, [leaderboardToppers, currentSearchedAddress]); return (
@@ -326,6 +341,7 @@ export default function Leaderboard() { handleSuggestionClick={(address) => { setCurrentSearchedAddress(address); setSearchResults([]); + setCustomResult(true); }} />
@@ -356,11 +372,13 @@ export default function Leaderboard() { <> 0 + ? currentSearchedAddress + : hexToDecimal(userAddress) + } paginationLoading={paginationLoading} setPaginationLoading={setPaginationLoading} - userAddress={userAddress ?? ""} - searchedAddress={currentSearchedAddress} /> Date: Sat, 11 Nov 2023 13:21:26 +0530 Subject: [PATCH 06/10] feat: add no results found --- pages/leaderboard.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx index ca64a105..87fc48c7 100644 --- a/pages/leaderboard.tsx +++ b/pages/leaderboard.tsx @@ -44,6 +44,7 @@ export default function Leaderboard() { const [searchResults, setSearchResults] = useState([]); const { starknetIdNavigator } = useContext(StarknetIdJsContext); const [paginationLoading, setPaginationLoading] = useState(false); + const [showNoresults, setShowNoresults] = useState(false); const [userAddress, setUserAddress] = useState(""); const [ranking, setRanking] = useState({ first_elt_position: 0, @@ -270,6 +271,7 @@ export default function Leaderboard() { ]?.position ) { setUserPercentile(-1); + setShowNoresults(true); return; } @@ -287,6 +289,7 @@ export default function Leaderboard() { ]?.length ?? 0 ); setUserPercentile(res); + setShowNoresults(false); }, [leaderboardToppers, currentSearchedAddress]); return ( @@ -368,7 +371,11 @@ export default function Leaderboard() { /> {/* this will be if searched user is not present in leaderboard or server returns 500 */} - {ranking ? ( + {showNoresults ? ( +
+

No Results Found!

+
+ ) : ranking ? ( <> Date: Sat, 11 Nov 2023 13:25:38 +0530 Subject: [PATCH 07/10] feat: handle empty query --- components/leaderboard/searchbar.tsx | 1 + pages/leaderboard.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/components/leaderboard/searchbar.tsx b/components/leaderboard/searchbar.tsx index b0a8fbf8..9bf39376 100644 --- a/components/leaderboard/searchbar.tsx +++ b/components/leaderboard/searchbar.tsx @@ -60,6 +60,7 @@ const Searchbar: FunctionComponent = ({ return ""; }); if (!addr) return; + handleChange(option); handleSuggestionClick(hexToDecimal(addr)); }; diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx index 87fc48c7..22beb044 100644 --- a/pages/leaderboard.tsx +++ b/pages/leaderboard.tsx @@ -336,7 +336,7 @@ export default function Leaderboard() { />
- Date: Sat, 11 Nov 2023 13:29:54 +0530 Subject: [PATCH 08/10] fix: first onload issue --- pages/leaderboard.tsx | 64 ++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx index 22beb044..fa898a28 100644 --- a/pages/leaderboard.tsx +++ b/pages/leaderboard.tsx @@ -336,7 +336,7 @@ export default function Leaderboard() { />
- {/* this will be if searched user is not present in leaderboard or server returns 500 */} - {showNoresults ? ( -
-

No Results Found!

-
- ) : ranking ? ( - <> - 0 - ? currentSearchedAddress - : hexToDecimal(userAddress) - } - paginationLoading={paginationLoading} - setPaginationLoading={setPaginationLoading} - /> - - - + {ranking ? ( + showNoresults ? ( +
+

No Results Found!

+
+ ) : ( + <> + 0 + ? currentSearchedAddress + : hexToDecimal(userAddress) + } + paginationLoading={paginationLoading} + setPaginationLoading={setPaginationLoading} + /> + + + + ) ) : null}
From 1d760c939b4bf5f6b2a8f26411dae047c4756240 Mon Sep 17 00:00:00 2001 From: ayushtom Date: Sat, 11 Nov 2023 14:44:15 +0530 Subject: [PATCH 09/10] fix: no results page --- pages/leaderboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx index fa898a28..e46673ef 100644 --- a/pages/leaderboard.tsx +++ b/pages/leaderboard.tsx @@ -271,7 +271,7 @@ export default function Leaderboard() { ]?.position ) { setUserPercentile(-1); - setShowNoresults(true); + if (currentSearchedAddress.length > 0) setShowNoresults(true); return; } From a258dd4cbbbec52f23c9055bd09b0d6a4eaf79a4 Mon Sep 17 00:00:00 2001 From: ayushtom Date: Sat, 11 Nov 2023 14:57:11 +0530 Subject: [PATCH 10/10] fix: update leaderboard search query --- pages/leaderboard.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx index e46673ef..a80de56d 100644 --- a/pages/leaderboard.tsx +++ b/pages/leaderboard.tsx @@ -271,7 +271,11 @@ export default function Leaderboard() { ]?.position ) { setUserPercentile(-1); - if (currentSearchedAddress.length > 0) setShowNoresults(true); + if (currentSearchedAddress.length > 0 && isCustomResult) + setShowNoresults(true); + else { + setShowNoresults(false); + } return; }