diff --git a/src/assets/svgs/StarEmpty.tsx b/src/assets/svgs/StarEmpty.tsx new file mode 100644 index 0000000..8375111 --- /dev/null +++ b/src/assets/svgs/StarEmpty.tsx @@ -0,0 +1,16 @@ + + + +import React from 'react'; + + +export const StarEmptyIcon = () => { + return ( + + + + + + + ); +}; diff --git a/src/assets/svgs/StarFilled.tsx b/src/assets/svgs/StarFilled.tsx new file mode 100644 index 0000000..fb82e49 --- /dev/null +++ b/src/assets/svgs/StarFilled.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +export const StarFilledIcon = () => { + return ( + + + + ); +}; diff --git a/src/components/Apps/AppInfoSnippet.tsx b/src/components/Apps/AppInfoSnippet.tsx index 9153945..e63b4d0 100644 --- a/src/components/Apps/AppInfoSnippet.tsx +++ b/src/components/Apps/AppInfoSnippet.tsx @@ -20,8 +20,9 @@ import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { Spacer } from "../../common/Spacer"; import { executeEvent } from "../../utils/events"; +import { AppRating } from "./AppRating"; -export const AppInfoSnippet = ({ app }) => { +export const AppInfoSnippet = ({ app, myName }) => { const isInstalled = app?.status?.status === 'READY' @@ -85,6 +86,7 @@ export const AppInfoSnippet = ({ app }) => { { app?.name} + diff --git a/src/components/Apps/AppRating.tsx b/src/components/Apps/AppRating.tsx index b063563..5e58271 100644 --- a/src/components/Apps/AppRating.tsx +++ b/src/components/Apps/AppRating.tsx @@ -1,14 +1,208 @@ -import { Rating } from '@mui/material' -import React, { useState } from 'react' +import { Box, Rating, Typography } from "@mui/material"; +import React, { useCallback, useContext, useEffect, useRef, useState } from "react"; +import { getFee } from "../../background"; +import { MyContext, getBaseApiReact } from "../../App"; +import { CustomizedSnackbars } from "../Snackbar/Snackbar"; +import { StarFilledIcon } from "../../assets/svgs/StarFilled"; +import { StarEmptyIcon } from "../../assets/svgs/StarEmpty"; +import { AppInfoUserName } from "./Apps-styles"; -export const AppRating = () => { - const [value, setValue] = useState(0) +export const AppRating = ({app, myName, ratingCountPosition = 'right'}) => { + const [value, setValue] = useState(0); + const { show } = useContext(MyContext); +const [hasPublishedRating, setHasPublishedRating] = useState(null) +const [pollInfo, setPollInfo] = useState(null) +const [votesInfo, setVotesInfo] = useState(null) +const [openSnack, setOpenSnack] = useState(false); +const [infoSnack, setInfoSnack] = useState(null); +const hasCalledRef = useRef(false) +console.log(`pollinfo-${app?.service}-${app?.name}`, value) + +console.log('hasPublishedRating', hasPublishedRating) +const getRating = useCallback(async (name, service)=> { + try { + + hasCalledRef.current = true + const pollName = `app-library-${service}-rating-${name}` + const url = `${getBaseApiReact()}/polls/${pollName}`; + + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + const responseData = await response.json(); + console.log('responseData', responseData) + if(responseData?.message?.includes('POLL_NO_EXISTS')){ + setHasPublishedRating(false) + } else if(responseData?.pollName){ + setPollInfo(responseData) + setHasPublishedRating(true) + const urlVotes = `${getBaseApiReact()}/polls/votes/${pollName}`; + + const responseVotes = await fetch(urlVotes, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + const responseDataVotes = await responseVotes.json(); + setVotesInfo(responseDataVotes) + const voteCount = responseDataVotes.voteCounts + // Include initial value vote in the calculation + const ratingVotes = voteCount.filter(vote => !vote.optionName.startsWith("initialValue-")); + const initialValueVote = voteCount.find(vote => vote.optionName.startsWith("initialValue-")); + console.log('initialValueVote', initialValueVote) + if (initialValueVote) { + // Convert "initialValue-X" to just "X" and add it to the ratingVotes array + const initialRating = parseInt(initialValueVote.optionName.split("-")[1], 10); + console.log('initialRating', initialRating) + ratingVotes.push({ + optionName: initialRating.toString(), + voteCount: 1, + }); + } + + // Calculate the weighted average + let totalScore = 0; + let totalVotes = 0; + + ratingVotes.forEach(vote => { + const rating = parseInt(vote.optionName, 10); // Extract rating value (1-5) + const count = vote.voteCount; + totalScore += rating * count; // Weighted score + totalVotes += count; // Total number of votes + }); + console.log('ratingVotes', ratingVotes, totalScore, totalVotes) + + // Calculate average rating (ensure no division by zero) + const averageRating = totalVotes > 0 ? (totalScore / totalVotes) : 0; + setValue(averageRating); + } + } catch (error) { + console.log('error rating', error) + if(error?.message?.includes('POLL_NO_EXISTS')){ + setHasPublishedRating(false) + } + } + + +}, []) + useEffect(()=> { + if(hasCalledRef.current) return + if(!app) return + getRating(app?.name, app?.service) + }, [getRating, app?.name]) + + const rateFunc = async (event, newValue)=> { + try { + if(!myName) throw new Error('You need a name to rate.') + if(!app?.name) return + console.log('newValue', newValue) + const fee = await getFee("ARBITRARY"); + + await show({ + message: `Would you like to rate this app a rating of ${newValue}?`, + publishFee: fee.fee + " QORT", + }); + + if(hasPublishedRating === false){ + const pollName = `app-library-${app.service}-rating-${app.name}` + const pollOptions = [`1, 2, 3, 4, 5, initialValue-${newValue}`] + await new Promise((res, rej)=> { + chrome?.runtime?.sendMessage({ + action: 'CREATE_POLL', type: 'qortalRequest', payload: { + pollName: pollName , pollDescription: `Rating for ${app.service} ${app.name}`, pollOptions: pollOptions , pollOwnerAddress : myName + } + }, (response) => { + console.log('response', response); + if (response.error) { + rej(response?.message) + return + } else { + res(response) + setInfoSnack({ + type: "success", + message: + "Successfully rated. Please wait a couple minutes for the network to propogate the changes.", + }); + setOpenSnack(true); + } + }); + }) + } else { + const pollName = `app-library-${app.service}-rating-${app.name}` + const optionIndex = pollInfo?.pollOptions.findIndex((option)=> +option.optionName === +newValue) + if(isNaN(optionIndex) || optionIndex === -1) throw new Error('Cannot find rating option') + await new Promise((res, rej)=> { + chrome?.runtime?.sendMessage({ + action: 'VOTE_ON_POLL', type: 'qortalRequest', payload: { + pollName: pollName , optionIndex + } + }, (response) => { + console.log('response', response); + if (response.error) { + rej(response?.message) + return + } else { + res(response) + setInfoSnack({ + type: "success", + message: + "Successfully rated. Please wait a couple minutes for the network to propogate the changes.", + }); + setOpenSnack(true); + } + }); + }) + } + + } catch (error) { + setInfoSnack({ + type: "error", + message: error.message || "An error occurred while trying to rate.", + }); + setOpenSnack(true); + } + } + console.log('vvotes', (votesInfo?.totalVotes ?? 0 ) + votesInfo?.voteCounts?.length === 6 ? 1 : 0, votesInfo) return (
- { - - }} precision={0.1} /> + + } + emptyIcon={} + sx={{ + display: "flex", + gap: "2px", + }} + /> + {ratingCountPosition && ( + + { (votesInfo?.totalVotes ?? 0) + (votesInfo?.voteCounts?.length === 6 ? 1 : 0)} + + )} + + +
- ) -} + ); +}; diff --git a/src/components/Apps/Apps-styles.tsx b/src/components/Apps/Apps-styles.tsx index 74b0526..ca0f660 100644 --- a/src/components/Apps/Apps-styles.tsx +++ b/src/components/Apps/Apps-styles.tsx @@ -175,12 +175,14 @@ import { fontSize: '16px', fontWeight: 500, lineHeight: 1.2, + textAlign: 'start' })); export const AppInfoUserName = styled(Typography)(({ theme }) => ({ fontSize: '13px', fontWeight: 400, lineHeight: 1.2, - color: '#8D8F93' + color: '#8D8F93', + textAlign: 'start' })); diff --git a/src/components/Apps/Apps.tsx b/src/components/Apps/Apps.tsx index 2ea6037..0c2d1f3 100644 --- a/src/components/Apps/Apps.tsx +++ b/src/components/Apps/Apps.tsx @@ -263,6 +263,8 @@ export const Apps = ({ mode, setMode, show , myName}) => { downloadedQapps={downloadedQapps} availableQapps={availableQapps} setMode={setMode} + myName={myName} + hasPublishApp={!!(myApp || myWebsite)} /> )} {mode === "appInfo" && } diff --git a/src/components/Apps/AppsLibrary.tsx b/src/components/Apps/AppsLibrary.tsx index 8f89078..d220a4e 100644 --- a/src/components/Apps/AppsLibrary.tsx +++ b/src/components/Apps/AppsLibrary.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { AppCircle, AppCircleContainer, @@ -55,12 +55,42 @@ const ScrollerStyled = styled('div')({ "-ms-overflow-style": "none", }); + const StyledVirtuosoContainer = styled('div')({ + position: 'relative', + width: '100%', + display: 'flex', + flexDirection: 'column', + + // Hide scrollbar for WebKit browsers (Chrome, Safari) + "::-webkit-scrollbar": { + width: "0px", + height: "0px", + }, + + // Hide scrollbar for Firefox + scrollbarWidth: "none", + + // Hide scrollbar for IE and older Edge + "-ms-overflow-style": "none", + }); -export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode }) => { +export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode, myName, hasPublishApp }) => { const [searchValue, setSearchValue] = useState(""); const virtuosoRef = useRef(); const { rootHeight } = useContext(MyContext); + const [appStates, setAppStates] = useState({}); + const handleStateChange = (appId, newState) => { + setAppStates((prevState) => ({ + ...prevState, + [appId]: { + ...(prevState[appId] || {}), // Preserve existing state for the app + ...newState, // Merge in the new state properties + }, + })); + }; + + console.log('appStates', appStates) const officialApps = useMemo(() => { return availableQapps.filter( (app) => @@ -75,7 +105,7 @@ export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode }) => { useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(searchValue); - }, 250); + }, 350); // Cleanup timeout if searchValue changes before the timeout completes return () => { @@ -97,28 +127,10 @@ export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode }) => { let app = searchedList[index]; console.log('appi', app) - return ; + return ; }; - const StyledVirtuosoContainer = styled('div')({ - position: 'relative', - height: rootHeight, - width: '100%', - display: 'flex', - flexDirection: 'column', - - // Hide scrollbar for WebKit browsers (Chrome, Safari) - "::-webkit-scrollbar": { - width: "0px", - height: "0px", - }, - - // Hide scrollbar for Firefox - scrollbarWidth: "none", - - // Hide scrollbar for IE and older Edge - "-ms-overflow-style": "none", - }); + return ( @@ -162,7 +174,9 @@ export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode }) => { {searchedList?.length > 0 ? ( - + { })} - Create Apps! + {hasPublishApp ? 'Update Apps!' : 'Create Apps!'} @@ -245,7 +259,7 @@ export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode }) => { setMode('publish') }}> - Publish + {hasPublishApp ? 'Update' : 'Publish'} diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 1f460ec..104c4e5 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -74,7 +74,7 @@ const _createPoll = async (pollName, pollDescription, options) => { const signedBytes = Base58.encode(tx.signedBytes); const res = await processTransactionVersion2(signedBytes); if (!res?.signature) - throw new Error("Transaction was not able to be processed"); + throw new Error(res?.message || "Transaction was not able to be processed"); return res; } else { throw new Error("User declined request"); @@ -172,8 +172,9 @@ const _voteOnPoll = async (pollName, optionIndex, optionName) => { }); const signedBytes = Base58.encode(tx.signedBytes); const res = await processTransactionVersion2(signedBytes); + console.log('res', res) if (!res?.signature) - throw new Error("Transaction was not able to be processed"); + throw new Error(res?.message || "Transaction was not able to be processed"); return res; } else { throw new Error("User declined request");