From b18796002a455923c290e68a4829a82e4b1c42e3 Mon Sep 17 00:00:00 2001
From: Emmanuel Mutisya <emmanuelmutisya254@gmail.com>
Date: Mon, 16 Sep 2024 11:40:03 +0300
Subject: [PATCH 1/4] add pagination to DRep delegators list

---
 backend/src/drep/drep.controller.ts           | 15 +++
 backend/src/drep/drep.service.ts              | 95 ++++++++++++-------
 .../queries/drepDelegatorsWithVotingPower.ts  | 29 +++++-
 .../dreps/[drepid]/delegators/page.tsx        |  4 +-
 .../components/atoms/DrepDelegatorsList.tsx   | 80 ++++++++++++----
 .../src/components/molecules/DRepsTable.tsx   | 47 ++-------
 .../molecules/DrepProfileMetrics.tsx          |  4 +-
 .../src/components/molecules/Pagination.tsx   | 43 +++++++--
 frontend/src/constants/queryKeys.ts           |  3 +-
 .../src/hooks/useGetDrepDelegatorsQuery.ts    | 19 ++++
 .../services/requests/getDrepDelegators.ts    | 12 +++
 frontend/types/api.ts                         | 12 ++-
 12 files changed, 250 insertions(+), 113 deletions(-)
 create mode 100644 frontend/src/hooks/useGetDrepDelegatorsQuery.ts
 create mode 100644 frontend/src/services/requests/getDrepDelegators.ts

diff --git a/backend/src/drep/drep.controller.ts b/backend/src/drep/drep.controller.ts
index 1ebb6fbe..763ad55b 100644
--- a/backend/src/drep/drep.controller.ts
+++ b/backend/src/drep/drep.controller.ts
@@ -148,4 +148,19 @@ export class DrepController {
   isRegistered(@Param('voterId') voterId: string) {
     return this.drepService.isDrepRegistered(voterId);
   }
+
+  @Get(':voterId/delegators')
+  getDrepDelegators(
+    @Param('voterId') voterId: string,
+    @Query('page', new DefaultValuePipe(1), ParseIntPipe)
+    page: number,
+    @Query('perPage', new DefaultValuePipe(24), ParseIntPipe)
+    perPage: number,
+  ) {
+    return this.drepService.getDrepDelegatorsWithVotingPower(
+      voterId,
+      page,
+      perPage
+    );
+  }
 }
diff --git a/backend/src/drep/drep.service.ts b/backend/src/drep/drep.service.ts
index 51b6fe7a..60efa772 100644
--- a/backend/src/drep/drep.service.ts
+++ b/backend/src/drep/drep.service.ts
@@ -1,15 +1,21 @@
-import {HttpException, HttpStatus, Injectable, Logger, NotFoundException,} from '@nestjs/common';
-import {createDrepDto, ValidateMetadataDTO} from 'src/dto';
-import {faker} from '@faker-js/faker';
+import {
+  HttpException,
+  HttpStatus,
+  Injectable,
+  Logger,
+  NotFoundException,
+} from '@nestjs/common';
+import { createDrepDto, ValidateMetadataDTO } from 'src/dto';
+import { faker } from '@faker-js/faker';
 import * as blake from 'blakejs';
