Переглянути джерело

20230218: Migrated hooks, types from JoystreamVoterDashboard

mkbeefcake 1 рік тому
батько
коміт
386db819ca

+ 2 - 1
src/App.tsx

@@ -317,8 +317,9 @@ class App extends React.Component<IProps, IState> {
   }
 
   componentDidMount() {
+    debugger;
     this.loadData(); // local storage + bootstrap
-    this.joyApi(); // joystream rpc connection
+    // this.joyApi(); // joystream rpc connection
     //this.initializeSocket() // jsstats socket.io
   }
 

+ 16 - 0
src/hooks/index.ts

@@ -0,0 +1,16 @@
+export * from './useChannels';
+export * from './useElectedCouncils';
+export * from './useElection';
+export * from './useGetValidation';
+export * from './useGroupWorkers';
+export * from './useMemberships';
+export * from './useNfts';
+export * from './useNumberProposal';
+export * from './usePostData';
+export * from './useProposals';
+export * from './useThread';
+export * from './useTokenMinted';
+export * from './useVideos';
+export * from './useWorkingGroups';
+export * from './useWorker';
+export * from './useCouncilMembers';

+ 5 - 0
src/hooks/types.ts

@@ -0,0 +1,5 @@
+import { ElectedCouncil } from '@/types';
+
+export interface ForSelectedCouncil {
+  council?: ElectedCouncil;
+}

+ 39 - 0
src/hooks/useChannels.ts

@@ -0,0 +1,39 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetChannelsCountLazyQuery } from '@/queries';
+
+import { ForSelectedCouncil } from './types';
+
+export function useChannels({ council }: ForSelectedCouncil) {
+  const [fetchCreated, createdQuery] = useGetChannelsCountLazyQuery();
+  const [totalCreated, totalQuery] = useGetChannelsCountLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+    let variables = {
+      where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    fetchCreated({
+      variables,
+    });
+
+    variables = {
+      where: { createdAt_gt: '2013-01-10T22:50:12.000Z', createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    totalCreated({
+      variables,
+    });
+  }, [council]);
+
+  const created = useMemo(() => createdQuery.data?.channelsConnection.totalCount, [createdQuery.data]);
+  const total = useMemo(() => totalQuery.data?.channelsConnection.totalCount, [totalQuery.data]);
+
+  return {
+    created,
+    total,
+    loading: createdQuery.loading,
+    error: createdQuery.error,
+  };
+}

+ 33 - 0
src/hooks/useCouncilMembers.ts

@@ -0,0 +1,33 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetCouncilMembersLazyQuery } from '@/queries';
+
+import { ForSelectedCouncil } from './types';
+import { asCouncilMember } from '@/types';
+
+export function useCouncilMembers({ council }: ForSelectedCouncil) {
+
+  const [fetch, query] = useGetCouncilMembersLazyQuery();
+
+  useEffect(() => {
+
+    if (!council) return;
+
+    const variables = {
+      where: { electedInCouncil: { id_eq: council.id } },
+    };
+
+    fetch({
+      variables,
+    });
+  }, [council]);
+
+
+  const member = useMemo(() => query.data?.councilMembers.map(asCouncilMember), [query.data]);
+
+  return {
+    member,
+    loading: query.loading,
+    error: query.error,
+  };
+}

+ 11 - 0
src/hooks/useElectedCouncils.ts

@@ -0,0 +1,11 @@
+import { ElectedCouncilOrderByInput, GetElectedCouncilsQueryVariables, useGetElectedCouncilsQuery } from '@/queries';
+import { asElectedCouncil } from '@/types';
+
+export const useElectedCouncils = ({
+  orderBy = ElectedCouncilOrderByInput.CreatedAtDesc,
+  ...rest
+}: GetElectedCouncilsQueryVariables) => {
+  const { data, error, loading } = useGetElectedCouncilsQuery({ variables: { orderBy, ...rest } });
+
+  return { error, loading, data: data?.electedCouncils.map(asElectedCouncil) };
+};

+ 31 - 0
src/hooks/useElection.ts

@@ -0,0 +1,31 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetElectionsLazyQuery } from '@/queries';
+
+import { ForSelectedCouncil } from './types';
+
+export function useElection({ council }: ForSelectedCouncil) {
+  const [fetch, query] = useGetElectionsLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+
+    const variables = {
+      where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    fetch({
+      variables,
+    });
+  }, [council]);
+
+  const election = useMemo(
+    () => ((query.data?.electionRounds.length || 0) > 0 ? query.data?.electionRounds[0] : undefined),
+    [query.data]
+  );
+  return {
+    election,
+    loading: query.loading,
+    error: query.error,
+  };
+}

+ 37 - 0
src/hooks/useGetValidation.ts

@@ -0,0 +1,37 @@
+import { useEffect, useMemo } from 'react';
+
+// import { useGetCandidatesCountQuery, useGetCastVotesCountLazyQuery } from '@/queries/__generated__/GetElection.generated';
+import { ForSelectedCouncil } from './types';
+
+export function useValidation({ council }: ForSelectedCouncil) {
+  // const [fetchCandidates, CandidateQuery] = useGetCandidatesCountQuery();
+  // const [fetchVotes, VotesQuery] = useGetCastVotesCountLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+
+    // var variables = {
+    //   where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    // };
+
+    // fetchCandidates({
+    //   variables,
+    // });
+    // fetchVotes({
+    //   variables,
+    // });
+  }, [council]);
+
+  // const candidates = useMemo(() => CandidateQuery.data?.candidatesConnection.totalCount, [CandidateQuery.data]);
+  // const votes = useMemo(() => VotesQuery.data?.castVotesConnection.totalCount, [VotesQuery.data]);
+  const validator = '-'; ///  ------
+  const stake = '-';
+  const mint = '-';
+  return {
+    validator,
+    stake,
+    mint,
+    loading: false, // CandidateQuery.loading || VotesQuery.loading,
+    error: false, //CandidateQuery.error || VotesQuery.loading,
+  };
+}

