Projekt

Obecné

Profil

« Předchozí | Další » 

Revize 9c55d3bb

Přidáno uživatelem Schwobik před asi 2 roky(ů)

SearchPage implementations with base of list view for results of the search
re #10342

Zobrazit rozdíly:

package.json
22 22
    "react-native": "0.71.6",
23 23
    "react-native-deck-swiper": "^2.0.13",
24 24
    "react-native-gesture-handler": "~2.9.0",
25
    "react-native-multiple-select": "^0.5.12",
25 26
    "react-native-reanimated": "~2.14.4",
26 27
    "react-native-safe-area-context": "4.5.0",
27 28
    "react-native-screens": "~3.20.0",
28 29
    "react-native-svg": "13.4.0",
30
    "react-native-vector-icons": "^9.2.0",
29 31
    "react-redux": "^8.0.5",
30 32
    "redux": "^4.2.1",
31 33
    "typescript": "^4.9.4"
src/api/searchFormService.ts
1
import { axiosInstance } from "./api"
2

  
3
export const fetchInventoriesRequest = async () => {
4
    return await axiosInstance.get("/inventories")
5
}
6

  
7
export const fetchCountriesRequest = async () => {
8
    return await axiosInstance.get("/form/country")
9
}
10

  
11
export const fetchInstitutionsRequest = async () => {
12
    return await axiosInstance.get("/form/institution")
13
}
14

  
15
export const fetchCitiesRequest = async () => {
16
    return await axiosInstance.get("/form/city")
17
}
18

  
19
export const fetchNationalitiesRequest = async () => {
20
    return await axiosInstance.get("/form/nationality")
21
}
22

  
23
export const fetchTechniquesRequest = async () => {
24
    return await axiosInstance.get("/form/technique")
25
}
26

  
27
export const fetchSubjectsRequest = async () => {
28
    return await axiosInstance.get("/subject")
29
}
30

  
31
export const fetchArtistNamesRequest = async () => {
32
    return await axiosInstance.get("/name/all")
33
}
34

  
35
export const fetchPlanRequest = async () => {
36
    return await axiosInstance.get("/plan/list")
37
}
src/api/searchService.ts
1
import { axiosInstance } from "./api"
2

  
3
export interface SearchParams {
4
    inventory?: string
5
    //TODO: add other search params
6
}
7

  
8
export const searchRequest = async (params: SearchParams) => {
9
    return await axiosInstance.get(`/api/search_v2/${ (params.inventory ? `inventory=${ params.inventory }` : "")}`)
10
}
src/components/SearchFormControl.tsx
1
import { CheckIcon, FormControl, Select } from "native-base"
2
import MultiSelect from "react-native-multiple-select"
3

  
4
export interface SearchFormProps {
5
    data: { label: string, value: string, key: string }[],
6
    label: string,
7
    placeholder: string,
8
}
9

  
10
const SearchFormControl = (props: SearchFormProps) => {
11
    return (
12
        <FormControl key={props.label}>
13
            <FormControl.Label>{props.label}</FormControl.Label>
14
            <Select
15
                placeholder={props.placeholder}
16
                minWidth="200"
17
                accessibilityLabel="Choose Service"
18
                _selectedItem={{
19
                    bg: "teal.600",
20
                    endIcon: <CheckIcon size={5} />
21
                }}
22
                mt="1"
23
                key={props.label}
24
            >
25
                {props.data.map((row) => (
26
                    <Select.Item label={row.label} value={row.value} key={row.key}/>
27
                ))}
28
            </Select>
29
        </FormControl>
30
    )
31
}
32

  
33
export default SearchFormControl
src/components/listView/ItemPreview.tsx
1
import { Center } from "native-base"
2

  
3
interface ItemPreviewProps {
4
    caption: string
5
    title: string
6
    name: string
7
    image: string
8
}
9

  
10
const ItemPreview = (props: ItemPreviewProps) => {
11

  
12
    return (
13
        <>
14
            <Center>
15

  
16
            </Center>
17
        </>
18
    )
19
}
20

  
21
export default ItemPreview
src/components/listView/ListView.tsx
1
import { useEffect, useState } from "react"
2
import { Text } from "native-base"
3
import { useSelector } from "react-redux"
4
import { RootState } from "../../stores/store"
5

  
6
const ListView = () => {
7
    const items = useSelector((state: RootState) => state.listView.data)
8

  
9
    return (
10
        <>
11
            <Text>ListView</Text>
12
        </>
13
    )
14
}
15

  
16
export default ListView
src/components/search/SearchForm.tsx
1
import { Button, Center, CheckIcon, FormControl, KeyboardAvoidingView, ScrollView, Select, Text } from "native-base"
2
import { useDispatch, useSelector } from "react-redux"
3
import React, { useEffect, useState } from "react"
4
import { AppDispatch, RootState } from "../../stores/store"
5
import { Inventory } from "../../types/searchFormTypes"
6
import {
7
    fetchArtists, fetchCities, fetchCountries, fetchInstitutions,
8
    fetchInventories,
9
    fetchNationalities,
10
    fetchPlans,
11
    fetchSubjects
12
} from "../../stores/actions/searchFormThunks"
13
import SearchFormControl from "./SearchFormControl"
14
import { Platform } from "react-native"
15
import { search } from "../../stores/actions/listViewThunks"
16

  
17

  
18
const SearchForm = () => {
19
    const inventories: Inventory[] = useSelector((state: RootState) => state.searchForm.inventories)
20
    const nationalities = useSelector((state: RootState) => state.searchForm.nationalities)
21
    const artists = useSelector((state: RootState) => state.searchForm.artists)
22
    const subjects = useSelector((state: RootState) => state.searchForm.subjects)
23
    const rooms = useSelector((state: RootState) => state.searchForm.rooms)
24
    const countries = useSelector((state: RootState) => state.searchForm.countries)
25
    const cities = useSelector((state: RootState) => state.searchForm.cities)
26
    const institutions = useSelector((state: RootState) => state.searchForm.institutions)
27

  
28

  
29
    const [selectedInventories, setSelectedInventories] = useState<string[]>([])
30
    const [selectedNationalities, setSelectedNationalities] = useState<string[]>([])
31
    const [selectedArtists, setSelectedArtists] = useState<string[]>([])
32
    const [selectedSubjects, setSelectedSubjects] = useState<string[]>([])
33
    const [selectedRooms, setSelectedRooms] = useState<string[]>([])
34
    const [selectedCountries, setSelectedCountries] = useState<string[]>([])
35
    const [selectedCities, setSelectedCities] = useState<string[]>([])
36
    const [selectedInstitutions, setSelectedInstitutions] = useState<string[]>([])
37

  
38
    const dispatch = useDispatch<AppDispatch>()
39

  
40
    useEffect(() => {
41
        dispatch(fetchInventories())
42
        dispatch(fetchNationalities())
43
        dispatch(fetchArtists())
44
        dispatch(fetchSubjects())
45
        dispatch(fetchPlans())
46
        dispatch(fetchCountries())
47
        dispatch(fetchCities())
48
        dispatch(fetchInstitutions())
49
    }, [dispatch])
50

  
51

  
52
    const searchSubmit = () => {
53
        console.log("search")
54
        dispatch(search({inventory: selectedInventories ? selectedInventories[0] : undefined}))
55
    }
56

  
57
    return (
58
        <Center>
59
            <ScrollView>
60
                <SearchFormControl
61
                    data={ inventories.map(inventory => {
62
                        return {label: inventory.label, value: inventory.name, key: inventory.name}
63
                    }) }
64
                    label="Inventory"
65
                    placeholder="Choose Inventory"
66
                    selectedItems={ selectedInventories }
67
                    onSelectedItemsChange={ setSelectedInventories }
68
                />
69
                <SearchFormControl
70
                    data={ rooms.map(room => {
71
                        return {label: room.label, value: room.id.toString(), key: room.id.toString()}
72
                    }) }
73
                    label="Rooms"
74
                    placeholder="Room..."
75
                    selectedItems={ selectedRooms }
76
                    onSelectedItemsChange={ setSelectedRooms }
77
                />
78
                <SearchFormControl
79
                    data={ artists.map(art => {
80
                        return {label: art.display_name, value: art.display_name, key: art.display_name}
81
                    }) }
82
                    label="Artists/Copyists"
83
                    placeholder="Artist/Copyist..."
84
                    selectedItems={ selectedArtists }
85
                    onSelectedItemsChange={ setSelectedArtists }
86
                />
87
                <SearchFormControl
88
                    data={ nationalities.map(nat => {
89
                        return {label: nat, value: nat, key: nat}
90
                    }) }
91
                    label="Artist`s Origin"
92
                    placeholder="Nationality..."
93
                    selectedItems={ selectedNationalities }
94
                    onSelectedItemsChange={ setSelectedNationalities }
95
                />
96
                <FormControl key={ "lab" }>
97
                    <FormControl.Label>label</FormControl.Label>
98
                    <Select
99
                        placeholder={ "Select" }
100
                        minWidth="100"
101
                        accessibilityLabel="Choose Service"
102
                        _selectedItem={ {
103
                            bg: "teal.600",
104
                            endIcon: <CheckIcon size={ 5 }/>
105
                        } }
106
                        mt="1"
107
                        key={ "props.label" }
108

  
109
                    >
110
                        { inventories.map((row) => (
111
                            <Select.Item label={ row.label } value={ row.name } key={ row.name }/>
112
                        )) }
113
                    </Select>
114
                </FormControl>
115
                <Button
116
                    onPress={ () => searchSubmit()}
117
                    colorScheme="primary.100"
118
                >
119
                    Submit
120
                </Button>
121
            </ScrollView>
122

  
123
        </Center>
124
    )
125
}
126

  
127
export default SearchForm
src/components/search/SearchFormControl.tsx
1
import { CheckIcon, FormControl, Select, View } from "native-base"
2
import MultiSelect from "react-native-multiple-select"
3

  
4
export interface SearchFormProps {
5
    data: { label: string, value: string, key: string }[],
6
    label: string,
7
    placeholder: string,
8
    selectedItems: string[],
9
    onSelectedItemsChange: (selectedItems: string[]) => void,
10
}
11

  
12
const SearchFormControl = (props: SearchFormProps) => {
13
    return (
14
        <View w={350}>
15
            <MultiSelect
16
                items={ props.data }
17
                uniqueKey="key"
18
                onSelectedItemsChange={ props.onSelectedItemsChange }
19
                selectedItems={ props.selectedItems }
20
                selectText={ props.label }
21
                searchInputPlaceholderText={ props.placeholder }
22
                displayKey="label"
23
                submitButtonText={ "Submit" }
24
                onChangeInput={ (text: string) => console.log(text) }
25
                tagRemoveIconColor="#CCC"
26
                tagBorderColor="#CCC"
27
                tagTextColor="#CCC"
28
                selectedItemTextColor="#CCC"
29
                selectedItemIconColor="#CCC"
30
                itemTextColor="#000"
31
                searchInputStyle={ { color: "#CCC" } }
32
                submitButtonColor="#CCC"
33

  
34
            />
35
        </View>
36
    )
37
}
38

  
39
export default SearchFormControl
src/pages/SearchPage.tsx
1
import { Center, Text } from "native-base"
1
import { Center, KeyboardAvoidingView, Text } from "native-base"
2
import ListView from "../components/listView/ListView"
3
import SearchForm from "../components/search/SearchForm"
4
import { Platform } from "react-native"
2 5

  
3 6
const SearchPage = () => {
4 7

  
5 8
    return (
6
        <Center>
7
            <Text>Search Page</Text>
8
        </Center>
9
        <KeyboardAvoidingView
10
            h={ {
11
                base: "400px",
12
                lg: "auto"
13
            } }
14
            behavior={ Platform.OS === "ios" ? "padding" : "height" }
15
        >
16
            <Center>
17
                <SearchForm/>
18
                <ListView/>
19
            </Center>
20
        </KeyboardAvoidingView>
9 21
    )
10 22
}
11 23

  
src/stores/actions/listViewThunks.ts
1
import { createAsyncThunk } from "@reduxjs/toolkit"
2
import { SearchParams, searchRequest } from "../../api/searchService"
3
import { SearchResponse } from "../../types/listViewTypes"
4

  
5

  
6
export const search = createAsyncThunk(
7
    "listView/search",
8
    async (searchParams: SearchParams, thunkAPI) => {
9
        try {
10
            const response = await searchRequest(searchParams)
11
            if (response.status === 200) {
12
                return response.data as SearchResponse
13
            } else {
14
                return Promise.reject(response.data ? response.data : "Search failed")
15
            }
16
        } catch (err: any) {
17
            return Promise.reject(err.response.data)
18
        }
19
    }
20
)
src/stores/actions/searchFormThunks.ts
1
import { createAsyncThunk } from "@reduxjs/toolkit"
2
import {
3
    fetchArtistNamesRequest,
4
    fetchCitiesRequest,
5
    fetchCountriesRequest,
6
    fetchInstitutionsRequest,
7
    fetchInventoriesRequest,
8
    fetchNationalitiesRequest,
9
    fetchPlanRequest,
10
    fetchSubjectsRequest,
11
    fetchTechniquesRequest
12
} from "../../api/searchFormService"
13

  
14
export const fetchInventories = createAsyncThunk(
15
    "searchForm/fetchInventories",
16
    async () => {
17
        try {
18
            const response = await fetchInventoriesRequest()
19
            if (response.status === 200) {
20
                return response.data
21
            } else {
22
                return Promise.reject(response.data ? response.data : "Fetch inventories failed")
23
            }
24
        } catch (err: any) {
25
            return Promise.reject(err.response.data)
26
        }
27
    }
28
)
29

  
30
export const fetchCountries = createAsyncThunk(
31
    "searchForm/fetchCountries",
32
    async () => {
33
        try {
34
            const response = await fetchCountriesRequest()
35
            if (response.status === 200) {
36
                return response.data
37
            } else {
38
                return Promise.reject(response.data ? response.data : "Fetch countries failed")
39
            }
40
        } catch (err: any) {
41
            return Promise.reject(err.response.data)
42
        }
43
    }
44
)
45

  
46
export const fetchInstitutions = createAsyncThunk(
47
    "searchForm/fetchInstitutions",
48
    async () => {
49
        try {
50
            const response = await fetchInstitutionsRequest()
51
            if (response.status === 200) {
52
                return response.data
53
            } else {
54
                return Promise.reject(response.data ? response.data : "Fetch institutions failed")
55
            }
56
        } catch (err: any) {
57
            return Promise.reject(err.response.data)
58
        }
59
    }
60
)
61

  
62
export const fetchArtists = createAsyncThunk(
63
    "searchForm/fetchArtists",
64
    async () => {
65
        try {
66
            const response = await fetchArtistNamesRequest()
67
            if (response.status === 200) {
68
                return response.data
69
            } else {
70
                return Promise.reject(response.data ? response.data : "Fetch artists failed")
71
            }
72
        } catch (err: any) {
73
            return Promise.reject(err.response.data)
74
        }
75
    }
76
)
77

  
78
export const fetchTechniques = createAsyncThunk(
79
    "searchForm/fetchTechniques",
80
    async () => {
81
        try {
82
            const response = await fetchTechniquesRequest()
83
            if (response.status === 200) {
84
                return response.data
85
            } else {
86
                return Promise.reject(response.data ? response.data : "Fetch techniques failed")
87
            }
88
        } catch (err: any) {
89
            return Promise.reject(err.response.data)
90
        }
91
    }
92
)
93

  
94
export const fetchNationalities = createAsyncThunk(
95
    "searchForm/fetchNationalities",
96
    async () => {
97
        try {
98
            const response = await fetchNationalitiesRequest()
99
            if (response.status === 200) {
100
                return response.data
101
            } else {
102
                return Promise.reject(response.data ? response.data : "Fetch nationalities failed")
103
            }
104
        } catch (err: any) {
105
            return Promise.reject(err.response.data)
106
        }
107
    }
108
)
109

  
110
export const fetchCities = createAsyncThunk(
111
    "searchForm/fetchCities",
112
    async () => {
113
        try {
114
            const response = await fetchCitiesRequest()
115
            if (response.status === 200) {
116
                return response.data
117
            } else {
118
                return Promise.reject(response.data ? response.data : "Fetch cities failed")
119
            }
120
        } catch (err: any) {
121
            return Promise.reject(err.response.data)
122
        }
123
    }
124
)
125

  
126
export const fetchSubjects = createAsyncThunk(
127
    "searchForm/fetchSubjects",
128
    async () => {
129
        try {
130
            const response = await fetchSubjectsRequest()
131
            if (response.status === 200) {
132
                return response.data
133
            } else {
134
                return Promise.reject(response.data ? response.data : "Fetch subjects failed")
135
            }
136
        } catch (err: any) {
137
            return Promise.reject(err.response.data)
138
        }
139
    }
140
)
141

  
142
export const fetchPlans = createAsyncThunk(
143
    "searchForm/fetchPlans",
144
    async () => {
145
        try {
146
            const response = await fetchPlanRequest()
147
            if (response.status === 200) {
148
                return response.data
149
            } else {
150
                return Promise.reject(response.data ? response.data : "Fetch plans failed")
151
            }
152
        } catch (err: any) {
153
            return Promise.reject(err.response.data)
154
        }
155
    }
156
)
157

  
src/stores/reducers/listViewSlice.ts
1
import { createSlice } from "@reduxjs/toolkit"
2
import { Inventory } from "../../types/searchFormTypes"
3
import { ItemPreview } from "../../types/listViewTypes"
4
import { search } from "../actions/listViewThunks"
5

  
6
export interface ListViewState {
7
    inventories: Inventory[]
8
    data: ItemPreview[]
9
    loading: boolean
10
    lastError?: string
11
}
12

  
13
const initialState: ListViewState = {
14
    inventories: [],
15
    data: [],
16
    loading: false,
17
}
18

  
19
export const listViewSlice = createSlice({
20
    name: "listView",
21
    initialState: initialState,
22
    reducers: {
23
        resetListView: () => initialState,
24
    },
25
    extraReducers: (builder) => {
26
        builder.addCase(search.fulfilled, (state, action) => {
27
            state.inventories = action.payload.pagination.inventories
28
            state.data = action.payload.data
29
            state.loading = false
30
        })
31
        builder.addCase(search.pending, (state, action) => {
32
            state.loading = true
33
        })
34
        builder.addCase(search.rejected, (state, action) => {
35
            state.lastError = action.error.message
36
            state.loading = false
37
        })
38

  
39

  
40
    }
41
})
42

  
43
export const { resetListView } = listViewSlice.actions
44
export default listViewSlice.reducer
src/stores/reducers/searchFormSlice.ts
1
import { createSlice } from "@reduxjs/toolkit"
2
import { Artist, Inventory, Room } from "../../types/searchFormTypes"
3
import {
4
    fetchArtists, fetchCities,
5
    fetchCountries,
6
    fetchInstitutions,
7
    fetchInventories,
8
    fetchNationalities, fetchPlans, fetchSubjects, fetchTechniques
9
} from "../actions/searchFormThunks"
10

  
11
export interface SearchFormState {
12
    inventories: Inventory[]
13
    rooms: Room[]
14
    artists: Artist[]
15
    nationalities: string[]
16
    techniques: string[]
17
    institutions: string[]
18
    countries: string[]
19
    cities: string[]
20
    subjects: string[]
21
    lastError?: string
22
}
23

  
24
const initialState: SearchFormState = {
25
    inventories: [],
26
    rooms: [],
27
    artists: [],
28
    nationalities: [],
29
    techniques: [],
30
    institutions: [],
31
    countries: [],
32
    cities: [],
33
    subjects: [],
34
}
35

  
36
export const searchFormSlice = createSlice({
37
    name: "searchForm",
38
    initialState: initialState,
39
    reducers: {
40
        resetSearchForm: () => initialState,
41
        setInventories: (state, action) => {
42
            state.inventories = action.payload
43
        },
44
        setRooms: (state, action) => {
45
            state.rooms = action.payload
46
        },
47
        setArtists: (state, action) => {
48
            state.artists = action.payload
49
        },
50
        setNationalities: (state, action) => {
51
            state.nationalities = action.payload
52
        },
53
    },
54
    extraReducers: (builder) => {
55
        // ------------ Inventories ------------
56
        builder.addCase(fetchInventories.fulfilled, (state, action) => {
57
            state.inventories = action.payload
58
            state.lastError = ""
59
        })
60
        builder.addCase(fetchInventories.rejected, (state, action) => {
61
            state.lastError = action.error.message
62
        })
63

  
64
        // ------------ Institutions ------------
65
        builder.addCase(fetchInstitutions.fulfilled, (state, action) => {
66
            state.institutions = action.payload
67
            state.lastError = ""
68
        })
69
        builder.addCase(fetchInstitutions.rejected, (state, action) => {
70
            state.lastError = action.error.message
71
        })
72

  
73
        // ------------ Nationalities ------------
74
        builder.addCase(fetchNationalities.fulfilled, (state, action) => {
75
            state.nationalities = action.payload
76
            state.lastError = ""
77
        })
78
        builder.addCase(fetchNationalities.rejected, (state, action) => {
79
            state.lastError = action.error.message
80
        })
81

  
82
        // ------------ Countries ------------
83
        builder.addCase(fetchCountries.fulfilled, (state, action) => {
84
            state.countries = action.payload
85
            state.lastError = ""
86
        })
87
        builder.addCase(fetchCountries.rejected, (state, action) => {
88
            state.lastError = action.error.message
89
        })
90

  
91
        // ------------ Artists ------------
92
        builder.addCase(fetchArtists.fulfilled, (state, action) => {
93
            state.artists = action.payload
94
            state.lastError = ""
95
        })
96
        builder.addCase(fetchArtists.rejected, (state, action) => {
97
            state.lastError = action.error.message
98
        })
99

  
100
        // ------------ Rooms ------------
101
        builder.addCase(fetchPlans.fulfilled, (state, action) => {
102
            state.rooms = action.payload
103
            state.lastError = ""
104
        })
105
        builder.addCase(fetchPlans.rejected, (state, action) => {
106
            state.lastError = action.error.message
107
        })
108

  
109
        // ------------ Subjects ------------
110
        builder.addCase(fetchSubjects.fulfilled, (state, action) => {
111
            state.subjects = action.payload
112
            state.lastError = ""
113
        })
114
        builder.addCase(fetchSubjects.rejected, (state, action) => {
115
            state.lastError = action.error.message
116
        })
117

  
118
        // ------------ Cities ------------
119
        builder.addCase(fetchCities.fulfilled, (state, action) => {
120
            state.cities = action.payload
121
            state.lastError = ""
122
        })
123
        builder.addCase(fetchCities.rejected, (state, action) => {
124
            state.lastError = action.error.message
125
        })
126

  
127
        // ------------ Techniques ------------
128
        builder.addCase(fetchTechniques.fulfilled, (state, action) => {
129
            state.techniques = action.payload
130
            state.lastError = ""
131
        })
132
        builder.addCase(fetchTechniques.rejected, (state, action) => {
133
            state.lastError = action.error.message
134
        })
135
    }
136
})
137

  
138
export const { resetSearchForm, setInventories, setRooms, setArtists, setNationalities } = searchFormSlice.actions
139

  
140
export default searchFormSlice.reducer
src/stores/store.ts
1 1
import { configureStore } from "@reduxjs/toolkit"
2 2
import userReducer from "./reducers/userSlice"
3
import searchFormReducer from "./reducers/searchFormSlice"
4
import listViewReducer from "./reducers/listViewSlice"
3 5

  
4 6
const store = configureStore({
5 7
    reducer: {
6 8
        user: userReducer,
9
        searchForm: searchFormReducer,
10
        listView: listViewReducer,
7 11
    },
8 12
})
9 13

  
src/types/listViewTypes.ts
1
import { Inventory } from "./searchFormTypes"
2

  
3

  
4
export type SearchResponse = {
5
    pagination: Pagination
6
    data: ItemPreview[]
7
}
8

  
9
export type Pagination = {
10
    cursor: number
11
    records: number
12
    items: number
13
    inventories: Inventory[]
14
}
15

  
16
export type ItemPreview = {
17
    xml_id: string
18
    iconclass_external_id: string
19
    text: string
20
    title: string
21
    object: Item
22
    inventory: Inventory
23
    src?: ItemSource
24
}
25

  
26
export type Item = {
27
    caption: string
28
    type: string
29
    name?: PersonName[]
30
    images?: ItemImage[]
31
    origDate?: string
32

  
33
}
34

  
35
export type PersonName = {
36
    value?: string
37
    role: string
38
    getty_external_id: string
39
    getty_data: GettyData
40
}
41

  
42
export type GettyData = {
43
    display_name: string
44
}
45

  
46
export type ItemImage = {
47
    file: string
48
    text: string
49
}
50

  
51
export type ItemSource = {
52
    value: string
53
}
src/types/searchFormTypes.ts
1
export type Inventory = {
2
    count?: number
3
    name: string
4
    label: string
5
    order: number
6
}
7

  
8
export type Room = {
9
    id: number
10
    in_plan: boolean
11
    label: string
12
    places?: RoomPlace[]
13
}
14

  
15
export type RoomPlace = {
16
    id: number
17
    label: string
18
}
19

  
20
export type Artist = {
21
    display_name: string
22
    getty_id: string
23
}

Také k dispozici: Unified diff