From e9c09ad1be94078c45123bf71377af1fc818f21a Mon Sep 17 00:00:00 2001 From: numtel Date: Tue, 10 Sep 2024 01:10:26 -0400 Subject: [PATCH 1/5] chore(web): split project page tabs into individual components --- web/src/components/Avatars.tsx | 14 +- web/src/components/CircuitAbout.tsx | 62 +++ web/src/components/CircuitStats.tsx | 74 ++++ web/src/components/ProjectTabAbout.tsx | 40 ++ .../components/ProjectTabContributions.tsx | 68 +++ web/src/components/ProjectTabStats.tsx | 32 ++ web/src/components/ProjectTabZKey.tsx | 132 ++++++ web/src/context/ProjectPageContext.tsx | 76 +++- web/src/helpers/interfaces.ts | 13 - web/src/pages/ProjectPage.tsx | 395 ++---------------- 10 files changed, 509 insertions(+), 397 deletions(-) create mode 100644 web/src/components/CircuitAbout.tsx create mode 100644 web/src/components/CircuitStats.tsx create mode 100644 web/src/components/ProjectTabAbout.tsx create mode 100644 web/src/components/ProjectTabContributions.tsx create mode 100644 web/src/components/ProjectTabStats.tsx create mode 100644 web/src/components/ProjectTabZKey.tsx diff --git a/web/src/components/Avatars.tsx b/web/src/components/Avatars.tsx index 4cd48c3..5ebb102 100644 --- a/web/src/components/Avatars.tsx +++ b/web/src/components/Avatars.tsx @@ -1,15 +1,13 @@ import React from "react" import { Avatar, Box } from "@chakra-ui/react" -interface ScrollingAvatarsProps { - images?: string[] - } - +import { useProjectPageContext } from "../context/ProjectPageContext"; + /** * Display the participants Avatars in a scrolling list - * @param {ScrollingAvatarsProps} - the images to show */ -const ScrollingAvatars: React.FC = ({ images }) => { +const ScrollingAvatars: React.FC = () => { + const {avatars} = useProjectPageContext(); return ( = ({ images }) => { px={2} borderColor="gray.200" > - {images && images.length > 0 && images.map((image: string, index: any) => ( + {avatars && avatars.length > 0 && avatars.map((image: string, index: any) => ( = ({ images }) => { ) } -export default ScrollingAvatars \ No newline at end of file +export default ScrollingAvatars diff --git a/web/src/components/CircuitAbout.tsx b/web/src/components/CircuitAbout.tsx new file mode 100644 index 0000000..0400221 --- /dev/null +++ b/web/src/components/CircuitAbout.tsx @@ -0,0 +1,62 @@ +import { + Box, + Stat, + StatLabel, + StatNumber, + Heading, + Flex, + SimpleGrid, +} from "@chakra-ui/react"; + +import { + truncateString, + parseRepoRoot +} from "../helpers/utils"; + +export const CircuitAbout: React.FC = ({ circuit }) => { + return ( + + + {circuit.name} - {circuit.description} + + + + + Parameters + + { + circuit.template.paramsConfiguration && circuit.template.paramsConfiguration.length > 0 ? + circuit.template.paramsConfiguration.join(" ") : + circuit.template.paramConfiguration && circuit.template.paramConfiguration.length > 0 ? + circuit.template.paramConfiguration.join(" ") : + "No parameters" + } + + + + + Commit Hash + + + {truncateString(circuit.template.commitHash, 6)} + + + + + Template Link + + + {truncateString(circuit.template.source, 16)} + + + + + Compiler Version + + {circuit.compiler.version} + + + + + ); +} diff --git a/web/src/components/CircuitStats.tsx b/web/src/components/CircuitStats.tsx new file mode 100644 index 0000000..0f85cc0 --- /dev/null +++ b/web/src/components/CircuitStats.tsx @@ -0,0 +1,74 @@ +import { + Box, + Stat, + StatLabel, + StatNumber, + Tag, + Heading, + Flex, + Icon, + SimpleGrid, +} from "@chakra-ui/react"; +import { FiTarget, FiZap, FiEye, FiUser, FiMapPin, FiWifi } from "react-icons/fi"; + +export const CircuitStats: React.FC = ({ circuit }) => { + return ( + + + {circuit.name} - {circuit.description} + + + + + Constraints: {circuit.constraints} + + + + Pot: {circuit.pot} + + + + Private Inputs: {circuit.privateInputs} + + + + Public Inputs: {circuit.publicInputs} + + + + Curve: {circuit.curve} + + + + Wires: {circuit.wires} + + + + + + Completed Contributions + + {circuit.completedContributions} + + + + + Memory Requirement + + {circuit.memoryRequirement} mb + + + + Avg Contribution Time + + {circuit.avgTimingContribution}s + + + + Max Contribution Time + {circuit.maxTiming}s + + + + ); +} diff --git a/web/src/components/ProjectTabAbout.tsx b/web/src/components/ProjectTabAbout.tsx new file mode 100644 index 0000000..e5f665b --- /dev/null +++ b/web/src/components/ProjectTabAbout.tsx @@ -0,0 +1,40 @@ +import { + Box, + VStack, + SimpleGrid, + TabPanel, +} from "@chakra-ui/react"; + +import { useProjectPageContext } from "../context/ProjectPageContext"; +import { CircuitAbout } from "../components/CircuitAbout"; + +export const ProjectTabAbout: React.FC = () => { + const { circuitsClean } = useProjectPageContext(); + return ( + + + + + {circuitsClean.map((circuit, index) => ( + + ))} + + + + + ); +} diff --git a/web/src/components/ProjectTabContributions.tsx b/web/src/components/ProjectTabContributions.tsx new file mode 100644 index 0000000..633e859 --- /dev/null +++ b/web/src/components/ProjectTabContributions.tsx @@ -0,0 +1,68 @@ +import { + Box, + HStack, + TabPanel, + Tag, + Heading, + Spacer, + Table, + Tbody, + Td, + Th, + Thead, + Tooltip, + Tr, + Skeleton, + Stack, +} from "@chakra-ui/react"; + +import { useProjectPageContext } from "../context/ProjectPageContext"; + +export const ProjectTabContributions: React.FC = () => { + const { contributionsClean } = useProjectPageContext(); + return ( + + + + Contributions + + + + + {!contributionsClean ? ( + + + + + + ) : ( + + + + + + + + + + {contributionsClean.map((contribution, index) => ( + + + + + + ))} + +
DocContribution DateHashes
{contribution.doc}{contribution.lastUpdated} + + {contribution.lastZkeyBlake2bHash} + +
+ )} +
+
+ ); +} diff --git a/web/src/components/ProjectTabStats.tsx b/web/src/components/ProjectTabStats.tsx new file mode 100644 index 0000000..cd99b0d --- /dev/null +++ b/web/src/components/ProjectTabStats.tsx @@ -0,0 +1,32 @@ +import { + Box, + SimpleGrid, + TabPanel, +} from "@chakra-ui/react"; + +import { useProjectPageContext } from "../context/ProjectPageContext"; + +import { CircuitStats } from "../components/CircuitStats"; + +export const ProjectTabStats: React.FC = () => { + const { circuitsClean } = useProjectPageContext(); + return ( + + + + {circuitsClean.map((circuit, index) => + + )} + + + + ); +} diff --git a/web/src/components/ProjectTabZKey.tsx b/web/src/components/ProjectTabZKey.tsx new file mode 100644 index 0000000..e58d49f --- /dev/null +++ b/web/src/components/ProjectTabZKey.tsx @@ -0,0 +1,132 @@ +import { + Box, + Text, + TabPanel, + Button, + useClipboard, +} from "@chakra-ui/react"; +import { FaCloudDownloadAlt, FaCopy } from "react-icons/fa"; + +import { useProjectPageContext } from "../context/ProjectPageContext"; +import { CeremonyState } from "../helpers/interfaces"; +import { truncateString } from "../helpers/utils"; + +export const ProjectTabZKey: React.FC = ({ project }) => { + const { + latestZkeys, + finalBeacon, + finalZkeys, + } = useProjectPageContext(); + + const beaconValue = finalBeacon?.beacon + const beaconHash = finalBeacon?.beaconHash + + const { onCopy: copyBeaconValue, hasCopied: copiedBeaconValue } = useClipboard(beaconValue || "") + const { onCopy: copyBeaconHash, hasCopied: copiedBeaconHash } = useClipboard(beaconHash || "") + + return ( + + { + project?.ceremony.data.state === CeremonyState.FINALIZED && beaconHash && beaconValue && +
+ + Final contribution beacon + + + +
+ } + + Download Final ZKey(s) + + + Press the button below to download the final ZKey files from the S3 bucket. + + { + finalZkeys?.map((zkey, index) => { + return ( + + + + ) + }) + } + { + project?.ceremony.data.state === CeremonyState.FINALIZED && + <> + + Download Last ZKey(s) + + + You can use this zKey(s) with the beacon value to verify that the final zKey(s) was computed correctly. + + { + latestZkeys?.map((zkey, index) => { + return ( + + + + ) + }) + } + + } +
+ ); +} + diff --git a/web/src/context/ProjectPageContext.tsx b/web/src/context/ProjectPageContext.tsx index e4a19d7..765de39 100644 --- a/web/src/context/ProjectPageContext.tsx +++ b/web/src/context/ProjectPageContext.tsx @@ -15,11 +15,18 @@ import { CircuitDocumentReferenceAndData, FinalBeacon, ParticipantDocumentReferenceAndData, - ProjectData, ProjectPageContextProps, ZkeyDownloadLink } from "../helpers/interfaces"; -import { checkIfUserContributed, findLargestConstraint, formatZkeyIndex, processItems } from "../helpers/utils"; +import { + bytesToMegabytes, + checkIfUserContributed, + findLargestConstraint, + formatZkeyIndex, + parseDate, + processItems, + truncateString, +} from "../helpers/utils"; import { awsRegion, bucketPostfix, finalContributionIndex, maxConstraintsForBrowser } from "../helpers/constants"; export const ProjectDataSchema = z.object({ @@ -28,15 +35,12 @@ export const ProjectDataSchema = z.object({ contributions: z.optional(z.array(z.any())) }); -export const defaultProjectData: ProjectData = {}; - type ProjectPageProviderProps = { children: React.ReactNode; }; const ProjectPageContext = createContext({ hasUserContributed: false, - projectData: defaultProjectData, isLoading: false, runTutorial: false, avatars: [], @@ -49,13 +53,14 @@ export const useProjectPageContext = () => useContext(ProjectPageContext); export const ProjectPageProvider: React.FC = ({ children }) => { const navigate = useNavigate(); const { loading: isLoading, setLoading: setIsLoading, runTutorial } = useContext(StateContext); - const [ projectData, setProjectData ] = useState(defaultProjectData); const [ avatars, setAvatars ] = useState([]); const [ hasUserContributed, setHasUserContributed ] = useState(false); const [ largestCircuitConstraints, setLargestCircuitConstraints ] = useState(maxConstraintsForBrowser + 1) // contribution on browser has 100000 max constraints const [ finalZkeys, setFinalZkeys ] = useState([]) const [ latestZkeys, setLatestZkeys ] = useState([]) const [ finalBeacon, setFinalBeacon ] = useState() + const [ circuitsClean, setCircuitsClean ] = useState([]); + const [ contributionsClean, setContributionsClean ] = useState([]); const { projects } = useStateContext(); const { ceremonyName } = useParams(); @@ -123,7 +128,51 @@ export const ProjectPageProvider: React.FC = ({ childr const parsedData = ProjectDataSchema.parse(updatedProjectData); - setProjectData(parsedData); + setCircuitsClean( + parsedData.circuits?.map((circuit) => ({ + template: circuit.data.template, + compiler: circuit.data.compiler, + name: circuit.data.name, + description: circuit.data.description, + constraints: circuit.data.metadata?.constraints, + pot: circuit.data.metadata?.pot, + privateInputs: circuit.data.metadata?.privateInputs, + publicInputs: circuit.data.metadata?.publicInputs, + curve: circuit.data.metadata?.curve, + wires: circuit.data.metadata?.wires, + completedContributions: circuit.data.waitingQueue?.completedContributions, + currentContributor: circuit.data.waitingQueue?.currentContributor, + memoryRequirement: bytesToMegabytes(circuit.data.zKeySizeInBytes ?? Math.pow(1024, 2)) + .toString() + .slice(0, 5), + avgTimingContribution: Math.round(Number(circuit.data.avgTimings?.fullContribution) / 1000), + maxTiming: Math.round((Number(circuit.data.avgTimings?.fullContribution) * 1.618) / 1000) + })) ?? [] + ); + + // parse contributions and sort by zkey name. FIlter for valid contribs. + setContributionsClean( + parsedData.contributions?.map((contribution) => ({ + doc: contribution.data.files?.lastZkeyFilename ?? "", + verificationComputationTime: contribution.data?.verificationComputationTime ?? "", + valid: contribution.data?.valid ?? false, + lastUpdated: parseDate(contribution.data?.lastUpdated ?? ""), + lastZkeyBlake2bHash: truncateString(contribution.data?.files?.lastZkeyBlake2bHash ?? "", 10), + transcriptBlake2bHash: truncateString( + contribution.data?.files?.transcriptBlake2bHash ?? "", + 10 + ) + })).slice() + .filter((c: any) => c.valid) + .sort((a: any, b: any) => { + const docA = a.doc.toLowerCase() + const docB = b.doc.toLowerCase() + + if (docA < docB) return -1 + if (docA > docB) return 1 + return 0 + }) ?? [] + ); const avatars = await getParticipantsAvatar(projectId) setAvatars(avatars) @@ -146,7 +195,18 @@ export const ProjectPageProvider: React.FC = ({ childr return ( - + {children} ); diff --git a/web/src/helpers/interfaces.ts b/web/src/helpers/interfaces.ts index 76087f3..c7718ce 100644 --- a/web/src/helpers/interfaces.ts +++ b/web/src/helpers/interfaces.ts @@ -1,6 +1,5 @@ import { DocumentData, DocumentReference } from "firebase/firestore"; import { z } from "zod"; -import { ProjectDataSchema } from "../context/ProjectPageContext"; /** * Define different states of a ceremony. @@ -533,21 +532,10 @@ export interface State { waitingQueue: WaitingQueue[]; } -/** - * Define the data structure for the project page context. - * @typedef {Object} ProjectData - * @property {string} ceremonyName - the name of the ceremony. - * @property {string} ceremonyDescription - the description of the ceremony. - * @property {string} ceremonyState - the state of the ceremony. - * @property {string} ceremonyType - the type of the ceremony. - */ -export type ProjectData = z.infer; - /** * Define the data structure for the project page context. * @typedef {Object} ProjectPageContextProps * @property {boolean} hasUserContributed - true if the user has contributed to the project; otherwise false. - * @property {ProjectData} projectData - the data about the project. * @property {boolean} isLoading - true if the data is being loaded; otherwise false. * @property {boolean} runTutorial - true if the tutorial should be run; otherwise false. * @property {string[]} [avatars] - the list of avatars for the participants. @@ -558,7 +546,6 @@ export type ProjectData = z.infer; */ export type ProjectPageContextProps = { hasUserContributed: boolean - projectData: ProjectData | null isLoading: boolean runTutorial: boolean avatars?: string[] diff --git a/web/src/pages/ProjectPage.tsx b/web/src/pages/ProjectPage.tsx index ec40cb9..32912fc 100644 --- a/web/src/pages/ProjectPage.tsx +++ b/web/src/pages/ProjectPage.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useState, useEffect, useContext } from "react"; import { useParams } from "react-router-dom"; import { Box, @@ -10,50 +10,29 @@ import { TabList, TabPanels, Tab, - TabPanel, Button, - Stat, - StatLabel, - StatNumber, - Tag, Heading, - Spacer, Breadcrumb, BreadcrumbItem, - Flex, - Icon, - SimpleGrid, - SkeletonText, - Table, - Tbody, - Td, - Th, - Thead, - Tooltip, - Tr, - Skeleton, - Stack, SkeletonCircle, + SkeletonText, useClipboard, } from "@chakra-ui/react"; import { StateContext } from "../context/StateContext"; +import { useProjectPageContext } from "../context/ProjectPageContext"; +import { FaCopy } from "react-icons/fa"; +import { CeremonyState } from "../helpers/interfaces"; import { - ProjectDataSchema, - useProjectPageContext -} from "../context/ProjectPageContext"; -import { FaCloudDownloadAlt, FaCopy } from "react-icons/fa"; -import { CeremonyState, ProjectData } from "../helpers/interfaces"; -import { FiTarget, FiZap, FiEye, FiUser, FiMapPin, FiWifi } from "react-icons/fi"; -import { - bytesToMegabytes, formatDate, getTimeDifference, - parseDate, singleProjectPageSteps, truncateString, - parseRepoRoot } from "../helpers/utils"; import Joyride, { STATUS } from "react-joyride"; +import { ProjectTabStats } from "../components/ProjectTabStats"; +import { ProjectTabContributions } from "../components/ProjectTabContributions"; +import { ProjectTabAbout } from "../components/ProjectTabAbout"; +import { ProjectTabZKey } from "../components/ProjectTabZKey"; import ScrollingAvatars from "../components/Avatars"; import { Contribution } from "../components/Contribution"; import { maxConstraintsForBrowser } from "../helpers/constants"; @@ -65,7 +44,18 @@ type RouteParams = { const ProjectPage: React.FC = () => { const { ceremonyName } = useParams(); const { user, projects, setRunTutorial, runTutorial } = useContext(StateContext); - const { latestZkeys, finalBeacon, finalZkeys, hasUserContributed, projectData, isLoading, avatars, largestCircuitConstraints } = useProjectPageContext(); + const { + hasUserContributed, + isLoading, + largestCircuitConstraints + } = useProjectPageContext(); + + const [project, setProject] = useState(null); + useEffect(() => { + // find a project with the given ceremony name + setProject(projects.find((p) => p.ceremony.data.title === ceremonyName)); + }, [projects]); + // handle the callback from joyride const handleJoyrideCallback = (data: any) => { const { status } = data; @@ -75,56 +65,6 @@ const ProjectPage: React.FC = () => { } }; - // find a project with the given ceremony name - const project = projects.find((p) => p.ceremony.data.title === ceremonyName); - - // Validate the project data against the schema - const validatedProjectData: ProjectData = ProjectDataSchema.parse(projectData); - - const circuitsClean = - validatedProjectData.circuits?.map((circuit) => ({ - template: circuit.data.template, - compiler: circuit.data.compiler, - name: circuit.data.name, - description: circuit.data.description, - constraints: circuit.data.metadata?.constraints, - pot: circuit.data.metadata?.pot, - privateInputs: circuit.data.metadata?.privateInputs, - publicInputs: circuit.data.metadata?.publicInputs, - curve: circuit.data.metadata?.curve, - wires: circuit.data.metadata?.wires, - completedContributions: circuit.data.waitingQueue?.completedContributions, - currentContributor: circuit.data.waitingQueue?.currentContributor, - memoryRequirement: bytesToMegabytes(circuit.data.zKeySizeInBytes ?? Math.pow(1024, 2)) - .toString() - .slice(0, 5), - avgTimingContribution: Math.round(Number(circuit.data.avgTimings?.fullContribution) / 1000), - maxTiming: Math.round((Number(circuit.data.avgTimings?.fullContribution) * 1.618) / 1000) - })) ?? []; - - // parse contributions and sort by zkey name. FIlter for valid contribs. - const contributionsClean = - validatedProjectData.contributions?.map((contribution) => ({ - doc: contribution.data.files?.lastZkeyFilename ?? "", - verificationComputationTime: contribution.data?.verificationComputationTime ?? "", - valid: contribution.data?.valid ?? false, - lastUpdated: parseDate(contribution.data?.lastUpdated ?? ""), - lastZkeyBlake2bHash: truncateString(contribution.data?.files?.lastZkeyBlake2bHash ?? "", 10), - transcriptBlake2bHash: truncateString( - contribution.data?.files?.transcriptBlake2bHash ?? "", - 10 - ) - })).slice() - .filter((c: any) => c.valid) - .sort((a: any, b: any) => { - const docA = a.doc.toLowerCase() - const docB = b.doc.toLowerCase() - - if (docA < docB) return -1 - if (docA > docB) return 1 - return 0 - }) ?? []; - // Commands const contributeCommand = !project || isLoading @@ -132,15 +72,11 @@ const ProjectPage: React.FC = () => { : `phase2cli auth && phase2cli contribute -c ${project?.ceremony.data.prefix}`; const installCommand = `npm install -g @p0tion/phase2cli`; const authCommand = `phase2cli auth`; - const beaconValue = finalBeacon?.beacon - const beaconHash = finalBeacon?.beaconHash - + // Hook for clipboard const { onCopy: copyContribute, hasCopied: copiedContribute } = useClipboard(contributeCommand); const { onCopy: copyInstall, hasCopied: copiedInstall } = useClipboard(installCommand); const { onCopy: copyAuth, hasCopied: copiedAuth } = useClipboard(authCommand); - const { onCopy: copyBeaconValue, hasCopied: copiedBeaconValue } = useClipboard(beaconValue || "") - const { onCopy: copyBeaconHash, hasCopied: copiedBeaconHash } = useClipboard(beaconHash || "") return ( <> @@ -311,7 +247,7 @@ const ProjectPage: React.FC = () => { maxW={["390px", "390px", "100%"]} minW={["390px", "390px", null]} > - + { - - - - {circuitsClean.map((circuit, index) => ( - - - {circuit.name} - {circuit.description} - - - - - Constraints: {circuit.constraints} - - - - Pot: {circuit.pot} - - - - Private Inputs: {circuit.privateInputs} - - - - Public Inputs: {circuit.publicInputs} - - - - Curve: {circuit.curve} - - - - Wires: {circuit.wires} - - - - - - Completed Contributions - - {circuit.completedContributions} - - - - - Memory Requirement - - {circuit.memoryRequirement} mb - - - - Avg Contribution Time - - {circuit.avgTimingContribution}s - - - - Max Contribution Time - {circuit.maxTiming}s - - - - ))} - - - - - - - Contributions - - - - - {!contributionsClean || isLoading ? ( - - - - - - ) : ( - - - - - - - - - - {contributionsClean.map((contribution, index) => ( - - - - - - ))} - -
DocContribution DateHashes
{contribution.doc}{contribution.lastUpdated} - - {contribution.lastZkeyBlake2bHash} - -
- )} -
-
- - - - - {circuitsClean.map((circuit, index) => ( - - - {circuit.name} - {circuit.description} - - - - - Parameters - - { - circuit.template.paramsConfiguration && circuit.template.paramsConfiguration.length > 0 ? - circuit.template.paramsConfiguration.join(" ") : - circuit.template.paramConfiguration && circuit.template.paramConfiguration.length > 0 ? - circuit.template.paramConfiguration.join(" ") : - "No parameters" - } - - - - - Commit Hash - - - {truncateString(circuit.template.commitHash, 6)} - - - - - Template Link - - - {truncateString(circuit.template.source, 16)} - - - - - Compiler Version - - {circuit.compiler.version} - - - - - ))} - - - - - - - { - project?.ceremony.data.state === CeremonyState.FINALIZED && beaconHash && beaconValue && -
- - Final contribution beacon - - - -
- } - - Download Final ZKey(s) - - - Press the button below to download the final ZKey files from the S3 bucket. - - { - finalZkeys?.map((zkey, index) => { - return ( - - - - ) - }) - } - { - project?.ceremony.data.state === CeremonyState.FINALIZED && - <> - - Download Last ZKey(s) - - - You can use this zKey(s) with the beacon value to verify that the final zKey(s) was computed correctly. - - { - latestZkeys?.map((zkey, index) => { - return ( - - - - ) - }) - } - - } - -
+ + + +
From 01b04c96344065ce9cd629d1034354828f2796fe Mon Sep 17 00:00:00 2001 From: numtel Date: Tue, 10 Sep 2024 22:20:33 -0400 Subject: [PATCH 2/5] chore(web): pagination for contributions tab --- web/src/components/Pagination.tsx | 34 +++++++++++++++++++ .../components/ProjectTabContributions.tsx | 25 ++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 web/src/components/Pagination.tsx diff --git a/web/src/components/Pagination.tsx b/web/src/components/Pagination.tsx new file mode 100644 index 0000000..79c6463 --- /dev/null +++ b/web/src/components/Pagination.tsx @@ -0,0 +1,34 @@ +import React, { useState } from "react"; +import { + Button, + HStack, + Text, +} from "@chakra-ui/react"; + +export const Pagination: React.FC = ({ currentPage, totalPages, onPageChange }) => { + const handlePrevious = () => { + if (currentPage > 1) { + onPageChange(currentPage - 1); + } + }; + + const handleNext = () => { + if (currentPage < totalPages) { + onPageChange(currentPage + 1); + } + }; + + return ( + + + + Page {currentPage} of {totalPages} + + + + ); +}; diff --git a/web/src/components/ProjectTabContributions.tsx b/web/src/components/ProjectTabContributions.tsx index 633e859..910c4af 100644 --- a/web/src/components/ProjectTabContributions.tsx +++ b/web/src/components/ProjectTabContributions.tsx @@ -1,3 +1,4 @@ +import React, { useState } from "react"; import { Box, HStack, @@ -17,9 +18,17 @@ import { } from "@chakra-ui/react"; import { useProjectPageContext } from "../context/ProjectPageContext"; +import { Pagination } from "./Pagination"; export const ProjectTabContributions: React.FC = () => { const { contributionsClean } = useProjectPageContext(); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 100; + const totalPages = Math.ceil(contributionsClean.length / itemsPerPage); + + const startIndex = (currentPage - 1) * itemsPerPage; + const currentItems = contributionsClean.slice(startIndex, startIndex + itemsPerPage); + return ( @@ -35,7 +44,12 @@ export const ProjectTabContributions: React.FC = () => { - ) : ( + ) : (<> + @@ -45,7 +59,7 @@ export const ProjectTabContributions: React.FC = () => { - {contributionsClean.map((contribution, index) => ( + {currentItems.map((contribution, index) => ( @@ -61,7 +75,12 @@ export const ProjectTabContributions: React.FC = () => { ))}
{contribution.doc} {contribution.lastUpdated}
- )} + + )}
); From 5cd51aea0d18e066ed66499485e30dbf8bc7dd07 Mon Sep 17 00:00:00 2001 From: numtel Date: Tue, 10 Sep 2024 22:21:31 -0400 Subject: [PATCH 3/5] chore(web): prevent scrolljacking and only load active TabPanel --- web/src/pages/ProjectPage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/pages/ProjectPage.tsx b/web/src/pages/ProjectPage.tsx index 32912fc..fb4cb44 100644 --- a/web/src/pages/ProjectPage.tsx +++ b/web/src/pages/ProjectPage.tsx @@ -112,7 +112,6 @@ const ProjectPage: React.FC = () => { callback={handleJoyrideCallback} continuous run={runTutorial} - scrollToFirstStep showProgress showSkipButton steps={singleProjectPageSteps} @@ -259,7 +258,7 @@ const ProjectPage: React.FC = () => { flexGrow={1} justifyContent={"flex-start"} > - + Live Stats From 1c5cdf24c494f07d02e1127f1074701f96d18d28 Mon Sep 17 00:00:00 2001 From: numtel Date: Tue, 10 Sep 2024 22:26:48 -0400 Subject: [PATCH 4/5] chore(web): only the avatars currently in view are loaded --- web/src/components/Avatars.tsx | 114 ++++++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 23 deletions(-) diff --git a/web/src/components/Avatars.tsx b/web/src/components/Avatars.tsx index 5ebb102..3be9640 100644 --- a/web/src/components/Avatars.tsx +++ b/web/src/components/Avatars.tsx @@ -1,32 +1,100 @@ -import React from "react" -import { Avatar, Box } from "@chakra-ui/react" - +import React, { useState, useEffect, useRef } from "react"; +import { Avatar, Box } from "@chakra-ui/react"; import { useProjectPageContext } from "../context/ProjectPageContext"; /** - * Display the participants Avatars in a scrolling list + * Hook to observe when a specific avatar is in the viewport + */ +const useIntersectionObserver = (setInView: (inView: boolean) => void) => { + const observerRef = useRef(null); + + const observe = (element: HTMLElement | null) => { + if (observerRef.current) { + observerRef.current.disconnect(); + } + + observerRef.current = new IntersectionObserver(([entry]) => { + setInView(entry.isIntersecting); + }); + + if (element) { + observerRef.current.observe(element); + } + }; + + useEffect(() => { + return () => { + if (observerRef.current) { + observerRef.current.disconnect(); + } + }; + }, []); + + return observe; +}; + +/** + * Display the participants' Avatars in a scrolling list with lazy loading. */ const ScrollingAvatars: React.FC = () => { - const {avatars} = useProjectPageContext(); - return ( - - {avatars && avatars.length > 0 && avatars.map((image: string, index: any) => ( - ({}); + + const handleAvatarInView = (index: number, inView: boolean) => { + if (inView) { + setLoadedAvatars((prev) => ({ ...prev, [index]: true })); + } + }; + + return ( + + {avatars && + avatars.length > 0 && + avatars.map((image: string, index: number) => ( + handleAvatarInView(index, inView)} /> ))} - - ) - } - -export default ScrollingAvatars + + ); +}; + +/** + * Lazy loading Avatar component + */ +const LazyAvatar: React.FC<{ + src: string; + index: number; + isLoaded: boolean; + onInViewChange: (inView: boolean) => void; +}> = ({ src, index, isLoaded, onInViewChange }) => { + const avatarRef = useRef(null); + const observe = useIntersectionObserver((inView) => onInViewChange(inView)); + + useEffect(() => { + if (avatarRef.current) { + observe(avatarRef.current); + } + }, [avatarRef, observe]); + + return ( + + {isLoaded ? : } + + ); +}; + +export default ScrollingAvatars; + From 843489532113aacfb85d1e3bc538f5bae7e31d57 Mon Sep 17 00:00:00 2001 From: numtel Date: Thu, 19 Sep 2024 10:04:29 -0700 Subject: [PATCH 5/5] chore(web): contribution circuit select instead of pagination --- web/src/components/Pagination.tsx | 34 ------------ .../components/ProjectTabContributions.tsx | 54 ++++++++++++------- web/src/context/ProjectPageContext.tsx | 15 +++--- 3 files changed, 43 insertions(+), 60 deletions(-) delete mode 100644 web/src/components/Pagination.tsx diff --git a/web/src/components/Pagination.tsx b/web/src/components/Pagination.tsx deleted file mode 100644 index 79c6463..0000000 --- a/web/src/components/Pagination.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useState } from "react"; -import { - Button, - HStack, - Text, -} from "@chakra-ui/react"; - -export const Pagination: React.FC = ({ currentPage, totalPages, onPageChange }) => { - const handlePrevious = () => { - if (currentPage > 1) { - onPageChange(currentPage - 1); - } - }; - - const handleNext = () => { - if (currentPage < totalPages) { - onPageChange(currentPage + 1); - } - }; - - return ( - - - - Page {currentPage} of {totalPages} - - - - ); -}; diff --git a/web/src/components/ProjectTabContributions.tsx b/web/src/components/ProjectTabContributions.tsx index 910c4af..76be8ce 100644 --- a/web/src/components/ProjectTabContributions.tsx +++ b/web/src/components/ProjectTabContributions.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Box, HStack, @@ -13,21 +13,36 @@ import { Thead, Tooltip, Tr, + Select, Skeleton, Stack, } from "@chakra-ui/react"; import { useProjectPageContext } from "../context/ProjectPageContext"; -import { Pagination } from "./Pagination"; export const ProjectTabContributions: React.FC = () => { const { contributionsClean } = useProjectPageContext(); - const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 100; - const totalPages = Math.ceil(contributionsClean.length / itemsPerPage); + const [currentPage, setCurrentPage] = useState(''); + const [currentItems, setCurrentItems] = useState([]); - const startIndex = (currentPage - 1) * itemsPerPage; - const currentItems = contributionsClean.slice(startIndex, startIndex + itemsPerPage); + useEffect(() => { + setCurrentPage(Object.keys(contributionsClean)[0]); + }, [ contributionsClean ]); + + useEffect(() => { + if(!(currentPage in contributionsClean)) { + setCurrentItems([]); + return; + } + setCurrentItems(contributionsClean[currentPage].sort((a, b) => { + const docA = a.doc.toLowerCase() + const docB = b.doc.toLowerCase() + + if (docA < docB) return -1 + if (docA > docB) return 1 + return 0 + })); + }, [ currentPage ]); return ( @@ -45,11 +60,19 @@ export const ProjectTabContributions: React.FC = () => { ) : (<> - + @@ -59,7 +82,7 @@ export const ProjectTabContributions: React.FC = () => { - {currentItems.map((contribution, index) => ( + {currentItems?.map((contribution, index) => ( @@ -75,11 +98,6 @@ export const ProjectTabContributions: React.FC = () => { ))}
{contribution.doc} {contribution.lastUpdated}
- )}
diff --git a/web/src/context/ProjectPageContext.tsx b/web/src/context/ProjectPageContext.tsx index 765de39..d4a0a2f 100644 --- a/web/src/context/ProjectPageContext.tsx +++ b/web/src/context/ProjectPageContext.tsx @@ -154,6 +154,8 @@ export const ProjectPageProvider: React.FC = ({ childr setContributionsClean( parsedData.contributions?.map((contribution) => ({ doc: contribution.data.files?.lastZkeyFilename ?? "", + circuit: contribution.data.files?.lastZkeyFilename + .replace(`_${contribution.data.zkeyIndex}.zkey`, ''), verificationComputationTime: contribution.data?.verificationComputationTime ?? "", valid: contribution.data?.valid ?? false, lastUpdated: parseDate(contribution.data?.lastUpdated ?? ""), @@ -164,14 +166,11 @@ export const ProjectPageProvider: React.FC = ({ childr ) })).slice() .filter((c: any) => c.valid) - .sort((a: any, b: any) => { - const docA = a.doc.toLowerCase() - const docB = b.doc.toLowerCase() - - if (docA < docB) return -1 - if (docA > docB) return 1 - return 0 - }) ?? [] + .reduce((out, cur) => { + if(!(cur.circuit in out)) out[cur.circuit] = []; + out[cur.circuit].push(cur); + return out; + }, {}) ?? {} ); const avatars = await getParticipantsAvatar(projectId)