+ 24 - 0
src/hooks/useGroupWorkers.ts

@@ -0,0 +1,24 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetWorkersLazyQuery } from '@/queries';
+import { asWorker, WorkingGroup } from '@/types';
+
+import { ForSelectedCouncil } from './types';
+
+export interface UseGroupWorkers extends ForSelectedCouncil {
+  workingGroup: WorkingGroup;
+}
+
+export function useGroupWorkers({ council, workingGroup }: UseGroupWorkers) {
+  const [fetch, query] = useGetWorkersLazyQuery();
+
+  useEffect(() => {
+    fetch({ variables: { where: { groupId_contains: workingGroup.id } } });
+  }, [council, workingGroup]);
+
+  const workers = useMemo(() => {
+    return query.data?.workers.map(asWorker);
+  }, [query.data]);
+
+  return { workers, loading: query.loading, error: query.error };
+}

+ 45 - 0
src/hooks/useMemberships.ts

@@ -0,0 +1,45 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetInvitedMembersCountLazyQuery, useGetMembersCountLazyQuery } from '@/queries';
+
+import { ForSelectedCouncil } from './types';
+
+export function useMemberships({ council }: ForSelectedCouncil) {
+  const [fetchTotal, totalQuery] = useGetMembersCountLazyQuery();
+  const [fetchCreated, createdQuery] = useGetMembersCountLazyQuery();
+  const [fetchInvited, invitedQuery] = useGetInvitedMembersCountLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+    let variables = {
+      where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    };
+    fetchCreated({
+      variables,
+    });
+
+    fetchInvited({
+      variables,
+    });
+
+    variables = {
+      where: { createdAt_gt: '2013-01-10T22:50:12.000Z', createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    fetchTotal({
+      variables,
+    });
+  }, [council]);
+
+  const created = useMemo(() => createdQuery.data?.membershipsConnection.totalCount, [createdQuery.data]);
+  const invited = useMemo(() => invitedQuery.data?.memberInvitedEventsConnection.totalCount, [invitedQuery.data]);
+  const total = useMemo(() => totalQuery.data?.membershipsConnection.totalCount, [totalQuery.data]);
+
+  return {
+    created,
+    invited,
+    total,
+    loading: createdQuery.loading || invitedQuery.loading,
+    error: createdQuery.error || invitedQuery.error,
+  };
+}

+ 36 - 0
src/hooks/useNfts.ts

@@ -0,0 +1,36 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetNftIssuedCountLazyQuery, useGetNftSaleCountLazyQuery } from '@/queries';
+
+import { ForSelectedCouncil } from './types';
+
+export function useNFTs({ council }: ForSelectedCouncil) {
+  const [fetchIssued, IssuedQuery] = useGetNftIssuedCountLazyQuery();
+  const [totalSale, SaleQuery] = useGetNftSaleCountLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+
+    const variables = {
+      where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    fetchIssued({
+      variables,
+    });
+    totalSale({
+      variables,
+    });
+  }, [council]);
+
+  const issued = useMemo(() => IssuedQuery.data?.nftIssuedEventsConnection.totalCount, [IssuedQuery.data]);
+  const sale = useMemo(() => SaleQuery.data?.nftBoughtEventsConnection.totalCount, [SaleQuery.data]);
+  const fee = '-'; //// ???------
+  return {
+    issued,
+    sale,
+    fee,
+    loading: IssuedQuery.loading || SaleQuery.loading,
+    error: IssuedQuery.error || SaleQuery.loading,
+  };
+}

+ 62 - 0
src/hooks/useNumberProposal.ts

@@ -0,0 +1,62 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetCreatedProposalsCountLazyQuery, useGetExecutedProposalsCountLazyQuery, useGetProposalsLazyQuery } from '@/queries';
+
+import { ForSelectedCouncil } from './types';
+
+export function useNumberProposal({ council }: ForSelectedCouncil) {
+  const [fetchCreated, createdQuery] = useGetCreatedProposalsCountLazyQuery();
+  const [fetchExcuted, excutedQuery] = useGetExecutedProposalsCountLazyQuery();
+  const [fetchProposals, proposalsQuery] = useGetProposalsLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+
+    const variables = {
+      where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    fetchCreated({
+      variables,
+    });
+    fetchExcuted({
+      variables,
+    });
+    fetchProposals({
+      variables,
+    });
+  }, [council]);
+
+  const created = useMemo(() => createdQuery.data?.proposalCreatedEventsConnection.totalCount, [createdQuery.data]);
+  const executed = useMemo(() => excutedQuery.data?.proposalExecutedEventsConnection.totalCount, [excutedQuery.data]);
+
+  const wait = useMemo(() => {
+    var test: number = 0;
+    proposalsQuery.data?.proposals.map(d => {
+      if (d.status.__typename === "ProposalStatusDormant")
+        test = test + 1;
+    }
+    )
+    return (test)
+  }, [proposalsQuery.data]);
+
+  const deciding = useMemo(() => {
+    var test: number = 0;
+    proposalsQuery.data?.proposals.map(d => {
+      if (d.status.__typename === "ProposalStatusDeciding")
+        test = test + 1;
+    }
+    )
+    return (test)
+  }, [proposalsQuery.data]);
+
+  const failed = created! - executed! - wait - deciding;
+  return {
+    created,
+    executed,
+    failed,
+    wait,
+    loading: createdQuery.loading || excutedQuery.loading,
+    error: createdQuery.error || excutedQuery.loading,
+  };
+}

+ 41 - 0
src/hooks/usePostData.ts

