Viktoriia 1 år sedan
förälder
incheckning
f6d7585c1d

+ 5 - 0
Route.tsx

@@ -35,6 +35,7 @@ import { openDatabases } from './src/db';
 import TabBarButton from './src/components/TabBarButton';
 import { ParamListBase, RouteProp } from '@react-navigation/native';
 import setupDatabaseAndSync from 'src/database';
+import { SeriesItemScreen } from 'src/screens/InAppScreens/TravelsScreen/SeriesItemScreen';
 
 const ScreenStack = createStackNavigator();
 const BottomTab = createBottomTabNavigator();
@@ -154,6 +155,10 @@ const Route = () => {
                     name={NAVIGATION_PAGES.SERIES}
                     component={SeriesScreen}
                   />
+                  <ScreenStack.Screen
+                    name={NAVIGATION_PAGES.SERIES_ITEM}
+                    component={SeriesItemScreen}
+                  />
                 </ScreenStack.Navigator>
               )}
             </BottomTab.Screen>

+ 5 - 0
assets/icons/info.svg

@@ -0,0 +1,5 @@
+<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.63351 0C10.9396 0 13.1076 0.898028 14.7383 2.52868C16.369 4.15934 17.267 6.32739 17.267 8.63347C17.267 9.77707 17.0556 10.8804 16.6387 11.9128C16.2244 12.9388 15.6271 13.8462 14.8634 14.6098C12.9836 16.4897 9.13545 19.2278 8.97255 19.3435L8.04782 20V17.2475C5.96223 17.1087 4.02053 16.2301 2.52868 14.7383C0.898067 13.1076 0 10.9396 0 8.63347C0 6.32739 0.898028 4.15934 2.52868 2.52868C4.15934 0.898028 6.32743 0 8.63351 0ZM8.5 14.5C11.8137 14.5 14.5 11.8137 14.5 8.5C14.5 5.18629 11.8137 2.5 8.5 2.5C5.18629 2.5 2.5 5.18629 2.5 8.5C2.5 11.8137 5.18629 14.5 8.5 14.5Z" fill="#0F3F4F"/>
+<path d="M8.50001 8C7.94861 8 7.5 8.55766 7.5 9.2431V11.7569C7.5 12.4423 7.94861 13 8.50001 13C9.05141 13 9.5 12.4423 9.5 11.7569V9.2431C9.50002 8.55763 9.05141 8 8.50001 8Z" fill="#0F3F4F"/>
+<path d="M8.45021 4.5C7.76096 4.5 7.2002 5.06076 7.2002 5.74999C7.2002 6.43921 7.76096 7 8.45021 7C9.13946 7 9.7002 6.43924 9.7002 5.74999C9.7002 5.06073 9.13946 4.5 8.45021 4.5Z" fill="#0F3F4F"/>
+</svg>

+ 1 - 1
assets/icons/search.svg

@@ -1,3 +1,3 @@
 <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M9.00005 2.70005C5.52065 2.70005 2.70005 5.52065 2.70005 9.00005C2.70005 12.4794 5.52065 15.3 9.00005 15.3C10.6547 15.3 12.1581 14.6637 13.2831 13.6202C14.5253 12.4681 15.3 10.8254 15.3 9.00005C15.3 5.52065 12.4794 2.70005 9.00005 2.70005ZM0.300049 9.00005C0.300049 4.19517 4.19517 0.300049 9.00005 0.300049C13.8049 0.300049 17.7001 4.19517 17.7001 9.00005C17.7001 11.0721 16.9746 12.976 15.7659 14.4697L18.9476 17.6515C19.4163 18.1201 19.4163 18.8799 18.9476 19.3486C18.479 19.8172 17.7192 19.8172 17.2506 19.3486L14.0132 16.1112C12.5964 17.1115 10.866 17.7001 9.00005 17.7001C4.19517 17.7001 0.300049 13.8049 0.300049 9.00005Z" fill="#0F3F4F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.00005 2.70005C5.52065 2.70005 2.70005 5.52065 2.70005 9.00005C2.70005 12.4794 5.52065 15.3 9.00005 15.3C10.6547 15.3 12.1581 14.6637 13.2831 13.6202C14.5253 12.4681 15.3 10.8254 15.3 9.00005C15.3 5.52065 12.4794 2.70005 9.00005 2.70005ZM0.300049 9.00005C0.300049 4.19517 4.19517 0.300049 9.00005 0.300049C13.8049 0.300049 17.7001 4.19517 17.7001 9.00005C17.7001 11.0721 16.9746 12.976 15.7659 14.4697L18.9476 17.6515C19.4163 18.1201 19.4163 18.8799 18.9476 19.3486C18.479 19.8172 17.7192 19.8172 17.2506 19.3486L14.0132 16.1112C12.5964 17.1115 10.866 17.7001 9.00005 17.7001C4.19517 17.7001 0.300049 13.8049 0.300049 9.00005Z"/>
 </svg>

