Revize 852de08e
Přidáno uživatelem Fantič před téměř 2 roky(ů)
App.tsx | ||
---|---|---|
11 | 11 |
<Provider store={store}> |
12 | 12 |
<NativeBaseProvider> |
13 | 13 |
{/* TODO: remove placeholder pro header / nav */} |
14 |
<Center w="100%" h="40" bg="primary.100" rounded="md" shadow={3} />
|
|
14 |
<Center w="100%" h="40" rounded="md" background="success.300"/>
|
|
15 | 15 |
<ItemViewPage itemId={'PrgA-811'} /> |
16 | 16 |
</NativeBaseProvider> |
17 | 17 |
</Provider> |
src/components/item/ItemView.tsx | ||
---|---|---|
1 |
import { Center, Box, Heading, VStack, FormControl, Link, Input, Button, HStack, Text, Divider } from "native-base"
|
|
2 |
import React, { useState } from "react"
|
|
1 |
import { Center, Box, VStack, Button, HStack, Text, Image, ScrollView, View, Spacer } from "native-base"
|
|
2 |
import React from "react" |
|
3 | 3 |
import { Item } from "../../types/item" |
4 | 4 |
import { Note } from "../../types/note" |
5 |
import LoadingBox from "../loading/loadingBox" |
|
5 | 6 |
|
6 | 7 |
interface ItemDetailProps { |
7 | 8 |
item: Item, |
8 |
notes: Note[] |
|
9 |
itemLoading: boolean, |
|
10 |
notes: Note[], |
|
11 |
notesLoading: boolean |
|
9 | 12 |
} |
10 | 13 |
|
11 | 14 |
|
12 | 15 |
const ItemDetail = (props: { item: Item }) => { |
13 | 16 |
const item = props.item; |
17 |
|
|
14 | 18 |
return ( |
15 |
<Center>
|
|
19 |
<View alignItems="center" flex="1" justifyContent="center">
|
|
16 | 20 |
{ |
17 | 21 |
item && ( |
18 |
<VStack> |
|
19 |
<Text>id: {item.id}</Text> |
|
20 |
<Text>name: {item.workName}</Text> |
|
21 |
</VStack> |
|
22 |
<ScrollView> |
|
23 |
<VStack> |
|
24 |
<Spacer/> |
|
25 |
<Spacer/> |
|
26 |
<Text>id: {item.id}</Text> |
|
27 |
<Text>name: {item.workName}</Text> |
|
28 |
{ |
|
29 |
item.fullView && item.imageUrl && ( |
|
30 |
<Image size={250} alt="image" source={{ uri: `http:147.228.173.159/static/images/${item.imageUrl}` }} /> |
|
31 |
) |
|
32 |
} |
|
33 |
</VStack> |
|
34 |
</ScrollView> |
|
35 |
|
|
22 | 36 |
) |
23 | 37 |
} |
24 |
</Center>
|
|
38 |
</View>
|
|
25 | 39 |
); |
26 | 40 |
} |
27 | 41 |
|
... | ... | |
53 | 67 |
// item shown / note shown |
54 | 68 |
const [itemShown, setItemShown] = React.useState(true); |
55 | 69 |
|
70 |
const {itemLoading, notesLoading} = props; |
|
71 |
|
|
56 | 72 |
const handleItemClick = () => { |
57 | 73 |
setItemShown(true); |
58 | 74 |
} |
... | ... | |
61 | 77 |
setItemShown(false); |
62 | 78 |
} |
63 | 79 |
|
64 |
|
|
65 | 80 |
return ( |
66 |
<> |
|
81 |
<Box display="flex" flex={1} flexDirection="column" justifyContent="space-between">
|
|
67 | 82 |
{itemShown ? ( |
83 |
itemLoading ? |
|
84 |
<LoadingBox text="Item loading"/> |
|
85 |
: |
|
68 | 86 |
<ItemDetail item={props.item} /> |
69 | 87 |
) : ( |
88 |
notesLoading ? |
|
89 |
<LoadingBox text="Notes loading"/> |
|
90 |
: |
|
70 | 91 |
<ItemNotes notes={props.notes} /> |
71 | 92 |
)} |
72 |
<HStack mt={4}> |
|
73 |
<Button |
|
74 |
colorScheme={itemShown ? "blue" : undefined} |
|
93 |
|
|
94 |
{/* item notes switch tab */} |
|
95 |
<HStack alignItems="center"> |
|
96 |
<Button |
|
97 |
variant={itemShown ? "subtle" : "outline"} |
|
98 |
w="50%" |
|
75 | 99 |
onPress={handleItemClick} |
76 | 100 |
> |
77 | 101 |
Item |
78 | 102 |
</Button> |
79 |
<Button |
|
80 |
colorScheme={!itemShown ? "blue" : undefined} |
|
103 |
<Button |
|
104 |
variant={itemShown ? "outline" : "subtle"} |
|
105 |
w="50%" |
|
81 | 106 |
onPress={handleNotesClick} |
82 | 107 |
> |
83 | 108 |
Notes |
84 | 109 |
</Button> |
85 | 110 |
</HStack> |
86 |
</> |
|
111 |
</Box>
|
|
87 | 112 |
) |
88 | 113 |
} |
89 | 114 |
|
src/components/loading/loadingBox.tsx | ||
---|---|---|
1 |
import { Center, Box, VStack, Button, HStack, Text, Image, ScrollView, Heading, Spinner } from "native-base" |
|
2 |
import React from "react" |
|
3 |
|
|
4 |
|
|
5 |
interface loadingBoxProps { |
|
6 |
text: string |
|
7 |
} |
|
8 |
|
|
9 |
const LoadingBox = (props: loadingBoxProps) => { |
|
10 |
const { text } = props; |
|
11 |
|
|
12 |
return ( |
|
13 |
<HStack alignItems="center" flex="1" justifyContent="center"> |
|
14 |
<Spinner/> |
|
15 |
<Heading color="primary.400" fontSize="md"> |
|
16 |
{text} |
|
17 |
</Heading> |
|
18 |
</HStack> |
|
19 |
); |
|
20 |
} |
|
21 |
|
|
22 |
|
|
23 |
export default LoadingBox |
src/pages/ItemViewPage.tsx | ||
---|---|---|
1 |
import { Center, Box, Heading, VStack, FormControl, Link, Input, Button, HStack, Text, StatusBar, Container, Divider, Spacer, Row, View } from "native-base" |
|
2 |
import React, { useState, useEffect } from "react" |
|
1 |
import React, { useEffect } from "react" |
|
3 | 2 |
import { useDispatch, useSelector } from "react-redux" |
4 | 3 |
import { AppDispatch, RootState } from "../stores/store" |
5 | 4 |
import { getItem, getItemNotes, setConcordances, setSelectedConcordance } from "../stores/actions/itemThunks" |
6 | 5 |
import { login } from "../stores/actions/userThunks" |
7 |
import { SceneRendererProps, TabView } from "react-native-tab-view" |
|
8 |
import { useWindowDimensions } from "react-native" |
|
6 |
import { NavigationState, SceneRendererProps, TabBar, TabView } from "react-native-tab-view" |
|
9 | 7 |
import ItemView from "../components/item/ItemView" |
8 |
import { Button, Pressable, View, Text, Icon, CircleIcon, ScrollView, HStack, VStack } from "native-base" |
|
9 |
import { useWindowDimensions } from "react-native" |
|
10 |
import LoadingBox from "../components/loading/loadingBox" |
|
11 |
import { CertaintyWithColors } from "../stores/reducers/itemSlice" |
|
10 | 12 |
|
11 | 13 |
|
12 | 14 |
interface ItemViewProps { |
... | ... | |
16 | 18 |
const ItemViewPage = (props: ItemViewProps) => { |
17 | 19 |
|
18 | 20 |
const layout = useWindowDimensions(); |
19 |
|
|
20 | 21 |
const [routes, setRoutes] = React.useState([ |
21 | 22 |
// initial tab |
22 | 23 |
{ key: 'loading', title: 'Loading item...' }, |
23 | 24 |
]); |
25 |
const [loadedScenes, setLoadedScenes] = React.useState({}); |
|
24 | 26 |
|
25 |
const dispatch = useDispatch<AppDispatch>(); |
|
26 | 27 |
|
27 |
const { item, notes, concordances, selectedConcordance } = useSelector((state: RootState) => state.itemViewState) |
|
28 | 28 |
|
29 |
const dispatch = useDispatch<AppDispatch>(); |
|
30 |
|
|
31 |
const { item, itemLoading, notes, notesLoading, concordances, selectedConcordance } = useSelector((state: RootState) => state.itemViewState) |
|
29 | 32 |
|
30 | 33 |
// initial: load main item |
31 | 34 |
useEffect(() => { |
32 | 35 |
// TODO remove login from here |
33 | 36 |
dispatch(login({ username: "Viktorie", password: "Golem123." })); |
34 | 37 |
dispatch(getItem(props.itemId)); |
35 |
}, [props.itemId]);
|
|
38 |
}, []); |
|
36 | 39 |
|
37 | 40 |
|
38 | 41 |
// concordances are loaded |
... | ... | |
52 | 55 |
}, [concordances]); |
53 | 56 |
|
54 | 57 |
const handleTabChange = (index: number) => { |
58 |
|
|
55 | 59 |
dispatch(setSelectedConcordance(index)); |
56 | 60 |
}; |
57 | 61 |
|
... | ... | |
65 | 69 |
|
66 | 70 |
// item changes |
67 | 71 |
useEffect(() => { |
68 |
|
|
69 |
if (item && item.id) { |
|
72 |
if (!itemLoading && item && item.id) { |
|
70 | 73 |
if (selectedConcordance == 0) { |
71 | 74 |
dispatch(setConcordances(item.concordances)); |
72 | 75 |
} |
... | ... | |
75 | 78 |
dispatch(getItemNotes(item.id)); |
76 | 79 |
} |
77 | 80 |
} |
78 |
|
|
79 | 81 |
}, [item]); |
80 | 82 |
|
83 |
// custom render Tab |
|
84 |
const _renderTabBar = (props: { navigationState: { routes: any[] } }) => { |
|
85 |
if (concordances && concordances.length > 0) |
|
86 |
return ( |
|
87 |
<View h="16"> |
|
88 |
<ScrollView horizontal={true} > |
|
89 |
<HStack> |
|
90 |
{props.navigationState.routes.map((route, i) => { |
|
91 |
return ( |
|
92 |
<Button size="lg" key={i} |
|
93 |
variant={selectedConcordance == i ? "subtle" : "outline"} |
|
94 |
rightIcon={<CircleIcon size="2" color={CertaintyWithColors[concordances[i].cert].color} />} |
|
95 |
onPress={() => handleTabChange(i)} |
|
96 |
> |
|
97 |
{concordances[i].id} |
|
98 |
</Button> |
|
99 |
); |
|
100 |
})} |
|
101 |
</HStack> |
|
102 |
|
|
103 |
</ScrollView> |
|
104 |
</View> |
|
105 |
) |
|
106 |
else { |
|
107 |
return <LoadingBox text={`Loading concordances...`}></LoadingBox> |
|
108 |
} |
|
109 |
}; |
|
110 |
|
|
81 | 111 |
return ( |
82 |
<TabView |
|
83 |
navigationState={{ index: selectedConcordance, routes }} |
|
84 |
renderScene={() => <ItemView item={item} notes={notes} />} |
|
85 |
onIndexChange={handleTabChange} |
|
86 |
initialLayout={{ width: layout.width }} |
|
87 |
/> |
|
112 |
!concordances || concordances.length < 1 ? |
|
113 |
<LoadingBox text={`Loading item ${props.itemId}...`}></LoadingBox> |
|
114 |
: |
|
115 |
<TabView |
|
116 |
renderTabBar={_renderTabBar} |
|
117 |
navigationState={{ index: selectedConcordance, routes }} |
|
118 |
renderScene={() => ( |
|
119 |
<ItemView item={item} notes={notes} itemLoading={itemLoading} notesLoading={notesLoading} /> |
|
120 |
)} |
|
121 |
initialLayout={{ width: layout.width }} |
|
122 |
|
|
123 |
// prevent default action on index change |
|
124 |
onIndexChange={handleTabChange} |
|
125 |
|
|
126 |
/> |
|
88 | 127 |
); |
89 | 128 |
} |
90 | 129 |
|
130 |
|
|
131 |
|
|
91 | 132 |
export default ItemViewPage; |
src/stores/reducers/itemSlice.ts | ||
---|---|---|
2 | 2 |
import { getItem, getItemNotes } from "../actions/itemThunks" |
3 | 3 |
import { Certainty, ItemViewState, Item } from "../../types/item"; |
4 | 4 |
|
5 |
|
|
5 |
// TODO set colors |
|
6 | 6 |
export const CertaintyWithColors: Record<Certainty, { certainty: Certainty; color: string }> = { |
7 |
same_as: { certainty: Certainty.SameAs, color: "#000000" },
|
|
8 |
high: { certainty: Certainty.High, color: "#FF0000" },
|
|
9 |
medium: { certainty: Certainty.Medium, color: "#FFFF00" },
|
|
10 |
low: { certainty: Certainty.Low, color: "#00FF00" },
|
|
11 |
unknown : { certainty: Certainty.Unknown, color: "#000000"}
|
|
7 |
same_as: { certainty: Certainty.SameAs, color: "light.300" },
|
|
8 |
high: { certainty: Certainty.High, color: "success.300" },
|
|
9 |
medium: { certainty: Certainty.Medium, color: "warning.300" },
|
|
10 |
low: { certainty: Certainty.Low, color: "danger.300" },
|
|
11 |
unknown : { certainty: Certainty.Unknown, color: "light.300"}
|
|
12 | 12 |
}; |
13 | 13 |
|
14 | 14 |
const initialState: ItemViewState = { |
... | ... | |
16 | 16 |
selectedConcordance: 0, |
17 | 17 |
concordances: [], |
18 | 18 |
notes: [], |
19 |
lastError: "" |
|
19 |
lastError: "", |
|
20 |
itemLoading: true, |
|
21 |
notesLoading: true |
|
20 | 22 |
} |
21 | 23 |
|
22 | 24 |
export const itemSlice = createSlice({ |
... | ... | |
62 | 64 |
state.item.provenance = undefined; |
63 | 65 |
state.item.description = undefined; |
64 | 66 |
} |
67 |
|
|
68 |
state.itemLoading = false; |
|
69 |
}) |
|
70 |
builder.addCase(getItem.pending, (state, action) => { |
|
71 |
state.itemLoading = true; |
|
65 | 72 |
}) |
66 | 73 |
builder.addCase(getItem.rejected, (state, action) => { |
74 |
state.itemLoading = false; |
|
67 | 75 |
state.lastError = action.error.message |
68 | 76 |
}) |
69 | 77 |
|
70 | 78 |
// getItemNotes |
71 | 79 |
builder.addCase(getItemNotes.fulfilled, (state, action) => { |
80 |
state.notesLoading = false; |
|
72 | 81 |
state.notes = action.payload.notes; |
73 | 82 |
}) |
83 |
builder.addCase(getItemNotes.pending, (state, action) => { |
|
84 |
state.notesLoading = true; |
|
85 |
}) |
|
74 | 86 |
builder.addCase(getItemNotes.rejected, (state, action) => { |
87 |
state.notesLoading = false; |
|
75 | 88 |
state.lastError = action.error.message |
76 | 89 |
}) |
77 | 90 |
|
src/types/item.ts | ||
---|---|---|
28 | 28 |
|
29 | 29 |
export type ItemViewState = { |
30 | 30 |
item: Item |
31 |
itemLoading: boolean |
|
31 | 32 |
concordances: Concordance[] |
32 | 33 |
selectedConcordance: number |
33 | 34 |
notes: Note[] |
35 |
notesLoading: boolean |
|
34 | 36 |
lastError?: string |
35 | 37 |
} |
36 | 38 |
|
... | ... | |
46 | 48 |
Low = "low", |
47 | 49 |
Unknown = "unknown" |
48 | 50 |
} |
51 |
|
Také k dispozici: Unified diff
re #10454: ItemView: Concordance scroll view, Loading Component