-import {HttpService} from '@nestjs/axios';
-import {AttachmentService} from 'src/attachment/attachment.service';
-import {catchError, firstValueFrom, Observable} from 'rxjs';
-import {AxiosResponse} from 'axios';
-import {InjectDataSource} from '@nestjs/typeorm';
-import {DataSource} from 'typeorm';
-import {ReactionsService} from 'src/reactions/reactions.service';
-import {CommentsService} from 'src/comments/comments.service';
+import { HttpService } from '@nestjs/axios';
+import { AttachmentService } from 'src/attachment/attachment.service';
+import { catchError, firstValueFrom, Observable } from 'rxjs';
+import { AxiosResponse } from 'axios';
+import { InjectDataSource } from '@nestjs/typeorm';
+import { DataSource } from 'typeorm';
+import { ReactionsService } from 'src/reactions/reactions.service';
+import { CommentsService } from 'src/comments/comments.service';
 import {
   Delegation,
   IPFSResponse,
@@ -18,16 +24,23 @@ import {
   MetadataValidationStatus,
   ValidateMetadataResult,
 } from 'src/common/types';
-import {AuthService} from 'src/auth/auth.service';
-import {getAllDRepsQuery, getTotalResultsQuery} from 'src/queries/getDReps';
-import {getDRepDelegatorsCountQuery, getDRepVotesCountQuery, getDRepVotingPowerQuery,} from 'src/queries/drepStats';
-import {Metadata} from 'src/entities/metadata.entity';
-import {getEpochParams} from 'src/queries/getEpochParams';
-import {getDRepDelegatorsHistory} from 'src/queries/drepDelegatorsHistory';
-import {JsonLd} from 'jsonld/jsonld-spec';
-import {Response} from 'express';
-import {getDrepCexplorerDetailsQuery} from 'src/queries/drepCexplorerDetails';
-import {getDrepDelegatorsWithVotingPowerQuery} from 'src/queries/drepDelegatorsWithVotingPower';
+import { AuthService } from 'src/auth/auth.service';
+import { getAllDRepsQuery, getTotalResultsQuery } from 'src/queries/getDReps';
+import {
+  getDRepDelegatorsCountQuery,
+  getDRepVotesCountQuery,
+  getDRepVotingPowerQuery,
+} from 'src/queries/drepStats';
+import { Metadata } from 'src/entities/metadata.entity';
+import { getEpochParams } from 'src/queries/getEpochParams';
+import { getDRepDelegatorsHistory } from 'src/queries/drepDelegatorsHistory';
+import { JsonLd } from 'jsonld/jsonld-spec';
+import { Response } from 'express';
+import { getDrepCexplorerDetailsQuery } from 'src/queries/drepCexplorerDetails';
+import {
+  getDrepDelegatorsCountQuery,
+  getDrepDelegatorsWithVotingPowerQuery,
+} from 'src/queries/drepDelegatorsWithVotingPower';
 import { BlockfrostService } from 'src/blockfrost/blockfrost.service';
 
 @Injectable()
@@ -275,12 +288,9 @@ export class DrepService {
     if (drep.length > 0) drepVoterId = drep[0].signature_drepVoterId;
     const drepCexplorer = await this.getDrepCexplorerDetails(drepVoterId);
 
-    const drepDelegators =
-      await this.getDrepDelegatorsWithVotingPower(drepVoterId);
     const combinedResult = {
       ...drep[0],
       cexplorerDetails: drepCexplorer,
-      delegators: drepDelegators,
     };
     if (
       (!drep || drep.length === 0) &&
@@ -308,12 +318,9 @@ export class DrepService {
       .where('signature.drepVoterId = :drepVoterId', { drepVoterId })
       .getRawMany();
     const drepCexplorer = await this.getDrepCexplorerDetails(drepVoterId);
-    const drepDelegators =
-      await this.getDrepDelegatorsWithVotingPower(drepVoterId);
     const combinedResult = {
       ...drep[0],
       cexplorerDetails: drepCexplorer,
-      delegators: drepDelegators,
     };
     if (
       (!drep || drep.length === 0) &&
@@ -695,17 +702,37 @@ export class DrepService {
     }
   }
 
-  async getDrepDelegatorsWithVotingPower(drepVoterId: string) {
+  async getDrepDelegatorsWithVotingPower(
+    drepVoterId: string,
+    currentPage: number,
+    itemsPerPage: number,
+  ) {
+    const offset = (currentPage - 1) * itemsPerPage;
+
     const delegatorsWithVotingPower = await this.cexplorerService.manager.query(
-      getDrepDelegatorsWithVotingPowerQuery,
+      getDrepDelegatorsWithVotingPowerQuery(itemsPerPage, offset),
       [drepVoterId],
     );
 
-    return delegatorsWithVotingPower.map((delegator) => ({
-      stakeAddress: delegator?.stake_address,
-      delegationEpoch: delegator?.delegation_epoch,
-      votingPower: delegator?.voting_power,
-    }));
+    const totalResults = await this.cexplorerService.manager.query(
+      getDrepDelegatorsCountQuery(),
+      [drepVoterId],
+    );
+
+    const totalItems = parseInt(totalResults[0].total, 10);
+    const totalPages = Math.ceil(totalItems / itemsPerPage);
+
+    return {
+      data: delegatorsWithVotingPower.map((delegator) => ({
+        stakeAddress: delegator?.stake_address,
+        delegationEpoch: delegator?.delegation_epoch,
+        votingPower: delegator?.voting_power,
+      })),
+      totalItems,
+      currentPage,
+      itemsPerPage,
+      totalPages,
+    };
   }
 
   async updateDrepInfo(drepId: number, drep: createDrepDto) {
diff --git a/backend/src/queries/drepDelegatorsWithVotingPower.ts b/backend/src/queries/drepDelegatorsWithVotingPower.ts
index a00f942a..61e8341c 100644
--- a/backend/src/queries/drepDelegatorsWithVotingPower.ts
+++ b/backend/src/queries/drepDelegatorsWithVotingPower.ts
@@ -38,7 +38,10 @@
 //         voting_power DESC
 //       `;
 
-export const getDrepDelegatorsWithVotingPowerQuery: string = `
+export const getDrepDelegatorsWithVotingPowerQuery = (
+  itemsPerPage: number,
+  offset?: number,
+) => `
     WITH latest_delegations AS (
         SELECT delegation_vote.addr_id, MAX(block.time) AS latest_time
         FROM drep_hash
@@ -63,5 +66,27 @@ export const getDrepDelegatorsWithVotingPowerQuery: string = `
     GROUP BY sa.view,
          b.epoch_no
     ORDER BY voting_power DESC
-    LIMIT 24
+    LIMIT ${itemsPerPage}
+    OFFSET ${offset}
+`;
+
+export const getDrepDelegatorsCountQuery = () => `
+    WITH latest_delegations AS (
+        SELECT delegation_vote.addr_id, MAX(block.time) AS latest_time
+        FROM drep_hash
+        JOIN delegation_vote ON delegation_vote.drep_hash_id = drep_hash.id
+        JOIN stake_address ON delegation_vote.addr_id = stake_address.id
+        JOIN tx ON delegation_vote.tx_id = tx.id
+        JOIN block ON tx.block_id = block.id
+        WHERE drep_hash.view = $1
+        GROUP BY delegation_vote.addr_id
+    )
+    SELECT COUNT(DISTINCT sa.id) AS total
+    FROM drep_hash AS dh
+           JOIN delegation_vote AS dv ON dh.id = dv.drep_hash_id
+           JOIN stake_address sa ON dv.addr_id = sa.id
+           JOIN tx ON dv.tx_id = tx.id
+           JOIN block b ON tx.block_id = b.id
+           JOIN latest_delegations ld ON dv.addr_id = ld.addr_id
+    AND b.time = ld.latest_time
 `;
diff --git a/frontend/src/app/[locale]/dreps/[drepid]/delegators/page.tsx b/frontend/src/app/[locale]/dreps/[drepid]/delegators/page.tsx
index 82f27290..c84590b9 100644
--- a/frontend/src/app/[locale]/dreps/[drepid]/delegators/page.tsx
+++ b/frontend/src/app/[locale]/dreps/[drepid]/delegators/page.tsx
@@ -1,12 +1,10 @@
 'use client';
 import DrepProfileMetrics from '@/components/molecules/DrepProfileMetrics';
-import { useGetSingleDRepQuery } from '@/hooks/useGetSingleDRepQuery';
 import { useParams } from 'next/navigation';
 
 const DelegatorsPage = () => {
   const { drepid } = useParams();
-  const { dRep } = useGetSingleDRepQuery(drepid);
-  return <DrepProfileMetrics drepMetrics={dRep} />;
+  return <DrepProfileMetrics voterId={String(drepid)} />;
 };
 
 export default DelegatorsPage;
diff --git a/frontend/src/components/atoms/DrepDelegatorsList.tsx b/frontend/src/components/atoms/DrepDelegatorsList.tsx
index eba93c13..c690fae7 100644
--- a/frontend/src/components/atoms/DrepDelegatorsList.tsx
+++ b/frontend/src/components/atoms/DrepDelegatorsList.tsx
@@ -6,8 +6,13 @@ import {
   formattedAda,
   lovelaceToAda,
 } from '@/lib';
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 import HoverText from './HoverText';
+import { useGetDrepDelegators } from '@/hooks/useGetDrepDelegatorsQuery';
+import { Box, Skeleton } from '@mui/material';
+import Pagination from '../molecules/Pagination';
+import { useSearchParams } from 'next/navigation';
+
 const ViewProfileAction = () => {
   return (
     <div className="flex w-fit flex-row items-center gap-2 rounded-full bg-gray-200 px-3 py-1 text-sm">
@@ -16,19 +21,42 @@ const ViewProfileAction = () => {
     </div>
   );
 };
-const DrepDelegatorslist = ({ delegators }: { delegators: any[] }) => {
+
+const DrepDelegatorslist = ({ voterId }: { voterId: string }) => {
+  const [currentPage, setCurrentPage] = useState(1);
   const { latestEpoch } = useCardano();
   const { isMobile, screenWidth } = useScreenDimension();
+  const searchParams = useSearchParams();
+
+  useEffect(() => {
+    setCurrentPage(Number(searchParams.get('page') || 1));
+  }, [searchParams]);
+
+  const { Delegators, isDelegatorsLoading } = useGetDrepDelegators(
+    voterId,
+    currentPage,
+  );
+
   return (
-    <div className="w-full overflow-x-scroll">
+    <div className="w-full">
       <p className="text-3xl font-bold">Delegators</p>
-      {delegators && delegators.length > 0 ? (
-        delegators.map((delegator, index) => {
-          return (
-            <div key={index}>
-              <div className="flex flex-col">
-                <div className="flex w-full flex-row items-center justify-between text-nowrap py-4">
-                  <div className="flex min-w-40 flex-col">
+      {isDelegatorsLoading && (
+        <div className="px-1">
+          {Array.from({ length: 24 }).map((_, index) => (
+            <Skeleton height={80} key={index} />
+          ))}
+        </div>
+      )}
+      {!isDelegatorsLoading && Delegators.data.length > 0 && (
+        <div className="flex flex-col overflow-x-auto">
+          {Delegators.data.map((delegator, index) => {
+            return (
+              <div className="flex w-full flex-col" key={index}>
+                <div
+                  className="flex w-full flex-row items-center justify-between text-nowrap py-4"
+                  
+                >
+                  <div className="flex w-40 shrink-0 flex-col lg:w-60">
                     <p className="font-bold">
                       Epoch {delegator?.delegationEpoch}{' '}
                       {delegator?.delegationEpoch == latestEpoch && '(actual)'}
@@ -41,7 +69,7 @@ const DrepDelegatorslist = ({ delegators }: { delegators: any[] }) => {
                     </p>
                   </div>
 
-                  <div className="flex min-w-40 flex-col items-center justify-start">
+                  <div className="flex w-40 shrink-0 flex-col items-center justify-start">
                     <p className="font-bold">Active Stake</p>
                     <div>
                       <HoverText
@@ -53,26 +81,40 @@ const DrepDelegatorslist = ({ delegators }: { delegators: any[] }) => {
                     </div>
                   </div>
 
-                  <div className="flex min-w-40 flex-col items-center justify-start">
+                  <div className="flex w-40 shrink-0 flex-col items-center justify-start">
                     <p className="font-bold">Epoch</p>
                     <p> {delegator.delegationEpoch}</p>
                   </div>
 
-                  <div className="flex min-w-40 flex-col items-start justify-start">
+                  <div className="flex max-w-40 shrink-0 flex-col items-start justify-start">
                     <p className="font-bold">Actions</p>
                     <div className="flex items-center gap-2">
                       <ViewProfileAction />
                     </div>
                   </div>
                 </div>
-                <hr className="w-dvw border" />
+                <hr className="w-full border" />
               </div>
-            </div>
-          );
-        })
-      ) : (
-        <p>No delegators to show</p>
+            );
+          })}
+        </div>
       )}
+      {!isDelegatorsLoading && Delegators.data.length < 1 && (
+        <p className="text-center">No delegators to show</p>
+      )}
+
+      {!isDelegatorsLoading &&
+        Delegators?.data &&
+        Delegators?.data.length > 0 && (
+          <Box className="mt-6 flex justify-end">
+            <Pagination
+              currentPage={Delegators.currentPage}
+              totalPages={Delegators.totalPages}
+              totalItems={Delegators.totalItems}
+              dataType="Delegators"
+            />
+          </Box>
+        )}
     </div>
   );
 };
diff --git a/frontend/src/components/molecules/DRepsTable.tsx b/frontend/src/components/molecules/DRepsTable.tsx
index 0057a873..27a7b091 100644
--- a/frontend/src/components/molecules/DRepsTable.tsx
+++ b/frontend/src/components/molecules/DRepsTable.tsx
@@ -6,7 +6,8 @@ import { useGetDRepsQuery } from '@/hooks/useGetDRepsQuery';
 import {
   convertString,
   formatAsCurrency,
-  handleCopyText, percentageDifference,
+  handleCopyText,
+  percentageDifference,
   shortNumber,
 } from '@/lib';
 import { useScreenDimension } from '@/hooks';
@@ -15,13 +16,12 @@ import Button from '../atoms/Button';
 import Link from 'next/link';
 import HoverText from '../atoms/HoverText';
 import Pagination from './Pagination';
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
 import CopyToClipBoard from '../atoms/svgs/CopyToClipBoardIcon';
 import ArrowDownIcon from '../atoms/svgs/ArrowDownIcon';
 import ArrowUpIcon from '../atoms/svgs/ArrowUpIcon';
 import DatabaseNullIcon from '../atoms/svgs/DatabaseNullIcon';
 import CrossIcon from '../atoms/svgs/CrossIcon';
-import Typography from "@mui/material/Typography";
+import Typography from '@mui/material/Typography';
 
 type DRepsTableProps = {
   query?: string;
@@ -49,9 +49,6 @@ const DRepsTable = ({
   campaignStatus,
   type,
 }: DRepsTableProps) => {
-  const searchParams = useSearchParams();
-  const pathName = usePathname();
-  const { replace } = useRouter();
   const { isMobile } = useScreenDimension();
 
   const { DReps, isDRepsLoading, isError } = useGetDRepsQuery(
@@ -64,36 +61,9 @@ const DRepsTable = ({
     type,
   );
 
-  // Handle table pagination
-  function moveToPage(targetPage: number) {
-    const params = new URLSearchParams(searchParams);
-
-    if (page !== targetPage) {
-      params.set('page', targetPage.toString());
-    }
-    replace(`${pathName}?${params.toString()}`);
-    window.scrollTo({ top: 0, behavior: 'smooth' });
-  }
-
-  function moveToFirstPage(firstPage: number) {
-    moveToPage(firstPage);
-  }
-
-  function moveToLastPage(lastPage: number) {
-    moveToPage(lastPage);
-  }
-
-  function moveToPreviousPage(previousPage: number) {
-    moveToPage(previousPage);
-  }
-
-  function moveToNextPage(nextPage: number) {
-    moveToPage(nextPage);
-  }
-
   return (
-    <div className="flex flex-col overflow-x-auto dreps-table-wrapper">
-      <table className="min-w-full dreps-table">
+    <div className="dreps-table-wrapper flex flex-col overflow-x-auto">
+      <table className="dreps-table min-w-full">
         <thead className="mb-2">
           <tr className="overflow-x-auto text-nowrap bg-white text-left text-xl font-black">
             <th className="py-2">DRep</th>
@@ -125,7 +95,7 @@ const DRepsTable = ({
               </div>
             </th>
             <th className="py-2">
-              <div className="flex items-center justifiy-end text-left">
+              <div className="justify-end flex items-center text-left">
                 <span>Delegators</span>
                 {sort === 'delegators' &&
                   (order === 'desc' ? (
@@ -331,10 +301,7 @@ const DRepsTable = ({
             currentPage={DReps.currentPage}
             totalPages={DReps.totalPages}
             totalItems={DReps.totalItems}
-            moveToFirstPage={moveToFirstPage}
-            moveToLastPage={moveToLastPage}
-            moveToPreviousPage={moveToPreviousPage}
-            moveToNextPage={moveToNextPage}
+            dataType="DReps"
           />
         </Box>
       )}
diff --git a/frontend/src/components/molecules/DrepProfileMetrics.tsx b/frontend/src/components/molecules/DrepProfileMetrics.tsx
index 8201200f..23ab8097 100644
--- a/frontend/src/components/molecules/DrepProfileMetrics.tsx
+++ b/frontend/src/components/molecules/DrepProfileMetrics.tsx
@@ -1,10 +1,10 @@
 import React from 'react';
 import DrepDelegatorsList from '../atoms/DrepDelegatorsList';
 
-const DrepProfileMetrics = ({drepMetrics}:{drepMetrics: any}) => {
+const DrepProfileMetrics = ({voterId}:{voterId: string}) => {
   return (
     <div className='bg-white p-5 min-h-screen'>
-      <DrepDelegatorsList delegators={drepMetrics?.delegators} />
+      <DrepDelegatorsList voterId={voterId} />
     </div>
   );
 };
diff --git a/frontend/src/components/molecules/Pagination.tsx b/frontend/src/components/molecules/Pagination.tsx
index d71872a0..09c2f19a 100644
--- a/frontend/src/components/molecules/Pagination.tsx
+++ b/frontend/src/components/molecules/Pagination.tsx
@@ -4,29 +4,54 @@ import ChevronsRightIcon from '../atoms/svgs/ChevronsRightIcon';
 import ChevronRightIcon from '../atoms/svgs/ChevronRightIcon';
 import ChevronLeftIcon from '../atoms/svgs/ChevronLeftIcon';
 import ChevronsLeftIcon from '../atoms/svgs/ChevronsLeftIcon';
+import { usePathname, useRouter, useSearchParams } from 'next/navigation';
 
 type PaginationProps = {
   currentPage: number;
   totalPages: number;
   totalItems: number;
-  moveToFirstPage?: Function;
-  moveToPreviousPage?: Function;
-  moveToNextPage?: Function;
-  moveToLastPage?: Function;
+  dataType: string;
 };
 
 const Pagination = ({
   currentPage,
   totalPages,
   totalItems,
-  moveToFirstPage,
-  moveToPreviousPage,
-  moveToNextPage,
-  moveToLastPage,
+  dataType
 }: PaginationProps) => {
+  const searchParams = useSearchParams();
+  const pathName = usePathname();
+  const { replace } = useRouter();
+
   const isLastPage = currentPage === totalPages;
   const isFirstPage = currentPage === 1;
 
+  function moveToPage(targetPage: number) {
+    const params = new URLSearchParams(searchParams);
+
+    if (currentPage !== targetPage) {
+      params.set('page', targetPage.toString());
+    }
+    replace(`${pathName}?${params.toString()}`);
+    window.scrollTo({ top: 0, behavior: 'smooth' });
+  }
+
+  function moveToFirstPage(firstPage: number) {
+    moveToPage(firstPage);
+  }
+
+  function moveToLastPage(lastPage: number) {
+    moveToPage(lastPage);
+  }
+
+  function moveToPreviousPage(previousPage: number) {
+    moveToPage(previousPage);
+  }
+
+  function moveToNextPage(nextPage: number) {
+    moveToPage(nextPage);
+  }
+
   return (
     <>
       {!totalItems && totalPages && currentPage ? (
@@ -92,7 +117,7 @@ const Pagination = ({
             </Box>
           </Box>
           <span className="textColor2 mr-2 text-sm">
-            Total DReps: {totalItems}
+            Total {dataType}: {totalItems}
           </span>
         </Box>
       )}
diff --git a/frontend/src/constants/queryKeys.ts b/frontend/src/constants/queryKeys.ts
index c4c7cea4..bbb43fbc 100644
--- a/frontend/src/constants/queryKeys.ts
+++ b/frontend/src/constants/queryKeys.ts
@@ -5,5 +5,6 @@ export const QUERY_KEYS = {
   getSingleDRepKey: 'getSingleDRepKey',
   getAllDRepsKey: 'getAllDRepsKey',
   getDRepStatsKey: 'getDRepStatsKey',
-  getDRepTimelineKey: 'getDRepTimelineKey'
+  getDRepTimelineKey: 'getDRepTimelineKey',
+  getDrepDelegators: 'getDrepDelegators'
 };
diff --git a/frontend/src/hooks/useGetDrepDelegatorsQuery.ts b/frontend/src/hooks/useGetDrepDelegatorsQuery.ts
new file mode 100644
index 00000000..43077766
--- /dev/null
+++ b/frontend/src/hooks/useGetDrepDelegatorsQuery.ts
@@ -0,0 +1,19 @@
+import { QUERY_KEYS } from '@/constants/queryKeys';
+import { useQuery } from 'react-query';
+import { Delegators } from '../../types/api';
+import { getDrepDelegators } from '@/services/requests/getDrepDelegators';
+
+export const useGetDrepDelegators = (
+  voterId: string,
+  page?: number,
+  perPage?: number,
+) => {
+  const { data, isLoading } = useQuery<Delegators>({
+    queryKey: [QUERY_KEYS.getDrepDelegators, voterId, page],
+    queryFn: async () => await getDrepDelegators(voterId, page, perPage),
+    enabled: !!voterId,
+    refetchOnWindowFocus: false,
+  });
+
+  return { Delegators: data, isDelegatorsLoading: isLoading };
+};
diff --git a/frontend/src/services/requests/getDrepDelegators.ts b/frontend/src/services/requests/getDrepDelegators.ts
new file mode 100644
index 00000000..af303865
--- /dev/null
+++ b/frontend/src/services/requests/getDrepDelegators.ts
@@ -0,0 +1,12 @@
+import axiosInstance from '../axiosInstance';
+
+export const getDrepDelegators = async (
+  voterId: string,
+  page?: number,
+  perPage?: number,
+) => {
+  const response = await axiosInstance.get(`dreps/${voterId}/delegators`, {
+    params: { page, perPage },
+  });
+  return response.data;
+};
diff --git a/frontend/types/api.ts b/frontend/types/api.ts
index fc594256..c41f9f84 100644
--- a/frontend/types/api.ts
+++ b/frontend/types/api.ts
@@ -26,6 +26,14 @@ export type Delegator = {
   votingPower: number | null;
 };
 
+export type Delegators = {
+  data: Delegator[];
+  totalItems: number;
+  currentPage: number;
+  itemsPerPage: number;
+  totalPages: number;
+};
+
 export type SingleDRep = {
   drep_deletedAt?: string | null;
   drep_id?: number | null;
@@ -57,8 +65,6 @@ export type SingleDRep = {
   signature_drepSignatureKey?: string | null;
   signature_drepId?: number | null;
   cexplorerDetails: DRepCExplorerDetails;
-  activity?: any[];
-  delegators?: Delegator[];
 };
 
 export type DRepStats = {
@@ -78,4 +84,4 @@ export type DelegationData = {
   type: string;
   total_stake: string;
   added_power: boolean;
-};
\ No newline at end of file
+};
-- 
GitLab


From f9208f79346ffb6973ed3c92fb45652bf479fbee Mon Sep 17 00:00:00 2001
From: theeurbanlegend <mbuto152@gmail.com>
Date: Mon, 16 Sep 2024 16:55:13 +0300
Subject: [PATCH 2/4] Updated model, updated usage

---
 backend/src/drep/drep.controller.ts           |  2 +-
 backend/src/drep/drep.service.ts              | 16 +++++------
 backend/src/entities/drep.entity.ts           |  8 ------
 backend/src/entities/note.entity.ts           |  6 ++++-
 backend/src/entities/signatures.entity.ts     | 27 +++++++++++++------
 backend/src/note/note.service.ts              |  4 +--
 .../components/atoms/DrepClaimProfileCard.tsx |  2 +-
 .../src/components/atoms/DrepProfileCard.tsx  |  4 +--
 .../organisms/UpdateProfileStep2.tsx          |  6 ++---
 frontend/src/context/drepContext.tsx          |  2 +-
 frontend/types/api.ts                         |  8 +++---
 11 files changed, 46 insertions(+), 39 deletions(-)

diff --git a/backend/src/drep/drep.controller.ts b/backend/src/drep/drep.controller.ts
index 1ebb6fbe..6a21d4d3 100644
--- a/backend/src/drep/drep.controller.ts
+++ b/backend/src/drep/drep.controller.ts
@@ -82,7 +82,7 @@ export class DrepController {
 
     if (drepId) {
       drep = await this.drepService.getSingleDrepViaID(drepId);
-      drepVoterId = drep.signature_drepVoterId;
+      drepVoterId = drep.signature_voterId;
     } else if (drepVoterId) {
       drep = await this.drepService.getSingleDrepViaVoterID(drepVoterId);
     }
diff --git a/backend/src/drep/drep.service.ts b/backend/src/drep/drep.service.ts
index 51b6fe7a..e9bc07c1 100644
--- a/backend/src/drep/drep.service.ts
+++ b/backend/src/drep/drep.service.ts
@@ -60,7 +60,7 @@ export class DrepService {
     // if (query) {
     //   const nameFilteredDReps = query ? await this.getDRepsByName(query) : [];
     //   nameFilteredDRepViews = nameFilteredDReps.map(
-    //     (drep) => drep.signature_drepVoterId,
+    //     (drep) => drep.signature_voterId,
     //   );
     // }
 
@@ -77,7 +77,7 @@ export class DrepService {
 
     if (campaignStatus) {
       const voltaireDReps = (await this.getAllDRepsVoltaire()) ?? [];
-      dRepViews = voltaireDReps.map((drep) => drep.signature_drepVoterId);
+      dRepViews = voltaireDReps.map((drep) => drep.signature_voterId);
     }
 
     const drepList = await this.getAllDRepsCexplorer(
@@ -101,7 +101,7 @@ export class DrepService {
 
     const mergedDRepsData = drepList.data.map((drep) => {
       const voltaireDrep = voltaireDReps.find(
-        (voltaireDrep) => voltaireDrep.signature_drepVoterId === drep.view,
+        (voltaireDrep) => voltaireDrep.signature_voterId === drep.view,
       );
       //account for voting options
       if (
@@ -252,7 +252,7 @@ export class DrepService {
       .getRepository('Drep')
       .createQueryBuilder('drep')
       .leftJoinAndSelect('drep.signatures', 'signature')
-      .where('signature.drepVoterId IN (:...views)', { views })
+      .where('signature.voterId IN (:...views)', { views })
       .getRawMany();
   }
 
@@ -272,7 +272,7 @@ export class DrepService {
       .where('drep.id = :drepId', { drepId })
       .getRawMany();
     let drepVoterId;
-    if (drep.length > 0) drepVoterId = drep[0].signature_drepVoterId;
+    if (drep.length > 0) drepVoterId = drep[0].signature_voterId;
     const drepCexplorer = await this.getDrepCexplorerDetails(drepVoterId);
 
     const drepDelegators =
@@ -305,7 +305,7 @@ export class DrepService {
       .getRepository('Drep')
       .createQueryBuilder('drep')
       .leftJoinAndSelect('signature', 'signature', 'signature.drepId = drep.id')
-      .where('signature.drepVoterId = :drepVoterId', { drepVoterId })
+      .where('signature.voterId = :drepVoterId', { drepVoterId })
       .getRawMany();
     const drepCexplorer = await this.getDrepCexplorerDetails(drepVoterId);
     const drepDelegators =
@@ -590,7 +590,7 @@ export class DrepService {
     // 'delegators' visibility
     if (delegation) {
       visibilityConditions.push(
-        'note.note_visibility = :delegators AND signature.drepVoterId = :drepVoterId',
+        'note.note_visibility = :delegators AND signature.voterId = :drepVoterId',
       );
       visibilityParams.delegators = 'delegators';
       visibilityParams.drepVoterId = delegation.drep_view;
@@ -599,7 +599,7 @@ export class DrepService {
     // 'myself' visibility
     if (stakeKeyBech32) {
       visibilityConditions.push(
-        'note.note_visibility = :myself AND signature.drepStakeKey = :stakeKeyBech32',
+        'note.note_visibility = :myself AND signature.stakeKey = :stakeKeyBech32',
       );
       visibilityParams.myself = 'myself';
       visibilityParams.stakeKeyBech32 = stakeKeyBech32;
diff --git a/backend/src/entities/drep.entity.ts b/backend/src/entities/drep.entity.ts
index c1d21efe..5f7c4a39 100644
--- a/backend/src/entities/drep.entity.ts
+++ b/backend/src/entities/drep.entity.ts
@@ -4,14 +4,6 @@ import { BaseEntity } from 'src/global';
 
 @Entity()
 export class Drep extends BaseEntity {
-  @Column({ type: 'json', nullable: true })
-  social: Record<string, any>;
-  @Column({ nullable: true })
-  platform_statement: string;
-  @Column({ nullable: true })
-  expertise: string;
-  @Column({ nullable: true })
-  perspective: string;
   @OneToMany(() => Signature, (signature) => signature.drep)
   signatures: Signature[];
 }
diff --git a/backend/src/entities/note.entity.ts b/backend/src/entities/note.entity.ts
index e0dc3a34..7d8b59e6 100644
--- a/backend/src/entities/note.entity.ts
+++ b/backend/src/entities/note.entity.ts
@@ -2,6 +2,7 @@ import { Entity, Column, ManyToOne, OneToMany } from 'typeorm';
 import { Drep } from './drep.entity';
 import { BaseEntity } from 'src/global';
 import { Reaction } from './reaction.entity';
+import { Signature } from './signatures.entity';
 
 @Entity()
 export class Note extends BaseEntity {
@@ -15,7 +16,10 @@ export class Note extends BaseEntity {
   note_content: string;
 
   @ManyToOne(() => Drep, (drep) => drep.id)
-  voter: Drep;
+  drep: Drep; // This is the Drep/ drep page that the note belongs to/will be hosted by
+  
+  @ManyToOne(() => Signature, (signature) => signature.id)
+  author: Signature; // This is the Signature/ user that wrote the note
 
   @Column()
   note_visibility: string;
diff --git a/backend/src/entities/signatures.entity.ts b/backend/src/entities/signatures.entity.ts
index 54dd90d0..3ec4f8a7 100644
--- a/backend/src/entities/signatures.entity.ts
+++ b/backend/src/entities/signatures.entity.ts
@@ -3,16 +3,27 @@ import { Drep } from './drep.entity';
 
 @Entity()
 export class Signature {
+  //can belong to a Drep or voter
   @PrimaryGeneratedColumn()
   id: number;
-  @ManyToOne(() => Drep, (drep) => drep.id, { onDelete: 'CASCADE' })
-  drep: number;
-  @Column()
-  drepVoterId: string;
-  @Column()
-  drepStakeKey: string;
+
+  @ManyToOne(() => Drep, (drep) => drep.id, {
+    nullable: true,
+    onDelete: 'CASCADE',
+  })
+  drep: Drep;
+
+  @Column({nullable: true})
+  voterId: string;
+
+  @Column({nullable: true})
+  stakeKey: string;
+
   @Column({ nullable: true, unique: false, default: null })
-  drepSignature: string;
+  signature: string;
+
   @Column({ nullable: true, unique: false, default: null })
-  drepSignatureKey: string;
+  signatureKey: string;
+
+  
 }
diff --git a/backend/src/note/note.service.ts b/backend/src/note/note.service.ts
index d228c012..372d6651 100644
--- a/backend/src/note/note.service.ts
+++ b/backend/src/note/note.service.ts
@@ -135,7 +135,7 @@ export class NoteService {
     // 'delegators' visibility
     if (delegation) {
       queryBuilder.orWhere(
-        'note.note_visibility = :delegators AND signature.drepVoterId = :drepVoterId',
+        'note.note_visibility = :delegators AND signature.voterId = :drepVoterId',
         {
           delegators: 'delegators',
           drepVoterId: delegation.drep_view,
@@ -145,7 +145,7 @@ export class NoteService {
     // 'myself' visibility
     if (stakeKeyBech32) {
       queryBuilder.orWhere(
-        'note.note_visibility = :myself AND signature.drepStakeKey = :stakeKeyBech32',
+        'note.note_visibility = :myself AND signature.stakeKey = :stakeKeyBech32',
         {
           myself: 'myself',
           stakeKeyBech32: stakeKeyBech32,
diff --git a/frontend/src/components/atoms/DrepClaimProfileCard.tsx b/frontend/src/components/atoms/DrepClaimProfileCard.tsx
index adcd263e..b08970c6 100644
--- a/frontend/src/components/atoms/DrepClaimProfileCard.tsx
+++ b/frontend/src/components/atoms/DrepClaimProfileCard.tsx
@@ -165,7 +165,7 @@ const DrepClaimProfileCard = ({
         )}
       </div>
       {(drep?.cexplorerDetails?.view == dRepIDBech32 ||
-        drep?.signature_drepVoterId == dRepIDBech32) &&
+        drep?.signature_voterId == dRepIDBech32) &&
         isLoggedIn && (
           <div className="flex max-w-prose flex-col gap-2">
             <Link href={`/dreps/workflow/profile/new`}>
diff --git a/frontend/src/components/atoms/DrepProfileCard.tsx b/frontend/src/components/atoms/DrepProfileCard.tsx
index 4bdaf8df..3af38ef2 100644
--- a/frontend/src/components/atoms/DrepProfileCard.tsx
+++ b/frontend/src/components/atoms/DrepProfileCard.tsx
@@ -340,11 +340,11 @@ const DrepProfileCard = ({ drep, state }: { drep: any; state: boolean }) => {
           />
         )}
         {(drep?.cexplorerDetails?.view == dRepIDBech32 ||
-          drep?.signature_drepVoterId == dRepIDBech32) &&
+          drep?.signature_voterId == dRepIDBech32) &&
           renderUnsavedChanges()}
       </div>
       {(drep?.cexplorerDetails?.view == dRepIDBech32 ||
-        drep?.signature_drepVoterId == dRepIDBech32) && (
+        drep?.signature_voterId == dRepIDBech32) && (
         <div className="flex max-w-prose flex-col gap-2">
           <Button
             handleClick={
diff --git a/frontend/src/components/organisms/UpdateProfileStep2.tsx b/frontend/src/components/organisms/UpdateProfileStep2.tsx
index 86b333fa..8514e0a9 100644
--- a/frontend/src/components/organisms/UpdateProfileStep2.tsx
+++ b/frontend/src/components/organisms/UpdateProfileStep2.tsx
@@ -52,10 +52,10 @@ const UpdateProfileStep2 = () => {
           drep = await getSingleDRepViaVoterId(dRepIDBech32);
         }
         setNewDrepId(drep.drep_id);
-        setValue('signature', drep.signature_drepSignature);
+        setValue('signature', drep.signature_signature);
         setSignature({
-          signature: drep.signature_drepSignature,
-          key: drep.signature_drepSignatureKey,
+          signature: drep.signature_signature,
+          key: drep.signature_signatureKey,
         });
         if (drep.drep_platform_statement) {
           setStep2Status('update');
diff --git a/frontend/src/context/drepContext.tsx b/frontend/src/context/drepContext.tsx
index 14572485..9b640930 100644
--- a/frontend/src/context/drepContext.tsx
+++ b/frontend/src/context/drepContext.tsx
@@ -129,7 +129,7 @@ function DRepProvider(props: Props) {
       if (drep?.drep_id) {
         setNewDrepId(drep?.drep_id);
       }
-      if (drep?.signature_drepSignature) {
+      if (drep?.signature_signature) {
         setStep2Status('success');
       }
       //check for metadata locally first
diff --git a/frontend/types/api.ts b/frontend/types/api.ts
index fc594256..bbf874a0 100644
--- a/frontend/types/api.ts
+++ b/frontend/types/api.ts
@@ -51,10 +51,10 @@ export type SingleDRep = {
   attachment_drepId?: number | null;
   attachment_commentId?: number | null;
   signature_id?: number | null;
-  signature_drepVoterId?: string | null;
-  signature_drepStakeKey?: string | null;
-  signature_drepSignature?: string | null;
-  signature_drepSignatureKey?: string | null;
+  signature_voterId?: string | null;
+  signature_stakeKey?: string | null;
+  signature_signature?: string | null;
+  signature_signatureKey?: string | null;
   signature_drepId?: number | null;
   cexplorerDetails: DRepCExplorerDetails;
   activity?: any[];
-- 
GitLab


From 2bf0754251d2feb9e56921ecd2ed63ee4b490529 Mon Sep 17 00:00:00 2001
From: theeurbanlegend <mbuto152@gmail.com>
Date: Mon, 16 Sep 2024 17:54:59 +0300
Subject: [PATCH 3/4] bug fix, added transition

---
 frontend/src/components/atoms/PageBanner.tsx | 89 +++++++++++---------
 frontend/src/hooks/useGetNodeStatusQuery.ts  |  3 +-
 2 files changed, 52 insertions(+), 40 deletions(-)

diff --git a/frontend/src/components/atoms/PageBanner.tsx b/frontend/src/components/atoms/PageBanner.tsx
index d93d4757..0ce0f7ba 100644
--- a/frontend/src/components/atoms/PageBanner.tsx
+++ b/frontend/src/components/atoms/PageBanner.tsx
@@ -1,64 +1,75 @@
 'use client';
+import { useDRepContext } from '@/context/drepContext';
 import { useGetNodeStatusQuery } from '@/hooks/useGetNodeStatusQuery';
-import { Box, Typography } from '@mui/material';
+import { Box, Slide, Typography } from '@mui/material';
 import { usePathname } from 'next/navigation';
 import React, { useEffect, useState } from 'react';
 
 const PageBanner = () => {
-  const { NodeStatus, isLoading, isFetching, isError } = useGetNodeStatusQuery();
+  const { NodeStatus, isLoading, isFetching, isError, isFetchedAfterMount } =
+    useGetNodeStatusQuery();
   const pathname = usePathname();
+  const [showBanner, setShowBanner] = useState(false);
   const [nodeStats, setNodeStats] = useState(null);
+  const {currentLocale}=useDRepContext();
   const dbNonDependentPages = [
-    '/',
-    '/dreps',
-    '/dreps/workflow/profile/new',
-    '/dreps/workflow/profile/update',
-  ];  
+    `/${currentLocale}`,
+    `/${currentLocale}/dreps`,
+    `/${currentLocale}/dreps/workflow/profile/new`,
+    `/${currentLocale}/dreps/workflow/profile/update`,
+  ];
   useEffect(() => {
     if (NodeStatus) {
       setNodeStats(NodeStatus);
     }
   }, [NodeStatus, isLoading, isFetching]);
+  useEffect(() => {
+    setShowBanner(renderCondition());
+  }, [pathname, isFetchedAfterMount, isError, nodeStats]);
+  const renderCondition = () => {
+    return (
+      isFetchedAfterMount &&
+      !dbNonDependentPages.some((page) => pathname == page) &&
+      (isError || (nodeStats && nodeStats?.behindBy >= 30))
+    );
+  };
   const renderStatus = () => {
     if (!nodeStats && !isError) return '-';
     if (nodeStats && !isError) {
-      return nodeStats?.behindBy > 30 ? 'Lagging' : 'Following';
+      return nodeStats?.behindBy >= 30 ? 'Lagging' : 'Following';
     }
     if (isError) return 'Offline';
   };
-
-  if (
-    dbNonDependentPages.some(page => pathname == page) ||
-    (!isError && (!nodeStats || (nodeStats && nodeStats?.behindBy <= 30)))
-  )
-    return null;
+  if (!showBanner) return null;
 
   return (
-    <Box component={'div'} className="flex items-center justify-center gap-2">
-      <div className="inline-flex items-center gap-1">
-        <Typography>Epoch:</Typography>
-        <Typography>{nodeStats?.epoch_no || '-'}</Typography>
-      </div>
-      <div className="inline-flex items-center gap-1">
-        <Typography>Slot:</Typography>
-        <Typography>{nodeStats?.epoch_slot_no || '-'}</Typography>
-      </div>
-      <div className="inline-flex items-center gap-1">
-        <Typography>Status:</Typography>
-        <Typography
-          className={`${renderStatus() === 'Offline' && 'text-extra_red'}`}
-        >
-          {renderStatus()}
-        </Typography>
-      </div>
-      <div>
-        <Typography variant="caption">
-          {nodeStats &&
-            `Last updated ${new Date(nodeStats?.time).toLocaleString('en-US')}`}
-        </Typography>
-      </div>
-    </Box>
+    <Slide in={showBanner} appear exit direction="down">
+      <Box component={'div'} className="flex items-center justify-center gap-2">
+        <div className="inline-flex items-center gap-1">
+          <Typography>Epoch:</Typography>
+          <Typography>{nodeStats?.epoch_no || '-'}</Typography>
+        </div>
+        <div className="inline-flex items-center gap-1">
+          <Typography>Slot:</Typography>
+          <Typography>{nodeStats?.epoch_slot_no || '-'}</Typography>
+        </div>
+        <div className="inline-flex items-center gap-1">
+          <Typography>Status:</Typography>
+          <Typography
+            className={`${renderStatus() === 'Offline' && 'text-extra_red'}`}
+          >
+            {renderStatus()}
+          </Typography>
+        </div>
+        <div>
+          <Typography variant="caption">
+            {nodeStats &&
+              `Last updated ${new Date(nodeStats?.time).toLocaleString('en-US')}`}
+          </Typography>
+        </div>
+      </Box>
+    </Slide>
   );
 };
 
-export default PageBanner;
\ No newline at end of file
+export default PageBanner;
diff --git a/frontend/src/hooks/useGetNodeStatusQuery.ts b/frontend/src/hooks/useGetNodeStatusQuery.ts
index 05b8152b..60ceb7b4 100644
--- a/frontend/src/hooks/useGetNodeStatusQuery.ts
+++ b/frontend/src/hooks/useGetNodeStatusQuery.ts
@@ -2,7 +2,7 @@ import { getCurrentNodeStatus } from '@/services/requests/getCurrentNodeStatus';
 import { useQuery } from 'react-query';
 
 export const useGetNodeStatusQuery = () => {
-  const { data, isLoading, isFetching, isError, error , isSuccess} = useQuery({
+  const { data, isLoading, isFetching, isError, error , isSuccess, isFetchedAfterMount} = useQuery({
     queryKey: 'nodeStatus',
     queryFn: async () => getCurrentNodeStatus(),
     refetchInterval: 10000,
@@ -12,6 +12,7 @@ export const useGetNodeStatusQuery = () => {
     NodeStatus: data,
     isLoading,
     isFetching,
+    isFetchedAfterMount,
     isError: !isSuccess || isError,
     error,
   };
-- 
GitLab


From 75202a6d34295f28093848f48f20c8e3746173b3 Mon Sep 17 00:00:00 2001
From: Emmanuel Mutisya <emmanuelmutisya254@gmail.com>
Date: Mon, 16 Sep 2024 23:26:26 +0300
Subject: [PATCH 4/4] account for deregistration when checking DRep register
 status

---
 backend/src/drep/drep.service.ts        | 10 ++++++++--
 backend/src/queries/drepRegistration.ts |  7 +++++++
 2 files changed, 15 insertions(+), 2 deletions(-)
 create mode 100644 backend/src/queries/drepRegistration.ts

diff --git a/backend/src/drep/drep.service.ts b/backend/src/drep/drep.service.ts
index 60efa772..b40783ac 100644
--- a/backend/src/drep/drep.service.ts
+++ b/backend/src/drep/drep.service.ts
@@ -42,6 +42,7 @@ import {
   getDrepDelegatorsWithVotingPowerQuery,
 } from 'src/queries/drepDelegatorsWithVotingPower';
 import { BlockfrostService } from 'src/blockfrost/blockfrost.service';
+import { drepRegistrationQuery } from 'src/queries/drepRegistration';
 
 @Injectable()
 export class DrepService {
@@ -967,8 +968,13 @@ export class DrepService {
   }
 
   async isDrepRegistered(voterId: string) {
-    const registration = await this.getDrepDateofRegistration(voterId);
+    const latestRegistration = await this.cexplorerService.manager.query(
+      drepRegistrationQuery,
+      [voterId],
+    );
+
+    const regDeposit = latestRegistration[0]?.deposit;
 
-    return !!registration ? true : false;
+    return regDeposit === null || regDeposit > 0;
   }
 }
diff --git a/backend/src/queries/drepRegistration.ts b/backend/src/queries/drepRegistration.ts
new file mode 100644
index 00000000..2e972879
--- /dev/null
+++ b/backend/src/queries/drepRegistration.ts
@@ -0,0 +1,7 @@
+export const drepRegistrationQuery = `
+    SELECT dh.view,dr.deposit,dr.tx_id,  
+            ROW_NUMBER() OVER (PARTITION BY drep_hash_id ORDER BY tx_id DESC) AS rn
+    FROM drep_hash dh
+    JOIN drep_registration dr ON dh.id = dr.drep_hash_id
+    where dh.view = $1 
+    limit 1`;
-- 
GitLab