+ 12 - 0
package-lock.json

@@ -7,6 +7,7 @@
     "": {
       "name": "nomadmania-app",
       "version": "1.0.0",
+      "hasInstallScript": true,
       "dependencies": {
         "@react-native-community/datetimepicker": "7.2.0",
         "@react-native-community/masked-view": "^0.1.11",
@@ -11778,6 +11779,17 @@
       "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz",
       "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q=="
     },
+    "node_modules/immer": {
+      "version": "10.0.3",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
+      "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==",
+      "optional": true,
+      "peer": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/immer"
+      }
+    },
     "node_modules/import-fresh": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",

+ 2 - 1
package.json

@@ -8,7 +8,8 @@
     "ios": "expo start --ios",
     "build:dev": "eas build --profile development",
     "build:prod": "ENV=production eas build --platform all --profile production",
-    "publish:prod": "ENV=production eas update --branch production"
+    "publish:prod": "ENV=production eas update --branch production",
+    "postinstall": "patch-package"
   },
   "dependencies": {
     "@react-native-community/datetimepicker": "7.2.0",

+ 13 - 0
patches/react-native-tab-view+3.5.2.patch

@@ -0,0 +1,13 @@
+diff --git a/node_modules/react-native-tab-view/src/TabBar.tsx b/node_modules/react-native-tab-view/src/TabBar.tsx
+index e8d0b4c..cd5a424 100644
+--- a/node_modules/react-native-tab-view/src/TabBar.tsx
++++ b/node_modules/react-native-tab-view/src/TabBar.tsx
+@@ -405,6 +405,8 @@ export function TabBar<T extends Route>({
+                 // When we have measured widths for all of the tabs, we should updates the state
+                 // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
+                 setTabWidths({ ...measuredTabWidths.current });
++              } else {
++                setTabWidths({ ...measuredTabWidths?.current });
+               }
+             }
+           : undefined,

+ 5 - 2
src/components/CheckBox/index.tsx

@@ -9,16 +9,19 @@ type Props = {
   onChange?: (value: boolean) => void;
   value?: boolean;
   label?: string;
+  color?: string;
+  disabled?: boolean;
 };
 
-export const CheckBox: FC<Props> = ({ value, onChange, label }) => {
+export const CheckBox: FC<Props> = ({ value, onChange, label, color, disabled }) => {
   return (
     <View style={styles.wrapper}>
       <Checkbox
         style={{ backgroundColor: Colors.WHITE }}
-        color={Colors.ORANGE}
+        color={color || Colors.ORANGE}
         value={value}
         onValueChange={onChange}
+        disabled={disabled}
       />
       {label ? <Text style={styles.text}>{label}</Text> : null}
     </View>

+ 5 - 1
src/components/Header/index.tsx

@@ -24,7 +24,11 @@ export const Header: FC<Props> = ({ label, rightElement }) => {
         </View>
       </TouchableOpacity>
       <Text style={styles.label}>{label}</Text>
-      <View>{rightElement ? rightElement : <Text>Text</Text>}</View>
+      {rightElement ? (
+        <View>{rightElement}</View>
+      ) : (
+        <View style={styles.placeholder} />
+      )}
     </View>
   );
 };

+ 5 - 1
src/components/Header/style.ts

@@ -23,5 +23,9 @@ export const styles = StyleSheet.create({
     fontFamily: 'redhat-700',
     fontSize: getFontSize(14),
     color: Colors.DARK_BLUE
-  }
+  },
+  placeholder: {
+    width: 25,
+    height: 25,
+  },
 });

+ 1 - 1
src/components/Modal/ModalHeader/modal-header.tsx

@@ -19,7 +19,7 @@ export const ModalHeader: FC<Props> = ({ textHeader, onRequestClose }) => {
         <CloseSVG />
       </TouchableOpacity>
       <Text style={styles.textHeader}>{textHeader}</Text>
-      <Text>Temp</Text>
+      <View style={{ height: 30, width: 30 }} />
     </View>
   );
 };

+ 9 - 21
src/components/Modal/index.tsx

@@ -1,5 +1,6 @@
 import React, { FC, ReactNode } from 'react';
-import { Dimensions, DimensionValue, Modal as ReactModal, PixelRatio, View } from 'react-native';
+import { Dimensions, DimensionValue, PixelRatio, View } from 'react-native';
+import ReactModal from 'react-native-modal';
 import { ModalHeader } from './ModalHeader/modal-header';
 import { styles } from './style';
 
