Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
21 changes: 21 additions & 0 deletions src/screens/Bundestag/List/Components/ErrorState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from "react";
import { Text } from "react-native";

import { Button } from "../../../../components/Button";
import { Centered } from "../../../../components/Centered";

interface ErrorStateProps {
onRetry: () => void;
}

export const ErrorState: React.FC<ErrorStateProps> = ({ onRetry }) => (
<Centered>
<Text>Verbindungsfehler</Text>
<Button
onPress={onRetry}
text="Nochmal versuchen"
textColor="blue"
backgroundColor="transparent"
/>
</Centered>
);
34 changes: 34 additions & 0 deletions src/screens/Bundestag/List/Components/RetryState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";
import styled from "styled-components/native";

import { Centered } from "../../../../components/Centered";
import { ListLoading } from "../../../../components/ListLoading";

interface RetryStateProps {
remainingAttempts: number;
}

export const RetryState: React.FC<RetryStateProps> = ({
remainingAttempts,
}) => (
<Centered>
<ListLoading />
<RetryMessage>Verbindung wird wiederhergestellt…</RetryMessage>
<RetryHint>
Automatischer Versuch in Kürze. Verbleibende Versuche: {remainingAttempts}
</RetryHint>
</Centered>
);

const RetryMessage = styled.Text`
margin-top: 16px;
text-align: center;
color: ${({ theme }) => theme.colors.text.primary};
`;

const RetryHint = styled.Text`
margin-top: 4px;
text-align: center;
color: ${({ theme }) => theme.colors.text.secondary};
font-size: 12px;
`;
222 changes: 222 additions & 0 deletions src/screens/Bundestag/List/hooks/useProceduresList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ApolloError, ApolloQueryResult } from "@apollo/client";
import { useRecoilValue } from "recoil";

import { useListFilter } from "../../../../api/hooks/useListFilter";
import { constituencyState } from "../../../../api/state/constituency";
import { useLegislaturePeriodStore } from "../../../../api/state/legislaturePeriod";
import {
ParlamentIdentifier,
parlaments,
} from "../../../../api/state/parlament";
import {
ListType,
ProceduresListQuery,
useProceduresListQuery,
} from "../../../../__generated__/graphql";

export interface SegmentedData {
title: string;
data: ProceduresListQuery["procedures"];
}

interface UseProceduresListResult {
procedures: ProceduresListQuery["procedures"];
segmentedData: SegmentedData[];
loading: boolean;
isRetrying: boolean;
remainingAttempts: number;
error: ApolloError | undefined;
hasMore: boolean;
networkStatus?: number;
handleManualRefetch: () => Promise<ApolloQueryResult<ProceduresListQuery>>;
handleRefresh: () => Promise<ApolloQueryResult<ProceduresListQuery>>;
handleEndReached: () => Promise<void>;
}

const MAX_RETRY_ATTEMPTS = 3;
const BASE_RETRY_DELAY_MS = 1500;