@@ -0,0 +1,41 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetForumPostsCountLazyQuery } from '@/queries';
+
+import { ForSelectedCouncil } from './types';
+
+export function usePostTokenData({ council }: ForSelectedCouncil) {
+  const [fetchCreated, createdQuery] = useGetForumPostsCountLazyQuery();
+  const [totalCreated, totalQuery] = useGetForumPostsCountLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+    let variables = {
+      where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    fetchCreated({
+      variables,
+    });
+
+    variables = {
+      where: { createdAt_gt: '2013-01-10T22:50:12.000Z', createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    totalCreated({
+      variables,
+    });
+  }, [council]);
+
+  const created = useMemo(() => createdQuery.data?.forumPostsConnection.totalCount, [createdQuery.data]);
+  const total = useMemo(() => totalQuery.data?.forumPostsConnection.totalCount, [totalQuery.data]);
+  const forum = useMemo(() => totalQuery.data?.forumPosts, [totalQuery.data]);
+
+  return {
+    created,
+    total,
+    forum,
+    loading: createdQuery.loading,
+    error: createdQuery.error,
+  };
+}

+ 29 - 0
src/hooks/useProposals.ts

@@ -0,0 +1,29 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetProposalsLazyQuery } from '@/queries';
+import { asProposal } from '@/types';
+
+import { ForSelectedCouncil } from './types';
+
+export function useProposals({ council }: ForSelectedCouncil) {
+  const [fetchProposals, proposalsQuery] = useGetProposalsLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+    const variables = {
+      where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    fetchProposals({
+      variables,
+    });
+  }, [council]);
+
+  const proposals = useMemo(() => proposalsQuery.data?.proposals.map(asProposal), [proposalsQuery.data]);
+
+  return {
+    proposals,
+    loading: proposalsQuery.loading,
+    error: proposalsQuery.error,
+  };
+}

+ 39 - 0
src/hooks/useThread.ts

@@ -0,0 +1,39 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetForumThreadsCountLazyQuery } from '@/queries';
+
+import { ForSelectedCouncil } from './types';
+
+export function useThreadData({ council }: ForSelectedCouncil) {
+  const [fetchCreated, createdQuery] = useGetForumThreadsCountLazyQuery();
+  const [totalCreated, totalQuery] = useGetForumThreadsCountLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+    let variables = {
+      where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    fetchCreated({
+      variables,
+    });
+
+    variables = {
+      where: { createdAt_gt: '2013-01-10T22:50:12.000Z', createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    totalCreated({
+      variables,
+    });
+  }, [council]);
+
+  const created = useMemo(() => createdQuery.data?.forumThreadsConnection.totalCount, [createdQuery.data]);
+  const total = useMemo(() => totalQuery.data?.forumThreadsConnection.totalCount, [totalQuery.data]);
+
+  return {
+    created,
+    total,
+    loading: createdQuery.loading,
+    error: createdQuery.error,
+  };
+}

+ 52 - 0
src/hooks/useTokenMinted.ts

@@ -0,0 +1,52 @@
+import { useEffect, useMemo } from 'react';
+
+
+import { ForSelectedCouncil } from './types';
+import { useGetCouncilTokenLazyQuery, useGetWorkingGroupTokenLazyQuery, useGetMintedTokenLazyQuery } from '@/queries'
+
+export function useTokenMinted({ council }: ForSelectedCouncil) {
+
+  const [fetchCouncilToken, CouncilTokenQuery] = useGetCouncilTokenLazyQuery();
+  const [fetchWorkingGroupToken, WorkingGroupTokenQuery] = useGetWorkingGroupTokenLazyQuery();
+  const [fetchMintedToken, MintedTokenQuery] = useGetMintedTokenLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+
+    var variables = {
+      where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    fetchCouncilToken({
+      variables,
+    });
+
+    fetchWorkingGroupToken({
+      variables,
+    });
+
+    fetchMintedToken({
+      variables,
+    });
+  }, [council]);
+
+  const proposal = useMemo(() => WorkingGroupTokenQuery.data?.budgetUpdatedEvents.reduce((a: number, b) => {
+    return a + (b.budgetChangeAmount / 10000000000);
+  }, 0), [WorkingGroupTokenQuery.data]);
+
+  const councildata = useMemo(() => MintedTokenQuery.data?.rewardPaymentEvents.reduce((a: number, b) => {
+    return a + (b.paidBalance / 10000000000);
+  }, 0), [MintedTokenQuery.data]);
+
+  const minted = useMemo(() => CouncilTokenQuery.data?.budgetRefillEvents.reduce((a: number, b) => {
+    return a + (b.balance / 10000000000);
+  }, 0), [CouncilTokenQuery.data]);
+
+  return {
+    proposal,
+    minted,
+    councildata,
+    loading: MintedTokenQuery.loading || CouncilTokenQuery.loading || WorkingGroupTokenQuery.loading,
+    error: MintedTokenQuery.error || CouncilTokenQuery.error || WorkingGroupTokenQuery.error,
+  };
+}

+ 40 - 0
src/hooks/useVideos.ts

