mirror of
https://github.com/Qortal/chrome-extension.git
synced 2025-02-11 17:55:49 +00:00
rate apps feature
This commit is contained in:
parent
9685a40e6a
commit
4a026bccc7
16
src/assets/svgs/StarEmpty.tsx
Normal file
16
src/assets/svgs/StarEmpty.tsx
Normal 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>
|
||||
|
||||
|
||||
|
||||
);
|
||||
};
|
18
src/assets/svgs/StarFilled.tsx
Normal file
18
src/assets/svgs/StarFilled.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -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'
|
||||
}));
|
||||
|
||||
|
||||
|
@ -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} />}
|
||||
|
@ -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>
|
||||
|
@ -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");
|
||||
|
Loading…
x
Reference in New Issue
Block a user