{
+ refresh().catch(() => {});
+ }}
+ />
+ }
+ />
+ )
+ ) : query.isLoading ? (
+
+ ) : (
+
+ );
+}
+
+export default List;
diff --git a/frontend/src/common/components/ParentChildList.tsx b/frontend/src/common/components/ParentChildList.tsx
new file mode 100644
index 00000000..86985514
--- /dev/null
+++ b/frontend/src/common/components/ParentChildList.tsx
@@ -0,0 +1,88 @@
+import { BottomSheetFlatList } from "@gorhom/bottom-sheet";
+import { useFocusEffect } from "@react-navigation/native";
+import { FlatList, type ViewProps } from "react-native";
+
+import { type Query } from "../query";
+
+import Divider from "./Divider";
+import ErrorMessage from "./ErrorMessage";
+import SkeletonList from "./SkeletonList";
+
+export interface ParentChildListProps extends ViewProps {
+ /** Returns the header to render for the parent. */
+ header: (parent: P) => React.JSX.Element;
+ /** Returns the skeleton header to render for the parent. */
+ headerSkeleton?: () => React.JSX.Element;
+ /** Returns the item to render for the child. */
+ item: (parent: P, child: C) => React.JSX.Element;
+ /** Returns the skeleton item to render for the child. */
+ itemSkeleton: () => React.JSX.Element;
+ /** Whether to show a divider or a spacer */
+ divider: "line" | "space";
+ /** Returns the key for the item. */
+ keyExtractor: (item: C) => string;
+ /** Whether to use a bottom sheet or regular flat list */
+ bottomSheet: boolean;
+ /** Maps the parent data to the child data. */
+ map: (parent: P) => C[];
+ /** The query to use to fetch the data */
+ query: Query
;
+ /** The function to call to refresh the query. */
+ refresh: () => Promise