rate apps feature

This commit is contained in:
PhilReact 2024-10-20 01:46:22 +03:00
parent 9685a40e6a
commit 4a026bccc7
8 changed files with 289 additions and 40 deletions

View File

@ -0,0 +1,16 @@
import React from 'react';
export const StarEmptyIcon = () => {
return (
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.2726 0.162533L7.89126 3.31595C7.9357 3.40243 8.02078 3.46234 8.11994 3.47588L11.7399 3.98173C11.8542 3.99736 11.9496 4.07446 11.9853 4.18022C12.0206 4.28598 11.9913 4.40215 11.9084 4.47977L9.28882 6.93449V6.93397C9.21729 7.00117 9.18478 7.09807 9.20157 7.19288L9.81988 10.6588C9.83939 10.7682 9.79278 10.8786 9.69903 10.9443C9.60529 11.0094 9.48119 11.0182 9.37931 10.9667L6.14144 9.32987C6.05311 9.28559 5.9469 9.28559 5.85856 9.32987L2.62069 10.9667C2.51881 11.0182 2.39472 11.0094 2.30096 10.9443C2.20722 10.8786 2.16062 10.7682 2.18012 10.6588L2.79842 7.19288C2.81522 7.09807 2.78271 7.00117 2.71118 6.93397L0.0916083 4.47978C0.0086971 4.40216 -0.0205644 4.28599 0.0146582 4.18023C0.0504232 4.07448 0.145798 3.99738 0.260135 3.98175L3.88006 3.47589C3.97923 3.46235 4.0643 3.40244 4.10874 3.31596L5.7274 0.162545C5.77888 0.0630431 5.88455 0 5.99997 0C6.11539 0 6.22113 0.0630238 6.2726 0.162533Z" fill="#727376"/>
</svg>
);
};

View File

@ -0,0 +1,18 @@
import React from "react";
export const StarFilledIcon = () => {
return (
<svg
width="12"
height="11"
viewBox="0 0 12 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.2726 0.162533L7.89126 3.31595C7.9357 3.40243 8.02078 3.46234 8.11994 3.47588L11.7399 3.98173C11.8542 3.99736 11.9496 4.07446 11.9853 4.18022C12.0206 4.28598 11.9913 4.40215 11.9084 4.47977L9.28882 6.93449V6.93397C9.21729 7.00117 9.18478 7.09807 9.20157 7.19288L9.81988 10.6588C9.83939 10.7682 9.79278 10.8786 9.69903 10.9443C9.60529 11.0094 9.48119 11.0182 9.37931 10.9667L6.14144 9.32987C6.05311 9.28559 5.9469 9.28559 5.85856 9.32987L2.62069 10.9667C2.51881 11.0182 2.39472 11.0094 2.30096 10.9443C2.20722 10.8786 2.16062 10.7682 2.18012 10.6588L2.79842 7.19288C2.81522 7.09807 2.78271 7.00117 2.71118 6.93397L0.0916083 4.47978C0.0086971 4.40216 -0.0205644 4.28599 0.0146582 4.18023C0.0504232 4.07448 0.145798 3.99738 0.260135 3.98175L3.88006 3.47589C3.97923 3.46235 4.0643 3.40244 4.10874 3.31596L5.7274 0.162545C5.77888 0.0630431 5.88455 0 5.99997 0C6.11539 0 6.22113 0.0630238 6.2726 0.162533Z"
fill="white"
/>
</svg>
);
};

View File

@ -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}
</AppInfoUserName>
<Spacer height="3px" />
<AppRating app={app} myName={myName} />
</AppInfoSnippetMiddle>
</AppInfoSnippetLeft>
<AppInfoSnippetRight>

View File

@ -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 | boolean>(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 (
<div>
<Rating value={value}
onChange={(event, newValue) => {
}} precision={0.1} />
<Box sx={{
display: 'flex',
gap: '5px',
alignItems: 'center'
}}>
<Rating
value={value}
onChange={rateFunc}
precision={1}
readOnly={hasPublishedRating === null}
size="small"
icon={<StarFilledIcon />}
emptyIcon={<StarEmptyIcon />}
sx={{
display: "flex",
gap: "2px",
}}
/>
{ratingCountPosition && (
<AppInfoUserName>
{ (votesInfo?.totalVotes ?? 0) + (votesInfo?.voteCounts?.length === 6 ? 1 : 0)}
</AppInfoUserName>
)}
</Box>
<CustomizedSnackbars
duration={2000}
open={openSnack}
setOpen={setOpenSnack}
info={infoSnack}
setInfo={setInfoSnack}
/>
</div>
)
}
);
};

View File

@ -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'
}));

View File

@ -263,6 +263,8 @@ export const Apps = ({ mode, setMode, show , myName}) => {
downloadedQapps={downloadedQapps}
availableQapps={availableQapps}
setMode={setMode}
myName={myName}
hasPublishApp={!!(myApp || myWebsite)}
/>
)}
{mode === "appInfo" && <AppInfo app={selectedAppInfo} />}

View File

@ -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 <AppInfoSnippet key={`${app?.service}-${app?.name}`} app={app} />;
return <AppInfoSnippet key={`${app?.service}-${app?.name}`} app={app} myName={myName} />;
};
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 (
<AppsLibraryContainer>
@ -162,7 +174,9 @@ export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode }) => {
<Spacer height="25px" />
{searchedList?.length > 0 ? (
<AppsWidthLimiter>
<StyledVirtuosoContainer>
<StyledVirtuosoContainer sx={{
height: rootHeight
}}>
<Virtuoso
ref={virtuosoRef}
data={searchedList}
@ -229,7 +243,7 @@ export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode }) => {
})}
</AppsContainer>
<Spacer height="30px" />
<AppLibrarySubTitle>Create Apps!</AppLibrarySubTitle>
<AppLibrarySubTitle>{hasPublishApp ? 'Update Apps!' : 'Create Apps!'}</AppLibrarySubTitle>
<Spacer height="18px" />
</AppsWidthLimiter>
<PublishQAppCTAParent>
@ -245,7 +259,7 @@ export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode }) => {
setMode('publish')
}}>
<PublishQAppCTAButton>
Publish
{hasPublishApp ? 'Update' : 'Publish'}
</PublishQAppCTAButton>
<Spacer width="20px" />
</PublishQAppCTARight>

View File

@ -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");