diff --git a/backend/quests/inputs.go b/backend/quests/inputs.go index d5d0097a..4406d59b 100644 --- a/backend/quests/inputs.go +++ b/backend/quests/inputs.go @@ -12,6 +12,10 @@ type VoteQuestInputs struct { DayIndex uint32 } +type HodlQuestInputs struct { + Amount int +} + func NewPixelQuestInputs(encodedInputs []int) *PixelQuestInputs { return &PixelQuestInputs{ PixelsNeeded: uint32(encodedInputs[0]), @@ -27,3 +31,9 @@ func NewVoteQuestInputs(encodedInputs []int) *VoteQuestInputs { DayIndex: uint32(encodedInputs[0]), } } + +func NewHodlQuestInputs(encodedInputs []int) *HodlQuestInputs { + return &HodlQuestInputs{ + Amount: encodedInputs[0], + } +} diff --git a/backend/quests/status.go b/backend/quests/status.go index 096b58e1..30a9d109 100644 --- a/backend/quests/status.go +++ b/backend/quests/status.go @@ -1,6 +1,8 @@ package quests -import "github.com/keep-starknet-strange/art-peace/backend/core" +import ( + "github.com/keep-starknet-strange/art-peace/backend/core" +) var QuestChecks = map[int]func(*Quest, string) (int, int){ AuthorityQuestType: CheckAuthorityStatus, @@ -29,8 +31,13 @@ func CheckAuthorityStatus(q *Quest, user string) (progress int, needed int) { } func CheckHodlStatus(q *Quest, user string) (progress int, needed int) { - // TODO: Implement this - return 0, 1 + hodlQuestInputs := NewHodlQuestInputs(q.InputData) + available, err := core.PostgresQueryOne[int]("SELECT available FROM ExtraPixels WHERE address = $1", user) + + if err != nil { + return 0, hodlQuestInputs.Amount + } + return *available, hodlQuestInputs.Amount } func CheckNftStatus(q *Quest, user string) (progress int, needed int) { @@ -97,9 +104,18 @@ func CheckFactionStatus(q *Quest, user string) (progress int, needed int) { return *count, 1 } +type RainbowStatus struct { + Used int `json:"used"` + Colors int `json:"colors"` +} + func CheckRainbowStatus(q *Quest, user string) (progress int, needed int) { - // TODO: Implement this - return 0, 1 + status, err := core.PostgresQueryOne[RainbowStatus]("SELECT COUNT(DISTINCT p.color) as used, (SELECT COUNT(*) FROM Colors) as colors FROM Pixels p WHERE p.address = $1", user) + if err != nil { + return 0, 1 + } + + return status.Used, status.Colors } func CheckTemplateStatus(q *Quest, user string) (progress int, needed int) { diff --git a/backend/routes/nft.go b/backend/routes/nft.go index ca266ed5..7e3a7534 100644 --- a/backend/routes/nft.go +++ b/backend/routes/nft.go @@ -22,6 +22,7 @@ func InitNFTRoutes() { // http.HandleFunc("/like-nft", LikeNFT) // http.HandleFunc("/unlike-nft", UnLikeNFT) http.HandleFunc("/get-top-nfts", getTopNFTs) + http.HandleFunc("/get-hot-nfts", getHotNFTs) if !core.ArtPeaceBackend.BackendConfig.Production { http.HandleFunc("/mint-nft-devnet", mintNFTDevnet) http.HandleFunc("/like-nft-devnet", likeNFTDevnet) @@ -94,7 +95,7 @@ func getMyNFTs(w http.ResponseWriter, r *http.Request) { SELECT nfts.*, COALESCE(like_count, 0) AS likes, - COALESCE((SELECT true FROM nftlikes WHERE liker = $1), false) as liked + COALESCE((SELECT true FROM nftlikes WHERE liker = $1 AND nftlikes.nftkey = nfts.token_id), false) as liked FROM nfts LEFT JOIN ( @@ -108,6 +109,7 @@ func getMyNFTs(w http.ResponseWriter, r *http.Request) { ) nftlikes ON nfts.token_id = nftlikes.nftKey WHERE nfts.owner = $1 + ORDER BY nfts.token_id DESC LIMIT $2 OFFSET $3` nfts, err := core.PostgresQueryJson[NFTData](query, address, pageLength, offset) if err != nil { @@ -152,7 +154,7 @@ func getNFTs(w http.ResponseWriter, r *http.Request) { SELECT nfts.*, COALESCE(like_count, 0) AS likes, - COALESCE((SELECT true FROM nftlikes WHERE liker = $1), false) as liked + COALESCE((SELECT true FROM nftlikes WHERE liker = $1 AND nftlikes.nftkey = nfts.token_id), false) as liked FROM nfts LEFT JOIN ( @@ -164,6 +166,7 @@ func getNFTs(w http.ResponseWriter, r *http.Request) { GROUP BY nftKey ) nftlikes ON nfts.token_id = nftlikes.nftKey + ORDER BY nfts.token_id DESC LIMIT $2 OFFSET $3` nfts, err := core.PostgresQueryJson[NFTData](query, address, pageLength, offset) if err != nil { @@ -195,7 +198,7 @@ func getNewNFTs(w http.ResponseWriter, r *http.Request) { SELECT nfts.*, COALESCE(like_count, 0) AS likes, - COALESCE((SELECT true FROM nftlikes WHERE liker = $1), false) as liked + COALESCE((SELECT true FROM nftlikes WHERE liker = $1 AND nftlikes.nftkey = nfts.token_id), false) as liked FROM nfts LEFT JOIN ( @@ -343,7 +346,7 @@ func getTopNFTs(w http.ResponseWriter, r *http.Request) { SELECT nfts.*, COALESCE(like_count, 0) AS likes, - COALESCE((SELECT true FROM nftlikes WHERE liker = $1), false) as liked + COALESCE((SELECT true FROM nftlikes WHERE liker = $1 AND nftlikes.nftkey = nfts.token_id), false) as liked FROM nfts LEFT JOIN ( @@ -421,3 +424,57 @@ func unlikeNFTDevnet(w http.ResponseWriter, r *http.Request) { routeutils.WriteResultJson(w, "NFT unliked on devnet") } +func getHotNFTs(w http.ResponseWriter, r *http.Request) { + address := r.URL.Query().Get("address") + if address == "" { + address = "0" + } + // hot limit is the number of last likes to consider when calculating hotness + hotLimit, err := strconv.Atoi(r.URL.Query().Get("hotLimit")) + if err != nil || hotLimit <= 0 { + hotLimit = 100 + } + pageLength, err := strconv.Atoi(r.URL.Query().Get("pageLength")) + if err != nil || pageLength <= 0 { + pageLength = 25 + } + if pageLength > 50 { + pageLength = 50 + } + page, err := strconv.Atoi(r.URL.Query().Get("page")) + if err != nil || page <= 0 { + page = 1 + } + offset := (page - 1) * pageLength + + query := ` + SELECT + nfts.*, + COALESCE(like_count, 0) AS likes, + COALESCE((SELECT true FROM nftlikes WHERE liker = $1 AND nftlikes.nftkey = nfts.token_id), false) as liked + FROM + nfts + LEFT JOIN ( + SELECT + nftKey, + COUNT(*) AS like_count FROM nftlikes GROUP BY nftKey + ) nftlikes ON nfts.token_id = nftlikes.nftKey + LEFT JOIN ( + SELECT + latestlikes.nftKey, + COUNT(*) as rank + FROM ( + SELECT * FROM nftlikes + ORDER BY key DESC LIMIT $2 + ) latestlikes + GROUP BY nftkey + ) rank ON nfts.token_id = rank.nftkey + ORDER BY rank DESC + LIMIT $3 OFFSET $4;` + nfts, err := core.PostgresQueryJson[NFTData](query, address, hotLimit, pageLength, offset) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to retrieve Hot NFTs") + return + } + routeutils.WriteDataJson(w, string(nfts)) +} diff --git a/frontend/src/services/apiService.js b/frontend/src/services/apiService.js index ad76c3b5..01219b6f 100644 --- a/frontend/src/services/apiService.js +++ b/frontend/src/services/apiService.js @@ -94,3 +94,10 @@ export const getTopNftsFn = async (params) => { `get-top-nfts?address=${queryAddress}&page=${page}&pageLength=${pageLength}` ); }; + +export const getHotNftsFn = async (params) => { + const { page, pageLength, queryAddress } = params; + return await fetchWrapper( + `get-hot-nfts?address=${queryAddress}&page=${page}&pageLength=${pageLength}` + ); +}; diff --git a/frontend/src/tabs/nfts/NFTs.js b/frontend/src/tabs/nfts/NFTs.js index 811814eb..0b80bccb 100644 --- a/frontend/src/tabs/nfts/NFTs.js +++ b/frontend/src/tabs/nfts/NFTs.js @@ -8,7 +8,8 @@ import { getMyNftsFn, getNftsFn, getNewNftsFn, - getTopNftsFn + getTopNftsFn, + getHotNftsFn } from '../../services/apiService.js'; import { PaginationView } from '../../ui/pagination.js'; @@ -177,7 +178,12 @@ const NFTs = (props) => { async function getNfts() { try { let result; - if (activeFilter === 'new') { + if (activeFilter === 'hot') { + result = await getHotNftsFn({ + page: allNftPagination.page, + pageLength: allNftPagination.pageLength + }); + } else if (activeFilter === 'new') { result = await getNewNftsFn({ page: allNftPagination.page, pageLength: allNftPagination.pageLength diff --git a/postgres/init.sql b/postgres/init.sql index d7291daa..2e4d9e6c 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -172,9 +172,10 @@ CREATE TABLE NFTs ( ); CREATE TABLE NFTLikes ( + key int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, nftKey integer NOT NULL, liker char(64) NOT NULL, - PRIMARY KEY (nftKey, liker) + UNIQUE (nftKey, liker) ); CREATE INDEX nftLikes_nft_key_index ON NFTLikes (nftKey); CREATE INDEX nftLikes_liker_index ON NFTLikes (liker);