Add Search Api/Components.

This commit is contained in:
Brian Cao 2023-08-10 13:26:33 -07:00
parent 45888fabe6
commit dcf8b2edaa
37 changed files with 1069 additions and 287 deletions

View file

@ -0,0 +1,37 @@
import styles from './Pager.module.css';
import { Button, Flexbox, Icon, Icons } from 'react-basics';
export function Pager({ page, pageSize, count, onPageChange, onPageSizeChange }) {
const maxPage = Math.ceil(count / pageSize);
const lastPage = page === maxPage;
const firstPage = page === 1;
if (count === 0) {
return null;
}
const handlePageChange = value => {
const nextPage = page + value;
if (nextPage > 0 && nextPage <= maxPage) {
onPageChange(nextPage);
}
};
return (
<Flexbox justifyContent="center" className={styles.container}>
<Button onClick={() => handlePageChange(-1)} disabled={firstPage}>
<Icon size="lg" className={styles.icon} rotate={90}>
<Icons.ChevronDown />
</Icon>
</Button>
<Flexbox alignItems="center" className={styles.text}>{`Page ${page} of ${maxPage}`}</Flexbox>
<Button onClick={() => handlePageChange(1)} disabled={lastPage}>
<Icon size="lg" className={styles.icon} rotate={270}>
<Icons.ChevronDown />
</Icon>
</Button>
</Flexbox>
);
}
export default Pager;

View file

@ -0,0 +1,7 @@
.container {
margin-top: 20px;
}
.text {
margin: 0 10px;
}

View file