@@ -20,31 +21,18 @@ export const Modal: FC<Props> = ({
   visibleInPercent,
   headerTitle
 }) => {
-  // const heightPercentageToDP = (heightPercent: string) => {
-  //   const screenHeight = Dimensions.get('window').height;
-  //   const elemHeight = parseFloat(heightPercent);
-  //   return PixelRatio.roundToNearestPixel((screenHeight * elemHeight) / 100);
-  // };
+  const screenHeight = Dimensions.get('window').height;
 
   return (
     <ReactModal
-      animationType={'slide'}
+      isVisible={visible}
+      onBackdropPress={onRequestClose}
+      onBackButtonPress={onRequestClose}
+      style={styles.modal}
       statusBarTranslucent={true}
-      presentationStyle={'pageSheet'}
-      visible={visible}
-      onRequestClose={onRequestClose}
-      transparent={true}
+      presentationStyle="overFullScreen"
     >
-      <View
-        style={[
-          styles.wrapper,
-          {
-            height: visibleInPercent ?? '100%',
-            borderTopLeftRadius: visibleInPercent ? 10 : 0,
-            borderTopRightRadius: visibleInPercent ? 10 : 0
-          }
-        ]}
-      >
+      <View style={[styles.wrapper, { height: visibleInPercent ?? screenHeight }]}>
         <ModalHeader onRequestClose={onRequestClose} textHeader={headerTitle} />
         <Drawer />
         {children}

+ 8 - 3
src/components/Modal/style.ts

@@ -8,9 +8,14 @@ export const styles = StyleSheet.create({
     backgroundColor: '#E5E5E5'
   },
   wrapper: {
-    marginTop: 'auto',
     backgroundColor: Colors.WHITE,
     paddingLeft: 15,
-    paddingRight: 15
-  }
+    paddingRight: 15,
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10
+  },
+  modal: {
+    justifyContent: 'flex-end',
+    margin: 0,
+  },
 });

+ 3 - 1
src/modules/api/series/queries/index.ts

@@ -1,3 +1,5 @@
 export * from './use-post-get-series';
 export * from './use-post-get-series-groups';
-export * from './use-post-get-series-with-group'
+export * from './use-post-get-series-with-group';
+export * from './use-post-get-items-for-series';
+export * from './use-post-get-toggle-item';

+ 19 - 0
src/modules/api/series/queries/use-post-get-items-for-series.tsx

@@ -0,0 +1,19 @@
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi, type PostGetItems } from '../series-api';
+
+import { queryClient } from 'src/utils/queryClient';
+
+export const fetchItemsForSeries = async (token: string, series_id: string) => {
+  try {
+    const data: PostGetItems = await queryClient.fetchQuery({
+      queryKey: seriesQueryKeys.getItemsForSeries(token, series_id),
+      queryFn: () => seriesApi.getItemsForSeries(token, series_id).then((res) => res.data),
+      gcTime: 0,
+      staleTime: 0
+    });
+
+    return data;
+  } catch (error) {
+    console.error('Failed to fetch items for series:', error);
+  }
+};

+ 28 - 0
src/modules/api/series/queries/use-post-get-toggle-item.tsx

@@ -0,0 +1,28 @@
+import { useQuery } from '@tanstack/react-query';
+import { useMutation } from '@tanstack/react-query';
+
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi, type PostSetToggleItemReturn, type PostSetToggleItem } from '../series-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostSetToggleItem = () => {
+  return useMutation<
+    PostSetToggleItemReturn,
+    BaseAxiosError,
+    { token: string; series_id: number; item_id: number; checked: 0 | 1; double: 0 | 1 },
+    PostSetToggleItemReturn
+  >({
+    mutationKey: seriesQueryKeys.setToggleItem(),
+    mutationFn: async (variables) => {
+      const response = await seriesApi.setToggleItem(
+        variables.token,
+        variables.series_id,
+        variables.item_id,
+        variables.checked,
+        variables.double
+      );
+      return response.data;
+    }
+  });
+};

+ 37 - 0
src/modules/api/series/series-api.tsx

@@ -35,6 +35,39 @@ export interface PostGetSeriesList extends ResponseType {
   }[]
 }
 
