@canva/cli 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +2 -0
- package/cli.js +593 -580
- package/package.json +7 -2
- package/templates/base/package.json +6 -6
- package/templates/common/jest.config.mjs +1 -1
- package/templates/dam/backend/server.ts +8 -0
- package/templates/dam/canva-app.json +4 -0
- package/templates/dam/package.json +6 -6
- package/templates/data_connector/README.md +1 -1
- package/templates/data_connector/package.json +6 -6
- package/templates/gen_ai/backend/server.ts +17 -0
- package/templates/gen_ai/package.json +6 -6
- package/templates/gen_ai/src/api/api.ts +4 -0
- package/templates/hello_world/package.json +6 -6
- package/templates/mls/README.md +81 -0
- package/templates/mls/canva-app.json +25 -0
- package/templates/mls/declarations/declarations.d.ts +29 -0
- package/templates/mls/eslint.config.mjs +14 -0
- package/templates/mls/jest.config.mjs +36 -0
- package/templates/mls/jest.setup.ts +37 -0
- package/templates/mls/package.json +117 -0
- package/templates/mls/scripts/copy_env.ts +13 -0
- package/templates/mls/scripts/ssl/ssl.ts +131 -0
- package/templates/mls/scripts/start/app_runner.ts +223 -0
- package/templates/mls/scripts/start/context.ts +171 -0
- package/templates/mls/scripts/start/start.ts +46 -0
- package/templates/mls/src/__tests__/app.tests.tsx +11 -0
- package/templates/mls/src/__tests__/office_selection_page.tests.tsx +72 -0
- package/templates/mls/src/__tests__/utils.tsx +19 -0
- package/templates/mls/src/adapter.ts +126 -0
- package/templates/mls/src/components/agent/agent_card.tsx +57 -0
- package/templates/mls/src/components/agent/agent_grid.tsx +37 -0
- package/templates/mls/src/components/agent/agent_list.tsx +17 -0
- package/templates/mls/src/components/agent/agent_search_filters.tsx +88 -0
- package/templates/mls/src/components/breadcrumb/breadcrumb.tsx +40 -0
- package/templates/mls/src/components/listing/listing_card.tsx +64 -0
- package/templates/mls/src/components/listing/listing_grid.tsx +37 -0
- package/templates/mls/src/components/listing/listing_list.tsx +21 -0
- package/templates/mls/src/components/listing/listing_search_filters.tsx +145 -0
- package/templates/mls/src/components/placeholders/placeholders.tsx +65 -0
- package/templates/mls/src/data.ts +359 -0
- package/templates/mls/src/index.tsx +4 -0
- package/templates/mls/src/intents/design_editor/app.tsx +44 -0
- package/templates/mls/src/intents/design_editor/index.tsx +25 -0
- package/templates/mls/src/pages/agent_details_page/agent_details_page.tsx +175 -0
- package/templates/mls/src/pages/list_page/agent_tab_panel.tsx +126 -0
- package/templates/mls/src/pages/list_page/list_page.tsx +67 -0
- package/templates/mls/src/pages/list_page/listing_tab_panel.tsx +135 -0
- package/templates/mls/src/pages/listing_details_page/listing_details_page.tsx +418 -0
- package/templates/mls/src/pages/loading_page/loading_page.tsx +152 -0
- package/templates/mls/src/pages/office_selection_page/office_selection_page.tsx +144 -0
- package/templates/mls/src/real_estate.type.ts +44 -0
- package/templates/mls/src/util/use_add_element.tsx +62 -0
- package/templates/mls/src/util/use_drag_element.tsx +68 -0
- package/templates/mls/styles/components.css +56 -0
- package/templates/mls/tsconfig.json +55 -0
- package/templates/mls/webpack.config.ts +254 -0
- package/templates/base/backend/routers/oauth.ts +0 -393
- package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +0 -99
- package/templates/base/utils/backend/bearer_middleware/index.ts +0 -1
- package/templates/base/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +0 -192
- package/templates/common/utils/backend/base_backend/create.ts +0 -104
- package/templates/gen_ai/backend/database/database.ts +0 -42
- package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +0 -99
- package/templates/gen_ai/utils/backend/bearer_middleware/index.ts +0 -1
- /package/templates/base/{utils/backend → backend}/base_backend/create.ts +0 -0
- /package/templates/base/{utils/backend → backend}/jwt_middleware/index.ts +0 -0
- /package/templates/base/{utils/backend → backend}/jwt_middleware/jwt_middleware.ts +0 -0
- /package/templates/{common → gen_ai}/utils/backend/jwt_middleware/index.ts +0 -0
- /package/templates/{common → gen_ai}/utils/backend/jwt_middleware/jwt_middleware.ts +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { agents, listings } from "./data";
|
|
2
|
+
import type { Agent, Office, Property } from "./real_estate.type";
|
|
3
|
+
|
|
4
|
+
// TODO (App Developer): replace with the real API call
|
|
5
|
+
export const fetchAgents = async (
|
|
6
|
+
office?: Office | null,
|
|
7
|
+
query?: string,
|
|
8
|
+
continuation?: string,
|
|
9
|
+
sortBy?: string,
|
|
10
|
+
): Promise<{ agents: Agent[]; continuation?: string }> => {
|
|
11
|
+
// eslint-disable-next-line no-console
|
|
12
|
+
console.log("fetchingAgents", { query, continuation, office });
|
|
13
|
+
|
|
14
|
+
if (office?.id === "listings-only-office" || office?.id === "empty-office") {
|
|
15
|
+
return { agents: [], continuation: undefined };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (office?.id === "error-office" || query === "error") {
|
|
19
|
+
throw new Error("Error fetching agents");
|
|
20
|
+
}
|
|
21
|
+
// Apply sorting
|
|
22
|
+
if (sortBy) {
|
|
23
|
+
const [field, direction] = sortBy.split("-");
|
|
24
|
+
|
|
25
|
+
if (field === "name") {
|
|
26
|
+
agents.sort((a, b) => {
|
|
27
|
+
const comparison = a.name.localeCompare(b.name);
|
|
28
|
+
return direction === "asc" ? comparison : -comparison;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Simulate a delay
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
35
|
+
|
|
36
|
+
let filteredAgents = agents;
|
|
37
|
+
|
|
38
|
+
// Apply search query
|
|
39
|
+
if (query) {
|
|
40
|
+
filteredAgents = filteredAgents.filter(
|
|
41
|
+
(agent) =>
|
|
42
|
+
agent.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
43
|
+
(agent.officeId &&
|
|
44
|
+
agent.officeId.toLowerCase().includes(query.toLowerCase())),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (filteredAgents.length === 0) {
|
|
49
|
+
return { agents: [], continuation: undefined };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
agents: filteredAgents,
|
|
54
|
+
continuation:
|
|
55
|
+
continuation && !query ? `${Number(continuation) + 1}` : undefined,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// TODO (App Developer): replace with real API calls
|
|
60
|
+
export const fetchListings = async (
|
|
61
|
+
office?: Office | null,
|
|
62
|
+
query?: string,
|
|
63
|
+
propertyType?: string | null,
|
|
64
|
+
sortBy?: string | null,
|
|
65
|
+
continuation?: string,
|
|
66
|
+
): Promise<{ listings: Property[]; continuation?: string }> => {
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.log("fetchingListings", {
|
|
69
|
+
query,
|
|
70
|
+
continuation,
|
|
71
|
+
propertyType,
|
|
72
|
+
sortBy,
|
|
73
|
+
});
|
|
74
|
+
// Simulate a delay
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
76
|
+
|
|
77
|
+
if (office?.id === "empty-office" || office?.id === "agents-only-office") {
|
|
78
|
+
return { listings: [], continuation: undefined };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (office?.id === "error-office" || query === "error") {
|
|
82
|
+
throw new Error("Error fetching listings");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let filteredListings = listings;
|
|
86
|
+
|
|
87
|
+
// Example filters
|
|
88
|
+
// Apply property type filter
|
|
89
|
+
if (propertyType) {
|
|
90
|
+
filteredListings = filteredListings.filter(
|
|
91
|
+
(listing) =>
|
|
92
|
+
listing.listingType.toLowerCase() === propertyType.toLowerCase(),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Apply sorting filter
|
|
97
|
+
if (sortBy) {
|
|
98
|
+
filteredListings = [...filteredListings].sort((a, b) => {
|
|
99
|
+
const aPrice = parseFloat(a.price.replace(/[$,]/g, ""));
|
|
100
|
+
const bPrice = parseFloat(b.price.replace(/[$,]/g, ""));
|
|
101
|
+
return sortBy === "price-asc" ? aPrice - bPrice : bPrice - aPrice;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Apply search query
|
|
106
|
+
if (query) {
|
|
107
|
+
filteredListings = filteredListings.filter(
|
|
108
|
+
(listing) =>
|
|
109
|
+
listing.address.toLowerCase().includes(query.toLowerCase()) ||
|
|
110
|
+
listing.suburb.toLowerCase().includes(query.toLowerCase()) ||
|
|
111
|
+
listing.description.toLowerCase().includes(query.toLowerCase()) ||
|
|
112
|
+
listing.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
113
|
+
listing.title.toLowerCase().includes(query.toLowerCase()),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (filteredListings.length === 0) {
|
|
118
|
+
return { listings: [], continuation: undefined };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
listings: filteredListings,
|
|
123
|
+
continuation:
|
|
124
|
+
continuation && !query ? `${Number(continuation) + 1}` : undefined,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { HorizontalCard, ImageCard } from "@canva/app-ui-kit";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { useIntl } from "react-intl";
|
|
4
|
+
import type { Agent } from "../../real_estate.type";
|
|
5
|
+
|
|
6
|
+
interface AgentCardProps {
|
|
7
|
+
item: Agent;
|
|
8
|
+
onClick: (item: Agent) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ListAgentCard: React.FC<AgentCardProps> = ({ item, onClick }) => {
|
|
12
|
+
const intl = useIntl();
|
|
13
|
+
const headshot = item.headshots?.[0];
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<HorizontalCard
|
|
17
|
+
ariaLabel={item.name}
|
|
18
|
+
title={item.name}
|
|
19
|
+
description={item.officeId}
|
|
20
|
+
onClick={() => onClick(item)}
|
|
21
|
+
thumbnail={
|
|
22
|
+
headshot
|
|
23
|
+
? {
|
|
24
|
+
url: headshot.url,
|
|
25
|
+
alt: intl.formatMessage(
|
|
26
|
+
{
|
|
27
|
+
defaultMessage: "Profile photo of {name}",
|
|
28
|
+
description: "Alt text for agent profile photo",
|
|
29
|
+
},
|
|
30
|
+
{ name: item.name },
|
|
31
|
+
),
|
|
32
|
+
}
|
|
33
|
+
: undefined
|
|
34
|
+
}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const GridAgentCard: React.FC<AgentCardProps> = ({ item, onClick }) => {
|
|
40
|
+
const intl = useIntl();
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<ImageCard
|
|
44
|
+
selectable
|
|
45
|
+
thumbnailUrl={item.headshots?.[0]?.url || ""}
|
|
46
|
+
alt={intl.formatMessage(
|
|
47
|
+
{
|
|
48
|
+
defaultMessage: "Profile photo of {name}",
|
|
49
|
+
description: "Alt text for agent profile photo",
|
|
50
|
+
},
|
|
51
|
+
{ name: item.name },
|
|
52
|
+
)}
|
|
53
|
+
thumbnailHeight={150}
|
|
54
|
+
onClick={() => onClick(item)}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Column, Columns, Rows } from "@canva/app-ui-kit";
|
|
2
|
+
import type { Agent } from "../../real_estate.type";
|
|
3
|
+
import { GridAgentCard } from "./agent_card";
|
|
4
|
+
|
|
5
|
+
interface AgentGridProps {
|
|
6
|
+
agents: Agent[];
|
|
7
|
+
onAgentClick: (item: Agent) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const AgentGrid = ({ agents, onAgentClick }: AgentGridProps) => {
|
|
11
|
+
return (
|
|
12
|
+
<Rows spacing="2u">
|
|
13
|
+
{agents.map((item, index) => {
|
|
14
|
+
if (index % 2 === 0) {
|
|
15
|
+
const nextItem = agents[index + 1];
|
|
16
|
+
return (
|
|
17
|
+
<Columns
|
|
18
|
+
key={`row-${Math.floor(index / 2)}`}
|
|
19
|
+
spacing="2u"
|
|
20
|
+
alignY="stretch"
|
|
21
|
+
>
|
|
22
|
+
<Column key={item.id} width="1/2">
|
|
23
|
+
<GridAgentCard item={item} onClick={onAgentClick} />
|
|
24
|
+
</Column>
|
|
25
|
+
{nextItem && (
|
|
26
|
+
<Column key={nextItem.id} width="1/2">
|
|
27
|
+
<GridAgentCard item={nextItem} onClick={onAgentClick} />
|
|
28
|
+
</Column>
|
|
29
|
+
)}
|
|
30
|
+
</Columns>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
})}
|
|
35
|
+
</Rows>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Agent } from "../../real_estate.type";
|
|
2
|
+
import { ListAgentCard } from "./agent_card";
|
|
3
|
+
|
|
4
|
+
interface AgentListProps {
|
|
5
|
+
agents: Agent[];
|
|
6
|
+
onAgentClick: (item: Agent) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const AgentList = ({ agents, onAgentClick }: AgentListProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
{agents.map((item: Agent) => (
|
|
13
|
+
<ListAgentCard key={item.id} item={item} onClick={onAgentClick} />
|
|
14
|
+
))}
|
|
15
|
+
</>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Column,
|
|
4
|
+
Columns,
|
|
5
|
+
GridViewIcon,
|
|
6
|
+
ListBulletLtrIcon,
|
|
7
|
+
Rows,
|
|
8
|
+
SearchInputMenu,
|
|
9
|
+
Select,
|
|
10
|
+
} from "@canva/app-ui-kit";
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { useIntl } from "react-intl";
|
|
13
|
+
|
|
14
|
+
type Layout = "grid" | "list";
|
|
15
|
+
|
|
16
|
+
interface AgentSearchFiltersProps {
|
|
17
|
+
query: string;
|
|
18
|
+
onQueryChange: (query: string) => void;
|
|
19
|
+
layout: Layout;
|
|
20
|
+
onLayoutToggle: () => void;
|
|
21
|
+
sort: string;
|
|
22
|
+
onSortChange: (sort: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const AgentSearchFilters = ({
|
|
26
|
+
query,
|
|
27
|
+
onQueryChange,
|
|
28
|
+
layout,
|
|
29
|
+
onLayoutToggle,
|
|
30
|
+
sort,
|
|
31
|
+
onSortChange,
|
|
32
|
+
}: AgentSearchFiltersProps) => {
|
|
33
|
+
const intl = useIntl();
|
|
34
|
+
const [queryValue, setQueryValue] = useState(query);
|
|
35
|
+
return (
|
|
36
|
+
<Rows spacing="2u">
|
|
37
|
+
<SearchInputMenu
|
|
38
|
+
value={queryValue}
|
|
39
|
+
onChange={setQueryValue}
|
|
40
|
+
onChangeComplete={() => onQueryChange(queryValue)}
|
|
41
|
+
onClear={() => {
|
|
42
|
+
setQueryValue("");
|
|
43
|
+
onQueryChange("");
|
|
44
|
+
}}
|
|
45
|
+
placeholder={intl.formatMessage({
|
|
46
|
+
defaultMessage: "Search agents...",
|
|
47
|
+
description: "Placeholder text for agents search input",
|
|
48
|
+
})}
|
|
49
|
+
/>
|
|
50
|
+
<Columns spacing="1u" alignY="center">
|
|
51
|
+
<Column width="content">
|
|
52
|
+
<Button
|
|
53
|
+
icon={layout === "grid" ? GridViewIcon : ListBulletLtrIcon}
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={onLayoutToggle}
|
|
56
|
+
variant="secondary"
|
|
57
|
+
/>
|
|
58
|
+
</Column>
|
|
59
|
+
<Column width="1/3">
|
|
60
|
+
<Select
|
|
61
|
+
value={sort}
|
|
62
|
+
onChange={onSortChange}
|
|
63
|
+
options={[
|
|
64
|
+
{
|
|
65
|
+
label: intl.formatMessage({
|
|
66
|
+
defaultMessage: "Name: A to Z",
|
|
67
|
+
description: "Sort option - name ascending",
|
|
68
|
+
}),
|
|
69
|
+
value: "name-asc",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: intl.formatMessage({
|
|
73
|
+
defaultMessage: "Name: Z to A",
|
|
74
|
+
description: "Sort option - name descending",
|
|
75
|
+
}),
|
|
76
|
+
value: "name-desc",
|
|
77
|
+
},
|
|
78
|
+
]}
|
|
79
|
+
placeholder={intl.formatMessage({
|
|
80
|
+
defaultMessage: "Sort by",
|
|
81
|
+
description: "Placeholder text for agents sort dropdown",
|
|
82
|
+
})}
|
|
83
|
+
/>
|
|
84
|
+
</Column>
|
|
85
|
+
</Columns>
|
|
86
|
+
</Rows>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowLeftIcon,
|
|
3
|
+
Button,
|
|
4
|
+
Column,
|
|
5
|
+
Columns,
|
|
6
|
+
Text,
|
|
7
|
+
} from "@canva/app-ui-kit";
|
|
8
|
+
import { useIntl } from "react-intl";
|
|
9
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
10
|
+
import type { Office } from "../../real_estate.type";
|
|
11
|
+
|
|
12
|
+
export const Breadcrumb = () => {
|
|
13
|
+
const office = (useLocation().state as { office: Office })?.office;
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
const intl = useIntl();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Columns spacing="1u" alignY="center">
|
|
19
|
+
<Column width="content">
|
|
20
|
+
<Button
|
|
21
|
+
icon={ArrowLeftIcon}
|
|
22
|
+
size="small"
|
|
23
|
+
type="button"
|
|
24
|
+
variant="tertiary"
|
|
25
|
+
onClick={() => navigate("/entry")}
|
|
26
|
+
/>
|
|
27
|
+
</Column>
|
|
28
|
+
<Column>
|
|
29
|
+
<Text variant="bold">
|
|
30
|
+
{office
|
|
31
|
+
? office.name
|
|
32
|
+
: intl.formatMessage({
|
|
33
|
+
defaultMessage: "Back",
|
|
34
|
+
description: "Back button",
|
|
35
|
+
})}
|
|
36
|
+
</Text>
|
|
37
|
+
</Column>
|
|
38
|
+
</Columns>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { HorizontalCard, ImageCard, Rows, Text } from "@canva/app-ui-kit";
|
|
2
|
+
import type { Property } from "../../real_estate.type";
|
|
3
|
+
|
|
4
|
+
export interface ListingCardProps<T extends Property> {
|
|
5
|
+
item: T;
|
|
6
|
+
onClick?: (item: T) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface BaseListingCardProps<T extends Property> {
|
|
10
|
+
item: T;
|
|
11
|
+
onClick?: (item: T) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const GridListingCard = <T extends Property>({
|
|
15
|
+
item,
|
|
16
|
+
onClick,
|
|
17
|
+
}: BaseListingCardProps<T>) => {
|
|
18
|
+
const handleClick = () => {
|
|
19
|
+
onClick?.(item);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div>
|
|
24
|
+
<Rows spacing="1u">
|
|
25
|
+
<ImageCard
|
|
26
|
+
selectable
|
|
27
|
+
thumbnailUrl={item.thumbnail.url}
|
|
28
|
+
alt={item.description}
|
|
29
|
+
borderRadius="standard"
|
|
30
|
+
onClick={handleClick}
|
|
31
|
+
thumbnailHeight={110}
|
|
32
|
+
/>
|
|
33
|
+
<Rows spacing="0">
|
|
34
|
+
<Text variant="bold" lineClamp={1}>
|
|
35
|
+
{item.title}
|
|
36
|
+
</Text>
|
|
37
|
+
{item.suburb && <Text size="small">{item.suburb}</Text>}
|
|
38
|
+
</Rows>
|
|
39
|
+
</Rows>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const ListListingCard = <T extends Property>({
|
|
45
|
+
item,
|
|
46
|
+
onClick,
|
|
47
|
+
}: BaseListingCardProps<T>) => {
|
|
48
|
+
const handleClick = () => {
|
|
49
|
+
onClick?.(item);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<HorizontalCard
|
|
54
|
+
ariaLabel={item.title}
|
|
55
|
+
title={item.title}
|
|
56
|
+
description={item.suburb}
|
|
57
|
+
onClick={handleClick}
|
|
58
|
+
thumbnail={{
|
|
59
|
+
url: item.thumbnail.url,
|
|
60
|
+
alt: item.description,
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Column, Columns, Rows } from "@canva/app-ui-kit";
|
|
2
|
+
import type { Property } from "../../real_estate.type";
|
|
3
|
+
import { GridListingCard } from "./listing_card";
|
|
4
|
+
|
|
5
|
+
interface ListingGridProps {
|
|
6
|
+
listings: Property[];
|
|
7
|
+
onListingClick: (item: Property) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ListingGrid = ({ listings, onListingClick }: ListingGridProps) => {
|
|
11
|
+
return (
|
|
12
|
+
<Rows spacing="2u">
|
|
13
|
+
{listings.map((item, index) => {
|
|
14
|
+
if (index % 2 === 0) {
|
|
15
|
+
const nextItem = listings[index + 1];
|
|
16
|
+
return (
|
|
17
|
+
<Columns
|
|
18
|
+
key={`row-${Math.floor(index / 2)}`}
|
|
19
|
+
spacing="2u"
|
|
20
|
+
alignY="stretch"
|
|
21
|
+
>
|
|
22
|
+
<Column key={item.id} width="1/2">
|
|
23
|
+
<GridListingCard item={item} onClick={onListingClick} />
|
|
24
|
+
</Column>
|
|
25
|
+
{nextItem && (
|
|
26
|
+
<Column key={nextItem.id} width="1/2">
|
|
27
|
+
<GridListingCard item={nextItem} onClick={onListingClick} />
|
|
28
|
+
</Column>
|
|
29
|
+
)}
|
|
30
|
+
</Columns>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
})}
|
|
35
|
+
</Rows>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Property } from "../../real_estate.type";
|
|
2
|
+
import { ListListingCard } from "./listing_card";
|
|
3
|
+
|
|
4
|
+
interface ListingListProps {
|
|
5
|
+
listings: Property[];
|
|
6
|
+
onListingClick: (item: Property) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ListingList = ({ listings, onListingClick }: ListingListProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
{listings.map((item: Property, index: number) => (
|
|
13
|
+
<ListListingCard
|
|
14
|
+
key={`${item.id}-${index}`}
|
|
15
|
+
item={item}
|
|
16
|
+
onClick={onListingClick}
|
|
17
|
+
/>
|
|
18
|
+
))}
|
|
19
|
+
</>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Column,
|
|
4
|
+
Columns,
|
|
5
|
+
GridViewIcon,
|
|
6
|
+
ListBulletLtrIcon,
|
|
7
|
+
Rows,
|
|
8
|
+
SearchInputMenu,
|
|
9
|
+
Select,
|
|
10
|
+
} from "@canva/app-ui-kit";
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { useIntl } from "react-intl";
|
|
13
|
+
|
|
14
|
+
type Layout = "grid" | "list";
|
|
15
|
+
|
|
16
|
+
interface ListingSearchFiltersProps {
|
|
17
|
+
query: string;
|
|
18
|
+
onQueryChange: (query: string) => void;
|
|
19
|
+
layout: Layout;
|
|
20
|
+
onLayoutToggle: () => void;
|
|
21
|
+
propertyType: string;
|
|
22
|
+
onPropertyTypeChange: (type: string) => void;
|
|
23
|
+
sort: string;
|
|
24
|
+
onSortChange: (sort: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ListingSearchFilters = ({
|
|
28
|
+
query,
|
|
29
|
+
onQueryChange,
|
|
30
|
+
layout,
|
|
31
|
+
onLayoutToggle,
|
|
32
|
+
propertyType,
|
|
33
|
+
onPropertyTypeChange,
|
|
34
|
+
sort,
|
|
35
|
+
onSortChange,
|
|
36
|
+
}: ListingSearchFiltersProps) => {
|
|
37
|
+
const intl = useIntl();
|
|
38
|
+
const [queryValue, setQueryValue] = useState(query);
|
|
39
|
+
return (
|
|
40
|
+
<Rows spacing="2u">
|
|
41
|
+
<SearchInputMenu
|
|
42
|
+
value={queryValue}
|
|
43
|
+
onChange={setQueryValue}
|
|
44
|
+
onChangeComplete={() => onQueryChange(queryValue)}
|
|
45
|
+
onClear={() => {
|
|
46
|
+
setQueryValue("");
|
|
47
|
+
onQueryChange("");
|
|
48
|
+
}}
|
|
49
|
+
placeholder={intl.formatMessage({
|
|
50
|
+
defaultMessage: "Search listings...",
|
|
51
|
+
description: "Placeholder text for listings search input",
|
|
52
|
+
})}
|
|
53
|
+
/>
|
|
54
|
+
<Columns spacing="1u" alignY="center">
|
|
55
|
+
<Column width="content">
|
|
56
|
+
<Button
|
|
57
|
+
icon={layout === "grid" ? GridViewIcon : ListBulletLtrIcon}
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={onLayoutToggle}
|
|
60
|
+
variant="secondary"
|
|
61
|
+
ariaLabel={intl.formatMessage(
|
|
62
|
+
{
|
|
63
|
+
defaultMessage:
|
|
64
|
+
"Toggle layout between grid and list. Current layout is {currentLayout}",
|
|
65
|
+
description: "Aria label for layout toggle button",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
currentLayout:
|
|
69
|
+
layout === "grid"
|
|
70
|
+
? intl.formatMessage({
|
|
71
|
+
defaultMessage: "grid",
|
|
72
|
+
description: "Layout option",
|
|
73
|
+
})
|
|
74
|
+
: intl.formatMessage({
|
|
75
|
+
defaultMessage: "list",
|
|
76
|
+
description: "Layout option",
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
</Column>
|
|
82
|
+
<Column width="1/3">
|
|
83
|
+
{/* TODO (App Developer): Review filter options and update to match your specific property data shape */}
|
|
84
|
+
<Select
|
|
85
|
+
value={propertyType}
|
|
86
|
+
onChange={onPropertyTypeChange}
|
|
87
|
+
options={[
|
|
88
|
+
{
|
|
89
|
+
label: intl.formatMessage({
|
|
90
|
+
defaultMessage: "House",
|
|
91
|
+
description: "Property type option - house",
|
|
92
|
+
}),
|
|
93
|
+
value: "house",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: intl.formatMessage({
|
|
97
|
+
defaultMessage: "Apartment",
|
|
98
|
+
description: "Property type option - apartment",
|
|
99
|
+
}),
|
|
100
|
+
value: "apartment",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
label: intl.formatMessage({
|
|
104
|
+
defaultMessage: "Townhouse",
|
|
105
|
+
description: "Property type option - townhouse",
|
|
106
|
+
}),
|
|
107
|
+
value: "townhouse",
|
|
108
|
+
},
|
|
109
|
+
]}
|
|
110
|
+
placeholder={intl.formatMessage({
|
|
111
|
+
defaultMessage: "Property type",
|
|
112
|
+
description: "Placeholder text for property type dropdown",
|
|
113
|
+
})}
|
|
114
|
+
/>
|
|
115
|
+
</Column>
|
|
116
|
+
<Column width="1/3">
|
|
117
|
+
<Select
|
|
118
|
+
value={sort}
|
|
119
|
+
onChange={onSortChange}
|
|
120
|
+
options={[
|
|
121
|
+
{
|
|
122
|
+
label: intl.formatMessage({
|
|
123
|
+
defaultMessage: "Price: Low to High",
|
|
124
|
+
description: "Sort option - price ascending",
|
|
125
|
+
}),
|
|
126
|
+
value: "price-asc",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
label: intl.formatMessage({
|
|
130
|
+
defaultMessage: "Price: High to Low",
|
|
131
|
+
description: "Sort option - price descending",
|
|
132
|
+
}),
|
|
133
|
+
value: "price-desc",
|
|
134
|
+
},
|
|
135
|
+
]}
|
|
136
|
+
placeholder={intl.formatMessage({
|
|
137
|
+
defaultMessage: "Sort by",
|
|
138
|
+
description: "Placeholder text for sort dropdown",
|
|
139
|
+
})}
|
|
140
|
+
/>
|
|
141
|
+
</Column>
|
|
142
|
+
</Columns>
|
|
143
|
+
</Rows>
|
|
144
|
+
);
|
|
145
|
+
};
|