@@ -0,0 +1,40 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetVideoCountLazyQuery } from '@/queries';
+
+import { ForSelectedCouncil } from './types';
+
+export function useVideos({ council }: ForSelectedCouncil) {
+  const [fetchCreated, createdQuery] = useGetVideoCountLazyQuery();
+  const [totalCreated, totalQuery] = useGetVideoCountLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+
+    let variables = {
+      where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    fetchCreated({
+      variables,
+    });
+
+    variables = {
+      where: { createdAt_gt: '2013-01-10T22:50:12.000Z', createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    totalCreated({
+      variables,
+    });
+  }, [council]);
+
+  const created = useMemo(() => createdQuery.data?.videosConnection.totalCount, [createdQuery.data]);
+  const total = useMemo(() => totalQuery.data?.videosConnection.totalCount, [totalQuery.data]);
+
+  return {
+    created,
+    total,
+    loading: createdQuery.loading,
+    error: createdQuery.error,
+  };
+}

+ 41 - 0
src/hooks/useWorker.ts

@@ -0,0 +1,41 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetTerminatedWorkdersLazyQuery, useGetWorkedExitedLazyQuery, useGetOpeningFilledLazyQuery } from '@/queries';
+import { asWorkingGroup } from '@/types';
+
+import { ForSelectedCouncil } from './types';
+
+export function useWorker({ council }: ForSelectedCouncil) {
+  const [fetchTerminated, terminatedQuery] = useGetTerminatedWorkdersLazyQuery();
+  const [fetchExited, exitedQuery] = useGetWorkedExitedLazyQuery();
+  const [fetchFilled, filledQuery] = useGetOpeningFilledLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+
+    var variables = {
+      where: { createdAt_gt: "1970-01-01T00:00:00.000Z", createdAt_lt: council.endedAt?.timestamp },
+    };
+
+
+    fetchTerminated({ variables });
+    fetchExited({
+      variables
+    })
+    fetchFilled({
+      variables
+    })
+  }, [council]);
+
+  const terminatedWorker = useMemo(() => terminatedQuery.data?.terminatedWorkerEvents, [terminatedQuery.data]);
+  const exitedWorker = useMemo(() => exitedQuery.data?.workerExitedEvents, [exitedQuery.data]);
+  const filledWorker = useMemo(() => filledQuery.data?.openingFilledEvents, [filledQuery.data]);
+
+  return {
+    terminatedWorker,
+    exitedWorker,
+    filledWorker,
+    loading: terminatedQuery.loading || exitedQuery.loading || filledQuery.loading,
+    error: terminatedQuery.error || exitedQuery.error || filledQuery.error,
+  };
+}

+ 55 - 0
src/hooks/useWorkingGroups.ts

@@ -0,0 +1,55 @@
+import { useEffect, useMemo } from 'react';
+
+import { useGetWorkingGroupsLazyQuery, useGetWorkingGroupTokenLazyQuery, useGetRewardsLazyQuery } from '@/queries';
+import { asWorkingGroup } from '@/types';
+
+import { ForSelectedCouncil } from './types';
+
+export function useWorkingGroups({ council }: ForSelectedCouncil) {
+
+  const [fetch, query] = useGetWorkingGroupsLazyQuery();
+  const [fetchToken, tokenQuery] = useGetWorkingGroupTokenLazyQuery();
+  const [fetchTokenReward, tokenQueryReward] = useGetWorkingGroupTokenLazyQuery();
+  const [fetchReward, tokenReward] = useGetRewardsLazyQuery();
+
+  useEffect(() => {
+    if (!council) return;
+
+    var variables = {
+      where: { createdAt_gt: council.electedAt.timestamp, createdAt_lt: council.endedAt?.timestamp },
+    };
+
+
+    fetch();
+    fetchToken({
+      variables
+    })
+
+
+    variables = {
+      where: { createdAt_gt: "1970-01-01T00:00:00.000Z", createdAt_lt: council.endedAt?.timestamp },
+    };
+
+    fetchTokenReward({
+      variables
+    })
+
+    fetchReward({
+      variables
+    })
+  }, [council]);
+
+  const workingGroups = useMemo(() => query.data?.workingGroups.map(asWorkingGroup), [query.data]);
+  const workingTokens = useMemo(() => tokenQuery.data?.budgetUpdatedEvents, [tokenQuery.data]);
+  const workingTokensReward = useMemo(() => tokenQueryReward.data?.budgetUpdatedEvents, [tokenQueryReward.data]);
+  const rewardToken = useMemo(() => tokenReward.data?.rewardPaidEvents, [tokenReward.data]);
+
+  return {
+    workingGroups,
+    workingTokens,
+    workingTokensReward,
+    rewardToken,
+    loading: query.loading || tokenQuery.loading || tokenReward.loading || tokenQueryReward.loading,
+    error: query.error || tokenQuery.error || tokenReward.error || tokenQueryReward.error,
+  };
+}

+ 1 - 0
src/lib/queries.ts

