commit 852de08eaa5cb051d905c8cc16782e9816921022
Author: Fantič <58391275+FantaFantic@users.noreply.github.com>
Date:   Wed Apr 26 14:57:26 2023 +0200

    re #10454: ItemView: Concordance scroll view, Loading Component

diff --git a/App.tsx b/App.tsx
index deee0b2..7a5287b 100644
--- a/App.tsx
+++ b/App.tsx
@@ -11,7 +11,7 @@ export default function App() {
     <Provider store={store}>
       <NativeBaseProvider>
           {/* TODO: remove placeholder pro header / nav */}
-          <Center w="100%" h="40" bg="primary.100" rounded="md" shadow={3} />
+          <Center w="100%" h="40" rounded="md" background="success.300"/>
           <ItemViewPage itemId={'PrgA-811'} />
       </NativeBaseProvider>
     </Provider>
diff --git a/src/components/item/ItemView.tsx b/src/components/item/ItemView.tsx
index 849cbb7..b40f23e 100644
--- a/src/components/item/ItemView.tsx
+++ b/src/components/item/ItemView.tsx
@@ -1,27 +1,41 @@
-import { Center, Box, Heading, VStack, FormControl, Link, Input, Button, HStack, Text, Divider } from "native-base"
-import React, { useState } from "react"
+import { Center, Box, VStack, Button, HStack, Text, Image, ScrollView, View, Spacer } from "native-base"
+import React from "react"
 import { Item } from "../../types/item"
 import { Note } from "../../types/note"
+import LoadingBox from "../loading/loadingBox"
 
 interface ItemDetailProps {
     item: Item,
-    notes: Note[]
+    itemLoading: boolean,
+    notes: Note[],
+    notesLoading: boolean
 }
 
 
 const ItemDetail = (props: { item: Item }) => {
     const item = props.item;
+
     return (
-        <Center>
+        <View alignItems="center" flex="1" justifyContent="center">
             {
                 item && (
-                    <VStack>
-                        <Text>id: {item.id}</Text>
-                        <Text>name: {item.workName}</Text>
-                    </VStack>
+                    <ScrollView>
+                        <VStack>
+                            <Spacer/>
+                            <Spacer/>
+                            <Text>id: {item.id}</Text>
+                            <Text>name: {item.workName}</Text>
+                            {
+                                item.fullView && item.imageUrl && (
+                                    <Image size={250} alt="image" source={{ uri: `http:147.228.173.159/static/images/${item.imageUrl}` }} />
+                                )
+                            }
+                        </VStack>
+                    </ScrollView>
+
                 )
             }
-        </Center>
+        </View>
     );
 }
 
@@ -53,6 +67,8 @@ const ItemView = (props: ItemDetailProps) => {
     // item shown / note shown
     const [itemShown, setItemShown] = React.useState(true);
 
+    const {itemLoading, notesLoading} = props;
+
     const handleItemClick = () => {
         setItemShown(true);
     }
@@ -61,29 +77,38 @@ const ItemView = (props: ItemDetailProps) => {
         setItemShown(false);
     }
 
-
     return (
-        <>
+        <Box display="flex" flex={1} flexDirection="column" justifyContent="space-between">
             {itemShown ? (
+                itemLoading ? 
+                <LoadingBox text="Item loading"/>
+                :
                 <ItemDetail item={props.item} />
             ) : (
+                notesLoading ? 
+                <LoadingBox text="Notes loading"/>
+                :
                 <ItemNotes notes={props.notes} />
             )}
-            <HStack mt={4}>
-                <Button
-                    colorScheme={itemShown ? "blue" : undefined}
+
+            {/* item notes switch tab */}
+            <HStack alignItems="center">
+                <Button 
+                    variant={itemShown ? "subtle" : "outline"}
+                    w="50%"
                     onPress={handleItemClick}
                 >
                     Item
                 </Button>
-                <Button
-                    colorScheme={!itemShown ? "blue" : undefined}
+                <Button 
+                    variant={itemShown ? "outline" : "subtle"}
+                    w="50%"
                     onPress={handleNotesClick}
                 >
                     Notes
                 </Button>
             </HStack>
-        </>
+        </Box>
     )
 }
 
diff --git a/src/components/loading/loadingBox.tsx b/src/components/loading/loadingBox.tsx
new file mode 100644
index 0000000..541eab2
--- /dev/null
+++ b/src/components/loading/loadingBox.tsx
@@ -0,0 +1,23 @@
+import { Center, Box, VStack, Button, HStack, Text, Image, ScrollView, Heading, Spinner } from "native-base"
+import React from "react"
+
+
+interface loadingBoxProps {
+    text: string
+}
+
+const LoadingBox = (props: loadingBoxProps) => {
+    const { text } = props;
+
+    return (
+        <HStack alignItems="center" flex="1" justifyContent="center">
+            <Spinner/>
+            <Heading color="primary.400" fontSize="md">
+                {text}
+            </Heading>
+        </HStack>
+    );
+}
+
+
+export default LoadingBox
\ No newline at end of file
diff --git a/src/pages/ItemViewPage.tsx b/src/pages/ItemViewPage.tsx
index 6652b6c..5718515 100644
--- a/src/pages/ItemViewPage.tsx
+++ b/src/pages/ItemViewPage.tsx
@@ -1,12 +1,14 @@
-import { Center, Box, Heading, VStack, FormControl, Link, Input, Button, HStack, Text, StatusBar, Container, Divider, Spacer, Row, View } from "native-base"
-import React, { useState, useEffect } from "react"
+import React, { useEffect } from "react"
 import { useDispatch, useSelector } from "react-redux"
 import { AppDispatch, RootState } from "../stores/store"
 import { getItem, getItemNotes, setConcordances, setSelectedConcordance } from "../stores/actions/itemThunks"
 import { login } from "../stores/actions/userThunks"
-import { SceneRendererProps, TabView } from "react-native-tab-view"
-import { useWindowDimensions } from "react-native"
+import { NavigationState, SceneRendererProps, TabBar, TabView } from "react-native-tab-view"
 import ItemView from "../components/item/ItemView"
+import { Button, Pressable, View, Text, Icon, CircleIcon, ScrollView, HStack, VStack } from "native-base"
+import { useWindowDimensions } from "react-native"
+import LoadingBox from "../components/loading/loadingBox"
+import { CertaintyWithColors } from "../stores/reducers/itemSlice"
 
 
 interface ItemViewProps {
@@ -16,23 +18,24 @@ interface ItemViewProps {
 const ItemViewPage = (props: ItemViewProps) => {
 
   const layout = useWindowDimensions();
-
   const [routes, setRoutes] = React.useState([
     // initial tab
     { key: 'loading', title: 'Loading item...' },
   ]);
+  const [loadedScenes, setLoadedScenes] = React.useState({});
 
-  const dispatch = useDispatch<AppDispatch>();
 
-  const { item, notes, concordances, selectedConcordance } = useSelector((state: RootState) => state.itemViewState)
 
+  const dispatch = useDispatch<AppDispatch>();
+
+  const { item, itemLoading, notes, notesLoading, concordances, selectedConcordance } = useSelector((state: RootState) => state.itemViewState)
 
   // initial: load main item
   useEffect(() => {
     // TODO remove login from here
     dispatch(login({ username: "Viktorie", password: "Golem123." }));
     dispatch(getItem(props.itemId));
-  }, [props.itemId]);
+  }, []);
 
 
   // concordances are loaded
@@ -52,6 +55,7 @@ const ItemViewPage = (props: ItemViewProps) => {
   }, [concordances]);
 
   const handleTabChange = (index: number) => {
+  
     dispatch(setSelectedConcordance(index));
   };
 
@@ -65,8 +69,7 @@ const ItemViewPage = (props: ItemViewProps) => {
 
   // item changes
   useEffect(() => {
-
-    if (item && item.id) {
+    if (!itemLoading && item && item.id) {
       if (selectedConcordance == 0) {
         dispatch(setConcordances(item.concordances));
       }
@@ -75,17 +78,55 @@ const ItemViewPage = (props: ItemViewProps) => {
         dispatch(getItemNotes(item.id));
       }
     }
-
   }, [item]);
 
+  // custom render Tab
+  const _renderTabBar = (props: { navigationState: { routes: any[] } }) => {
+    if (concordances && concordances.length > 0)
+      return (
+        <View h="16">
+          <ScrollView horizontal={true} >
+            <HStack>
+              {props.navigationState.routes.map((route, i) => {
+                return (
+                  <Button size="lg" key={i}
+                    variant={selectedConcordance == i ? "subtle" : "outline"}
+                    rightIcon={<CircleIcon size="2" color={CertaintyWithColors[concordances[i].cert].color} />}
+                    onPress={() => handleTabChange(i)}
+                  >
+                    {concordances[i].id}
+                  </Button>
+                );
+              })}
+            </HStack>
+
+          </ScrollView>
+        </View>
+      )
+    else {
+      return <LoadingBox text={`Loading concordances...`}></LoadingBox>
+    }
+  };
+
   return (
-    <TabView
-      navigationState={{ index: selectedConcordance, routes }}
-      renderScene={() => <ItemView item={item} notes={notes} />}
-      onIndexChange={handleTabChange}
-      initialLayout={{ width: layout.width }}
-    />
+    !concordances || concordances.length < 1 ?
+      <LoadingBox text={`Loading item ${props.itemId}...`}></LoadingBox>
+      :
+      <TabView
+        renderTabBar={_renderTabBar}
+        navigationState={{ index: selectedConcordance, routes }}
+        renderScene={() => (
+          <ItemView item={item} notes={notes} itemLoading={itemLoading} notesLoading={notesLoading} />
+        )}
+        initialLayout={{ width: layout.width }}
+
+        // prevent default action on index change
+        onIndexChange={handleTabChange}
+
+      />
   );
 }
 
+
+
 export default ItemViewPage;
diff --git a/src/stores/reducers/itemSlice.ts b/src/stores/reducers/itemSlice.ts
index 9cad31e..9d77f94 100644
--- a/src/stores/reducers/itemSlice.ts
+++ b/src/stores/reducers/itemSlice.ts
@@ -2,13 +2,13 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"
 import { getItem, getItemNotes } from "../actions/itemThunks"
 import { Certainty, ItemViewState, Item } from "../../types/item";
 
-
+// TODO set colors
 export const CertaintyWithColors: Record<Certainty, { certainty: Certainty; color: string }> = {
-    same_as: { certainty: Certainty.SameAs, color: "#000000" },
-    high: { certainty: Certainty.High, color: "#FF0000" },
-    medium: { certainty: Certainty.Medium, color: "#FFFF00" },
-    low: { certainty: Certainty.Low, color: "#00FF00" },
-    unknown : { certainty: Certainty.Unknown, color: "#000000"}
+    same_as: { certainty: Certainty.SameAs, color: "light.300" },
+    high: { certainty: Certainty.High, color: "success.300" },
+    medium: { certainty: Certainty.Medium, color: "warning.300" },
+    low: { certainty: Certainty.Low, color: "danger.300" },
+    unknown : { certainty: Certainty.Unknown, color: "light.300"}
 };
 
 const initialState: ItemViewState = {
@@ -16,7 +16,9 @@ const initialState: ItemViewState = {
     selectedConcordance: 0,
     concordances: [],
     notes: [],
-    lastError: ""
+    lastError: "",
+    itemLoading: true,
+    notesLoading: true
 }
 
 export const itemSlice = createSlice({
@@ -62,16 +64,27 @@ export const itemSlice = createSlice({
                 state.item.provenance = undefined;
                 state.item.description = undefined;
             }
+
+            state.itemLoading = false;
+        })
+        builder.addCase(getItem.pending, (state, action) => {
+            state.itemLoading = true;
         })
         builder.addCase(getItem.rejected, (state, action) => {
+            state.itemLoading = false;
             state.lastError = action.error.message
         })
 
         // getItemNotes
         builder.addCase(getItemNotes.fulfilled, (state, action) => {
+            state.notesLoading = false;
             state.notes = action.payload.notes;
         })
+        builder.addCase(getItemNotes.pending, (state, action) => {
+            state.notesLoading = true;
+        })
         builder.addCase(getItemNotes.rejected, (state, action) => {
+            state.notesLoading = false;
             state.lastError = action.error.message
         })
 
diff --git a/src/types/item.ts b/src/types/item.ts
index a086900..a56eb2a 100644
--- a/src/types/item.ts
+++ b/src/types/item.ts
@@ -28,9 +28,11 @@ export type  Item = {
 
 export type ItemViewState = {
     item: Item
+    itemLoading: boolean
     concordances: Concordance[]
     selectedConcordance: number
     notes: Note[]
+    notesLoading: boolean
     lastError?: string
 }
 
@@ -46,3 +48,4 @@ export enum Certainty {
     Low = "low",
     Unknown = "unknown"
 }
+