@ -1,37 +1,98 @@
import { Table, TableHeader, TableBody, TableRow, TableCell, TableColumn } from 'react-basics';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import useMessages from 'hooks/useMessages';
import { useState } from 'react';
import {
SearchField,
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from 'react-basics';
import styles from './SettingsTable.module.css';
import Pager from 'components/common/Pager';
export function SettingsTable({
columns = [],
data,
children,
cellRender,
showSearch,
showPaging,
onFilterChange,
onPageChange,
onPageSizeChange,
filterValue,
}) {
const { formatMessage, messages } = useMessages();
const [filter, setFilter] = useState(filterValue);
const { data: value, page, count, pageSize } = data;
const handleFilterChange = value => {
setFilter(value);
onFilterChange(value);
};
export function SettingsTable({ columns = [], data = [], children, cellRender }) {
return (
<Table columns={columns} rows={data}>
<TableHeader className={styles.header}>
{(column, index) => {
return (
<TableColumn key={index} className={styles.cell} style={columns[index].style}>
{column.label}
</TableColumn>
);
}}
</TableHeader>
<TableBody className={styles.body}>
{(row, keys, rowIndex) => {
row.action = children(row, keys, rowIndex);
<>
{showSearch && (
<SearchField
onChange={handleFilterChange}
delay={1000}
value={filter}
placeholder="Search"
style={{ maxWidth: '300px', 'margin-bottom': '10px' }}
/>
)}
{value.length === 0 && filterValue && (
<EmptyPlaceholder message={formatMessage(messages.noResultsFound)}></EmptyPlaceholder>
)}
{value.length > 0 && (
<Table columns={columns} rows={value}>
<TableHeader className={styles.header}>
{(column, index) => {
return (
<TableColumn key={index} className={styles.cell} style={columns[index].style}>
{column.label}
</TableColumn>
);
}}
</TableHeader>
<TableBody className={styles.body}>
{(row, keys, rowIndex) => {
row.action = children(row, keys, rowIndex);
return (
<TableRow key={rowIndex} data={row} keys={keys} className={styles.row}>
{(data, key, colIndex) => {
return (
<TableCell key={colIndex} className={styles.cell} style={columns[colIndex].style}>
<label className={styles.label}>{columns[colIndex].label}</label>
{cellRender ? cellRender(row, data, key, colIndex) : data[key]}
</TableCell>
);
}}
</TableRow>
);
}}
</TableBody>
</Table>
return (
<TableRow key={rowIndex} data={row} keys={keys} className={styles.row}>
{(data, key, colIndex) => {
return (
<TableCell
key={colIndex}
className={styles.cell}
style={columns[colIndex].style}
>
<label className={styles.label}>{columns[colIndex].label}</label>
{cellRender ? cellRender(row, data, key, colIndex) : data[key]}
</TableCell>
);
}}
</TableRow>
);
}}
</TableBody>
{showPaging && (
<Pager
page={page}
pageSize={pageSize}
count={count}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
/>
)}
</Table>
)}
</>
);
}

View file

@ -8,12 +8,12 @@ export function WebsiteSelect({ websiteId, onSelect }) {
const { data } = useQuery(['websites:me'], () => get('/me/websites'));
const renderValue = value => {
return data?.find(({ id }) => id === value)?.name;
return data?.data?.find(({ id }) => id === value)?.name;
};
return (
<Dropdown
items={data}
items={data?.data}
value={websiteId}
renderValue={renderValue}
onChange={onSelect}

View file

@ -12,16 +12,17 @@ import useDashboard from 'store/dashboard';
import useMessages from 'hooks/useMessages';
import useLocale from 'hooks/useLocale';
export function Dashboard({ userId }) {
export function Dashboard() {
const { formatMessage, labels, messages } = useMessages();
const dashboard = useDashboard();
const { showCharts, limit, editing } = dashboard;
const [max, setMax] = useState(limit);
const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['websites'], () =>
get('/websites', { userId, includeTeams: 1 }),
get('/websites', { includeTeams: 1 }),
);
const hasData = data && data.length !== 0;
const hasData = data && data?.data.length !== 0;
const { dir } = useLocale();
function handleMore() {
@ -47,8 +48,10 @@ export function Dashboard({ userId }) {
)}
{hasData && (
<>
{editing && <DashboardEdit websites={data} />}
{!editing && <WebsiteChartList websites={data} showCharts={showCharts} limit={max} />}
{editing && <DashboardEdit websites={data?.data} />}
{!editing && (
<WebsiteChartList websites={data?.data} showCharts={showCharts} limit={max} />
)}
{max < data.length && (
<Flexbox justifyContent="center">
<Button onClick={handleMore}>

View file

@ -5,7 +5,14 @@ import SettingsTable from 'components/common/SettingsTable';
import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
import { useMessages } from 'hooks';
export function ReportsTable({ data = [], onDelete = () => {} }) {
export function ReportsTable({
data = [],
onDelete = () => {},
filterValue,
onFilterChange,
onPageChange,
onPageSizeChange,
}) {
const [report, setReport] = useState(null);
const { formatMessage, labels } = useMessages();
@ -22,7 +29,16 @@ export function ReportsTable({ data = [], onDelete = () => {} }) {
return (
<>
<SettingsTable columns={columns} data={data}>
<SettingsTable
columns={columns}
data={data}
showSearch={true}
showPaging={true}
onFilterChange={onFilterChange}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
filterValue={filterValue}
>
{row => {
const { id } = row;

View file

@ -12,6 +12,8 @@ export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
const [newWebsites, setNewWebsites] = useState([]);
const formRef = useRef();
const hasData = websites && websites.data.length > 0;
const handleSubmit = () => {
mutate(
{ websiteIds: newWebsites },
@ -42,20 +44,22 @@ export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
return (
<>
<Form onSubmit={handleSubmit} error={error} ref={formRef}>
<FormRow label={formatMessage(labels.websites)}>
<Dropdown items={websites} onChange={handleAddWebsite} style={{ width: 300 }}>
{({ id, name }) => <Item key={id}>{name}</Item>}
</Dropdown>
</FormRow>
<WebsiteTags items={websites} websites={newWebsites} onClick={handleRemoveWebsite} />
<FormButtons flex>
<SubmitButton disabled={newWebsites && newWebsites.length === 0}>
{formatMessage(labels.addWebsite)}
</SubmitButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
</FormButtons>
</Form>
{hasData && (
<Form onSubmit={handleSubmit} error={error} ref={formRef}>
<FormRow label={formatMessage(labels.websites)}>
<Dropdown items={websites.data} onChange={handleAddWebsite} style={{ width: 300 }}>
{({ id, name }) => <Item key={id}>{name}</Item>}
</Dropdown>
</FormRow>
<WebsiteTags items={websites.data} websites={newWebsites} onClick={handleRemoveWebsite} />
<FormButtons flex>
<SubmitButton disabled={newWebsites && newWebsites.length === 0}>
{formatMessage(labels.addWebsite)}
</SubmitButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
</FormButtons>
</Form>
)}
</>
);
}

View file

@ -2,13 +2,22 @@ import { Loading, useToasts } from 'react-basics';
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
import useApiFilter from 'hooks/useApiFilter';
export function TeamMembers({ teamId, readOnly }) {
const { showToast } = useToasts();
const { get, useQuery } = useApi();
const { formatMessage, messages } = useMessages();
const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () =>
get(`/teams/${teamId}/users`),
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const { get, useQuery } = useApi();
const { data, isLoading, refetch } = useQuery(
['teams:users', teamId, filter, page, pageSize],
() =>
get(`/teams/${teamId}/users`, {
filter,
page,
pageSize,
}),
);
if (isLoading) {
@ -22,7 +31,15 @@ export function TeamMembers({ teamId, readOnly }) {
return (
<>
<TeamMembersTable onSave={handleSave} data={data} readOnly={readOnly} />
<TeamMembersTable
onSave={handleSave}
data={data}
readOnly={readOnly}
onFilterChange={handleFilterChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
filterValue={filter}
/>
</>
);
}

View file

@ -4,7 +4,15 @@ import { ROLES } from 'lib/constants';
import TeamMemberRemoveButton from './TeamMemberRemoveButton';
import SettingsTable from 'components/common/SettingsTable';
export function TeamMembersTable({ data = [], onSave, readOnly }) {
export function TeamMembersTable({
data = [],
onSave,
readOnly,
filterValue,
onFilterChange,
onPageChange,
onPageSizeChange,
}) {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
@ -16,7 +24,7 @@ export function TeamMembersTable({ data = [], onSave, readOnly }) {
const cellRender = (row, data, key) => {
if (key === 'username') {
return row?.user?.username;
return row?.username;
}
if (key === 'role') {
return formatMessage(
@ -27,13 +35,23 @@ export function TeamMembersTable({ data = [], onSave, readOnly }) {
};
return (
<SettingsTable data={data} columns={columns} cellRender={cellRender}>
<SettingsTable
data={data}
columns={columns}
cellRender={cellRender}
showSearch={true}
showPaging={true}
onFilterChange={onFilterChange}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
filterValue={filterValue}
>
{row => {
return (
!readOnly && (
<TeamMemberRemoveButton
teamId={row.teamId}
userId={row.userId}
userId={row.id}
disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}
onSave={onSave}
/>

View file

@ -13,13 +13,22 @@ import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable
import TeamAddWebsiteForm from 'components/pages/settings/teams/TeamAddWebsiteForm';
import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
import useApiFilter from 'hooks/useApiFilter';
export function TeamWebsites({ teamId }) {
const { showToast } = useToasts();
const { formatMessage, labels, messages } = useMessages();
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const { get, useQuery } = useApi();
const { data, isLoading, refetch } = useQuery(['teams:websites', teamId], () =>
get(`/teams/${teamId}/websites`),
const { data, isLoading, refetch } = useQuery(
['teams:websites', teamId, filter, page, pageSize],
() =>
get(`/teams/${teamId}/websites`, {
filter,
page,
pageSize,
}),
);
const hasData = data && data.length !== 0;
@ -49,7 +58,17 @@ export function TeamWebsites({ teamId }) {
return (
<div>
<ActionForm description={formatMessage(messages.teamWebsitesInfo)}>{addButton}</ActionForm>
{hasData && <TeamWebsitesTable teamId={teamId} data={data} onSave={handleSave} />}
{hasData && (
<TeamWebsitesTable
teamId={teamId}
data={data}
onSave={handleSave}
onFilterChange={handleFilterChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
filterValue={filter}
/>
)}
</div>
);
}

View file

@ -6,9 +6,17 @@ import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton';
import SettingsTable from 'components/common/SettingsTable';
import useConfig from 'hooks/useConfig';
export function TeamWebsitesTable({ data = [], onSave }) {
export function TeamWebsitesTable({
data = [],
onSave,
filterValue,
onFilterChange,
onPageChange,
onPageSizeChange,
}) {
const { formatMessage, labels } = useMessages();
const { openExternal } = useConfig();
const { user } = useUser();
const columns = [
{ name: 'name', label: formatMessage(labels.name) },
@ -17,11 +25,19 @@ export function TeamWebsitesTable({ data = [], onSave }) {
];
return (
<SettingsTable columns={columns} data={data}>
<SettingsTable
columns={columns}
data={data}
showSearch={true}
showPaging={true}
onFilterChange={onFilterChange}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
filterValue={filterValue}
>
{row => {
const { teamId } = row;
const { id: websiteId, name, domain, userId } = row.website;
const { teamUser } = row.team;
const { id: teamId, teamUser } = row.teamWebsite[0].team;
const { id: websiteId, name, domain, userId } = row;
const owner = teamUser[0];
const canRemove = user.id === userId || user.id === owner.userId;

View file

@ -1,24 +1,37 @@
import { useState } from 'react';
import { Button, Icon, Modal, ModalTrigger, useToasts, Text, Flexbox } from 'react-basics';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import TeamAddForm from 'components/pages/settings/teams/TeamAddForm';
import PageHeader from 'components/layout/PageHeader';
import TeamsTable from 'components/pages/settings/teams/TeamsTable';
import Page from 'components/layout/Page';
import Icons from 'components/icons';
import TeamJoinForm from './TeamJoinForm';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import TeamAddForm from 'components/pages/settings/teams/TeamAddForm';
import TeamsTable from 'components/pages/settings/teams/TeamsTable';
import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
import { ROLES } from 'lib/constants';
import useUser from 'hooks/useUser';
import { ROLES } from 'lib/constants';
import { useState } from 'react';
import { Button, Flexbox, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
import TeamJoinForm from './TeamJoinForm';
import useApiFilter from 'hooks/useApiFilter';
export default function TeamsList() {
const { user } = useUser();
const { formatMessage, labels, messages } = useMessages();
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const [update, setUpdate] = useState(0);
const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['teams', update], () => get(`/teams`));
const hasData = data && data.length !== 0;
const { data, isLoading, error } = useQuery(['teams', update, filter, page, pageSize], () => {
return get(`/teams`, {
filter,
page,
pageSize,
});
});
const hasData = data && data?.data.length !== 0;
const isFiltered = filter;
const { showToast } = useToasts();
const handleSave = () => {
@ -71,15 +84,26 @@ export default function TeamsList() {
return (
<Page loading={isLoading} error={error}>
<PageHeader title={formatMessage(labels.teams)}>
{hasData && (
{(hasData || isFiltered) && (
<Flexbox gap={10}>
{joinButton}
{createButton}
</Flexbox>
)}
</PageHeader>
{hasData && <TeamsTable data={data} onDelete={handleDelete} />}
{!hasData && (
{(hasData || isFiltered) && (
<TeamsTable
data={data}
onDelete={handleDelete}
onFilterChange={handleFilterChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
filterValue={filter}
/>
)}
{!hasData && !isFiltered && (
<EmptyPlaceholder message={formatMessage(messages.noTeams)}>
<Flexbox gap={10}>
{joinButton}

View file

@ -1,14 +1,21 @@
import SettingsTable from 'components/common/SettingsTable';
import useLocale from 'hooks/useLocale';
import useMessages from 'hooks/useMessages';
import useUser from 'hooks/useUser';
import { ROLES } from 'lib/constants';
import Link from 'next/link';
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
import TeamDeleteForm from './TeamDeleteForm';
import TeamLeaveForm from './TeamLeaveForm';
import useMessages from 'hooks/useMessages';
import useUser from 'hooks/useUser';
import { ROLES } from 'lib/constants';
import SettingsTable from 'components/common/SettingsTable';
import useLocale from 'hooks/useLocale';
export function TeamsTable({ data = [], onDelete }) {
export function TeamsTable({
data = { data: [] },
onDelete,
filterValue,
onFilterChange,
onPageChange,
onPageSizeChange,
}) {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
const { dir } = useLocale();
@ -27,7 +34,17 @@ export function TeamsTable({ data = [], onDelete }) {
};
return (
<SettingsTable data={data} columns={columns} cellRender={cellRender}>
<SettingsTable
data={data}
columns={columns}
cellRender={cellRender}
showSearch={true}
showPaging={true}
onFilterChange={onFilterChange}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
filterValue={filterValue}
>
{row => {
const { id, teamUser } = row;
const owner = teamUser.find(({ role }) => role === ROLES.teamOwner);

View file

@ -7,14 +7,27 @@ import UserAddButton from './UserAddButton';
import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser';
import useMessages from 'hooks/useMessages';
import useApiFilter from 'hooks/useApiFilter';
export function UsersList() {
const { formatMessage, labels, messages } = useMessages();
const { user } = useUser();
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const { get, useQuery } = useApi();
const { data, isLoading, error, refetch } = useQuery(['user'], () => get(`/users`), {
enabled: !!user,
});
const { data, isLoading, error, refetch } = useQuery(
['user', filter, page, pageSize],
() =>
get(`/users`, {
filter,
page,
pageSize,
}),
{
enabled: !!user,
},
);
const { showToast } = useToasts();
const hasData = data && data.length !== 0;
@ -33,8 +46,17 @@ export function UsersList() {
<PageHeader title={formatMessage(labels.users)}>
<UserAddButton onSave={handleSave} />
</PageHeader>
{hasData && <UsersTable data={data} onDelete={handleDelete} />}
{!hasData && (
{(hasData || filter) && (
<UsersTable
data={data}
onDelete={handleDelete}
onFilterChange={handleFilterChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
filterValue={filter}
/>
)}
{!hasData && !filter && (
<EmptyPlaceholder message={formatMessage(messages.noUsers)}>
<UserAddButton onSave={handleSave} />
</EmptyPlaceholder>

View file

@ -8,7 +8,14 @@ import useMessages from 'hooks/useMessages';
import SettingsTable from 'components/common/SettingsTable';
import useLocale from 'hooks/useLocale';
export function UsersTable({ data = [], onDelete }) {
export function UsersTable({
data = { data: [] },
onDelete,
filterValue,
onFilterChange,
onPageChange,
onPageSizeChange,
}) {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
const { dateLocale } = useLocale();
@ -36,7 +43,17 @@ export function UsersTable({ data = [], onDelete }) {
};
return (
<SettingsTable data={data} columns={columns} cellRender={cellRender}>
<SettingsTable
data={data}
columns={columns}
cellRender={cellRender}
showSearch={true}
showPaging={true}
onFilterChange={onFilterChange}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
filterValue={filterValue}
>
{(row, keys, rowIndex) => {
return (
<>

View file

@ -8,14 +8,22 @@ import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser';
import useMessages from 'hooks/useMessages';
import { ROLES } from 'lib/constants';
import useApiFilter from 'hooks/useApiFilter';
export function WebsitesList() {
const { formatMessage, labels, messages } = useMessages();
const { user } = useUser();
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const { get, useQuery } = useApi();
const { data, isLoading, error, refetch } = useQuery(
['websites', user?.id],
() => get(`/users/${user?.id}/websites`),
['websites', user?.id, filter, page, pageSize],
() =>
get(`/users/${user?.id}/websites`, {
filter,
page,
pageSize,
}),
{ enabled: !!user },
);
const { showToast } = useToasts();
@ -47,7 +55,15 @@ export function WebsitesList() {
return (
<Page loading={isLoading} error={error}>
<PageHeader title={formatMessage(labels.websites)}>{addButton}</PageHeader>
{hasData && <WebsitesTable data={data} />}
{hasData && (
<WebsitesTable
data={data}
onFilterChange={handleFilterChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
filterValue={filter}
/>
)}
{!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
{addButton}

View file

@ -4,7 +4,13 @@ import SettingsTable from 'components/common/SettingsTable';
import useMessages from 'hooks/useMessages';
import useConfig from 'hooks/useConfig';
export function WebsitesTable({ data = [] }) {
export function WebsitesTable({
data = [],
filterValue,
onFilterChange,
onPageChange,
onPageSizeChange,
}) {
const { formatMessage, labels } = useMessages();
const { openExternal } = useConfig();
@ -15,7 +21,16 @@ export function WebsitesTable({ data = [] }) {
];
return (
<SettingsTable columns={columns} data={data}>
<SettingsTable
columns={columns}
data={data}
showSearch={true}
showPaging={true}
onFilterChange={onFilterChange}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
filterValue={filterValue}
>
{row => {
const { id } = row;

View file

@ -7,7 +7,16 @@ import WebsiteHeader from './WebsiteHeader';
export function WebsiteReportsPage({ websiteId }) {
const { formatMessage, labels } = useMessages();
const { reports, error, isLoading, deleteReport } = useReports(websiteId);
const {
reports,
error,
isLoading,
deleteReport,
filter,
handleFilterChange,
handlePageChange,
handlePageSizeChange,
} = useReports(websiteId);
const handleDelete = async id => {
await deleteReport(id);
@ -26,7 +35,14 @@ export function WebsiteReportsPage({ websiteId }) {
</Button>
</Link>
</Flexbox>
<ReportsTable data={reports} onDelete={handleDelete} />
<ReportsTable
data={reports}
onDelete={handleDelete}
onFilterChange={handleFilterChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
filterValue={filter}
/>
</Page>
);
}