@@ -12,6 +12,7 @@ export const queryJstats = (route: string) => {
 };
 
 export const getTokenomics = async (old?: Tokenomics) => {
+  debugger;
   if (old?.timestamp + 300000 > new Date()) return;
   console.debug(`Updating tokenomics`);
   let { data } = await axios.get("https://status.joystream.org/status");

+ 22 - 0
src/types/Councilor.ts

@@ -0,0 +1,22 @@
+import BN from 'bn.js';
+
+import { CouncilMemberFieldsFragment } from '@/queries';
+
+import { asMember, Member } from './Member';
+
+export interface Councilor {
+  id: string;
+  member: Member;
+  numberOfTerms: number;
+  unpaidReward: BN;
+  stake: BN;
+  voterStake?: BN;
+}
+
+export const asCouncilor = (fields: CouncilMemberFieldsFragment): Councilor => ({
+  id: fields.id,
+  member: asMember(fields.member),
+  numberOfTerms: fields.member.councilMembers.length,
+  unpaidReward: new BN(fields.unpaidReward),
+  stake: new BN(fields.stake),
+});

+ 31 - 0
src/types/ElectedCouncil.ts

@@ -0,0 +1,31 @@
+import { ElectedCouncilFieldsFragment } from '@/queries';
+
+import { asBlock, Block } from './common/Block';
+import { asCouncilor, Councilor } from './Councilor';
+
+export interface ElectedCouncil {
+  id: string;
+  electedAt: Block;
+  endedAt?: Block;
+  councilors: Councilor[];
+  electionCycleId: number | undefined;
+}
+
+export const asElectedCouncil = (fields: ElectedCouncilFieldsFragment): ElectedCouncil => ({
+  id: fields.id,
+  councilors: fields.councilMembers.map(asCouncilor),
+  electionCycleId: fields.councilElections[0]?.cycleId,
+  electedAt: asBlock({
+    createdAt: fields.electedAtTime,
+    inBlock: fields.electedAtBlock,
+    network: fields.electedAtNetwork,
+  }),
+  endedAt:
+    fields.endedAtBlock && fields.endedAtNetwork
+      ? asBlock({
+          createdAt: fields.endedAtTime,
+          inBlock: fields.endedAtBlock,
+          network: fields.endedAtNetwork,
+        })
+      : undefined,
+});

+ 112 - 0
src/types/Member.ts

@@ -0,0 +1,112 @@
+import { MemberFieldsFragment, MembershipExternalResourceType, CouncilMemberFragment, GetCouncilMembersQuery } from '@/queries';
+
+import { Address, asBlock, Block, castQueryResult } from './common';
+import { asWorkingGroupName } from './WorkingGroup';
+
+type ID = string;
+
+export interface BoundAccountEvent {
+  createdAtBlock: Block;
+  account: Address;
+}
+
+export interface MemberRole {
+  id: string;
+  groupName: string;
+  createdAt?: string;
+  isLead: boolean;
+}
+
+export interface Member {
+  id: ID;
+  handle: string;
+  rootAccount: Address;
+  controllerAccount: Address;
+  boundAccounts: Address[];
+  name?: string;
+  avatar?: string;
+  inviteCount: number;
+  roles: MemberRole[];
+  isVerified: boolean;
+  isFoundingMember: boolean;
+  isCouncilMember: boolean;
+  createdAt: string;
+  boundAccountsEvents?: BoundAccountEvent[];
+}
+
+export type GenesisEntry = {
+  type: 'genesis';
+};
+
+export type InvitedEntry = {
+  type: 'invited';
+  block: Block;
+};
+
+export type PaidEntry = {
+  type: 'paid';
+  block: Block;
+};
+
+export type MemberEntry = GenesisEntry | InvitedEntry | PaidEntry;
+// Temporary fix for: https://github.com/Joystream/pioneer/issues/1493
+export type InvitedMember = Member; // & { entry: InvitedEntry }
+
+interface MembershipExternalResource {
+  source: MembershipExternalResourceType;
+  value: string;
+}
+
+export interface MemberWithDetails extends Member {
+  about?: string;
+  invitedBy?: Member;
+  entry: MemberEntry;
+  invitees: InvitedMember[];
+  externalResources?: MembershipExternalResource[];
+}
+
+export const asMember = (data: Omit<MemberFieldsFragment, '__typename'>): Member => ({
+  id: data.id,
+  handle: data.handle,
+  name: data.metadata.name ?? undefined,
+  avatar: castQueryResult(data.metadata.avatar, 'AvatarUri')?.avatarUri,
+  inviteCount: data.inviteCount,
+  isFoundingMember: data.isFoundingMember,
+  isCouncilMember: data.isCouncilMember,
+  isVerified: data.isVerified,
+  rootAccount: data.rootAccount,
+  controllerAccount: data.controllerAccount,
+  boundAccounts: [...data?.boundAccounts],
+  boundAccountsEvents: data.stakingaccountaddedeventmember?.map(asBoundAccountsEvent) ?? [],
+  roles: data.roles.map(asMemberRole),
+  createdAt: data.createdAt,
+});
+
+const asBoundAccountsEvent = (
+  fields: NonNullable<MemberFieldsFragment['stakingaccountaddedeventmember']>[0]
+): BoundAccountEvent => ({
+  createdAtBlock: asBlock({
+    createdAt: fields.createdAt,
+    inBlock: fields.inBlock,
+    network: fields.network,
+  }),
+  account: fields.account,
+});
+
+export const asMemberRole = (data: MemberFieldsFragment['roles'][0]): MemberRole => ({
+  id: data.id,
+  isLead: data.isLead,
+  groupName: asWorkingGroupName(data.group.name),
+  createdAt: data.createdAt,
+});
+
+export interface CouncilMember {
+  electedInCouncilId: string;
+  handler: string;
+}
+export const asCouncilMember = (data: CouncilMemberFragment): CouncilMember => {
+  return {
+    electedInCouncilId: data.electedInCouncilId,
+    handler: data.member.handle ?? ''
+  }
+}

+ 97 - 0
src/types/Proposal.ts

@@ -0,0 +1,97 @@
+import { lowerFirstLetter } from '@/helpers';
+import { ProposalFieldsFragment, ProposalPostFragment, VoteFieldsFragment } from '@/queries';
+
+import { asMember, Member } from './Member';
+
+export type ProposalStatus =
+  | 'deciding'
+  | 'gracing'
+  | 'dormant'
+  | 'vetoed'
+  | 'executed'
+  | 'executionFailed'
+  | 'slashed'
+  | 'rejected'
+  | 'expired'
+  | 'cancelled'
+  | 'canceledByRuntime';
+
+export type ProposalType =
+  | 'signal'
+  | 'runtimeUpgrade'
+  | 'fundingRequest'
+  | 'setMaxValidatorCount'
+  | 'createWorkingGroupLeadOpening'
+  | 'fillWorkingGroupLeadOpening'
+  | 'updateWorkingGroupBudget'
+  | 'decreaseWorkingGroupLeadStake'
+  | 'slashWorkingGroupLead'
+  | 'setWorkingGroupLeadReward'
+  | 'terminateWorkingGroupLead'
+  | 'amendConstitution'
+  | 'cancelWorkingGroupLeadOpening'
+  | 'setMembershipPrice'
+  | 'setCouncilBudgetIncrement'
+  | 'setCouncilorReward'
+  | 'setInitialInvitationBalance'
+  | 'setInitialInvitationCount'
+  | 'setMembershipLeadInvitationQuota'
+  | 'setReferralCut'
+  | 'createBlogPost'
+  | 'editBlogPost'
+  | 'lockBlogPost'
+  | 'unlockBlogPost'
+  | 'veto';
+
+
+
+export interface Proposal {
+  id: string;
+  title: string;
+  status: ProposalStatus;
+  type: ProposalType;
+  proposer: Member;
+  createdAt: string;
+  endedAt?: string;
+  councilApprovals: number;
+  exactExecutionBlock?: number;
+  votes: Array<VoteFieldsFragment>;
+  posts: Array<ProposalPostFragment>;
+}
+
+export const typenameToProposalStatus = (typename: string): ProposalStatus => {
+  const status = typename.replace('ProposalStatus', '');
+
+  return lowerFirstLetter(status) as ProposalStatus;
+};
+
+export const typenameToProposalDetails = (typename: string): ProposalType => {
+  const details = typename.replace('ProposalDetails', '');
+
+  return lowerFirstLetter(details) as ProposalType;
+};
+
+export const proposalActiveStatuses: ProposalStatus[] = ['deciding', 'gracing', 'dormant'];
+
+export const isProposalActive = (status: ProposalStatus) => proposalActiveStatuses.includes(status);
+
+export const asProposal = (fields: ProposalFieldsFragment): Proposal => {
+  const proposal: Proposal = {
+    id: fields.id,
+    title: fields.title,
+    status: typenameToProposalStatus(fields.status.__typename),
+    type: typenameToProposalDetails(fields.details.__typename),
+    proposer: asMember(fields.creator),
+    createdAt: fields.createdAt,
+    councilApprovals: fields.councilApprovals,
+    exactExecutionBlock: fields.exactExecutionBlock ?? undefined,
+    votes: fields.votes,
+    posts: fields.discussionThread.posts
+  };
+
+  if (!isProposalActive(proposal.status)) {
+    proposal.endedAt = fields.statusSetAtTime;
+  }
+
+  return proposal;
+};

+ 96 - 0
src/types/Worker.ts

@@ -0,0 +1,96 @@
+import { BN_ZERO } from '@polkadot/util';
+import BN from 'bn.js';
+
+import { PastWorkerFieldsFragment, WorkerDetailedFieldsFragment, WorkerFieldsFragment } from '@/queries';
+
+import { Address, asBlock, Block, castQueryResult } from './common';
+import { asMember, Member } from './Member';
+import { asWorkingGroupName, GroupIdName, WorkingGroup } from './WorkingGroup';
+
+export interface WorkerBaseInfo {
+  member: Member;
+  applicationId: string;
+}
+
+export interface Worker {
+  id: string;
+  runtimeId: number;
+  membership: Pick<Member, 'id' | 'controllerAccount'>;
+  group: Pick<WorkingGroup, 'id' | 'name'>;
+  status: WorkerStatusTypename;
+  isLead: boolean;
+  rewardPerBlock: BN;
+  owedReward: BN;
+  stake: BN;
+}
+
+export interface WorkerWithDetails extends Worker {
+  applicationId: string;
+  openingId: string;
+  roleAccount: Address;
+  rewardAccount: Address;
+  stakeAccount: Address;
+  hiredAtBlock: Block;
+  minStake: BN;
+}
+
+export interface PastWorker {
+  id: string;
+  member: Member;
+  dateStarted: Block;
+  dateFinished: Block;
+}
+
+export type WorkerStatus = 'active' | 'left' | 'leaving' | 'terminated';
+export type WorkerStatusTypename = WorkerDetailedFieldsFragment['status']['__typename'];
+export const WorkerStatusToTypename: Record<WorkerStatus, WorkerFieldsFragment['status']['__typename']> = {
+  active: 'WorkerStatusActive',
+  left: 'WorkerStatusLeft',
+  leaving: 'WorkerStatusLeaving',
+  terminated: 'WorkerStatusTerminated',
+};
+
+export const asWorkerBaseInfo = (fields: WorkerFieldsFragment): WorkerBaseInfo => ({
+  member: asMember(fields.membership),
+  applicationId: fields.applicationId,
+});
+
+export const asWorker = (fields: WorkerFieldsFragment): Worker => ({
+  id: fields.id,
+  runtimeId: fields.runtimeId,
+  group: {
+    id: fields.group.id as GroupIdName,
+    name: asWorkingGroupName(fields.group.name),
+  },
+  membership: {
+    id: fields.membership.id,
+    controllerAccount: fields.membership.controllerAccount,
+  },
+  status: fields.status.__typename,
+  isLead: fields.isLead,
+  rewardPerBlock: new BN(fields.rewardPerBlock),
+  stake: new BN(fields.stake),
+  owedReward: new BN(fields.missingRewardAmount || BN_ZERO),
+});
+
+export const asWorkerWithDetails = (fields: WorkerDetailedFieldsFragment): WorkerWithDetails => ({
+  ...asWorker(fields),
+  applicationId: fields.application.id,
+  openingId: fields.application.openingId,
+  roleAccount: fields.roleAccount,
+  rewardAccount: fields.rewardAccount,
+  stakeAccount: fields.stakeAccount,
+  minStake: new BN(fields.application.opening.stakeAmount),
+  hiredAtBlock: asBlock(fields.entry),
+});
+
+export const asPastWorker = (fields: PastWorkerFieldsFragment): PastWorker => ({
+  id: fields.id,
+  member: asMember(fields.membership),
+  dateStarted: asBlock(fields.entry),
+  dateFinished: asBlock(
+    castQueryResult(fields.status, 'WorkerStatusTerminated')?.terminatedWorkerEvent ??
+      castQueryResult(fields.status, 'WorkerStatusLeft')?.workerExitedEvent ??
+      fields.entry
+  ),
+});

+ 60 - 0
src/types/WorkingGroup.ts

@@ -0,0 +1,60 @@
+import BN from 'bn.js';
+
+import { sumStakes } from '@/helpers';
+import { WorkerFieldsFragment, WorkingGroupFieldsFragment, GetWorkingGroupTokenQuery } from '@/queries';
+
+export const GroupIdToGroupParam = {
+  contentWorkingGroup: 'Content',
+  forumWorkingGroup: 'Forum',
+  appWorkingGroup: 'App',
+  membershipWorkingGroup: 'Membership',
+  distributionWorkingGroup: 'Distribution',
+  storageWorkingGroup: 'Storage',
+  operationsWorkingGroupAlpha: 'OperationsAlpha',
+  operationsWorkingGroupBeta: 'OperationsBeta',
+  operationsWorkingGroupGamma: 'OperationsGamma',
+} as const;
+
+export type GroupIdName = keyof typeof GroupIdToGroupParam;
+
+export interface WorkingGroup {
+  id: string;
+  name: string;
+  image?: string;
+  about?: string;
+  leadId?: string;
+  status?: string;
+  description?: string;
+  statusMessage?: string;
+  budget?: BN;
+  averageStake?: BN;
+  isActive?: boolean;
+}
+
+
+
+export const asWorkingGroup = (group: WorkingGroupFieldsFragment): WorkingGroup => {
+  return {
+    id: group.id,
+    image: undefined,
+    name: group.name,
+    about: group.metadata?.about ?? '',
+    description: group.metadata?.description ?? '',
+    status: group.metadata?.status ?? '',
+    statusMessage: group.metadata?.statusMessage ?? '',
+    budget: new BN(group.budget),
+    averageStake: getAverageStake(group.workers),
+    leadId: group.leader?.membershipId,
+    isActive: group.leader?.isActive ?? false,
+  };
+};
+
+
+export const asWorkingGroupName = (name: string) =>
+  name
+    .replace('WorkingGroup', '')
+    .replace(/([a-z])([A-Z])/g, '$1 $2')
+    .replace(/^[a-z]/, (match) => match.toUpperCase());
+
+export const getAverageStake = (workers: Pick<WorkerFieldsFragment, 'stake'>[]) =>
+  sumStakes(workers).divn(workers.length);

+ 31 - 0
src/types/common/Block.ts

@@ -0,0 +1,31 @@
+import * as Types from '@/queries/__generated__/baseTypes.generated';
+
+import { isNumber } from './utils';
+
+export type NetworkType = 'BABYLON' | 'ALEXANDRIA' | 'ROME' | 'GIZA' | 'OLYMPIA';
+
+export interface Block {
+  number: number;
+  network: NetworkType;
+  timestamp: string;
+}
+
+export interface BlockFields {
+  inBlock: number;
+  createdAt: string;
+  network: Types.Network;
+}
+
+export const asBlock = (fields: BlockFields): Block => ({
+  number: fields.inBlock,
+  network: fields.network,
+  timestamp: fields.createdAt,
+});
+
+export const maybeAsBlock = (
+  number?: number | null,
+  dateTime?: string,
+  network?: Types.Network | null
+): Block | undefined => {
+  if (isNumber(number) && dateTime && network) return asBlock({ inBlock: number, createdAt: dateTime, network });
+};

+ 11 - 0
src/types/common/Dates.ts

@@ -0,0 +1,11 @@
+interface StartDate {
+  start: Date;
+}
+interface EndDate {
+  end: Date;
+}
+
+export type DateRange = StartDate & EndDate;
+export type PartialDateRange = undefined | StartDate | EndDate | DateRange;
+
+export type DateSelection = Date | PartialDateRange;

+ 28 - 0
src/types/common/casting.ts

@@ -0,0 +1,28 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// See https://spin.atomicobject.com/2021/11/10/discriminated-unions-typescript-project
+
+type TypedUnion<TypeKey extends string, TypeValue extends string = string> = { [key in TypeKey]: TypeValue };
+export type NarrowTypedUnion<
+  Union extends TypedUnion<TypeKey>,
+  TypeKey extends string,
+  TypeValue extends Union[TypeKey]
+> = Union extends TypedUnion<TypeKey, TypeValue>
+  ? Union
+  : TypeValue extends Union[TypeKey]
+  ? Omit<Union, TypeKey> & TypedUnion<TypeKey, TypeValue>
+  : never;
+
+export const castTypedUnion =
+  <TypeKey extends string>(typeKey: TypeKey) =>
+  <Result extends TypedUnion<TypeKey>, TypeValue extends Result[TypeKey]>(
+    result: Result | null | undefined,
+    typeValue: TypeValue
+  ) =>
+    result ? (result as NarrowTypedUnion<Result, TypeKey, TypeValue>) : undefined;
+
+export type NarrowQueryResult<
+  QueryResult extends TypedUnion<'__typename'>,
+  TypeName extends QueryResult['__typename']
+> = NarrowTypedUnion<QueryResult, '__typename', TypeName>;
+
+export const castQueryResult = castTypedUnion('__typename');

+ 1 - 0
src/types/common/form.ts

@@ -0,0 +1 @@
+export type WithNullableValues<T> = { [P in keyof T]: T[P] | null };

+ 7 - 0
src/types/common/helpers.ts

@@ -0,0 +1,7 @@
+export type Reducer<Accumulator, Value = Accumulator> = (acc: Accumulator, value: Value, index: number) => Accumulator;
+
+export type Defined<T> = T extends undefined ? never : T;
+
+export type EnumTypeString<TEnum extends string> = { [key in string]: TEnum | string };
+
+export type KeysOfUnion<T> = T extends T ? keyof T : never;

+ 7 - 0
src/types/common/index.ts

@@ -0,0 +1,7 @@
+export * from './Block';
+export * from './casting';
+export * from './Dates';
+export * from './form';
+export * from './helpers';
+export * from './utils';
+export type Address = string;

+ 127 - 0
src/types/common/utils.ts

@@ -0,0 +1,127 @@
+type Obj = Record<string, any>;
+
+// Type guards
+
+export const isFunction = (something: unknown): something is CallableFunction => typeof something === 'function';
+
+export const isDefined = <T>(something: T | undefined): something is T => typeof something !== 'undefined';
+
+export const isNumber = (something: unknown): something is number => typeof something === 'number';
+
+export const isString = (something: unknown): something is string => typeof something === 'string';
+
+export const isRecord = (something: unknown): something is Obj => typeof something === 'object' && something !== null;
+
+export const whenDefined = <T, R>(something: T | undefined, fn: (something: T) => R): R | undefined => {
+  if (isDefined(something)) return fn(something);
+};
+
+// Type Casting:
+
+export const toNumber = (value: any): number => value?.toNumber?.() ?? (isNumber(value) ? value : NaN);
+
+// Math:
+
+export const clamp = (min: number, value: number, max: number) => Math.max(min, Math.min(max, value));
+
+// Objects:
+
+interface EqualsOption {
+  checkExtraKeys?: boolean;
+  depth?: boolean | number;
+}
+
+export const objectEquals = <T extends Obj>(
+  reference: T,
+  { checkExtraKeys = false, depth = 1 }: EqualsOption = {}
+): ((compared: T) => boolean) => {
+  const equalsOption = { checkExtraKeys, depth: isNumber(depth) ? depth - 1 : depth };
+  const expectedKeys: Array<keyof T> = Object.keys(reference);
+  return (compared) =>
+    (!checkExtraKeys || expectedKeys.length === Object.keys(compared).length) &&
+    expectedKeys.every((key) => equals(compared[key], equalsOption)(reference[key]));
+};
+
+export const equals = <T>(reference: T, { depth = 1, ...option }: EqualsOption = {}): ((compared: T) => boolean) => {
+  if (depth > 0 && isRecord(reference)) {
+    const isEqual = objectEquals(reference, { depth, ...option });
+    return (compared) => isRecord(compared) && isEqual(compared);
+  } else {
+    return (compared) => compared === reference;
+  }
+};
+
+export const merge = <A extends Obj, B extends Obj = Partial<A>>(a: A, b: B): A & B => ({ ...a, ...b });
+
+export const propsEquals =
+  <T extends Obj>(...keys: (keyof T)[]) =>
+  (a: T, b: T) =>
+    keys.every((key) => a[key] === b[key]);
+
+export const definedValues = <T extends Record<any, any>>(obj: T): T =>
+  Object.fromEntries(Object.entries(obj).flatMap(([key, value]) => (isDefined(value) ? [[key, value]] : []))) as T;
+
+// Lists:
+
+export const dedupeObjects = <T extends Obj>(list: T[], options?: EqualsOption): T[] =>
+  list.reduce((remain: T[], item) => [...remain, ...(remain.some(objectEquals(item, options)) ? [] : [item])], []);
+
+export const intersperse = <T, S>(list: T[], toSeparator: (index: number, list: T[]) => S): (T | S)[] =>
+  list.length < 2 ? list : [list[0], ...list.slice(1).flatMap((item, index) => [toSeparator(index, list), item])];
+
+export const partition = <T>(list: T[], predicate: (x: T) => boolean): [T[], T[]] =>
+  list.reduce(
+    ([pass, fail]: [T[], T[]], item): [T[], T[]] =>
+      predicate(item) ? [[...pass, item], fail] : [pass, [...fail, item]],
+    [[], []]
+  );
+
+export const repeat = <T>(getItem: (index: number) => T, times: number): T[] =>
+  Array.from({ length: times }, (_, i) => getItem(i));
+
+export const debounce = <T extends (...params: any[]) => any>(fn: T, delay = 400) => {
+  type Result = (ReturnType<T> extends Promise<infer U> ? U : ReturnType<T>) | undefined;
+  let latestTimeout: ReturnType<typeof setTimeout> | undefined;
+
+  return (...params: Parameters<T>) =>
+    new Promise<Result>((resolve) => {
+      const resolveImmediately = !latestTimeout;
+
+      const timeout = setTimeout(() => {
+        if (timeout !== latestTimeout) {
+          !resolveImmediately && resolve(undefined);
+        } else {
+          latestTimeout = undefined;
+          !resolveImmediately && resolve(fn(...params));
+        }
+      }, delay);
+      latestTimeout = timeout;
+
+      resolveImmediately && resolve(fn(...params));
+    });
+};
+
+export const last = <T>(list: ArrayLike<T>): T => list[list.length - 1];
+export const flatten = <T>(nested: (T | T[])[]) => ([] as T[]).concat(...nested);
+
+export const asArray = <T>(item: undefined | T): T[] => (isDefined(item) ? [item] : []);
+
+type Item = Record<string, any>;
+
+export const arrayGroupBy = (items: Item[], key: keyof Item) =>
+  items.reduce(
+    (result, item) => ({
+      ...result,
+      [item[key]]: [...(result[item[key]] || []), item],
+    }),
+    {}
+  );
+
+// Promises:
+
+type MapperP<T, R> = (value: T, index: number, array: T[] | readonly T[]) => Promise<R>;
+export const mapP = <T, R>(list: T[] | readonly T[], mapper: MapperP<T, R>): Promise<R[]> =>
+  Promise.all(list.map(mapper));
+
+export const flatMapP = async <T, R>(list: T[] | readonly T[], mapper: MapperP<T, R | R[]>): Promise<R[]> =>
+  Promise.all(flatten(await mapP(list, mapper)));

+ 7 - 0
src/types/index.ts

@@ -0,0 +1,7 @@
+export * from './common';
+export * from './Councilor';
+export * from './ElectedCouncil';
+export * from './Member';
+export * from './Proposal';
+export * from './Worker';
+export * from './WorkingGroup';

+ 1 - 0
tsconfig.json

@@ -19,6 +19,7 @@
       "rootDir": "./src/",
       "baseUrl": ".",
       "paths": {
+        "@/*": ["./src/*"],
         "./*": ["./node_modules/*", "./src/*"]
       }
   },