+export interface PostGetItems extends ResponseType {
+  groups: {
+    name: string;
+    icon: string;
+    series_id: number;
+    series_name: string;
+    items: {
+      item_id: number,
+      icon: string | null,
+      name: string,
+      readonly: boolean,
+      info: string,
+      new: boolean,
+      checked: boolean,
+      checked_double: boolean,
+      series_id: number,
+      double: boolean
+    }[]
+  }[]
+}
+
+export interface PostSetToggleItemReturn extends ResponseType {
+  modified_items: number[];
+}
+
+export interface PostSetToggleItem {
+  token: string;
+  series_id: string;
+  item_id: string;
+  checked: 0 | 1;
+  double: 0 | 1;
+}
+
 export const seriesApi = {
   getSeries: (token: string | null, regions: string) =>
     request.postForm<PostGetSeries>(API.SERIES, { token, regions }),
@@ -42,4 +75,8 @@ export const seriesApi = {
     request.postForm<PostGetSeriesGroups>(API.SERIES_GROUPS),
   getSeriesWithGroup: (token: string, group: string) =>
     request.postForm<PostGetSeriesList>(API.SERIES_WITH_GROUP, { token, group }),
+  getItemsForSeries: (token: string, series_id: string) =>
+    request.postForm<PostGetItems>(API.GET_ITEMS_FOR_SERIES, { token, series_id }),
+  setToggleItem: (token: string, series_id: number, item_id: number, checked: 0 | 1, double: 0 | 1) =>
+    request.postForm<PostSetToggleItemReturn>(API.TOGGLE_ITEM_SERIES, { token, series_id, item_id, checked, double}),
 };

+ 3 - 1
src/modules/api/series/series-query-keys.tsx

@@ -2,4 +2,6 @@ export const seriesQueryKeys = {
   fetchSeriesData: () => ['fetchSeriesData'] as const,
   getSeriesGroups: () => ['getSeriesGroups'] as const,
   getSeriesWithGroup: () => ['getSeriesWithGroup'] as const,
-};
+  getItemsForSeries: (token: string, series_id: string) => ['getItemsForSeries', {token, series_id}] as const,
+  setToggleItem: () => ['setToggleItem'] as const,
+};

+ 1 - 1
src/screens/InAppScreens/MapScreen/index.tsx

@@ -470,7 +470,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
           </TouchableOpacity>
 
           <TouchableOpacity style={[styles.cornerButton, styles.topRightButton]}>
-            <SearchIcon />
+            <SearchIcon fill={'#0F3F4F'} />
           </TouchableOpacity>
 
           <TouchableOpacity

+ 137 - 0
src/screens/InAppScreens/TravelsScreen/Components/AccordionListItem.tsx

@@ -0,0 +1,137 @@
+import React, { useState } from 'react';
+import {
+  View,
+  Text,
+  TouchableOpacity,
+  Image,
+  LayoutAnimation,
+  Platform,
+  UIManager
+} from 'react-native';
+import { CheckBox } from 'src/components';
+
+import ChevronIcon from '../../../../../assets/icons/chevron-left.svg';
+import { API_HOST } from 'src/constants';
+import InfoIcon from '../../../../../assets/icons/info.svg';
+
+import { styles } from './styles';
+
+interface SeriesItem {
+  series_id: number;
+  item_id: number;
+  icon: string | null;
+  name: string;
+  readonly: boolean;
+  info: string;
+  new: boolean;
+  checked: boolean;
+  checked_double: boolean;
+  double: boolean;
+}
+
+interface SeriesGroup {
+  name: string;
+  icon: string;
+  series_id: number;
+  series_name: string;
+  items: SeriesItem[];
+}
+
+if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
+  UIManager.setLayoutAnimationEnabledExperimental(true);
+}
+
+export const AccordionListItem = React.memo(
+  ({
+    item,
+    onCheckboxChange,
+    setIsInfoModalVisible,
+    setInfoItem,
+    isSeries
+  }: {
+    item: SeriesGroup;
+    onCheckboxChange: (subItem: SeriesItem, groupName: string, double?: boolean) => void;
+    setIsInfoModalVisible: (isVisible: boolean) => void;
+    setInfoItem: (item: SeriesItem) => void;
+    isSeries: boolean;
+  }) => {
+    const [isExpanded, setIsExpanded] = useState(false);
+
+    const toggleExpand = () => {
+      LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
+      setIsExpanded(!isExpanded);
+    };
+
+    return (
+      <View style={styles.sectionContainer}>
+        <TouchableOpacity onPress={toggleExpand} style={styles.header}>
+          <View style={styles.headerContainer}>
+            {item.icon && (
+              <Image
+                source={{ uri: API_HOST + item.icon }}
+                style={[styles.headerImage, isSeries && { borderRadius: 0, borderWidth: 0 }]}
+              />
+            )}
+            <Text style={styles.headerText}>{item.name}</Text>
+          </View>
+
+          <View style={styles.chevronContainer}>
+            <ChevronIcon style={[styles.headerIcon, isExpanded ? styles.rotate : null]} />
+          </View>
+        </TouchableOpacity>
+
+        {isExpanded && (
+          <View style={[styles.content, { backgroundColor: '#FAFAFA' }]}>
+            {item.items.map((subItem, index) => (
+              <View key={index} style={styles.itemContainer}>
+                <TouchableOpacity
+                  style={styles.headerContainer}
+                  onPress={() => !subItem.readonly && onCheckboxChange(subItem, item.name)}
+                  activeOpacity={subItem.readonly ? 1 : 0.2}
+                >
+                  <CheckBox
+                    onChange={() => !subItem.readonly && onCheckboxChange(subItem, item.name)}
+                    value={subItem.checked}
+                    color={'#0F3F4F'}
+                    disabled={subItem.readonly}
+                  />
+                  {subItem.double && (
+                    <View style={{ marginLeft: 8 }}>
+                      <CheckBox
+                        onChange={() => onCheckboxChange(subItem, item.name, true)}
+                        value={subItem.checked_double}
+                        color={'#0F3F4F'}
+                      />
+                    </View>
+                  )}
+                  {subItem.icon && (
+                    <Image source={{ uri: API_HOST + subItem.icon }} style={styles.itemIcon} />
+                  )}
+                  <Text style={styles.contentText}>
+                    {subItem.name}
+                    {subItem.new && (
+                      <View>
+                        <Text style={styles.textNew}> NEW</Text>
+                      </View>
+                    )}
+                  </Text>
+                </TouchableOpacity>
+                {subItem.info && (
+                  <TouchableOpacity
+                    style={styles.info}
+                    onPress={() => {
+                      setIsInfoModalVisible(true);
+                      setInfoItem(subItem);
+                    }}
+                  >
+                    <InfoIcon />
+                  </TouchableOpacity>
+                )}
+              </View>
+            ))}
+          </View>
+        )}
+      </View>
+    );
+  }
+);

