How can I ensure the refresh of a sub component's props? - javascript

I have a problem with RTK queries : I have a first component that encapsulates a table. When a refresh occurs - for example when I delete some data - the parent component receives the data and passes it to the child through props but the Table is never updated. So the deleted results still appear on the table.
What is the proper "react/redux" way to generate a refresh of the table ?
Here's some code :
const Results = (props) => {
const router = useRouter()
const { data, error, isError, isLoading, isFetching } = useGetItemsQuery();
if(isLoading) {
return (
<>
<Spinner animation="border" size="sm" role="status" />{' '} Please wait while Loading...
</>
)
}
if(isError) {
return (
<>
<Alert key="warning" variant="warning" style={{marginLeft: "10px"}}>
Warning - There was an error with the request : {error.error}
</Alert>
</>
)
}
const sizePerPageList = [
{
text: '5',
value: 5,
},
];
if (data){
return(
<>
<Card>
<Card.Body>
Traffic results
<TableResults data={data} sizePerPageList={sizePerPageList} />
</Card.Body>
</Card>
</>
)
}
}
export default Results;
And for the second component with the table :
const TableResults = (props) => {
console.log('props - data ', props.data);
const data = React.useMemo(
() => props.data,
[]
)
const columns = React.useMemo(
() => [
{
Header: 'bla bla',
accessor: 'blabla',
}
],
[]
)
const IndeterminateCheckbox = React.forwardRef(
({ indeterminate, ...rest }, ref) => {
const defaultRef = React.useRef()
const resolvedRef = ref || defaultRef
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate
}, [resolvedRef, indeterminate])
return (
<>
<input type="checkbox" ref={resolvedRef} {...rest} />
</>
)
}
)
const {
getTableProps,
getTableBodyProps,
headerGroups,
page, // Instead of using 'rows', we'll use page,
// which has only the rows for the active page
// The rest of these things are super handy, too ;)
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
prepareRow,
state: { pageIndex, pageSize, selectedRowIds },
visibleColumns,
preGlobalFilteredRows,
setGlobalFilter,
selectedFlatRows
} = useTable({ columns, data }, useGlobalFilter, useFilters, useSortBy, usePagination, useRowSelect,
hooks => {
hooks.visibleColumns.push(columns => [
// Let's make a column for selection
{
id: 'selection',
// The header can use the table's getToggleAllRowsSelectedProps method
// to render a checkbox
Header: ({ getToggleAllPageRowsSelectedProps }) => (
<div>
<IndeterminateCheckbox {...getToggleAllPageRowsSelectedProps()} />
</div>
),
// The cell can use the individual row's getToggleRowSelectedProps method
// to the render a checkbox
Cell: ({ row }) => (
<div>
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
</div>
),
},
...columns,
])
} )
return (
<>
<DeleteItemButton items={selectedFlatRows} />
<BTable {...getTableProps()} striped bordered hover size="sm">
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th
{...column.getHeaderProps(column.getSortByToggleProps())}
>
{column.render('Header')}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map((row, i) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td
{...cell.getCellProps()}
style={{
// padding: '10px',
// border: 'solid 1px gray',
// background: 'papayawhip',
}}
>
{cell.render('Cell')}
</td>
)
})}
</tr>
)
})}
</tbody>
</BTable>
<div className="pagination">
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{'<<'}
</button>{' '}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'<'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'>'}
</button>{' '}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{'>>'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Go to page:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</>
)
}
export default TableResults;
The DeleteItemButton will generate a simple RTK query that is functioning very well, and that triggers a refresh of the data from the main component :
deleteItems: builder.mutation({
query(data) {
// send array
let sanitized;
sanitized = keywords.filter(item => item);
const data = {
items: sanitized
}
//console.log('data: ', data);
return {
url: `items`,
method: 'DELETE',
body: data
}
},
invalidatesTags: ['Items']
}),
The app is running under nextJS. Any help appreciated !
Thanks

What is the proper "react/redux" way to generate a refresh of the table?
The correct way to refresh your data is by using the providesTags and invalidatesTags features of RTK query, which you are already doing.
The reason that your table is not refreshing is because you explicitly told it to ignore all changes to props.data and to use the initial value for all subsequent renders. This is obviously not what you intended, but it's what you're doing here:
const data = React.useMemo(
() => props.data,
[]
)
A useMemo with an empty dependency array will only execute once. Generally speaking, all variables which you use inside the useMemo should be included as dependencies. So the dependencies array should be [props.data].
Of course this particular useMemo does not do anything and can be safely deleted. It would make sense to have a useMemo if you were doing some reformatting of the props.data variable and wanted to only execute the reformatting when the value of props.data changes.
const data = React.useMemo(
() => someExpensiveComputation(props.data),
[props.data]
);

