Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/src/components/founder/FounderCenterPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function FounderCenterPanel({
}: FounderCenterPanelProps) {
const { t } = useTranslation();
const { isConnected, address } = useAccount();

const { proposals, loading: proposalsLoading, refetch: refetchProposals } = useFounderProposals(founder.name);
const { totems: ofcTotems, loading: ofcLoading, dynamicCategories } = useAllOFCTotems();
const { votes: userVotes, loading: votesLoading, refetch: refetchVotes } = useUserVotesForFounder(address, founder.name);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/modal/ClaimExistsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { GET_USER_POSITION } from '../../lib/graphql/queries';
import { WithdrawModal } from './WithdrawModal';
import type { ExistingClaimInfo } from '../../types/claim';
import { SUPPORT_COLORS, OPPOSE_COLORS } from '../../config/colors';
import { getCacheFetchPolicy, FIVE_MINUTES } from '../../lib/queryCacheTTL';
import { getCacheFetchPolicy, TWO_MINUTES } from '../../lib/queryCacheTTL';

// Re-export for backward compatibility
export type { ExistingClaimInfo } from '../../types/claim';
Expand Down Expand Up @@ -78,7 +78,7 @@ export function ClaimExistsModal({
const positionFetchPolicy = getCacheFetchPolicy(
'GetUserPosition',
{ walletAddress: address?.toLowerCase(), termId: claim?.termId?.toLowerCase() },
FIVE_MINUTES
TWO_MINUTES
);

// Also query GraphQL as fallback (for display purposes)
Expand Down
51 changes: 51 additions & 0 deletions apps/web/src/contexts/FoundersDataContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createContext, useContext, type ReactNode } from 'react';
import type { ApolloError } from '@apollo/client';
import type { Triple, GetDepositsByTermIdsResult } from '../lib/graphql/types';
import type { FounderForHomePage } from '../types/founder';
import type { TopTotem } from '../hooks/data/useTopTotems';
import { useFoundersForHomePage } from '../hooks/data/useFoundersForHomePage';

type DepositEntry = GetDepositsByTermIdsResult['deposits'][number];

interface FoundersDataContextValue {
// Enriched founders list (42 founders with atomId, winningTotem, etc.)
founders: FounderForHomePage[];
// Homepage stats
stats: {
totalTrustVoted: number;
uniqueVoters: number;
foundersWithTotems: number;
totalProposals: number;
};
// Pre-computed top totems per founder (for carousel cards)
topTotemsMap: Map<string, TopTotem[]>;
// Raw proposals grouped by founder name (for panel hooks)
proposalsByFounder: Map<string, Triple[]>;
// Raw deposits indexed by term_id (for panel hooks)
depositsByTermId: Map<string, DepositEntry[]>;
// State
loading: boolean;
error: ApolloError | null;
// Invalidation after vote
refetch: () => Promise<void>;
}

const FoundersDataContext = createContext<FoundersDataContextValue | null>(null);

export function FoundersDataProvider({ children }: { children: ReactNode }) {
const data = useFoundersForHomePage();

return (
<FoundersDataContext.Provider value={data}>
{children}
</FoundersDataContext.Provider>
);
}

export function useFoundersData(): FoundersDataContextValue {
const ctx = useContext(FoundersDataContext);
if (!ctx) {
throw new Error('useFoundersData must be used within a FoundersDataProvider');
}
return ctx;
}
158 changes: 39 additions & 119 deletions apps/web/src/hooks/data/useFounderPanelStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,11 @@
* @see Phase 10 - Etape 6 in TODO_FIX_01_Discussion.md
*/

import { useMemo, useCallback } from 'react';
import { useQuery } from '@apollo/client';
import { useMemo } from 'react';
import { formatEther } from 'viem';
import {
GET_FOUNDER_PANEL_STATS,
GET_DEPOSITS_BY_TERM_IDS,
} from '../../lib/graphql/queries';
import type {
GetFounderPanelStatsResult,
GetDepositsByTermIdsResult,
} from '../../lib/graphql/types';
import { truncateAmount } from '../../utils/formatters';
import { useAllOFCTotems } from './useAllOFCTotems';
import { getCacheFetchPolicy, FIVE_MINUTES } from '../../lib/queryCacheTTL';
import { useFoundersData } from '../../contexts/FoundersDataContext';

export interface FounderPanelStats {
/** Total Market Cap in wei */
Expand Down Expand Up @@ -83,126 +74,55 @@ function formatMarketCap(value: bigint): string {
* ```
*/
export function useFounderPanelStats(founderName: string): UseFounderPanelStatsReturn {
// Get OFC category map for filtering
const { proposalsByFounder, depositsByTermId, loading, error, refetch } = useFoundersData();
const { categoryMap, loading: categoryLoading } = useAllOFCTotems();

// TTL-based cache policy: only fetch if data is older than 5 minutes
const triplesFetchPolicy = getCacheFetchPolicy(
'GetFounderPanelStats',
{ founderName },
FIVE_MINUTES
);
const stats = useMemo(() => {
const proposals = proposalsByFounder.get(founderName) ?? [];

// First query: get triples for this founder
const {
data: triplesData,
loading: triplesLoading,
error: triplesError,
refetch: refetchTriples,
} = useQuery<GetFounderPanelStatsResult>(GET_FOUNDER_PANEL_STATS, {
variables: { founderName },
skip: !founderName,
// TTL-based: 'cache-first' if fresh, 'cache-and-network' if stale
fetchPolicy: triplesFetchPolicy,
nextFetchPolicy: 'cache-first',
});
// Filter to OFC totems only
const ofcTriples = proposals.filter((t) =>
t.object?.term_id && categoryMap.has(t.object.term_id)
);

// Filter triples to only include OFC totems
const filteredTriples = useMemo(() => {
if (!triplesData?.triples) return [];
return triplesData.triples.filter((t) => {
// Only keep triples where the totem (object) has an OFC category
return t.object?.term_id && categoryMap.has(t.object.term_id);
});
}, [triplesData?.triples, categoryMap]);
// Total Market Cap = Σ(FOR + AGAINST)
let totalMarketCap = 0n;
const termIds = new Set<string>();

// Extract term_ids from filtered triples for the deposits query
// Include both term_id (FOR) and counter_term.id (AGAINST)
const termIds = useMemo(() => {
const ids: string[] = [];
filteredTriples.forEach((t) => {
ids.push(t.term_id);
if (t.counter_term?.id) {
ids.push(t.counter_term.id);
for (const triple of ofcTriples) {
if (triple.triple_vault?.total_assets) {
totalMarketCap += BigInt(triple.triple_vault.total_assets);
}
if (triple.counter_term?.total_assets) {
totalMarketCap += BigInt(triple.counter_term.total_assets);
}
termIds.add(triple.term_id);
if (triple.counter_term?.id) {
termIds.add(triple.counter_term.id);
}
});
return ids;
}, [filteredTriples]);

// TTL-based cache policy for deposits query
// Use founderName as key since termIds is derived from founder's triples
const depositsFetchPolicy = getCacheFetchPolicy(
'GetDepositsByTermIds_PanelStats',
{ founderName },
FIVE_MINUTES
);

// Second query: get deposits for those term_ids
// Apollo automatically refetches when termIds changes (via variables)
const {
data: depositsData,
loading: depositsLoading,
error: depositsError,
refetch: refetchDeposits,
} = useQuery<GetDepositsByTermIdsResult>(GET_DEPOSITS_BY_TERM_IDS, {
variables: { termIds },
skip: termIds.length === 0,
// TTL-based: 'cache-first' if fresh, 'cache-and-network' if stale
fetchPolicy: depositsFetchPolicy,
nextFetchPolicy: 'cache-first',
});

// Calculate Total Market Cap = Σ(FOR + AGAINST) on filtered OFC triples
let totalMarketCap = 0n;
for (const triple of filteredTriples) {
// FOR votes (triple_vault)
if (triple.triple_vault?.total_assets) {
totalMarketCap += BigInt(triple.triple_vault.total_assets);
}
// AGAINST votes (counter_term)
if (triple.counter_term?.total_assets) {
totalMarketCap += BigInt(triple.counter_term.total_assets);
}
}

// Calculate Total Holders = count distinct sender_id
const uniqueHolders = new Set<string>();
if (depositsData?.deposits) {
for (const deposit of depositsData.deposits) {
uniqueHolders.add(deposit.sender_id.toLowerCase());
}
}

// Claims = number of distinct OFC triples
const claims = filteredTriples.length;

const stats: FounderPanelStats = {
totalMarketCap,
formattedMarketCap: formatMarketCap(totalMarketCap),
totalHolders: uniqueHolders.size,
claims,
};

// Combined loading state (includes category loading for OFC filtering)
const loading = triplesLoading || categoryLoading || (termIds.length > 0 && depositsLoading);

// Combined error (return first error encountered)
const error = triplesError || depositsError;
// Total Holders = count distinct sender_id from deposits
const uniqueHolders = new Set<string>();
termIds.forEach((id) => {
const deposits = depositsByTermId.get(id);
if (deposits) {
deposits.forEach((d) => uniqueHolders.add(d.sender_id.toLowerCase()));
}
});

// Combined refetch function
// Memoize refetch to prevent unnecessary re-renders
const refetch = useCallback(() => {
refetchTriples();
if (termIds.length > 0) {
refetchDeposits();
}
}, [refetchTriples, refetchDeposits, termIds.length]);
return {
totalMarketCap,
formattedMarketCap: formatMarketCap(totalMarketCap),
totalHolders: uniqueHolders.size,
claims: ofcTriples.length,
} as FounderPanelStats;
}, [proposalsByFounder, depositsByTermId, founderName, categoryMap]);

// Memoize return value to prevent unnecessary re-renders
return useMemo(() => ({
stats,
loading,
loading: loading || categoryLoading,
error: error as Error | undefined,
refetch,
}), [stats, loading, error, refetch]);
}), [stats, loading, categoryLoading, error, refetch]);
}
Loading