+ 91 - 0
src/screens/InAppScreens/TravelsScreen/Components/styles.tsx

@@ -0,0 +1,91 @@
+import { StyleSheet, Dimensions } from 'react-native';
+import { Colors } from '../../../../theme';
+
+export const styles = StyleSheet.create({
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 10,
+    paddingHorizontal: 16,
+    justifyContent: 'space-between',
+    flex: 1,
+  },
+  headerText: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#0F3F4F',
+    flexShrink: 1,
+    marginRight: 10,
+  },
+  headerImage: {
+    width: 30,
+    height: 30,
+    marginRight: 16,
+    resizeMode: 'contain',
+    borderRadius: 15,
+    borderColor: '#B4C2C7',
+    borderWidth: 0.5
+  },
+  contentText: {
+    fontSize: 13,
+    color: '#0F3F4F',
+    textAlign: 'left',
+    marginLeft: 10,
+    flexShrink: 1,
+    marginRight: 10,
+  },
+  textNew: {
+    color: Colors.ORANGE,
+    fontSize: 10,
+    fontWeight: '800'
+  },
+  headerIcon: {
+    transform: [{ rotate: '-90deg' }],
+  },
+  itemIcon: {
+    width: 20,
+    height: 20,
+    marginLeft: 10,
+    resizeMode: 'contain',
+  },
+  rotate: {
+    transform: [{ rotate: '90deg' }]
+  },
+  content: {
+    padding: 20,
+    paddingBottom: 4,
+    paddingRight: 10,
+    backgroundColor: '#FAFAFA',
+    borderRadius: 8,
+    borderTopWidth: 1,
+    borderColor: '#E5E5E5'
+  },
+  sectionContainer: {
+    marginBottom: 5,
+    backgroundColor: '#FAFAFA',
+    borderRadius: 8
+  },
+  headerContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    flex: 1
+  },
+  chevronContainer: {
+    width: 18,
+    height: 18,
+    alignItems: 'center'
+  },
+  itemContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 16,
+    justifyContent: 'space-between',
+    flex: 1
+  },
+  info: {
+    paddingHorizontal: 10,
+    height: '100%',
+    alignItems: 'center',
+    justifyContent: 'center'
+  }
+});

+ 14 - 5
src/screens/InAppScreens/TravelsScreen/Series/index.tsx

@@ -1,5 +1,6 @@
 import React, { useCallback, useEffect, useState } from 'react';
 import { View, Text, FlatList, Image, TouchableOpacity } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
 
 import { Header, PageWrapper, HorizontalTabView, Loading } from 'src/components';
 import { useGetSeriesGroups, useGetSeriesWithGroup } from '@api/series';
@@ -8,6 +9,7 @@ import { useFocusEffect } from '@react-navigation/native';
 import { API_HOST } from 'src/constants';
 import { StoreType, storage } from 'src/storage';
 import { styles } from './styles';
