Skip to content

Commit 8a6b1a3

Browse files
author
Manuel Ruck
committed
Refactor procedures list handling and improve error/retry states with new components
1 parent 76754a4 commit 8a6b1a3

File tree

5 files changed

+413
-197
lines changed

5 files changed

+413
-197
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from "react";
2+
import { Text } from "react-native";
3+
4+
import { Button } from "../../../../components/Button";
5+
import { Centered } from "../../../../components/Centered";
6+
7+
interface ErrorStateProps {
8+
onRetry: () => void;
9+
}
10+
11+
export const ErrorState: React.FC<ErrorStateProps> = ({ onRetry }) => (
12+
<Centered>
13+
<Text>Verbindungsfehler</Text>
14+
<Button
15+
onPress={onRetry}
16+
text="Nochmal versuchen"
17+
textColor="blue"
18+
backgroundColor="transparent"
19+
/>
20+
</Centered>
21+
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from "react";
2+
import styled from "styled-components/native";
3+
4+
import { Centered } from "../../../../components/Centered";
5+
import { ListLoading } from "../../../../components/ListLoading";
6+
7+
interface RetryStateProps {
8+
remainingAttempts: number;
9+
}
10+
11+
export const RetryState: React.FC<RetryStateProps> = ({
12+
remainingAttempts,
13+
}) => (
14+
<Centered>
15+
<ListLoading />
16+
<RetryMessage>Verbindung wird wiederhergestellt…</RetryMessage>
17+
<RetryHint>
18+
Automatischer Versuch in Kürze. Verbleibende Versuche: {remainingAttempts}
19+
</RetryHint>
20+
</Centered>
21+
);
22+
23+
const RetryMessage = styled.Text`
24+
margin-top: 16px;
25+
text-align: center;
26+
color: ${({ theme }) => theme.colors.text.primary};
27+
`;
28+
29+
const RetryHint = styled.Text`
30+
margin-top: 4px;
31+
text-align: center;
32+
color: ${({ theme }) => theme.colors.text.secondary};
33+
font-size: 12px;
34+
`;
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2+
import { ApolloError, ApolloQueryResult } from "@apollo/client";
3+
import { useRecoilValue } from "recoil";
4+
5+
import { useListFilter } from "../../../../api/hooks/useListFilter";
6+
import { constituencyState } from "../../../../api/state/constituency";
7+
import { useLegislaturePeriodStore } from "../../../../api/state/legislaturePeriod";
8+
import {
9+
ParlamentIdentifier,
10+
parlaments,
11+
} from "../../../../api/state/parlament";
12+
import {
13+
ListType,
14+
ProceduresListQuery,
15+
useProceduresListQuery,
16+
} from "../../../../__generated__/graphql";
17+
18+
export interface SegmentedData {
19+
title: string;
20+
data: ProceduresListQuery["procedures"];
21+
}
22+
23+
interface UseProceduresListResult {
24+
procedures: ProceduresListQuery["procedures"];
25+
segmentedData: SegmentedData[];
26+
loading: boolean;
27+
isRetrying: boolean;
28+
remainingAttempts: number;
29+
error: ApolloError | undefined;
30+
hasMore: boolean;
31+
networkStatus?: number;
32+
handleManualRefetch: () => Promise<ApolloQueryResult<ProceduresListQuery>>;
33+
handleRefresh: () => Promise<ApolloQueryResult<ProceduresListQuery>>;
34+
handleEndReached: () => Promise<void>;
35+
}
36+
37+
const MAX_RETRY_ATTEMPTS = 3;
38+
const BASE_RETRY_DELAY_MS = 1500;
39+
40+
export const useProceduresList = (list: ListType): UseProceduresListResult => {
41+
const { proceduresFilter } = useListFilter();
42+
const constituency = useRecoilValue(constituencyState);
43+
const { legislaturePeriod } = useLegislaturePeriodStore();
44+
const parlamentIdentifier = `BT-${legislaturePeriod}` as ParlamentIdentifier;
45+
const parlament = parlaments[parlamentIdentifier];
46+
47+
const constituencies = useMemo(
48+
() => (constituency ? [constituency] : []),
49+
[constituency]
50+
);
51+
52+
const queryVariables = useMemo(
53+
() => ({
54+
listTypes: [list],
55+
pageSize: 10,
56+
filter: proceduresFilter,
57+
constituencies,
58+
period: parlament?.period,
59+
}),
60+
[constituencies, list, parlament?.period, proceduresFilter]
61+
);
62+
63+
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
64+
const latestQueryVariablesRef = useRef(queryVariables);
65+
const [retryAttempt, setRetryAttempt] = useState(0);
66+
const [isRetryScheduled, setIsRetryScheduled] = useState(false);
67+
const [hasMore, setHasMore] = useState(true);
68+
69+
const clearRetryTimeout = useCallback(() => {
70+
if (retryTimeoutRef.current) {
71+
clearTimeout(retryTimeoutRef.current);
72+
retryTimeoutRef.current = null;
73+
}
74+
}, []);
75+
76+
const { loading, data, error, fetchMore, networkStatus, refetch } =
77+
useProceduresListQuery({
78+
fetchPolicy: "network-only",
79+
errorPolicy: "all",
80+
variables: queryVariables,
81+
});
82+
83+
useEffect(() => {
84+
latestQueryVariablesRef.current = queryVariables;
85+
}, [queryVariables]);
86+
87+
useEffect(() => {
88+
if (!loading && (error || !data)) {
89+
setIsRetryScheduled((prev) => {
90+
if (prev || retryAttempt >= MAX_RETRY_ATTEMPTS) {
91+
return prev;
92+
}
93+
clearRetryTimeout();
94+
const delay = BASE_RETRY_DELAY_MS * (retryAttempt + 1);
95+
retryTimeoutRef.current = setTimeout(() => {
96+
setIsRetryScheduled(false);
97+
setRetryAttempt((attempt) => attempt + 1);
98+
void refetch(latestQueryVariablesRef.current);
99+
}, delay);
100+
return true;
101+
});
102+
}
103+
}, [clearRetryTimeout, data, error, loading, refetch, retryAttempt]);
104+
105+
useEffect(() => {
106+
if (data) {
107+
if (retryAttempt !== 0) {
108+
setRetryAttempt(0);
109+
}
110+
if (isRetryScheduled) {
111+
setIsRetryScheduled(false);
112+
}
113+
clearRetryTimeout();
114+
}
115+
}, [clearRetryTimeout, data, isRetryScheduled, retryAttempt]);
116+
117+
useEffect(() => () => clearRetryTimeout(), [clearRetryTimeout]);
118+
119+
useEffect(() => {
120+
setHasMore(true);
121+
setRetryAttempt(0);
122+
setIsRetryScheduled(false);
123+
clearRetryTimeout();
124+
}, [
125+
clearRetryTimeout,
126+
constituencies,
127+
list,
128+
parlament?.period,
129+
proceduresFilter,
130+
]);
131+
132+
const handleManualRefetch = useCallback(() => {
133+
setRetryAttempt(0);
134+
setIsRetryScheduled(false);
135+
clearRetryTimeout();
136+
return refetch(latestQueryVariablesRef.current);
137+
}, [clearRetryTimeout, refetch]);
138+
139+
const handleRefresh = useCallback(() => {
140+
setHasMore(true);
141+
return handleManualRefetch();
142+
}, [handleManualRefetch]);
143+
144+
const handleEndReached = useCallback(() => {
145+
if (loading || !hasMore || !data) {
146+
return Promise.resolve();
147+
}
148+
149+
return fetchMore({
150+
variables: {
151+
offset: data.procedures.length,
152+
},
153+
}).then(({ data: fetchMoreData }) => {
154+
if (!fetchMoreData || fetchMoreData.procedures.length === 0) {
155+
setHasMore(false);
156+
}
157+
});
158+
}, [data, fetchMore, hasMore, loading]);
159+
160+
const segmentedData: SegmentedData[] = useMemo(() => {
161+
if (data && ListType.Top100 === list) {
162+
return [
163+
{
164+
title: "",
165+
data: data.procedures,
166+
},
167+
];
168+
}
169+
170+
if (!data) {
171+
return [];
172+
}
173+
174+
return data.procedures.reduce<SegmentedData[]>((prev, procedure) => {
175+
const { voteWeek, voteYear } = procedure;
176+
const segment = voteWeek && voteYear ? `KW ${voteWeek}/${voteYear}` : "";
177+
const index = prev.findIndex(({ title }) => title === segment);
178+
179+
if (index !== -1) {
180+
const next = [...prev];
181+
const existingSegment = prev[index];
182+
next[index] = {
183+
...existingSegment,
184+
data: [...existingSegment.data, procedure],
185+
};
186+
return next;
187+
}
188+
189+
return [
190+
...prev,
191+
{
192+
title: segment,
193+
data: [procedure],
194+
},
195+
];
196+
}, []);
197+
}, [data, list]);
198+
199+
const isRetrying =
200+
!loading &&
201+
(error || !data) &&
202+
(isRetryScheduled || retryAttempt > 0) &&
203+
retryAttempt < MAX_RETRY_ATTEMPTS;
204+
205+
const remainingAttempts = Math.max(0, MAX_RETRY_ATTEMPTS - retryAttempt);
206+
const procedures = data?.procedures ?? [];
207+
208+
return {
209+
procedures,
210+
segmentedData,
211+
loading,
212+
isRetrying,
213+
remainingAttempts,
214+
error,
215+
hasMore,
216+
networkStatus,
217+
handleManualRefetch,
218+
handleRefresh,
219+
handleEndReached,
220+
};
221+
};
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useCallback } from "react";
2+
import { ListRenderItem } from "react-native";
3+
import { useRouter } from "expo-router";
4+
5+
import { useLocalVotes } from "../../../../api/state/localVotesStore";
6+
import { communityVoteData } from "../../../../lib/PieChartCommunityData";
7+
import { pieChartGovernmentData } from "../../../../lib/PieChartGovernmentData";
8+
import { Row } from "../../../../components/Row";
9+
import { ListItem } from "../../../../components/ListItem";
10+
import {
11+
ListType,
12+
ProceduresListQuery,
13+
} from "../../../../__generated__/graphql";
14+
15+
export type ProcedureListItem = ProceduresListQuery["procedures"][0];
16+
17+
export const useProceduresListItemRenderer = (
18+
list: ListType
19+
): ListRenderItem<ProcedureListItem> => {
20+
const router = useRouter();
21+
const localVotes = useLocalVotes();
22+
23+
const handleProcedurePress = useCallback(
24+
(procedureId: string) => {
25+
router.push(`/procedure/${procedureId}`);
26+
},
27+
[router]
28+
);
29+
30+
return useCallback<ListRenderItem<ProcedureListItem>>(
31+
({
32+
item: {
33+
procedureId,
34+
title,
35+
sessionTOPHeading,
36+
subjectGroups,
37+
voteDate,
38+
voteEnd,
39+
voted: votedServer,
40+
voteResults,
41+
votedGovernment,
42+
communityVotes,
43+
},
44+
index,
45+
}) => {
46+
let subline: string | null = null;
47+
if (sessionTOPHeading) {
48+
subline = sessionTOPHeading;
49+
} else if (subjectGroups) {
50+
subline = subjectGroups.join(", ");
51+
}
52+
53+
const govSlices = pieChartGovernmentData({
54+
voteResults,
55+
votedGovernment,
56+
});
57+
58+
const localSelection = localVotes.find(
59+
(localVote) => localVote.procedureId === procedureId
60+
)?.selection;
61+
const voted = votedServer || !!localSelection;
62+
63+
const communityVoteSlices = communityVoteData({
64+
communityVotes,
65+
localSelection,
66+
voted,
67+
});
68+
69+
return (
70+
<Row
71+
onPress={() => handleProcedurePress(procedureId)}
72+
testID={`ListItem-${list}-${index}`}
73+
>
74+
<ListItem
75+
title={title}
76+
subline={subline}
77+
voteDate={voteDate ? new Date(voteDate) : undefined}
78+
endDate={voteEnd ? new Date(voteEnd) : undefined}
79+
voted={voted}
80+
votes={communityVotes ? communityVotes.total || 0 : 0}
81+
govermentChart={{
82+
votes: govSlices,
83+
large: true,
84+
}}
85+
communityVotes={communityVoteSlices}
86+
/>
87+
</Row>
88+
);
89+
},
90+
[handleProcedurePress, list, localVotes]
91+
);
92+
};

0 commit comments

Comments
 (0)