@canva/cli 1.10.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.
Files changed (126) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +2 -0
  3. package/cli.js +596 -574
  4. package/lib/cjs/index.cjs +2 -2
  5. package/lib/esm/index.mjs +2 -2
  6. package/lib/index.d.ts +2 -0
  7. package/package.json +7 -2
  8. package/templates/base/package.json +9 -8
  9. package/templates/base/styles/components.css +18 -0
  10. package/templates/common/.env.template +1 -1
  11. package/templates/common/jest.config.mjs +1 -1
  12. package/templates/dam/backend/server.ts +8 -0
  13. package/templates/dam/canva-app.json +9 -0
  14. package/templates/dam/package.json +10 -8
  15. package/templates/dam/src/index.tsx +3 -21
  16. package/templates/dam/src/intents/design_editor/index.tsx +25 -0
  17. package/templates/data_connector/README.md +1 -1
  18. package/templates/data_connector/package.json +9 -8
  19. package/templates/data_connector/src/api/data_sources/designs.tsx +1 -1
  20. package/templates/data_connector/src/api/data_sources/templates.tsx +1 -1
  21. package/templates/data_connector/src/components/header.tsx +1 -1
  22. package/templates/data_connector/src/index.tsx +2 -66
  23. package/templates/data_connector/src/{app.tsx → intents/data_connector/app.tsx} +3 -3
  24. package/templates/data_connector/src/{entrypoint.tsx → intents/data_connector/entrypoint.tsx} +5 -5
  25. package/templates/data_connector/src/{home.tsx → intents/data_connector/home.tsx} +1 -1
  26. package/templates/data_connector/src/intents/data_connector/index.tsx +56 -0
  27. package/templates/data_connector/src/pages/error.tsx +1 -1
  28. package/templates/data_connector/src/pages/login.tsx +1 -1
  29. package/templates/data_connector/src/routes/protected_route.tsx +1 -1
  30. package/templates/data_connector/src/routes/routes.tsx +3 -3
  31. package/templates/data_connector/styles/components.css +18 -0
  32. package/templates/gen_ai/backend/server.ts +17 -0
  33. package/templates/gen_ai/canva-app.json +5 -0
  34. package/templates/gen_ai/package.json +10 -8
  35. package/templates/gen_ai/src/api/api.ts +4 -0
  36. package/templates/gen_ai/src/components/footer.tsx +1 -1
  37. package/templates/gen_ai/src/components/loading_results.tsx +1 -1
  38. package/templates/gen_ai/src/components/prompt_input.tsx +1 -1
  39. package/templates/gen_ai/src/index.tsx +3 -14
  40. package/templates/gen_ai/src/{app.tsx → intents/design_editor/app.tsx} +3 -3
  41. package/templates/gen_ai/src/{home.tsx → intents/design_editor/home.tsx} +1 -1
  42. package/templates/gen_ai/src/intents/design_editor/index.tsx +17 -0
  43. package/templates/gen_ai/src/pages/error.tsx +1 -1
  44. package/templates/gen_ai/src/routes/routes.tsx +2 -2
  45. package/templates/gen_ai/styles/components.css +18 -0
  46. package/templates/hello_world/canva-app.json +5 -0
  47. package/templates/hello_world/package.json +10 -8
  48. package/templates/hello_world/src/index.tsx +3 -21
  49. package/templates/hello_world/src/{app.tsx → intents/design_editor/app.tsx} +26 -3
  50. package/templates/hello_world/src/intents/design_editor/index.tsx +25 -0
  51. package/templates/hello_world/src/{tests → intents/design_editor/tests}/app.tests.tsx +19 -13
  52. package/templates/hello_world/styles/components.css +18 -0
  53. package/templates/mls/README.md +81 -0
  54. package/templates/mls/canva-app.json +25 -0
  55. package/templates/mls/declarations/declarations.d.ts +29 -0
  56. package/templates/mls/eslint.config.mjs +14 -0
  57. package/templates/mls/jest.config.mjs +36 -0
  58. package/templates/mls/jest.setup.ts +37 -0
  59. package/templates/mls/package.json +117 -0
  60. package/templates/mls/scripts/copy_env.ts +13 -0
  61. package/templates/mls/scripts/ssl/ssl.ts +131 -0
  62. package/templates/mls/scripts/start/app_runner.ts +223 -0
  63. package/templates/mls/scripts/start/context.ts +171 -0
  64. package/templates/mls/scripts/start/start.ts +46 -0
  65. package/templates/mls/src/__tests__/app.tests.tsx +11 -0
  66. package/templates/mls/src/__tests__/office_selection_page.tests.tsx +72 -0
  67. package/templates/mls/src/__tests__/utils.tsx +19 -0
  68. package/templates/mls/src/adapter.ts +126 -0
  69. package/templates/mls/src/components/agent/agent_card.tsx +57 -0
  70. package/templates/mls/src/components/agent/agent_grid.tsx +37 -0
  71. package/templates/mls/src/components/agent/agent_list.tsx +17 -0
  72. package/templates/mls/src/components/agent/agent_search_filters.tsx +88 -0
  73. package/templates/mls/src/components/breadcrumb/breadcrumb.tsx +40 -0
  74. package/templates/mls/src/components/listing/listing_card.tsx +64 -0
  75. package/templates/mls/src/components/listing/listing_grid.tsx +37 -0
  76. package/templates/mls/src/components/listing/listing_list.tsx +21 -0
  77. package/templates/mls/src/components/listing/listing_search_filters.tsx +145 -0
  78. package/templates/mls/src/components/placeholders/placeholders.tsx +65 -0
  79. package/templates/mls/src/data.ts +359 -0
  80. package/templates/mls/src/index.tsx +4 -0
  81. package/templates/mls/src/intents/design_editor/app.tsx +44 -0
  82. package/templates/mls/src/intents/design_editor/index.tsx +25 -0
  83. package/templates/mls/src/pages/agent_details_page/agent_details_page.tsx +175 -0
  84. package/templates/mls/src/pages/list_page/agent_tab_panel.tsx +126 -0
  85. package/templates/mls/src/pages/list_page/list_page.tsx +67 -0
  86. package/templates/mls/src/pages/list_page/listing_tab_panel.tsx +135 -0
  87. package/templates/mls/src/pages/listing_details_page/listing_details_page.tsx +418 -0
  88. package/templates/mls/src/pages/loading_page/loading_page.tsx +152 -0
  89. package/templates/mls/src/pages/office_selection_page/office_selection_page.tsx +144 -0
  90. package/templates/mls/src/real_estate.type.ts +44 -0
  91. package/templates/mls/src/util/use_add_element.tsx +62 -0
  92. package/templates/mls/src/util/use_drag_element.tsx +68 -0
  93. package/templates/mls/styles/components.css +56 -0
  94. package/templates/mls/tsconfig.json +55 -0
  95. package/templates/mls/webpack.config.ts +254 -0
  96. package/templates/optional/AGENTS.md +80 -2
  97. package/templates/optional/CLAUDE.md +80 -2
  98. package/templates/base/backend/routers/oauth.ts +0 -393
  99. package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +0 -99
  100. package/templates/base/utils/backend/bearer_middleware/index.ts +0 -1
  101. package/templates/base/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +0 -192
  102. package/templates/base/utils/use_add_element.ts +0 -58
  103. package/templates/base/utils/use_feature_support.ts +0 -28
  104. package/templates/common/utils/backend/base_backend/create.ts +0 -104
  105. package/templates/common/utils/table_wrapper.ts +0 -520
  106. package/templates/common/utils/use_add_element.ts +0 -58
  107. package/templates/common/utils/use_feature_support.ts +0 -28
  108. package/templates/common/utils/use_overlay_hook.ts +0 -76
  109. package/templates/common/utils/use_selection_hook.ts +0 -37
  110. package/templates/gen_ai/backend/database/database.ts +0 -42
  111. package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +0 -99
  112. package/templates/gen_ai/utils/backend/bearer_middleware/index.ts +0 -1
  113. package/templates/hello_world/utils/use_add_element.ts +0 -58
  114. package/templates/hello_world/utils/use_feature_support.ts +0 -28
  115. /package/templates/base/{utils/backend → backend}/base_backend/create.ts +0 -0
  116. /package/templates/base/{utils/backend → backend}/jwt_middleware/index.ts +0 -0
  117. /package/templates/base/{utils/backend → backend}/jwt_middleware/jwt_middleware.ts +0 -0
  118. /package/templates/dam/src/{adapter.ts → intents/design_editor/adapter.ts} +0 -0
  119. /package/templates/dam/src/{app.tsx → intents/design_editor/app.tsx} +0 -0
  120. /package/templates/dam/src/{config.ts → intents/design_editor/config.ts} +0 -0
  121. /package/templates/dam/src/{index.css → intents/design_editor/index.css} +0 -0
  122. /package/templates/data_connector/src/{paths.ts → routes/paths.ts} +0 -0
  123. /package/templates/gen_ai/src/{paths.ts → routes/paths.ts} +0 -0
  124. /package/templates/{common → gen_ai}/utils/backend/jwt_middleware/index.ts +0 -0
  125. /package/templates/{common → gen_ai}/utils/backend/jwt_middleware/jwt_middleware.ts +0 -0
  126. /package/templates/hello_world/src/{tests → intents/design_editor/tests}/__snapshots__/app.tests.tsx.snap +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
+ };