+import { NAVIGATION_PAGES } from 'src/types';
 
 interface SeriesGroup {
   key: string;
@@ -30,10 +32,11 @@ const SeriesScreen = () => {
   const [index, setIndex] = useState(0);
   const [routes, setRoutes] = useState<SeriesGroup[]>([]);
   const { data } = useGetSeriesGroups(true);
+  const navigation = useNavigation();
 
   useFocusEffect(
     useCallback(() => {
-      const fetchRanking = async () => {
+      const fetchGroups = async () => {
         let staticGroups = [
           {
             key: '-1',
@@ -60,7 +63,7 @@ const SeriesScreen = () => {
       };
 
       if (data && data.data) {
-        fetchRanking();
+        fetchGroups();
       }
     }, [data])
   );
@@ -74,13 +77,14 @@ const SeriesScreen = () => {
         index={index}
         setIndex={setIndex}
         routes={routes}
-        renderScene={({ route }: { route: SeriesGroup }) => <SeriesList groupId={route.key} />}
+        renderScene={({ route }: { route: SeriesGroup }) => <SeriesList groupId={route.key} navigation={navigation} />}
       />
     </PageWrapper>
   );
 };
 
-const SeriesList = React.memo(({ groupId }: { groupId: string }) => {
+const SeriesList = React.memo(({ groupId, navigation }: { groupId: string, navigation: any }) => {
+  // const navigation = useNavigation();
   const [seriesData, setSeriesData] = useState<SeriesList[]>([]);
   const [isLoading, setIsLoading] = useState(true);
 
@@ -112,7 +116,12 @@ const SeriesList = React.memo(({ groupId }: { groupId: string }) => {
 
   const renderItem = ({ item }: { item: SeriesList }) => {
     return (
-      <TouchableOpacity style={styles.itemContainer}>
+      <TouchableOpacity
+        style={styles.itemContainer}
+        onPress={() =>
+          navigation.navigate(NAVIGATION_PAGES.SERIES_ITEM, { id: item.id, name: item.name, token })
+        }
+      >
         <Image style={styles.icon} source={{ uri: API_HOST + item.icon_png }} />
 
         <View style={styles.infoContainer}>

+ 276 - 0
src/screens/InAppScreens/TravelsScreen/SeriesItemScreen/index.tsx

@@ -0,0 +1,276 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { useFocusEffect } from '@react-navigation/native';
+import { View, Text, FlatList } from 'react-native';
+import { Button, Header, Input, Loading, Modal, PageWrapper } from 'src/components';
+import { TabView, TabBar } from 'react-native-tab-view';
+
+import SearchIcon from '../../../../../assets/icons/search.svg';
+import { fetchItemsForSeries, usePostSetToggleItem } from '@api/series';
+import { Colors } from 'src/theme';
+import { ButtonVariants } from 'src/types/components';
+import { AccordionListItem } from '../Components/AccordionListItem';
+
+import { styles } from './styles';
+
+interface SeriesItem {
+  series_id: number;
+  item_id: number;
+  icon: string | null;
+  name: string;
+  readonly: boolean;
+  info: string;
+  new: boolean;
+  checked: boolean;
+  checked_double: boolean;
+  double: boolean;
+}
+
+interface SeriesGroup {
+  name: string;
+  icon: string;
+  series_id: number;
+  series_name: string;
+  items: SeriesItem[];
+}
+
+interface FilteredData {
+  [key: string]: SeriesGroup[];
+}
+
+interface Route {
+  key: string;
+  title: string;
+}
+
+export const SeriesItemScreen = ({ route }: { route: any }) => {
+  const { id, name, token } = route.params;
+  const { mutate: updateSeriesItem } = usePostSetToggleItem();
+
+  const [search, setSearch] = useState<string>('');
+  const [filteredData, setFilteredData] = useState<FilteredData>({});
+  const [seriesData, setSeriesData] = useState<SeriesGroup[]>([]);
+  const [index, setIndex] = useState<number>(0);
+  const [isLoading, setIsLoading] = useState<boolean>(true);
+  const [isInfoModalVisible, setIsInfoModalVisible] = useState<boolean>(false);
+  const [infoItem, setInfoItem] = useState<SeriesItem | null>(null);
+  const [activeFilteredData, setActiveFilteredData] = useState<SeriesGroup[]>([]);
+
+  useFocusEffect(
+    useCallback(() => {
+      const fetchGroups = async () => {
+        const data = await fetchItemsForSeries(token, id);
+        if (!data) {
+          setIsLoading(false);
+          return;
+        }
+        const allItems = data.groups;
+        const newItems = data.groups
+          .map((group) => ({
+            ...group,
+            items: group.items.filter((item) => item.new)
+          }))
+          .filter((group) => group.items.length > 0);
+
+        setFilteredData({ all: allItems, new: newItems, unchecked: [], checked: [] });
+        setSeriesData(data.groups);
+        setIsLoading(false);
+      };
+
+      fetchGroups();
+    }, [])
+  );
+
+  const [routes] = useState([
+    { key: 'all', title: 'All items' },
+    { key: 'new', title: 'New' },
+    { key: 'unchecked', title: 'Unchecked' },
+    { key: 'checked', title: 'Checked' }
+  ]);
+
+  const handleIndexChange = (index: number) => {
+    let dataToFilter = [...seriesData];
+
+    switch (index) {
+      case 1:
+        dataToFilter = filteredData[routes[index].key];
+        break;
+      case 2:
+        dataToFilter = dataToFilter
+          .map((group) => ({
+            ...group,
+            items: group.items.filter((item) => !item.checked)
+          }))
+          .filter((group) => group.items.length > 0);
+        break;
+      case 3:
+        dataToFilter = dataToFilter
+          .map((group) => ({
+            ...group,
+            items: group.items.filter((item) => item.checked)
+          }))
+          .filter((group) => group.items.length > 0);
+        break;
+      default:
+        break;
+    }
+
+    setActiveFilteredData(dataToFilter);
+    setFilteredData((prevState) => ({
+      ...prevState,
+      [routes[index].key]: dataToFilter
+    }));
+  };
+
+  useEffect(() => {
+    handleIndexChange(index);
+  }, [seriesData]);
+
+  useEffect(() => {
+    setActiveFilteredData(filteredData[routes[index].key]);
+  }, [filteredData]);
+
+  const renderScene = ({ route }: { route: Route }) => {
+    return isLoading ? (
+      <Loading />
+    ) : (
+      <FlatList
+        key={routes[index].key}
+        keyExtractor={(item, index) => index.toString()}
+        showsVerticalScrollIndicator={false}
+        initialNumToRender={15}
+        style={{ paddingTop: 10 }}
+        data={activeFilteredData}
+        renderItem={({ item }) => (
+          <AccordionListItem
+            item={item}
+            onCheckboxChange={handleCheckboxChange}
+            setIsInfoModalVisible={setIsInfoModalVisible}
+            setInfoItem={setInfoItem}
+            isSeries={id === -1}
+          />
+        )}
+      />
+    );
+  };
+
+  useEffect(() => {
+    const searchText = search.toLowerCase();
+    const searchData = filteredData[routes[index].key]
+      ?.map((group) => {
+        const groupNameMatch = group.name.toLowerCase().includes(searchText);
+        if (groupNameMatch) {
+          return group;
+        } else {
+          const filteredItems = group.items.filter((item) =>
+            item.name.toLowerCase().includes(searchText)
+          );
+          return filteredItems.length > 0 ? { ...group, items: filteredItems } : null;
+        }
+      })
+      .filter((group) => group !== null);
+
+    setActiveFilteredData(searchData as SeriesGroup[]);
+  }, [search]);
+
+  const handleCheckboxChange = useCallback(
+    async (item: SeriesItem, groupName: string, double?: boolean) => {
+      setSeriesData((currentData) => {
+        const groupIndex = currentData.findIndex((group) => group.name === groupName);
+
+        if (groupIndex === -1) return currentData;
+
+        const newData = [...currentData];
+        const newGroup = { ...newData[groupIndex] };
+
+        newGroup.items = newGroup.items.map((subItem) =>
+          subItem.item_id === item.item_id
+            ? {
+                ...subItem,
+                ...(double
+                  ? { checked_double: !subItem.checked_double }
+                  : { checked: !subItem.checked })
+              }
+            : subItem
+        );
+
+        newData[groupIndex] = newGroup;
+
+        return newData;
+      });
+
+      const itemData = {
+        token: token,
+        series_id: item.series_id,
+        item_id: item.item_id,
+        checked: (!double ? Number(!item.checked) : Number(item.checked)) as 0 | 1,
+        double: (double ? Number(!item.checked_double) : Number(item.checked_double)) as 0 | 1
+      };
+
+      try {
+        updateSeriesItem(itemData);
+      } catch (error) {
+        console.error('Failed to update checkbox state', error);
+      }
+    },
+    [token, updateSeriesItem]
+  );
+
+  return (
+    <PageWrapper>
+      <Header label={name} />
+      <Modal
+        visible={isInfoModalVisible}
+        children={
+          <View style={styles.modalView}>
+            <Text style={styles.infoTitle}>{infoItem?.name}</Text>
+            <Text style={styles.infoText}>{infoItem?.info}</Text>
+            <Button
+              variant={ButtonVariants.OPACITY}
+              containerStyles={styles.btnContainer}
+              textStyles={{
+                color: Colors.DARK_BLUE
+              }}
+              onPress={() => setIsInfoModalVisible(false)}
+              children={'Got it'}
+            />
+          </View>
+        }
+        onRequestClose={() => setIsInfoModalVisible(false)}
+        headerTitle={'Info'}
+        visibleInPercent={'auto'}
+      />
+      <Input
+        inputMode={'search'}
+        placeholder={'Search'}
+        onChange={(text) => setSearch(text)}
+        value={search}
+        icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
+      />
+      <TabView
+        navigationState={{ index, routes }}
+        renderScene={renderScene}
+        onIndexChange={(i) => {
+          handleIndexChange(i);
+          setIndex(i);
+        }}
+        lazy={true}
+        renderTabBar={(props) => (
+          <TabBar
+            {...props}
+            indicatorStyle={{ backgroundColor: Colors.DARK_BLUE }}
+            style={styles.tabBar}
+            tabStyle={styles.tabStyle}
+            pressColor={'transparent'}
+            renderLabel={({ route, focused }) => (
+              <Text
+                style={[styles.tabLabel, { color: Colors.DARK_BLUE, opacity: focused ? 1 : 0.4 }]}
+              >
+                {route.title}
+              </Text>
+            )}
+          />
+        )}
+      />
+    </PageWrapper>
+  );
+};

+ 49 - 0
src/screens/InAppScreens/TravelsScreen/SeriesItemScreen/styles.tsx

@@ -0,0 +1,49 @@
+import { StyleSheet, Dimensions } from 'react-native';
+import { Colors } from '../../../../theme';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({
+  tabBar: {
+    backgroundColor: 'transparent',
+    elevation: 0,
+    shadowOpacity: 0,
+    marginTop: 0,
+    height: 42
+  },
+  tabLabel: {
+    color: 'grey',
+    fontSize: getFontSize(14),
+    fontWeight: '700',
+    padding: 8,
+    width: Dimensions.get('window').width / 4,
+    textAlign: 'center'
+  },
+  tabStyle: {
+    padding: 0,
+    marginHorizontal: 2
+  },
+  modalView: {
+    paddingHorizontal: 8,
+    paddingVertical: 24,
+    alignItems: 'center'
+  },
+  infoTitle: {
+    color: Colors.DARK_BLUE,
+    fontSize: 16,
+    fontWeight: '700',
+    textAlign: 'center',
+    marginBottom: 16
+  },
+  infoText: {
+    color: Colors.DARK_BLUE,
+    fontSize: 14,
+    fontWeight: '400',
+    textAlign: 'left',
+    marginBottom: 24
+  },
+  btnContainer: {
+    borderColor: Colors.DARK_BLUE,
+    backgroundColor: Colors.WHITE,
+    width: '60%'
+  }
+});

+ 6 - 2
src/types/api.ts

@@ -36,7 +36,9 @@ export enum API_ENDPOINT {
   SERIES_WITH_GROUP = 'get-series-with-group-app',
   GET_COUNTRIES_RANKING = 'get-countries-ranking',
   GET_COUNTRIES_RANKING_LPI = 'get-countries-ranking-lpi',
-  GET_COUNTRIES_RANKING_MEMORIAM = 'get-countries-ranking-in-memoriam'
+  GET_COUNTRIES_RANKING_MEMORIAM = 'get-countries-ranking-in-memoriam',
+  GET_ITEMS_FOR_SERIES = 'get-items-for-series-grouped-app',
+  TOGGLE_ITEM_SERIES = 'toggle-item'
 }
 
 export enum API {
@@ -64,7 +66,9 @@ export enum API {
   SERIES_WITH_GROUP = `${API_ROUTE.SERIES}/${API_ENDPOINT.SERIES_WITH_GROUP}`,
   GET_COUNTRIES_RANKING = `${API_ROUTE.RANKING}/${API_ENDPOINT.GET_COUNTRIES_RANKING}`,
   GET_COUNTRIES_RANKING_LPI = `${API_ROUTE.RANKING}/${API_ENDPOINT.GET_COUNTRIES_RANKING_LPI}`,
-  GET_COUNTRIES_RANKING_MEMORIAM = `${API_ROUTE.RANKING}/${API_ENDPOINT.GET_COUNTRIES_RANKING_MEMORIAM}`
+  GET_COUNTRIES_RANKING_MEMORIAM = `${API_ROUTE.RANKING}/${API_ENDPOINT.GET_COUNTRIES_RANKING_MEMORIAM}`,
+  GET_ITEMS_FOR_SERIES = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_ITEMS_FOR_SERIES}`,
+  TOGGLE_ITEM_SERIES = `${API_ROUTE.SERIES}/${API_ENDPOINT.TOGGLE_ITEM_SERIES}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 2 - 1
src/types/navigation.ts

@@ -21,5 +21,6 @@ export enum NAVIGATION_PAGES {
   SETTINGS = 'settings',
   IN_APP_TRAVELS_TAB = 'Travels',
   TRAVELS_TAB = 'inAppTravels',
-  SERIES = 'inAppSeries'
+  SERIES = 'inAppSeries',
+  SERIES_ITEM = 'inAppSeriesItem',
 }