export const useProceduresList = (list: ListType): UseProceduresListResult => {
const { proceduresFilter } = useListFilter();
const constituency = useRecoilValue(constituencyState);
const { legislaturePeriod } = useLegislaturePeriodStore();
const parlamentIdentifier = `BT-${legislaturePeriod}` as ParlamentIdentifier;
const parlament = parlaments[parlamentIdentifier];

const constituencies = useMemo(
() => (constituency ? [constituency] : []),
[constituency]
);

const queryVariables = useMemo(
() => ({
listTypes: [list],
pageSize: 10,
filter: proceduresFilter,
constituencies,
period: parlament?.period,
}),
[constituencies, list, parlament?.period, proceduresFilter]
);

const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const latestQueryVariablesRef = useRef(queryVariables);
const [retryAttempt, setRetryAttempt] = useState(0);
const [isRetryScheduled, setIsRetryScheduled] = useState(false);
const [hasMore, setHasMore] = useState(true);

const clearRetryTimeout = useCallback(() => {
if (retryTimeoutRef.current) {
clearTimeout(retryTimeoutRef.current);
retryTimeoutRef.current = null;
}
}, []);

const { loading, data, error, fetchMore, networkStatus, refetch } =
useProceduresListQuery({
fetchPolicy: "network-only",
errorPolicy: "all",
variables: queryVariables,
});

useEffect(() => {
latestQueryVariablesRef.current = queryVariables;
}, [queryVariables]);

useEffect(() => {
if (!loading && (error || !data)) {
setIsRetryScheduled((prev) => {
if (prev || retryAttempt >= MAX_RETRY_ATTEMPTS) {
return prev;
}
clearRetryTimeout();
const delay = BASE_RETRY_DELAY_MS * (retryAttempt + 1);
retryTimeoutRef.current = setTimeout(() => {
setIsRetryScheduled(false);
setRetryAttempt((attempt) => attempt + 1);
void refetch(latestQueryVariablesRef.current);
}, delay);
return true;
});
}
}, [clearRetryTimeout, data, error, loading, refetch, retryAttempt]);

useEffect(() => {
if (data) {
if (retryAttempt !== 0) {
setRetryAttempt(0);
}
if (isRetryScheduled) {
setIsRetryScheduled(false);
}
clearRetryTimeout();
}
}, [clearRetryTimeout, data, isRetryScheduled, retryAttempt]);

useEffect(() => () => clearRetryTimeout(), [clearRetryTimeout]);

useEffect(() => {
setHasMore(true);
setRetryAttempt(0);
setIsRetryScheduled(false);
clearRetryTimeout();
}, [
clearRetryTimeout,
constituencies,
list,
parlament?.period,
proceduresFilter,
]);

const handleManualRefetch = useCallback(() => {
setRetryAttempt(0);
setIsRetryScheduled(false);
clearRetryTimeout();
return refetch(latestQueryVariablesRef.current);
}, [clearRetryTimeout, refetch]);

const handleRefresh = useCallback(() => {
setHasMore(true);
return handleManualRefetch();
}, [handleManualRefetch]);

const handleEndReached = useCallback(() => {
if (loading || !hasMore || !data) {
return Promise.resolve();
}

return fetchMore({
variables: {
offset: data.procedures.length,
},
}).then(({ data: fetchMoreData }) => {
if (!fetchMoreData || fetchMoreData.procedures.length === 0) {
setHasMore(false);
}
});
}, [data, fetchMore, hasMore, loading]);

const segmentedData: SegmentedData[] = useMemo(() => {
if (data && ListType.Top100 === list) {
return [
{
title: "",
data: data.procedures,
},
];
}

if (!data) {
return [];
}

return data.procedures.reduce<SegmentedData[]>((prev, procedure) => {
const { voteWeek, voteYear } = procedure;
const segment = voteWeek && voteYear ? `KW ${voteWeek}/${voteYear}` : "";
const index = prev.findIndex(({ title }) => title === segment);

if (index !== -1) {
const next = [...prev];
const existingSegment = prev[index];
next[index] = {
...existingSegment,
data: [...existingSegment.data, procedure],
};
return next;
}

return [
...prev,
{
title: segment,
data: [procedure],
},
];
}, []);
}, [data, list]);

const isNotLoading = !loading;
const hasErrorOrNoData = !!error || !data;
const isRetryActive = isRetryScheduled || retryAttempt > 0;
const hasRemainingAttempts = retryAttempt < MAX_RETRY_ATTEMPTS;
const isRetrying =
isNotLoading && hasErrorOrNoData && isRetryActive && hasRemainingAttempts;
Comment on lines +253 to +258
Copy link

Copilot AI Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These intermediate boolean variables add unnecessary complexity. Consider simplifying the retry state logic by directly computing isRetrying or consolidating related conditions.

Suggested change
const isNotLoading = !loading;
const hasErrorOrNoData = !!error || !data;
const isRetryActive = isRetryScheduled || retryAttempt > 0;
const hasRemainingAttempts = retryAttempt < MAX_RETRY_ATTEMPTS;
const isRetrying =
isNotLoading && hasErrorOrNoData && isRetryActive && hasRemainingAttempts;
const isRetrying =
!loading &&
(!!error || !data) &&
(isRetryScheduled || retryAttempt > 0) &&
retryAttempt < MAX_RETRY_ATTEMPTS;

Copilot uses AI. Check for mistakes.

const remainingAttempts = Math.max(0, MAX_RETRY_ATTEMPTS - retryAttempt);
const procedures = data?.procedures ?? [];

return {
procedures,
segmentedData,
loading,
isRetrying,
remainingAttempts,
error,
hasMore,
networkStatus,
handleManualRefetch,
handleRefresh,
handleEndReached,
};
};
92 changes: 92 additions & 0 deletions src/screens/Bundestag/List/hooks/useProceduresListItemRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useCallback } from "react";
import { ListRenderItem } from "react-native";
import { useRouter } from "expo-router";

import { useLocalVotes } from "../../../../api/state/localVotesStore";
import { communityVoteData } from "../../../../lib/PieChartCommunityData";
import { pieChartGovernmentData } from "../../../../lib/PieChartGovernmentData";
import { Row } from "../../../../components/Row";
import { ListItem } from "../../../../components/ListItem";
import {
ListType,
ProceduresListQuery,
} from "../../../../__generated__/graphql";

export type ProcedureListItem = ProceduresListQuery["procedures"][0];

export const useProceduresListItemRenderer = (
list: ListType
): ListRenderItem<ProcedureListItem> => {
const router = useRouter();
const localVotes = useLocalVotes();

const handleProcedurePress = useCallback(
(procedureId: string) => {
router.push(`/procedure/${procedureId}`);
},
[router]
);

return useCallback<ListRenderItem<ProcedureListItem>>(
({
item: {
procedureId,
title,
sessionTOPHeading,
subjectGroups,
voteDate,
voteEnd,
voted: votedServer,
voteResults,
votedGovernment,
communityVotes,
},
index,
}) => {
let subline: string | null = null;
if (sessionTOPHeading) {
subline = sessionTOPHeading;
} else if (subjectGroups) {
subline = subjectGroups.join(", ");
}

const govSlices = pieChartGovernmentData({
voteResults,
votedGovernment,
});

const localSelection = localVotes.find(
(localVote) => localVote.procedureId === procedureId
)?.selection;
const voted = votedServer || !!localSelection;

const communityVoteSlices = communityVoteData({
communityVotes,
localSelection,
voted,
});

return (
<Row
onPress={() => handleProcedurePress(procedureId)}
testID={`ListItem-${list}-${index}`}
>
<ListItem
title={title}
subline={subline}
voteDate={voteDate ? new Date(voteDate) : undefined}
endDate={voteEnd ? new Date(voteEnd) : undefined}
voted={voted}
votes={communityVotes ? communityVotes.total || 0 : 0}
govermentChart={{
votes: govSlices,
large: true,
}}
communityVotes={communityVoteSlices}
/>
</Row>
);
},
[handleProcedurePress, list, localVotes]
);
};
Loading