diff --git a/app/allocation/components/BudgetAllocation.tsx b/app/allocation/components/BudgetAllocation.tsx index b3385d9..16e5d60 100644 --- a/app/allocation/components/BudgetAllocation.tsx +++ b/app/allocation/components/BudgetAllocation.tsx @@ -1,146 +1,112 @@ -import { FC } from 'react'; +import React, { useMemo } from 'react'; import Image from 'next/image'; -import Link from 'next/link'; import { useAuth } from '@/app/utils/wallet/AuthProvider'; import { ArrowRightIcon } from '@/public/assets/icon-components/ArrowRightIcon'; import { CollectionProgressStatusEnum } from '@/app/comparison/utils/types'; -import { CheckIcon } from '@/public/assets/icon-components/Check'; -import { UserColabGroupIcon } from '@/public/assets/icon-components/UserColabGroup'; +import Loading from '@/app/components/Loading'; +import VotedCategory from './ProgressCards/VotedCategory'; +import DelegatedCategory from './ProgressCards/DelegatedCategory'; +import PendingCategory from './ProgressCards/PendingCategory'; export interface BudgetCategory { id: number imageSrc: string - title: string + name: string description: string - status: string - delegations: number } -interface BudgetAllocationProps extends BudgetCategory { +interface IBudgetAllocationProps extends BudgetCategory { + progress: CollectionProgressStatusEnum + delegations: number + loading: boolean + username?: string onDelegate: () => void onScore: () => void } -const BudgetAllocation: FC = ({ +const BudgetAllocation: React.FC = ({ + id, imageSrc, - title, + name, description, delegations, - status, - onDelegate, + loading, + progress = CollectionProgressStatusEnum.Pending, + username, onScore, + onDelegate, }) => { const { isAutoConnecting } = useAuth(); + const renderProgressState = useMemo(() => { + switch (progress) { + case CollectionProgressStatusEnum.Finished: + return ; + case CollectionProgressStatusEnum.Delegated: + return ( + + ); + case CollectionProgressStatusEnum.Pending: + default: + return ( + + ); + } + }, [progress, delegations, isAutoConnecting]); + return (
-
-
-
- {title} -
-
- - {title} - - -

{description}

-
-
-
+
+ +
-
+
- {status === CollectionProgressStatusEnum.Pending - ? ( -
-
- - -
- {!!delegations && ( -
- -

- - {delegations > 1 - ? delegations + ' people' - : delegations + ' person'} - - {' '} - delegated to you -

-
- )} -
- ) - : status === CollectionProgressStatusEnum.Finished - ? ( -
- -
-

Voted

- -
- -
- ) - : ( - status === CollectionProgressStatusEnum.Delegated && ( -
-
- -

Delegated

-
-
-

- You delegated to - {' '} - @username -

-
- -
- ) - )} + {loading ? : renderProgressState}
); }; +const ImageContainer: React.FC<{ src: string, alt: string }> = ({ + src, + alt, +}) => ( +
+ {alt} +
+); + +const ProjectInfo: React.FC<{ + name: string + description: string + isDelegated?: boolean + onScore?: () => void +}> = ({ name, description, isDelegated, onScore }) => ( +
+ +

{description}

+
+); + export default BudgetAllocation; diff --git a/app/allocation/components/CategoryAllocation.tsx b/app/allocation/components/CategoryAllocation.tsx index a460be8..5862f8d 100644 --- a/app/allocation/components/CategoryAllocation.tsx +++ b/app/allocation/components/CategoryAllocation.tsx @@ -1,61 +1,70 @@ -import debounce from 'lodash.debounce'; -import Image from 'next/image'; import { ChangeEventHandler, FC, useEffect, useRef } from 'react'; -import { useRouter } from 'next/navigation'; +import debounce from 'lodash.debounce'; import Link from 'next/link'; +import Image from 'next/image'; import { roundFractions } from '../utils'; import { useAuth } from '@/app/utils/wallet/AuthProvider'; +import { CollectionProgressStatusEnum } from '@/app/comparison/utils/types'; +import { TCategory } from '@/app/comparison/utils/data-fetching/categories'; import { ArrowRightIcon } from '@/public/assets/icon-components/ArrowRightIcon'; -import { LockIcon } from '@/public/assets/icon-components/Lock'; import { UnlockIcon } from '@/public/assets/icon-components/Unlock'; +import { LockIcon } from '@/public/assets/icon-components/Lock'; +import Loading from '@/app/components/Loading'; +import VotedCategory from './ProgressCards/VotedCategory'; +import DelegatedCategory from './ProgressCards/DelegatedCategory'; +import PendingCategory from './ProgressCards/PendingCategory'; import { - CollectionProgressStatus, - CollectionProgressStatusEnum, -} from '@/app/comparison/utils/types'; -import { CheckIcon } from '@/public/assets/icon-components/Check'; -import { UserColabGroupIcon } from '@/public/assets/icon-components/UserColabGroup'; -import { categoryIdSlugMap } from '@/app/comparison/utils/helpers'; + categoryIdSlugMap, + formatBudget, +} from '@/app/comparison/utils/helpers'; -export interface Category { - id: number - imageSrc: string - title: string - description: string - projectCount: number - status: CollectionProgressStatus - delegations: number -} +// Image source map for collections +const collectionsImageSrc = new Map([ + [1, '/assets/images/category-it.svg'], + [2, '/assets/images/category-gra.svg'], + [3, '/assets/images/category-gl.svg'], +]); -interface CategoryAllocationProps extends Category { +interface CategoryAllocationProps extends TCategory { allocationPercentage: number allocatingBudget: boolean + allocationBudget: number + locked: boolean + delegations: number + loading: boolean + username?: string onDelegate: () => void onScore: () => void onLockClick: () => void - locked: boolean onPercentageChange: (value: number) => void } const CategoryAllocation: FC = ({ id, allocatingBudget, - imageSrc, - title, + name, description, projectCount, - status, + progress, allocationPercentage, + allocationBudget, locked, delegations, + loading, + username, onDelegate, onScore, onLockClick, onPercentageChange, }) => { const { isAutoConnecting } = useAuth(); - const router = useRouter(); - const inputRef = useRef(null); + + const hrefLink + = progress === CollectionProgressStatusEnum.Finished + ? `/allocation/${categoryIdSlugMap.get(id)}` + : `/comparison/${categoryIdSlugMap.get(id)}`; + const handleInputChange: ChangeEventHandler = debounce( (event) => { const value = event.target.value; @@ -75,34 +84,47 @@ const CategoryAllocation: FC = ({ }, 150); useEffect(() => { - if (inputRef.current?.value) { + if (inputRef.current) { inputRef.current.value = `${allocationPercentage}`; } }, [allocationPercentage]); + const renderProgressState = () => { + if (loading) return ; + switch (progress) { + case CollectionProgressStatusEnum.Delegated: + return ( + + ); + case CollectionProgressStatusEnum.Finished: + return ; + case CollectionProgressStatusEnum.Pending: + default: + return ( + + ); + } + }; + return (
-
-
-
- {title} -
-
- - {title} - - -

{description}

- {projectCount && ( -

- {`${projectCount} project${projectCount > 1 ? 's' : ''}`} -

- )} -
-
-
+
+ +
-
+ +
{allocatingBudget ? ( @@ -132,8 +154,10 @@ const CategoryAllocation: FC = ({
-

- {(allocationPercentage * 100000).toLocaleString() + ' OP'} +

+ {formatBudget(allocationBudget)} + {' '} + OP

@@ -147,100 +171,51 @@ const CategoryAllocation: FC = ({
) - : status === CollectionProgressStatusEnum.Pending - ? ( -
-
- - -
- {!!delegations && ( -
- -

- - {delegations > 1 - ? delegations + ' people' - : delegations + ' person'} - - {' '} - delegated to you -

-
- )} -
- ) - : status === CollectionProgressStatusEnum.Finished - ? ( -
- -
-

Voted

- -
- -
- ) - : ( - status === CollectionProgressStatusEnum.Delegated && ( -
-
- -

Delegated

-
-
-

- You delegated to - {' '} - @username -

-
- -
- ) - )} + : ( + renderProgressState() + )}
); }; +const ImageContainer: FC<{ src: string, alt: string }> = ({ src, alt }) => ( +
+ {alt} +
+); + +const ProjectInfo: FC<{ + name: string + description: string + projectCount?: number + hrefLink: string + isDelegated?: boolean +}> = ({ name, description, projectCount, hrefLink, isDelegated }) => ( +
+ {isDelegated + ? ( +

+ {name} + +

+ ) + : ( + + {name} + + + )} +

{description}

+ {projectCount && ( +

+ {`${projectCount} project${projectCount > 1 ? 's' : ''}`} +

+ )} +
+); + export default CategoryAllocation; diff --git a/app/allocation/components/ProgressCards/DelegatedCategory.tsx b/app/allocation/components/ProgressCards/DelegatedCategory.tsx new file mode 100644 index 0000000..7e7c71f --- /dev/null +++ b/app/allocation/components/ProgressCards/DelegatedCategory.tsx @@ -0,0 +1,43 @@ +import { useRevokeDelegation } from '@/app/comparison/utils/data-fetching/delegation'; +import { CheckIcon } from '@/public/assets/icon-components/Check'; + +type TDelegatedCategoryProps = { + id: number + isAutoConnecting: boolean + username?: string +}; + +const DelegatedCategory = ({ + id, + isAutoConnecting, + username, +}: TDelegatedCategoryProps) => { + const { mutate: revokeDelegation } = useRevokeDelegation(id); + + return ( +
+
+ +

Delegated

+
+ {username && ( +
+

+ You delegated to + {' '} + {username} +

+
+ )} + +
+ ); +}; + +export default DelegatedCategory; diff --git a/app/allocation/components/ProgressCards/PendingCategory.tsx b/app/allocation/components/ProgressCards/PendingCategory.tsx new file mode 100644 index 0000000..d789946 --- /dev/null +++ b/app/allocation/components/ProgressCards/PendingCategory.tsx @@ -0,0 +1,58 @@ +import { UserColabGroupIcon } from '@/public/assets/icon-components/UserColabGroup'; + +type TPendingCategoryProps = { + onScore: () => void + onDelegate: () => void + isAutoConnecting: boolean + delegations?: number +}; + +const PendingCategory = ({ + onScore, + onDelegate, + isAutoConnecting, + delegations, +}: TPendingCategoryProps) => { + return ( +
+
+ + +
+ {!!delegations && ( +
+ +

+ + {delegations > 1 + ? delegations + ' people' + : delegations + ' person'} + + {' '} + delegated to you +

+
+ )} +
+ ); +}; + +export default PendingCategory; diff --git a/app/allocation/components/ProgressCards/VotedCategory.tsx b/app/allocation/components/ProgressCards/VotedCategory.tsx new file mode 100644 index 0000000..1d71b41 --- /dev/null +++ b/app/allocation/components/ProgressCards/VotedCategory.tsx @@ -0,0 +1,39 @@ +import { useRouter } from 'next/navigation'; +import { categoryIdSlugMap } from '@/app/comparison/utils/helpers'; +import { CheckIcon } from '@/public/assets/icon-components/Check'; + +type TVotedCategoryProps = { + id: number + isAutoConnecting: boolean +}; + +const VotedCategory = ({ + id, + isAutoConnecting, +}: TVotedCategoryProps) => { + const router = useRouter(); + + return ( +
+ +
+

Voted

+ +
+ +
+ ); +}; + +export default VotedCategory; diff --git a/app/allocation/components/hooks/getCategories.ts b/app/allocation/components/hooks/getCategories.ts deleted file mode 100644 index 60546f0..0000000 --- a/app/allocation/components/hooks/getCategories.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { AxiosResponse } from 'axios'; -import { axiosInstance } from '@/app/utils/axiosInstance'; -import { ICategory } from '../../../comparison/utils/types'; - -export const getCategories = async (): Promise> => { - return (await axiosInstance.get('flow/collections')).data; -}; - -export const useCategories = () => { - return useQuery({ - queryKey: ['categories'], - queryFn: getCategories, - }); -}; diff --git a/app/allocation/components/hooks/getCategoryRankings.ts b/app/allocation/components/hooks/getCategoryRankings.ts deleted file mode 100644 index e00fa06..0000000 --- a/app/allocation/components/hooks/getCategoryRankings.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { axiosInstance } from '@/app/utils/axiosInstance'; -import { IProjectsRankingResponse } from '../../../comparison/utils/data-fetching/ranking'; -import { ICategory } from '@/app/comparison/utils/types'; - -interface ICategoryRankingResponse - extends Omit { - ranking: ICategory[] -} - -export const getCategoryRankings - = async (): Promise => { - const res = await axiosInstance.get('flow/ranking'); - - return res.data; - }; - -export const useCategoryRankings = () => { - return useQuery({ - queryKey: ['category-ranking'], - queryFn: () => getCategoryRankings(), - }); -}; diff --git a/app/allocation/components/hooks/getProjectsRankingByCategoryId.ts b/app/allocation/components/hooks/getProjectsRankingByCategoryId.ts deleted file mode 100644 index 998b3bc..0000000 --- a/app/allocation/components/hooks/getProjectsRankingByCategoryId.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { AxiosResponse } from 'axios'; -import { IProject } from '@/app/comparison/utils/types'; -import { axiosInstance } from '@/app/utils/axiosInstance'; - -export interface IProjectsRankingResponse { - ranking: IProject[] - hasRanking: boolean - isFinished: boolean - progress: string - name: string - share: number - id: number -} - -export const getProjectsRankingByCategoryId = async ( - cid: number -): Promise> => { - return axiosInstance.get(`flow/ranking?cid=${cid}`); -}; - -export const useProjectsRankingByCategoryId = (cid: number) => { - return useQuery({ - queryKey: ['projects-ranking', cid], - queryFn: () => getProjectsRankingByCategoryId(cid), - staleTime: Infinity, - }); -}; diff --git a/app/allocation/components/hooks/updateCategoryVote.ts b/app/allocation/components/hooks/updateCategoryVote.ts deleted file mode 100644 index 8cc780e..0000000 --- a/app/allocation/components/hooks/updateCategoryVote.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { axiosInstance } from '@/app/utils/axiosInstance'; - -type CategoryVoteData = { - data: { - collection1Id: number - collection2Id: number - pickedId: number - } -}; - -export const updateCategoryVote = ({ data }: CategoryVoteData) => { - return axiosInstance.post('/flow/collections/vote', data); -}; - -export const useUpdateCategoryVote = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: updateCategoryVote, - onSuccess: ({ data }) => { - queryClient.refetchQueries({ - queryKey: ['category-pairs'], - }); - queryClient.refetchQueries({ - queryKey: ['category', data.collection1Id], - }); - }, - }); -}; diff --git a/app/allocation/components/hooks/updatePairwiseFinish.ts b/app/allocation/components/hooks/updatePairwiseFinish.ts deleted file mode 100644 index 1a5df09..0000000 --- a/app/allocation/components/hooks/updatePairwiseFinish.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; -import { axiosInstance } from '@/app/utils/axiosInstance'; - -type ProjectVoteData = { - data: { - cid: number - } -}; - -export const updatePairwiseFinish = ({ data }: ProjectVoteData) => { - return axiosInstance.post('flow/finish', data); -}; - -export const useUpdatePairwiseFinish = () => { - // const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: updatePairwiseFinish, - // onSuccess: - }); -}; diff --git a/app/allocation/page.tsx b/app/allocation/page.tsx index 56c28ef..744492c 100644 --- a/app/allocation/page.tsx +++ b/app/allocation/page.tsx @@ -1,13 +1,13 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useActiveWallet } from 'thirdweb/react'; import HeaderRF6 from '../comparison/card/Header-RF6'; import Modal from '../utils/Modal'; import EmailLoginModal from './components/EOA/EmailLoginModal'; -import CategoryAllocation, { Category } from './components/CategoryAllocation'; +import CategoryAllocation from './components/CategoryAllocation'; import BudgetAllocation, { BudgetCategory, } from './components/BudgetAllocation'; @@ -16,8 +16,8 @@ import { modifyPercentage, RankItem } from './utils'; import { ArrowRightIcon } from '@/public/assets/icon-components/ArrowRight'; import { ArrowLeft2Icon } from '@/public/assets/icon-components/ArrowLeft2'; import { CustomizedSlider } from './components/Slider'; -import { categoryIdSlugMap } from '../comparison/utils/helpers'; -import { useCategories } from './components/hooks/getCategories'; +import { categoryIdSlugMap, formatBudget } from '../comparison/utils/helpers'; +import { useCategories } from '../comparison/utils/data-fetching/categories'; import WorldIdSignInSuccessModal from './components/WorldIdSignInSuccessModal'; import FarcasterModal from './components/FarcasterModal'; import DelegateModal from '../delegation/DelegationModal'; @@ -25,68 +25,22 @@ import { FarcasterLookup } from '../delegation/farcaster/FarcasterLookup'; import FarcasterSuccess from '../delegation/farcaster/FarcasterSuccess'; import { axiosInstance } from '../utils/axiosInstance'; import { TargetDelegate } from '../delegation/farcaster/types'; +import { useGetDelegationStatus } from '@/app/utils/getConnectionStatus'; +import { ICategory, CollectionProgressStatusEnum } from '../comparison/utils/types'; +import SmallSpinner from '../components/SmallSpinner'; +import { + useCategoryRankings, + useUpdateCategoriesRanking, +} from '@/app/comparison/utils/data-fetching/ranking'; const budgetCategory: BudgetCategory = { id: -1, - title: 'Budget', + name: 'Budget', description: 'Choose how much OP should be dedicated to this round, or delegate this decision to someone you trust.', imageSrc: '/assets/images/budget-card.svg', - status: 'Pending', - delegations: 0, }; -const Categories: Category[] = [ - { - id: 1, - title: 'Governance Infrastructure & Tooling', - description: - 'Infrastructure and tooling that powered governance or that made the usage of governance infrastructure more accessible.', - imageSrc: '/assets/images/category-it.svg', - projectCount: 20, - status: 'Pending', - delegations: 2, - }, - { - id: 2, - title: 'Governance Analytics', - description: - 'Analytics that enabled accountability, provided transparency into Collective operations, promoted improved performance, or aided in the design of the Collective.', - imageSrc: '/assets/images/category-gra.svg', - projectCount: 15, - status: 'Delegated', - delegations: 1, - }, - { - id: 3, - title: 'Governance Leadership', - description: - 'Demonstrated leadership in the Collective, including but not limited to, hosting community calls and/or participation in councils, boards and commissions beyond executing on basic responsibilities outlined in Token House Charters.', - imageSrc: '/assets/images/category-gl.svg', - projectCount: 30, - status: 'Finished', - delegations: 3, - }, -]; - -const ranks: RankItem[] = [ - { - id: 1, - locked: false, - percentage: 33.4, - }, - { - id: 2, - locked: false, - percentage: 33.3, - }, - { - id: 3, - locked: false, - percentage: 33.3, - }, -]; - enum DelegationState { Initial, DelegationMethod, @@ -98,11 +52,17 @@ const AllocationPage = () => { const wallet = useActiveWallet(); const router = useRouter(); - const { data: categories, isLoading } = useCategories(); - console.log('categories => ', categories, isLoading); + const { data: categories, isLoading: categoriesLoading } = useCategories(); + const { data: delegations, isLoading: delegationsLoading } + = useGetDelegationStatus(); + const { data: categoryRankings } = useCategoryRankings(); + + const colDelegationToYou = delegations?.toYou?.collections; + const colDelegationFromYou = delegations?.fromYou?.collections; + const budgetDelegateToYou = delegations?.toYou?.budget; + const budgetDelegateFromYou = delegations?.fromYou?.budget; - const [categoryRanking, setCategoryRanking] = useState(ranks); - const [totalValue, setTotalValue] = useState(2); + const [totalValue, setTotalValue] = useState(categoryRankings?.budget || 0); const [percentageError, setPercentageError] = useState(); const [isOpenFarcasterModal, setIsOpenFarcasterModal] = useState(false); const [isWorldIdSignSuccessModal, setIsWorldIdSignSuccessModal] @@ -112,14 +72,24 @@ const AllocationPage = () => { const [selectedCategoryId, setSelectedCategoryId] = useState( null ); + const [categoriesRanking, setCategoriesRanking] = useState(); + const [dbudgetProgress, setDbudgetProgress] + = useState( + CollectionProgressStatusEnum.Pending + ); const [delegationState, setDelegationState] = useState( DelegationState.Initial ); const [categoryToDelegate, setCategoryToDelegate] - = useState>(); + = useState>(); const [targetDelegate, setTargetDelegate] = useState(); + const { mutate: updateCategoriesRanking } = useUpdateCategoriesRanking({ + budget: totalValue, + allocationPercentages: categoriesRanking?.map(el => el.percentage / 100) || [], + }); + const handleDelegate = async (username: string, target: TargetDelegate) => { if (!categoryToDelegate) return; @@ -134,12 +104,14 @@ const AllocationPage = () => { const handleLock = (id: RankItem['id']) => () => { try { - const currValue = categoryRanking.find(el => el.id === id)!; - const newRanking = modifyPercentage(categoryRanking, { + if (!categoriesRanking) return; + + const currValue = categoriesRanking.find(el => el.id === id)!; + const newRanking = modifyPercentage(categoriesRanking, { ...currValue, locked: !currValue.locked, }); - setCategoryRanking(newRanking); + setCategoriesRanking(newRanking); setPercentageError(undefined); } catch (e: any) { @@ -149,12 +121,15 @@ const AllocationPage = () => { const handleNewValue = (id: RankItem['id']) => (percentage: number) => { try { - const currValue = categoryRanking.find(el => el.id === id)!; - const newRanking = modifyPercentage(categoryRanking, { + if (!categoriesRanking) return; + + const currValue = categoriesRanking?.find(el => el.id === id)!; + const newRanking = modifyPercentage(categoriesRanking, { ...currValue, percentage, + budget: currValue.budget * (percentage / currValue.percentage), }); - setCategoryRanking(newRanking); + setCategoriesRanking(newRanking); setPercentageError(undefined); } catch (e: any) { @@ -186,6 +161,37 @@ const AllocationPage = () => { router.push(`/comparison/${categoryIdSlugMap.get(id)}`); }; + const getColNumOfDelegations = (id: number) => { + const colDelegation = colDelegationToYou?.filter( + el => el.collectionId === id + ); + return colDelegation?.length || 0; + }; + + useEffect(() => { + if (delegations) { + const budgetDelegateFromYou = delegations?.fromYou?.budget; + + if (budgetDelegateFromYou?.metadata?.username) { + setDbudgetProgress(CollectionProgressStatusEnum.Delegated); + } + } + }, [delegations]); + + useEffect(() => { + if (categoryRankings) { + setCategoriesRanking( + categoryRankings.ranking.map(el => ({ + id: el.projectId, + percentage: Math.round(el.share * 100 * 100) / 100, + locked: false, + budget: categoryRankings.budget * el.share, + })) + ); + setTotalValue(categoryRankings.budget); + } + }, [categoryRankings]); + return (
{ > {delegationState === DelegationState.DelegationMethod && ( { setDelegationState(DelegationState.Lookup); }} @@ -208,12 +214,12 @@ const AllocationPage = () => { {delegationState === DelegationState.Lookup && ( )} {delegationState === DelegationState.Success && targetDelegate && ( { 2M { /> 8M
- {(totalValue * 1_000_000).toLocaleString()} + {formatBudget(totalValue)} {' '} OP
@@ -315,39 +321,66 @@ const AllocationPage = () => { )}
-
- {!allocatingBudget && ( - { - setCategoryToDelegate(budgetCategory); - setDelegationState(DelegationState.DelegationMethod); - }} - onScore={() => { - setAllocatingBudget(true); - }} - /> - )} - {Categories.map((cat) => { - const rank = categoryRanking.find(el => el.id === cat.id)!; - return ( - { - setCategoryToDelegate(cat); - setDelegationState(DelegationState.DelegationMethod); - }} - onLockClick={handleLock(cat.id)} - allocatingBudget={allocatingBudget} - onScore={handleScoreProjects(cat.id)} - allocationPercentage={cat.id === 0 ? 0 : rank.percentage} - onPercentageChange={handleNewValue(cat.id)} - /> - ); - })} -
+ {categoriesLoading + ? ( +
+ +
+ ) + : ( + categories + && categories.length > 0 && ( +
+ {!allocatingBudget && ( + { + setCategoryToDelegate(budgetCategory); + setDelegationState(DelegationState.DelegationMethod); + }} + onScore={() => { + setAllocatingBudget(true); + }} + username={ + budgetDelegateFromYou?.metadata?.username + } + /> + )} + {categories.map((cat) => { + const rank = categoriesRanking?.find( + el => el.id === cat.id + ); + return ( + { + setCategoryToDelegate(cat); + setDelegationState(DelegationState.DelegationMethod); + }} + onLockClick={handleLock(cat.id)} + onScore={handleScoreProjects(cat.id)} + onPercentageChange={handleNewValue(cat.id)} + username={ + colDelegationFromYou?.find( + el => el.collectionId === cat.id + )?.metadata?.username + } + /> + ); + })} +
+ ) + )} {allocatingBudget && ( {percentageError ? `Error: ${percentageError}` : ''} @@ -363,7 +396,12 @@ const AllocationPage = () => { Back to Categories - diff --git a/app/allocation/utils.ts b/app/allocation/utils.ts index d1a94b1..8824548 100644 --- a/app/allocation/utils.ts +++ b/app/allocation/utils.ts @@ -2,6 +2,7 @@ export interface RankItem { id: number percentage: number locked: boolean + budget: number } export const roundFractions = (value: number, fractions: number) => { @@ -15,8 +16,6 @@ export const modifyPercentage = (values: T[], newValue: T): if (currIndex === -1) throw ({ msg: 'New value id not found' }); - console.log(values, newValue); - const newValueDifference = newValue.percentage - values[currIndex].percentage; const restSum = values.reduce((acc, curr) => { @@ -32,6 +31,7 @@ export const modifyPercentage = (values: T[], newValue: T): else return { ...item, percentage: roundFractions(item.percentage + (-1 * newValueDifference * item.percentage / restSum), 2), + budget: roundFractions(item.budget + (-1 * newValueDifference * item.budget / restSum), 2), }; }); diff --git a/app/comparison/utils/data-fetching/categories.ts b/app/comparison/utils/data-fetching/categories.ts new file mode 100644 index 0000000..db8a9c7 --- /dev/null +++ b/app/comparison/utils/data-fetching/categories.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query'; +import { axiosInstance } from '@/app/utils/axiosInstance'; + +export type TCategory = { + id: number + name: string + description: string + image: string + projectCount: number + progress: string +}; + +export const getCategories = async (): Promise => { + const res = await axiosInstance.get('flow/collections'); + return res.data; +}; + +export const useCategories = () => { + return useQuery({ + queryKey: ['categories'], + queryFn: getCategories, + }); +}; diff --git a/app/comparison/utils/data-fetching/delegation.ts b/app/comparison/utils/data-fetching/delegation.ts new file mode 100644 index 0000000..ff30aa5 --- /dev/null +++ b/app/comparison/utils/data-fetching/delegation.ts @@ -0,0 +1,12 @@ +import { useMutation } from '@tanstack/react-query'; +import { axiosInstance } from '@/app/utils/axiosInstance'; + +export const revokeDelegation = async (collectionId: number) => { + await axiosInstance.post('flow/delegate/revoke', { collectionId }); +}; + +export const useRevokeDelegation = (collectionId: number) => { + return useMutation({ + mutationFn: () => revokeDelegation(collectionId), + }); +}; diff --git a/app/comparison/utils/data-fetching/ranking.ts b/app/comparison/utils/data-fetching/ranking.ts index b2941dd..57653d1 100644 --- a/app/comparison/utils/data-fetching/ranking.ts +++ b/app/comparison/utils/data-fetching/ranking.ts @@ -2,9 +2,17 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { axiosInstance } from '@/app/utils/axiosInstance'; import { IProjectRanking, ICategory } from '@/app/comparison/utils/types'; +type TCategoryRanking = { + ranking: ICategory[] + userId: number + projectId: number + share: number + stars: number +}; + interface ICategoryRankingResponse extends Omit { - ranking: ICategory[] + ranking: TCategoryRanking[] } export interface IProjectsRankingResponse { @@ -23,6 +31,11 @@ export interface IProjectRankingObj { share: number } +export interface IUpdateCategoriesRankingBody { + budget: number + allocationPercentages: number[] +} + export const getCategoryRankings = async (): Promise => { const res = await axiosInstance.get('flow/ranking'); @@ -95,3 +108,24 @@ export const useUpdateProjectRanking = ({ }, }); }; + +export const updateCategoriesRanking = async (ranking: IUpdateCategoriesRankingBody) => { + return ( + await axiosInstance.post('flow/budget', { + ...ranking, + }) + ).data; +}; + +export const useUpdateCategoriesRanking = (data: IUpdateCategoriesRankingBody) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => updateCategoriesRanking(data), + onSuccess: () => { + queryClient.refetchQueries({ + queryKey: ['category-ranking'], + }); + }, + }); +}; diff --git a/app/comparison/utils/types.ts b/app/comparison/utils/types.ts index f77e980..a3fb657 100644 --- a/app/comparison/utils/types.ts +++ b/app/comparison/utils/types.ts @@ -1324,12 +1324,12 @@ export type ProjectMetadata = } testimonials: string }; - export interface ICategory { id: number name: string - poll_id: number + pollId: number url: string + description: string impactDescription: string contributionDescription: null | string RPGF5Id: null | number diff --git a/app/components/Loading.tsx b/app/components/Loading.tsx new file mode 100644 index 0000000..8adf7e3 --- /dev/null +++ b/app/components/Loading.tsx @@ -0,0 +1,9 @@ +import styles from '../styles/Spinner.module.css'; + +const Loading = () => ( +
+
+
+); + +export default Loading; diff --git a/app/components/SmallSpinner.tsx b/app/components/SmallSpinner.tsx new file mode 100644 index 0000000..6250db5 --- /dev/null +++ b/app/components/SmallSpinner.tsx @@ -0,0 +1,9 @@ +import styles from '../styles/Spinner.module.css'; + +const SmallSpinner = () => ( +
+
+
+); + +export default SmallSpinner; diff --git a/app/styles/Spinner.module.css b/app/styles/Spinner.module.css index 96db732..420973b 100644 --- a/app/styles/Spinner.module.css +++ b/app/styles/Spinner.module.css @@ -1,14 +1,54 @@ .spinner { - width: 96px; - height: 96px; - border: 8px solid #FF0420; - border-top: 8px solid white; - border-radius: 50%; - animation: spin 1.5s linear infinite; - } - - @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } - \ No newline at end of file + width: 96px; + height: 96px; + border: 8px solid #ff0420; + border-top: 8px solid white; + border-radius: 50%; + animation: spin 1.5s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.smallSpinner { + width: 48px; + height: 48px; + border: 4px solid #ff0420; + border-top: 4px solid white; + border-radius: 50%; + animation: spin 1.5s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* small gray loading */ +.loading { + width: 30px; + height: 30px; + border: 4px solid #d3d3d3; + border-top: 4px solid transparent; + border-radius: 50%; + animation: spin 1.5s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/app/utils/getConnectionStatus.ts b/app/utils/getConnectionStatus.ts index c7433b2..4f00517 100644 --- a/app/utils/getConnectionStatus.ts +++ b/app/utils/getConnectionStatus.ts @@ -17,17 +17,22 @@ interface IDelegateMetadata { username: string profileUrl: string } + +interface IBudget { + metadata: IDelegateMetadata +} + interface ICollection { collectionId: number metadata: IDelegateMetadata } export interface ISocialDelegateResponse { fromYou?: { - budget: IDelegateMetadata | null + budget: IBudget | null collections: ICollection[] } toYou?: { - budget: IDelegateMetadata[] + budget: IBudget[] collections: ICollection[] } }