Related

How to handle arrays in a Grommet DataTable?

I'm trying to use arrays in Grommet DataTable. My data looks like this :
{
customer: [
'BANANA',
'Banana',
'banana',
'republic of banana'
],
somethingelse: ['ABC','123','DEF']
}
In a regular Grommet Table , I'm able to use every cell by defining the first value from the array as title - for example customer[0] - and create an expandable arrow to show the rest of the data in 'customer' :
But I don't get how to do this on a cell basis for a Grommet DataTable ?
Here is the way I'm using it in the regular Grommet Table :
<TableCell scope="row" pad={{ left: '2px', righ: '3px' }}>
<TextInput name="tags" size="xsmall" />
</TableCell>
</TableRow>
{searchResults.length > 0 &&
searchResults.map((searchResult, index) => (
<TableRow key={index}>
<TableCell>
<Box direction="row">
<Text size="xsmall">{searchResult.customer[0]}</Text>
{searchResult.customer.length > 1 && (
<Button
plain
hoverIndicator={false}
icon={
isExpanded[index] ? (
<FormDown size="18px" />
) : (
<FormNext size="18px" />
)
}
onClick={() => toggleOpen(index)}
/>
)}
</Box>
<Box>
{isExpanded[index] && listElements(searchResult.customer)}
</Box>
</TableCell>
Here is my Form , using DataTable :
return (
<Form value={formData} onSubmit={onSubmit} onChange={onChange}>
...
<DataTable
fill
border={{ body: 'bottom' }}
paginate
columns={columns}
data={searchResults}
select={select}
onClickRow={(e) => console.log(e.datum)}
onSelect={() => {}}
step={8}
rowDetails={(row) => { // I'm able to use rowDetails to expand and display some data , but how can I use this to 1. Use the [0] element of the array as title and 2. apply to all cells in the row/table.
for (const cell in row) {
// if (cell.length > 1) {
// return listElements(cell);
// }
console.log(cell);
}
}}
...
/>
...
</Form>
);
I was able to achieve that by using the render function and passing a CellElement to it, in which I have created my rules :
const columns = [
{
property: 'customer',
header: <FormField label="Customer" name="customer" size="xsmall" />,
render: (datum) => <CellElement val={datum.customer} />,
},
CellElement.js
import { Box, Text, Button } from 'grommet';
import { FormNext, FormDown } from 'grommet-icons';
import React, { useState } from 'react';
const CellElement = ({ val }) => {
const title = Array.isArray(val) ? val[0] : val;
const [isExpanded, setIsExpanded] = useState({});
const toggleOpen = (category) => {
setIsExpanded({
...isExpanded,
[category]: !isExpanded[category],
});
};
const listElements = (arr) => {
return arr.slice(1).map((el, index) => (
<Text key={index} size="xsmall">
{el}
</Text>
));
};
return (
<Box>
<Box direction="row">
<Text size="xsmall">{title}</Text>
{Array.isArray(val) && val.length > 1 && (
<Button
plain
hoverIndicator={false}
icon={
isExpanded[title] ? (
<FormDown size="18px" />
) : (
<FormNext size="18px" />
)
}
onClick={() => toggleOpen(title)}
/>
)}
</Box>
<Box>{isExpanded[title] && listElements(val)}</Box>
</Box>
);
};
export default CellElement;

react functional component: After first value not able to continue type the value in input element

I'm trying to update value in react functional compoenent through input element but after the first value I'm unable to type
My Code:
import React from "react";
import "./App.css";
const { useState } = React;
function App() {
const [items, setItems] = useState([
{ value: "" },
{ value: "" },
{ value: "" },
]);
const [count, setCount] = useState(0);
const Item = React.memo(({ id, value, onChange }) => {
return (
<div className="item">Item
<input
onChange={(e) => onChange(id, e.target.value)}
value={value}
/>
</div>
);
});
return (
<div className="app">
<h1>Parent</h1>
<p style={{marginTop: '20px'}}>Holds state: {count}, Does't passes to it items</p>
<p style={{marginTop: '20px'}}>{JSON.stringify(items)}</p>
<button onClick={() => setCount((prev) => prev + 1)} style={{marginTop: '20px'}}>
Update Parent
</button>
<ul className="items" style={{marginTop: '20px'}}>
{items.map((item, index) => {
return (
<Item
key={index}
id={index}
value={item.value}
onChange={(id, value) =>
setItems(
items.map((item, index) => {
return index !== id
? item
: { value: value };
})
)
}
/>
);
})}
</ul>
</div>
);
}
export default App;
You should move the Item declaration outside the App component. Having a component declaration inside another one is almost always a bad idea. Explanation below.
import React, { useState } from "react";
const Item = React.memo(({ id, value, onChange }) => {
return (
<div className="item">
Item
<input onChange={(e) => onChange(id, e.target.value)} value={value} />
</div>
);
});
function App() {
const [items, setItems] = useState([
{ value: "" },
{ value: "" },
{ value: "" }
]);
const [count, setCount] = useState(0);
return (
<div className="app">
<h1>Parent</h1>
<p style={{ marginTop: "20px" }}>
Holds state: {count}, Does't passes to it items
</p>
<p style={{ marginTop: "20px" }}>{JSON.stringify(items)}</p>
<button
onClick={() => setCount((prev) => prev + 1)}
style={{ marginTop: "20px" }}
>
Update Parent
</button>
<ul className="items" style={{ marginTop: "20px" }}>
{items.map((item, index) => {
return (
<Item
key={index}
id={index}
value={item.value}
onChange={(id, value) =>
setItems(
items.map((item, index) => {
return index !== id ? item : { value: value };
})
)
}
/>
);
})}
</ul>
</div>
);
}
export default App;
When a component definition is inside another component, React will re-declare the inner component every time the parent re-renders. This means, that any state held by the inner component will be lost.
In your case, since every time there is an entirely new component, the input was not the same input as in the previous render. This means that the input that was in focus in the previous render is not present anymore, and the new input is not focused anymore.
You should also probably change
setItems(
items.map((item, index) => {
return index !== id ? item : { value: value };
})
)
to
prev.map((item, index) => {
return index !== id ? item : { value: value };
})
)
It's a good idea to use the function notation for set state when the new state depends on the old state value.

React Bootstrap Tabs - Keep Current Tab On Page Reload

I'm DELETING data from my database via an API call, then returning data from my database via another API call, but the 'Car' tab doesn't seem to update or refresh showing the new data.
I want to refresh the tab page via STATE or via a page reload and keep the user's selected tab using React Bootstrap, either:
reload just the TAB the user is in
RELOAD the whole page whilst keeping the TAB selected
React Bootstrap Tabs
<Tabs defaultActiveKey="profile" id="uncontrolled-tab-example" className="mb-3">
<Tab eventKey="bus" title="Bus">
<Bus />
</Tab>
<Tab eventKey="car" title="Car">
<Car />
</Tab>
<Tab eventKey="van" title="Van">
<Van />
</Tab>
</Tabs>;
myFile.js:
const Car = () => {
const [car, setCar] = useState([{ '': '' }]);
const [selectedCar, setSelectedCar] = useState('');
useEffect(() => {
async function fetchData() {
try {
const data = await getCompanyCar(car.id);
setCar(data)
console.log(data);
} catch (err) {}
}
fetchData();
}, []);
return (
<>
<CarModal car={selectedCar} onCancel={() => setSelectedCar(null)} />
<Table className="p-4 rounded-3" striped>
<thead className="bold fs-5" style={{ color: 'rgb(80, 80, 80)' }}>
<tr style={{ display: 'flex', justifyContent: 'space-between' }}>
<span className="text-nowrap" style={{ width: '0px' }}>
Car
</span>
<button className="addButton" onClick={null}>
Add
</button>
</tr>
</thead>
<tbody>
{/* TODO:PAGINATE ROWS */}
{car.map((car) => (
<React.Fragment key={car.client_id}>
<CarRow car={car} setSelectedCar={setSelectedCar} />
</React.Fragment>
))}
</tbody>
</Table>
</>
);
};
function CarFunction({ car, setSelectedCar }) {
const [showConfirmResetModal, setShowConfirmResetModal] = useState(false);
const handleDelete = async (event) => {
try {
const data = await deleteCompanyCar(Car.id);
if (data[0].completed === 'deleted') {
getCompanyCar(Car);
window.location.reload();
}
} catch (err) {
console.log(err);
alert('Error, unable to delete');
}
};
return (
<ConfirmationModal
header="Confirm Deletion"
show={showConfirmResetModal}
onHide={() => setShowConfirmResetModal(false)}
onConfirm={handleDelete}
annimation
>
Are you sure you want to delete this car?
</ConfirmationModal>
);
}
export default CarFunction;
Thanks in advance!

fetch data and display on popup component

I have an array of objects file and displaying data on frontend side & I applied on a onClick function on that data which I am fetching, but the issue is when click on that button I get last array of object value but I want that specific data,
data file -
const Data = [
{
id: 1,
HEXCode: "#FF6263",
RGBACode: "rgba(255, 98, 99, 1)",
},
........
]
my code -
const HashCode = () => {
const [redvalue, setRedValue] = useState(Data);
const [ButtonPopup, setButtonPopup] = useState(false);
return (
<Wrap>
<Home
code={"Hash Code"}
link={"/"}
link2={"/RGBCode"}
link3={"/Gradients"}
link4={"/TwoColorCombination"}
/>
<Content>
{redvalue.map((element, index) => {
return (
<div key={index}>
<Button
type="button"
style={{ background: `${element.HEXCode}` }}
onClick={() => setButtonPopup(true)}
>
{element.HEXCode}
</Button>
<Popup trigger={ButtonPopup} setTrigger={setButtonPopup}>
<CopyToClipboard text={element.HEXCode}>
<button type="btn">{element.HEXCode}</button>
</CopyToClipboard>
<CopyToClipboard text={element.RGBACode}>
<button type="btn">{element.RGBACode}</button>
</CopyToClipboard>
</Popup>
</div>
);
})}
</Content>
</Wrap>
);
};
export default HashCode;
Popup code -
const Popup = (props) => {
return props.trigger ? (
<Wrap className="popup">
<div className="popup_inner">
<button className="close_btn" onClick={() => props.setTrigger(false)}>
close
</button>
{props.children}
</div>
</Wrap>
) : (
""
);
};
export default Popup;
You should use index in state ButtonPopup instead boolean to check condition render of popup.
<Button
type="button"
style={{ background: `${element.HEXCode}` }}
onClick={() => setButtonPopup(index)}
>
{element.HEXCode}
</Button>
<Popup trigger={ButtonPopup === index} setTrigger={setButtonPopup}>

reactjs - React Table Pagination appears to be broken

I am new to using React JS. I have been using react-table to create a component that can filter, sort and paginate some sample data from a JSON file.
Here is the link to the tutorial I have been following:
https://www.freakyjolly.com/react-table-tutorial/#.YBfqqZP7SL4
Here is what I am seeing at the moment, the pagination appears to be broken, I am seeing all of the data appear (1000 rows). I am trying to have around 5-10 records showing at a time.
Here is the App.js code.
import React from 'react';
import logo from './logo.svg';
import './App.css';
import FilterTableComponent from './components/filter.pagination.sorting';
function App() {
return (
<div className="App">
<h3>Filter Table using <code>react-table</code></h3>
<FilterTableComponent />
</div>
);
}
export default App;
Here is the filter.paginate.sorting.js code
import React from "react";
import { useTable, useSortBy, usePagination, useFilters, useGlobalFilter, useAsyncDebounce } from 'react-table';
import 'bootstrap/dist/css/bootstrap.min.css';
import JSONDATA from './MOCK_DATA.json';
// Define a default UI for filtering
function GlobalFilter({
preGlobalFilteredRows,
globalFilter,
setGlobalFilter,
}) {
const count = preGlobalFilteredRows.length
const [value, setValue] = React.useState(globalFilter)
const onChange = useAsyncDebounce(value => {
setGlobalFilter(value || undefined)
}, 200)
return (
<span>
Search:{' '}
<input
className="form-control"
value={value || ""}
onChange={e => {
setValue(e.target.value);
onChange(e.target.value);
}}
placeholder={`${count} records...`}
/>
</span>
)
}
function DefaultColumnFilter({
column: { filterValue, preFilteredRows, setFilter },
}) {
const count = preFilteredRows.length
return (
<input
className="form-control"
value={filterValue || ''}
onChange={e => {
setFilter(e.target.value || undefined)
}}
placeholder={`Search ${count} records...`}
/>
)
}
function Table({ columns, data }) {
const defaultColumn = React.useMemo(
() => ({
// Default Filter UI
Filter: DefaultColumnFilter,
}),
[]
)
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
rows,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state,
state: { pageIndex, pageSize },
preGlobalFilteredRows,
setGlobalFilter,
} = useTable(
{
columns,
data,
defaultColumn,
initialState: { pageIndex: 0, pageSize: 10 }
},
useFilters,
useGlobalFilter,
useSortBy,
usePagination
)
return (
<div>
<ul className="pagination">
<li className="page-item" onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
<a className="page-link">First</a>
</li>
<li className="page-item" onClick={() => previousPage()} disabled={!canPreviousPage}>
<a className="page-link">{'<'}</a>
</li>
<li className="page-item" onClick={() => nextPage()} disabled={!canNextPage}>
<a className="page-link">{'>'}</a>
</li>
<li className="page-item" onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
<a className="page-link">Last</a>
</li>
<li>
<a className="page-link">
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</a>
</li>
<li>
<a className="page-link">
<input
className="form-control"
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px', height: '20px' }}
/>
</a>
</li>{' '}
<select
className="form-control"
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
style={{ width: '120px', height: '38px' }}
>
{[5, 10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</ul>
<GlobalFilter
preGlobalFilteredRows={preGlobalFilteredRows}
globalFilter={state.globalFilter}
setGlobalFilter={setGlobalFilter}
/>
<pre>
<code>
{JSON.stringify(
{
pageIndex,
pageSize,
pageCount,
canNextPage,
canPreviousPage,
},
null,
2
)}
</code>
</pre>
<table className="table" {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
{/* Render the columns filter UI */}
<div>{column.canFilter ? column.render('Filter') : null}</div>
<span>
{column.isSorted
? column.isSortedDesc
? ' '
: ' '
: ''}
</span>
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row, i) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
<br />
<div>Showing the first 20 results of {rows.length} rows</div>
<div>
<pre>
<code>{JSON.stringify(state.filters, null, 2)}</code>
</pre>
</div>
</div>
)
}
function FilterTableComponent() {
const columns = React.useMemo(
() => [
{
Header: 'Name',
columns: [
{
Header: 'WIP_ID',
accessor: 'WIP_ID',
},
{
Header: 'PRODUCT_SERIAL_NUMBER',
accessor: 'PRODUCT_SERIAL_NUMBER'
},
],
},
{
Header: 'Info',
columns: [
{
Header: 'DATE',
accessor: 'DATE'
},
{
Header: 'BUCKET_NAME',
accessor: 'BUCKET_NAME'
},
{
Header: 'CREATED_TS',
accessor: 'CREATED_TS'
},
{
Header: 'FILE_NAME',
accessor: 'FILE_NAME'
},
],
},
],
[]
)
const data = JSONDATA
return (
<Table columns={columns} data={data} />
)
}
export default FilterTableComponent;
Here is a sample of what the JSON file looks like. Named - MOCK_DATA.json
[{"WIP_ID":"56c97f3e-1c3f-4463-beb2-1af58ebe0db0","PRODUCT_SERIAL_NUMBER":"eab8304c-43e2-4f70-a23a-2db75bf2ce50","DATE":"29/06/2020","BUCKET_NAME":"Bytecard","CREATED_TS":"03/10/2020","FILE_NAME":"elit_proin.tiff"},
{"WIP_ID":"b358a03b-fee6-4957-9017-1de3d9846264","PRODUCT_SERIAL_NUMBER":"b974e89e-9bf9-4329-bc13-1afc3bdd52e0","DATE":"20/12/2020","BUCKET_NAME":"Vagram","CREATED_TS":"25/11/2020","FILE_NAME":"condimentum_id_luctus.mov"},
{"WIP_ID":"9fab6d70-72bc-40ae-a99f-5ae31dc47aec","PRODUCT_SERIAL_NUMBER":"8f9f70ce-b940-486b-a003-5c9db031e54e","DATE":"15/02/2020","BUCKET_NAME":"Domainer","CREATED_TS":"06/05/2020","FILE_NAME":"interdum_in_ante.tiff"},
{"WIP_ID":"5bb40cfb-99dc-413a-8b5f-0b6612e45d34","PRODUCT_SERIAL_NUMBER":"3bab0d2b-5464-4c5d-b1c8-0ba6b32e60fa","DATE":"14/03/2020","BUCKET_NAME":"Lotlux","CREATED_TS":"11/05/2020","FILE_NAME":"diam_nam_tristique.avi"},
{"WIP_ID":"95ae9754-e288-4ceb-b7cf-1b27892e7ace","PRODUCT_SERIAL_NUMBER":"1280dfb1-152d-44fd-aed7-4de8b8b573a6","DATE":"04/04/2020","BUCKET_NAME":"Gembucket","CREATED_TS":"14/10/2020","FILE_NAME":"vehicula.pdf"},
I think you need to change this in your tbody
{rows.map((row,i) =>
to
{page.map((row,i) =>

Categories