diff --git a/src/assets/styles/mobile/component-style/user/challengeItem.scss b/src/assets/styles/mobile/component-style/user/challengeItem.scss
index b36aaf2f..db43202c 100644
--- a/src/assets/styles/mobile/component-style/user/challengeItem.scss
+++ b/src/assets/styles/mobile/component-style/user/challengeItem.scss
@@ -6,6 +6,16 @@
border-width: .0625rem;
border-radius: .625rem;
transition: none;
+ .edit{
+ width: 1.75rem;
+ height: 1.75rem;
+ border-radius: .3125rem;
+ right: .3125rem;
+ top: .3125rem;
+ font-size: .625rem;
+ z-index: 1;
+ background-color: #fff;
+ }
.item-claimed, .item-claimable{
width: 2.8125rem;
height: 1.125rem;
diff --git a/src/assets/styles/mobile/view-style/explore.scss b/src/assets/styles/mobile/view-style/explore.scss
index d0096d88..0018ba24 100644
--- a/src/assets/styles/mobile/view-style/explore.scss
+++ b/src/assets/styles/mobile/view-style/explore.scss
@@ -17,7 +17,6 @@
}
// title
h3 {
- margin-bottom: 1.25rem;
font-size: 1.125rem;
font-weight: 600;
line-height: 1.5625rem;
@@ -34,6 +33,7 @@
// challenges
.challenges {
padding: 0;
+ padding-top: 1.25rem;
padding-bottom: 9.375rem;
gap: .625rem;
}
diff --git a/src/assets/styles/view-style/explore.scss b/src/assets/styles/view-style/explore.scss
index 9b202538..70163248 100644
--- a/src/assets/styles/view-style/explore.scss
+++ b/src/assets/styles/view-style/explore.scss
@@ -18,7 +18,6 @@
}
// title
h3 {
- margin-bottom: 80px;
font-size: 32px;
font-weight: 600;
line-height: 38px;
@@ -35,6 +34,7 @@
// challenges
.challenges {
padding: 0;
+ padding-top: 80px;
padding-bottom: 150px;
display: flex;
flex-wrap: wrap;
diff --git a/src/components/ChallengeItem/ChallengeItemIcons.jsx b/src/components/ChallengeItem/ChallengeItemIcons.jsx
new file mode 100644
index 00000000..dd900147
--- /dev/null
+++ b/src/components/ChallengeItem/ChallengeItemIcons.jsx
@@ -0,0 +1,101 @@
+import { Tooltip } from "antd";
+import {
+ CONTRACT_ADDR_1155,
+ CONTRACT_ADDR_1155_TESTNET,
+ CONTRACT_ADDR_721,
+ CONTRACT_ADDR_721_TESTNET,
+} from "@/config";
+import { useAddress } from "@/hooks/useAddress";
+import { constans } from "@/utils/constans";
+
+/*
+hasNft: Boolean
+hasVc: Boolean
+info: {
+ nft_address,
+ badge_chain_id,
+ badge_token_id,
+ tokenId
+}
+*/
+
+export default function ChallengeItemIcons({ hasNft, hasVc, info }) {
+ const isDev = process.env.REACT_APP_IS_DEV;
+ const contract721 = isDev ? CONTRACT_ADDR_721_TESTNET : CONTRACT_ADDR_721;
+ const contract1155 = isDev
+ ? CONTRACT_ADDR_1155_TESTNET
+ : CONTRACT_ADDR_1155;
+ const { openseaLink, openseaSolanaLink } = constans();
+ const { walletType } = useAddress();
+
+ function toOpensea(event) {
+ event.stopPropagation();
+ const { nft_address, badge_chain_id, badge_token_id, tokenId } = info;
+ let evmLink = openseaLink;
+ const solanaLink = `${openseaSolanaLink}/${nft_address}`;
+ if (!badge_token_id) {
+ evmLink = `${evmLink}/${isDev ? "mumbai" : "matic"}/${contract1155.Badge}/${tokenId}`;
+ } else {
+ const chainAddr = contract721[badge_chain_id];
+ evmLink = `${evmLink}/${chainAddr.opensea}/${chainAddr.Badge}/${badge_token_id}`;
+ }
+ window.open(walletType === "evm" ? evmLink : solanaLink, "_blank");
+ }
+
+ return (
+
+ {hasNft && (
+ <>
+ {/* 链 */}
+
event.stopPropagation()}
+ >
+
+
+ {/* opensea */}
+
+
+
+ >
+ )}
+ {hasVc && (
+ // zk
+
+ {
+ event.stopPropagation();
+ // showZk({address: profile.address, token_id: info.tokenId})
+ }}
+ >
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/ChallengeItem/ChallengeItemImg.jsx b/src/components/ChallengeItem/ChallengeItemImg.jsx
new file mode 100644
index 00000000..572da0e1
--- /dev/null
+++ b/src/components/ChallengeItem/ChallengeItemImg.jsx
@@ -0,0 +1,43 @@
+import { LazyLoadImage } from "react-lazy-load-image-component";
+import ChallengeItemIcons from "./ChallengeItemIcons";
+
+
+
+export default function ChallengeItemImg({img, claimedInfo, questNum}) {
+
+ return (
+
+
+
+
+ {/* 阴影文本: ERC-721展示 */}
+ {
+ claimedInfo?.info.version === "2" && !claimedInfo?.hasNft &&
+
+
{claimedInfo?.info.title}
+
+ }
+ {/* 挑战集合信息 */}
+ {
+ questNum &&
+
+
+
+
+
+
+
{questNum}
+
+
+ }
+ {
+ claimedInfo &&
+
+ }
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/ChallengeItem/ChallengeItemStatus.jsx b/src/components/ChallengeItem/ChallengeItemStatus.jsx
new file mode 100644
index 00000000..13f9240d
--- /dev/null
+++ b/src/components/ChallengeItem/ChallengeItemStatus.jsx
@@ -0,0 +1,32 @@
+import { useTranslation } from "react-i18next";
+
+/**
+ *
+ * @param {Boolean} claimable - 可领取状态
+ * @param {Boolean} claimed - 已领取状态
+ * @param {Boolean} review - 待打分状态
+ * @returns
+ */
+
+export default function ChallengeItemStatus({
+ claimable,
+ claimed,
+ review,
+}) {
+ const { t } = useTranslation(["profile"]);
+
+ return (
+ <>
+ {claimable && {t("claimble")}
}
+ {claimed && {t("explore:pass")}
}
+ {review && (
+
+ {t("explore:review")}
+
+ )}
+ >
+ );
+}
diff --git a/src/components/ChallengeItem/ChallengeItems.jsx b/src/components/ChallengeItem/ChallengeItems.jsx
new file mode 100644
index 00000000..6e445f37
--- /dev/null
+++ b/src/components/ChallengeItem/ChallengeItems.jsx
@@ -0,0 +1,117 @@
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useUpdateEffect } from "ahooks";
+import { useQuery } from "@tanstack/react-query";
+import { getCollection } from "@/state/explore/challenge";
+import { constans } from "@/utils/constans";
+import ChallengeItemStatus from "./ChallengeItemStatus";
+import ChallengeItemImg from "./ChallengeItemImg";
+import { convertTime } from "@/utils/convert";
+
+export default function ChallengeItems({ info, goCollection }) {
+ const { data } = useQuery({
+ queryKey: ["collection"],
+ queryFn: () => getCollection(Number(info.id)),
+ });
+
+ const { ipfsPath, imgPath } = constans();
+ const { t } = useTranslation(["profile", "explore"]);
+ let [timestamp, setTimeStamp] = useState();
+ let [collectionInfo, setCollectionInfo] = useState({
+ questNum: 0,
+ claimable: false,
+ claimed: false,
+ });
+
+ function getTimeDiff(time) {
+ const { type, time: num } = convertTime(time, "all")
+ return t(`translation:${type}`, {time: Math.round(num)})
+ }
+
+ async function init() {
+ try {
+ let tokenId = data.collection.tokenId;
+ collectionInfo.questNum = data.list.length;
+ timestamp = info.quest_data?.estimateTime
+ ? getTimeDiff(info?.estimateTime)
+ : null;
+ setTimeStamp(timestamp);
+ if (tokenId && tokenId !== "0") {
+ collectionInfo.claimable = !list.some((e) => !e.claimed);
+ collectionInfo.claimed = data.status;
+ }
+ setCollectionInfo({ ...collectionInfo });
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
+ useUpdateEffect(() => {
+ data && init();
+ }, [data]);
+
+ return (
+ goCollection(info.id)}>
+ {/* 挑战状态 */}
+
+
+
+
+
{info.title}
+
{info.description}
+
+
+ {info.author_info.avatar && (
+
+
+
+ )}
+
{info.author_info.nickname}
+
+
+
+ {info?.difficulty !== null && (
+ <>
+
{t("translation:diff")}
+
+ {new Array(3).fill(0).map((e, i) => (
+
= info?.difficulty + 1
+ ? "line"
+ : "full"
+ }.png`)}
+ />
+ ))}
+
+ >
+ )}
+
+
+ {info?.estimate_time !== null && (
+ <>
+
+ {timestamp}
+ >
+ )}
+
+
+
+
+ );
+}
diff --git a/src/components/ChallengeItem/index.jsx b/src/components/ChallengeItem/index.jsx
new file mode 100644
index 00000000..62fa0bc9
--- /dev/null
+++ b/src/components/ChallengeItem/index.jsx
@@ -0,0 +1,150 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { message } from "antd";
+import { ClockCircleFilled, EditOutlined } from "@ant-design/icons";
+import { constans } from "@/utils/constans";
+import { convertTime } from "@/utils/convert";
+import ChallengeItemStatus from "./ChallengeItemStatus";
+import ChallengeItemImg from "./ChallengeItemImg";
+
+/*
+isMe: Boolean,
+info: {
+ tokenId: String,
+ img: String,
+ title: String,
+ description: String,
+ difficulty: Number,
+ time: timestamp,
+ claimable: Boolean,
+ claimed: Boolean,
+ editable: Boolean,
+ has_claim: Boolean,
+ review: Boolean,
+}
+claimedInfo: {
+ hasNft: Boolean,
+ hasVc: Boolean,
+ info: {
+ version,
+ title,
+ nft_address,
+ badge_chain_id,
+ badge_token_id,
+ }
+}
+*/
+
+export default function ChallengeItem({ info, claimedInfo }) {
+ const navigateTo = useNavigate();
+ const { t } = useTranslation(["profile", "explore"]);
+ const { ipfsPath, defaultImg, openseaLink, openseaSolanaLink } = constans();
+ let [itemInfo, setItemInfo] = useState();
+
+ function toQuest() {
+ if (itemInfo?.claimable || itemInfo?.claimed || itemInfo?.review) {
+ // 个人查看完成的挑战
+ navigateTo(`/claim/${info.uuid}`);
+ } else {
+ navigateTo(`/quests/${info.uuid}`);
+ }
+ }
+
+ function editChallenge(event) {
+ event.stopPropagation();
+ // 已有人claim,终止
+ if (itemInfo?.has_claim) {
+ message.warning(t("edit.error"));
+ return;
+ }
+ window.open(`/publish?${itemInfo.tokenId}`, "_blank");
+ }
+
+ function getTimeDiff(time) {
+ const { type, time: num } = convertTime(time, "all");
+ return t(`translation:${type}`, { time: Math.round(num) });
+ }
+
+ function initInfo() {
+ const img =
+ info.metadata.image.indexOf("https://") !== -1
+ ? info.metadata.image
+ : info.metadata.image.split("//")[1]
+ ? `${ipfsPath}/${info.metadata.image.split("//")[1]}`
+ : info.metadata?.properties?.media.split("//")[1]
+ ? `${ipfsPath}/${
+ info.metadata?.properties?.media.split("//")[1]
+ }`
+ : defaultImg;
+
+ itemInfo = {
+ ...info,
+ difficulty: info.metadata?.attributes?.difficulty,
+ time: info.quest_data?.estimateTime
+ ? getTimeDiff(info.quest_data?.estimateTime)
+ : null,
+ review: info?.open_quest_review_status === 1,
+ img,
+ };
+ setItemInfo({ ...itemInfo });
+ }
+
+ useEffect(() => {
+ initInfo();
+ }, []);
+
+ return (
+
+ {/* 挑战状态 */}
+
+ {/* 编辑挑战 */}
+ {itemInfo?.editable && (
+
+
+
+ )}
+ {/* 挑战Img */}
+
+ {/* 挑战详情 */}
+
+
+
{itemInfo?.title}
+
+ {itemInfo?.description}
+
+
+
+ {itemInfo?.difficulty !== null && (
+
+
{t("translation:diff")}
+
+ {new Array(3).fill(0).map((e, i) => (
+
= itemInfo?.difficulty + 1
+ ? "line"
+ : "full"
+ }.png`)}
+ />
+ ))}
+
+
+ )}
+ {itemInfo?.time && (
+
+
+ {itemInfo?.time}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/User/ChallengeItem.js b/src/components/User/ChallengeItem.js
index a72198c2..8afbabd9 100644
--- a/src/components/User/ChallengeItem.js
+++ b/src/components/User/ChallengeItem.js
@@ -20,7 +20,6 @@ export default function ChallengeItem(props) {
const navigateTo = useNavigate();
const [messageApi, contextHolder] = message.useMessage();
const { ipfsPath, defaultImg, openseaLink, openseaSolanaLink } = constans(profile?.checkType);
- const arr = [0, 1, 2];
const toQuest = () => {
@@ -95,12 +94,7 @@ export default function ChallengeItem(props) {
function getTimeDiff(time) {
const { type, time: num } = convertTime(time, "all")
-
- return (
- <>
- {t(`translation:${type}`, {time: Math.round(num)})}
- >
- )
+ return t(`translation:${type}`, {time: Math.round(num)})
}
return (
@@ -221,7 +215,7 @@ export default function ChallengeItem(props) {
{t("translation:diff")}
{
- arr.map((e,i) => {
+ new Array(3).map((e,i) => {
if (i >= info.metadata?.attributes?.difficulty+1) {
return
}else{
diff --git a/src/router/index.js b/src/router/index.js
index 4a01a932..84fea61a 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -1,6 +1,6 @@
import Index from "@/views/Index"
-import Explore from "@/views/Explore"
-import NewPublish from "@/views/Publish/index"
+import Explore from "@/views/Explore/index"
+import Publish from "@/views/Publish/index"
import Question from "@/views/Question/index";
import Challenge from "@/views/Challenge/index";
import Claim from "@/views/Claim/index";
@@ -29,7 +29,7 @@ const routes = [
},
{
path: "/publish",
- element:
,
+ element:
,
},
{
path: "/quests/:questId",
diff --git a/src/state/explore/challenge.js b/src/state/explore/challenge.js
new file mode 100644
index 00000000..7ab65a61
--- /dev/null
+++ b/src/state/explore/challenge.js
@@ -0,0 +1,33 @@
+import { getQuests, hasClaimed } from "@/request/api/public";
+import { getCollectionQuest } from "@/request/api/quests";
+
+export const getChallengeList = async (pageParam) => {
+ try {
+ const res = await getQuests({ pageSize: 10, page: pageParam });
+ return res.data?.list || [];
+ } catch (error) {
+ console.log(error);
+ return null;
+ }
+};
+
+export const getCollection = async (pageParam) => {
+ try {
+ const res = await getCollectionQuest({ id: pageParam });
+ const tokenId = res.data.collection.tokenId;
+ let status = false;
+ if (tokenId && tokenId !== "0") {
+ await hasClaimed({ id: tokenId }).then((res) => {
+ status = res.data?.status === 2;
+ });
+ }
+ return {
+ list: res.data.list || [],
+ collection: res.data.collection,
+ status,
+ };
+ } catch (error) {
+ console.log(error);
+ return null;
+ }
+};
diff --git a/src/views/Explore.js b/src/views/Explore.js
deleted file mode 100644
index 61a1da73..00000000
--- a/src/views/Explore.js
+++ /dev/null
@@ -1,129 +0,0 @@
-import { useEffect, useState } from "react"
-import { getQuests } from "../request/api/public"
-import "@/assets/styles/view-style/explore.scss"
-import "@/assets/styles/mobile/view-style/explore.scss"
-import { useTranslation } from "react-i18next";
-import store from "@/redux/store";
-import ChallengeItem from "@/components/User/ChallengeItem";
-import ChallengeItems from "@/components/User/ChallengeItems";
-import { useLocation, useNavigate, useParams } from "react-router-dom";
-import { getCollectionQuest } from "@/request/api/quests";
-import {
- ArrowLeftOutlined,
- } from '@ant-design/icons';
-import InfiniteScroll from "react-infinite-scroll-component";
-import { Spin } from "antd";
-
-export default function Explore(params) {
-
- const { t } = useTranslation(["explore", "translation"]);
- const { id } = useParams();
- const location = useLocation();
- const navigateTo = useNavigate();
-
- let [page, setPage] = useState(0);
-
- let [isOver, setIsOver] = useState();
- let [challenges, setChallenges] = useState([]);
- let [isMobile, setIsMobile] = useState(store.getState().isMobile);
- let [loading, setLoading] = useState(false);
-
- function handleMobileChange() {
- isMobile = store.getState().isMobile;
- setIsMobile(isMobile);
- }
-
- store.subscribe(handleMobileChange);
-
- const goCollection = (id) => {
- navigateTo(`/collection/${id}`)
- }
-
- const getChallenge = async() => {
- if (loading) {
- return
- }
- setLoading(true);
- const cache = localStorage.getItem("decert.cache");
-
- page += 1;
- setPage(page);
- let res
- if (id) {
- res = await getCollectionQuest({id: Number(id)});
- }else{
- res = await getQuests({pageSize: 10, page: page});
- }
- challenges = challenges.concat(res.data.list);
- if (challenges.length === res.data.total || id) {
- setIsOver(true);
- }
- if (cache) {
- const claimable = JSON.parse(cache)?.claimable;
- if (claimable && claimable.length > 0) {
- challenges.map(e => {
- claimable.map((ele,index) => {
- if (e.uuid == ele.uuid && e.claimed) {
- const newCache = JSON.parse(cache);
- newCache.claimable.splice(index,1);
- localStorage.setItem("decert.cache", JSON.stringify(newCache));
- }else if (e.uuid == ele.uuid) {
- e.claimable = true;
- }
- })
- })
- }
- }
- setChallenges([...challenges]);
- setLoading(false);
- }
-
- useEffect(() => {
- challenges = [];
- setChallenges([...challenges]);
- page = 0;
- setPage(page);
- isOver = false;
- setIsOver(isOver);
- getChallenge()
- },[location])
-
- return (
-
-
- {
- id &&
-
navigateTo("/challenges")}>
-
-
返回
-
- }
- {/* title */}
-
{t("title")}
- {/* Challenge */}
-
}
- >
- {
- challenges.map(item => (
- item.style === 1 ?
-
- :
-
- ))
- }
-
-
- )
-}
\ No newline at end of file
diff --git a/src/views/Explore/index.jsx b/src/views/Explore/index.jsx
new file mode 100644
index 00000000..902f8323
--- /dev/null
+++ b/src/views/Explore/index.jsx
@@ -0,0 +1,71 @@
+import { Spin } from "antd";
+import { useNavigate } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { useInfiniteQuery } from "@tanstack/react-query";
+import InfiniteScroll from "react-infinite-scroll-component";
+import PageLoader from "@/components/Loader/PageLoader";
+import ChallengeItem from "@/components/ChallengeItem";
+import ChallengeItems from "@/components/ChallengeItem/ChallengeItems";
+import { getChallengeList } from "@/state/explore/challenge";
+import "./style/index.scss";
+import "./style/mobile.scss";
+
+
+export default function Explore(params) {
+ const navigateTo = useNavigate();
+ const { t } = useTranslation(["explore", "translation"]);
+ const { data, fetchNextPage, hasNextPage, status } = useInfiniteQuery({
+ queryKey: ["challenges"],
+ queryFn: ({ pageParam }) => getChallengeList(pageParam),
+ initialPageParam: 1,
+ getNextPageParam: (lastPage, allPages) => {
+ return lastPage.length > 0 ? allPages.length + 1 : undefined;
+ },
+ });
+
+ return (
+
+
+
{t("title")}
+
}
+ >
+ {status === "pending" ? (
+
+ ) : (
+ data.pages
+ .flat()
+ .map((item) =>
+ item.style === 1 ? (
+
+ ) : (
+
navigateTo(`/collection/${id}`)}
+ />
+ )
+ )
+ )}
+
+
+ );
+}
diff --git a/src/views/Explore/style/index.scss b/src/views/Explore/style/index.scss
new file mode 100644
index 00000000..70163248
--- /dev/null
+++ b/src/views/Explore/style/index.scss
@@ -0,0 +1,44 @@
+.Explore {
+ width: 1440px;
+ padding-top: 170px;
+ margin: 0 auto;
+ font-family: Source;
+ position: relative;
+ .back{
+ position: absolute;
+ display: flex;
+ align-items: center;
+ top: 95px;
+ font-size: 20px;
+ cursor: pointer;
+ .anticon{
+ margin-right: 11px;
+ font-size: 30px;
+ }
+ }
+ // title
+ h3 {
+ font-size: 32px;
+ font-weight: 600;
+ line-height: 38px;
+ }
+ // depass
+ .depass {
+ margin-top: 63px;
+ h4 {
+ font-size: 20px;
+ font-weight: 800;
+ color: #3d3d3d;
+ }
+ }
+ // challenges
+ .challenges {
+ padding: 0;
+ padding-top: 80px;
+ padding-bottom: 150px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 36px 40px;
+ }
+}
+
\ No newline at end of file
diff --git a/src/views/Explore/style/mobile.scss b/src/views/Explore/style/mobile.scss
new file mode 100644
index 00000000..0018ba24
--- /dev/null
+++ b/src/views/Explore/style/mobile.scss
@@ -0,0 +1,41 @@
+.Mobile{
+ .Explore {
+ width: 21.875rem;
+ min-height: 100vh;
+ padding-top: 5.625rem;
+ .back{
+ position: absolute;
+ display: flex;
+ align-items: center;
+ top: 4.3375rem;
+ font-size: .75rem;
+ cursor: pointer;
+ .anticon{
+ margin-right: .3125rem;
+ font-size: 1rem;
+ }
+ }
+ // title
+ h3 {
+ font-size: 1.125rem;
+ font-weight: 600;
+ line-height: 1.5625rem;
+ }
+ // depass
+ .depass {
+ margin-top: 3.9375rem;
+ h4 {
+ font-size: 1.25rem;
+ font-weight: 800;
+ color: #3d3d3d;
+ }
+ }
+ // challenges
+ .challenges {
+ padding: 0;
+ padding-top: 1.25rem;
+ padding-bottom: 9.375rem;
+ gap: .625rem;
+ }
+ }
+}
\ No newline at end of file