@salesforce/ui-bundle-template-app-react-sample-b2x 11.56.3 → 11.56.4
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/dist/CHANGELOG.md +8 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/package.json +4 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/app.tsx +2 -24
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/appLayout.tsx +60 -5
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ErrorBoundary.tsx +24 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/dashboard/WeatherWidget.tsx +10 -2
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/layout/TopBar.tsx +13 -7
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/layout/VerticalNav.tsx +12 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestList.tsx +9 -2
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestListItem.tsx +18 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertyMap.tsx +1 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertySearchFilters.tsx +8 -8
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useWeather.ts +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Application.tsx +25 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Contact.tsx +35 -21
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Dashboard.tsx +1 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Home.tsx +11 -8
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Maintenance.tsx +9 -5
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertyDetails.tsx +19 -8
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx +15 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/styles/global.css +1 -1
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [11.56.4](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.56.3...v11.56.4) (2026-08-13)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
## [11.56.3](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.56.2...v11.56.3) (2026-08-12)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"graphql:schema": "node scripts/get-graphql-schema.mjs"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@salesforce/platform-sdk": "^11.56.
|
|
22
|
-
"@salesforce/ui-bundle": "^11.56.
|
|
21
|
+
"@salesforce/platform-sdk": "^11.56.4",
|
|
22
|
+
"@salesforce/ui-bundle": "^11.56.4",
|
|
23
23
|
"@tailwindcss/vite": "^4.1.17",
|
|
24
24
|
"class-variance-authority": "^0.7.1",
|
|
25
25
|
"clsx": "^2.1.1",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"@graphql-eslint/eslint-plugin": "^4.1.0",
|
|
51
51
|
"@graphql-tools/utils": "^11.0.0",
|
|
52
52
|
"@playwright/test": "^1.49.0",
|
|
53
|
-
"@salesforce/graphiti": "^11.56.
|
|
54
|
-
"@salesforce/vite-plugin-ui-bundle": "^11.56.
|
|
53
|
+
"@salesforce/graphiti": "^11.56.4",
|
|
54
|
+
"@salesforce/vite-plugin-ui-bundle": "^11.56.4",
|
|
55
55
|
"@testing-library/jest-dom": "^6.6.3",
|
|
56
56
|
"@testing-library/react": "^16.1.0",
|
|
57
57
|
"@testing-library/user-event": "^14.5.2",
|
|
@@ -1,32 +1,10 @@
|
|
|
1
1
|
import { createBrowserRouter, RouterProvider } from "react-router";
|
|
2
2
|
import { routes } from "@/routes";
|
|
3
|
-
import { StrictMode
|
|
3
|
+
import { StrictMode } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
|
5
6
|
import "./styles/global.css";
|
|
6
7
|
|
|
7
|
-
class ErrorBoundary extends Component<
|
|
8
|
-
{ children: ReactNode },
|
|
9
|
-
{ hasError: boolean; error: Error | null }
|
|
10
|
-
> {
|
|
11
|
-
state = { hasError: false, error: null as Error | null };
|
|
12
|
-
static getDerivedStateFromError(error: Error) {
|
|
13
|
-
return { hasError: true, error };
|
|
14
|
-
}
|
|
15
|
-
render() {
|
|
16
|
-
if (this.state.hasError && this.state.error) {
|
|
17
|
-
return (
|
|
18
|
-
<div className="mx-auto max-w-[600px] p-8 font-sans">
|
|
19
|
-
<h1 className="text-xl text-destructive">Something went wrong</h1>
|
|
20
|
-
<pre className="mt-2 overflow-auto rounded-md bg-destructive/10 p-4 text-sm">
|
|
21
|
-
{this.state.error.message}
|
|
22
|
-
</pre>
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
return this.props.children;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
8
|
// Normalize basename: strip trailing slash so it matches URLs like /lwr/application/ai/c-app
|
|
31
9
|
const rawBasePath = (globalThis as any).SFDC_ENV?.basePath;
|
|
32
10
|
const basename = typeof rawBasePath === "string" ? rawBasePath.replace(/\/+$/, "") : undefined;
|
|
@@ -1,23 +1,78 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { Outlet } from "react-router";
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Outlet, useLocation } from "react-router";
|
|
3
3
|
import { TopBar } from "@/components/layout/TopBar";
|
|
4
4
|
import { VerticalNav } from "@/components/layout/VerticalNav";
|
|
5
5
|
import { Toaster } from "@/components/ui/sonner";
|
|
6
6
|
import { TenantAccessProvider } from "@/context/TenantAccessContext";
|
|
7
7
|
|
|
8
|
+
const APP_NAME = "ZENLEASE";
|
|
9
|
+
|
|
10
|
+
const atPath = (base: string) => (p: string) => p === base || p.startsWith(`${base}/`);
|
|
11
|
+
|
|
12
|
+
const PAGE_TITLES: { match: (path: string) => boolean; title: string }[] = [
|
|
13
|
+
{ match: (p) => p === "/", title: "Home" },
|
|
14
|
+
{ match: atPath("/properties"), title: "Property Search" },
|
|
15
|
+
{
|
|
16
|
+
match: (p) => p.startsWith("/property/") || p.startsWith("/object/"),
|
|
17
|
+
title: "Property Details",
|
|
18
|
+
},
|
|
19
|
+
{ match: atPath("/dashboard"), title: "Dashboard" },
|
|
20
|
+
{ match: atPath("/maintenance"), title: "Maintenance" },
|
|
21
|
+
{ match: atPath("/contact"), title: "Contact" },
|
|
22
|
+
{ match: atPath("/application"), title: "Application" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function pageTitleFor(pathname: string): string {
|
|
26
|
+
const page = PAGE_TITLES.find((entry) => entry.match(pathname))?.title ?? "Page Not Found";
|
|
27
|
+
return `${page} · ${APP_NAME}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
export default function AppLayout() {
|
|
9
31
|
const [isNavOpen, setIsNavOpen] = useState(false);
|
|
32
|
+
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
|
33
|
+
const mainRef = useRef<HTMLElement>(null);
|
|
34
|
+
const { pathname } = useLocation();
|
|
35
|
+
const previousPathname = useRef(pathname);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
document.title = pageTitleFor(pathname);
|
|
39
|
+
if (pathname !== previousPathname.current) {
|
|
40
|
+
previousPathname.current = pathname;
|
|
41
|
+
mainRef.current?.focus();
|
|
42
|
+
}
|
|
43
|
+
}, [pathname]);
|
|
44
|
+
|
|
45
|
+
const closeNav = () => {
|
|
46
|
+
setIsNavOpen(false);
|
|
47
|
+
menuButtonRef.current?.focus();
|
|
48
|
+
};
|
|
10
49
|
|
|
11
50
|
return (
|
|
12
51
|
<TenantAccessProvider>
|
|
13
52
|
<div className="flex min-h-screen flex-col bg-gray-50">
|
|
14
53
|
<Toaster />
|
|
15
|
-
<
|
|
54
|
+
<a
|
|
55
|
+
href="#main-content"
|
|
56
|
+
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded-lg focus:bg-primary focus:px-4 focus:py-2 focus:text-primary-foreground focus:ring-2 focus:ring-white focus:ring-offset-2"
|
|
57
|
+
>
|
|
58
|
+
Skip to main content
|
|
59
|
+
</a>
|
|
60
|
+
<TopBar
|
|
61
|
+
onMenuClick={() => setIsNavOpen((open) => !open)}
|
|
62
|
+
isNavOpen={isNavOpen}
|
|
63
|
+
menuButtonRef={menuButtonRef}
|
|
64
|
+
/>
|
|
16
65
|
|
|
17
66
|
<div className="flex flex-1 overflow-hidden">
|
|
18
|
-
<VerticalNav isOpen={isNavOpen} onClose={
|
|
67
|
+
<VerticalNav isOpen={isNavOpen} onClose={closeNav} />
|
|
19
68
|
|
|
20
|
-
<main
|
|
69
|
+
<main
|
|
70
|
+
id="main-content"
|
|
71
|
+
ref={mainRef}
|
|
72
|
+
tabIndex={-1}
|
|
73
|
+
className="flex-1 overflow-auto bg-gray-50 p-4 outline-none md:p-8"
|
|
74
|
+
role="main"
|
|
75
|
+
>
|
|
21
76
|
<Outlet />
|
|
22
77
|
</main>
|
|
23
78
|
</div>
|
package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ErrorBoundary.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Component, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export class ErrorBoundary extends Component<
|
|
4
|
+
{ children: ReactNode },
|
|
5
|
+
{ hasError: boolean; error: Error | null }
|
|
6
|
+
> {
|
|
7
|
+
state = { hasError: false, error: null as Error | null };
|
|
8
|
+
static getDerivedStateFromError(error: Error) {
|
|
9
|
+
return { hasError: true, error };
|
|
10
|
+
}
|
|
11
|
+
render() {
|
|
12
|
+
if (this.state.hasError && this.state.error) {
|
|
13
|
+
return (
|
|
14
|
+
<div role="alert" className="mx-auto max-w-[600px] p-8 font-sans">
|
|
15
|
+
<h1 className="text-xl text-destructive">Something went wrong</h1>
|
|
16
|
+
<pre className="mt-2 overflow-auto rounded-md bg-destructive/10 p-4 text-sm">
|
|
17
|
+
{this.state.error.message}
|
|
18
|
+
</pre>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return this.props.children;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useRef, useState } from "react";
|
|
2
2
|
import { Card } from "@/components/ui/card";
|
|
3
3
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
4
|
-
import { useWeather, type WeatherData, type WeatherHour } from "@/hooks/useWeather";
|
|
4
|
+
import { useWeather, WEATHER_LABELS, type WeatherData, type WeatherHour } from "@/hooks/useWeather";
|
|
5
5
|
import {
|
|
6
6
|
Sun,
|
|
7
7
|
Cloud,
|
|
@@ -256,6 +256,7 @@ function HourlyForecast({ hours, activeTab }: { hours: WeatherHour[]; activeTab:
|
|
|
256
256
|
>
|
|
257
257
|
<p className="whitespace-nowrap text-xs text-muted-foreground">{h.time}</p>
|
|
258
258
|
{getWeatherIcon(h.weatherCode, "h-5 w-5 text-gray-500")}
|
|
259
|
+
<span className="sr-only">{WEATHER_LABELS[h.weatherCode] ?? "Unknown"}</span>
|
|
259
260
|
<p className="text-base font-semibold text-foreground">{h.temp}°</p>
|
|
260
261
|
</div>
|
|
261
262
|
))}
|
|
@@ -271,7 +272,14 @@ export function WeatherWidget() {
|
|
|
271
272
|
<Card className="rounded-2xl border-0 p-6 shadow-md">
|
|
272
273
|
<h2 className="text-lg font-semibold text-primary">Weather</h2>
|
|
273
274
|
|
|
274
|
-
{loading &&
|
|
275
|
+
{loading && (
|
|
276
|
+
<>
|
|
277
|
+
<span role="status" className="sr-only">
|
|
278
|
+
Loading weather
|
|
279
|
+
</span>
|
|
280
|
+
<WeatherSkeleton />
|
|
281
|
+
</>
|
|
282
|
+
)}
|
|
275
283
|
|
|
276
284
|
{error && (
|
|
277
285
|
<p className="mt-4 text-sm text-destructive" role="alert">
|
package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/layout/TopBar.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ChevronDown, Menu, User } from "lucide-react";
|
|
2
2
|
import { Link } from "react-router";
|
|
3
|
+
import type { RefObject } from "react";
|
|
3
4
|
import { ROUTES } from "@/features/authentication/authenticationConfig";
|
|
4
5
|
import zenLogo from "@/assets/icons/zen-logo.svg";
|
|
5
6
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
@@ -8,20 +9,25 @@ import { AuthMenu } from "@/features/authentication/menu/AuthMenu";
|
|
|
8
9
|
|
|
9
10
|
export interface TopBarProps {
|
|
10
11
|
onMenuClick?: () => void;
|
|
12
|
+
isNavOpen?: boolean;
|
|
13
|
+
menuButtonRef?: RefObject<HTMLButtonElement | null>;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
export function TopBar({ onMenuClick }: TopBarProps) {
|
|
16
|
+
export function TopBar({ onMenuClick, isNavOpen, menuButtonRef }: TopBarProps) {
|
|
14
17
|
return (
|
|
15
18
|
<header className="flex h-16 items-center justify-between bg-teal-700 px-6 text-white">
|
|
16
19
|
<div className="flex items-center gap-4">
|
|
17
20
|
<Button
|
|
21
|
+
ref={menuButtonRef}
|
|
18
22
|
variant="ghost"
|
|
19
23
|
size="icon"
|
|
20
24
|
onClick={onMenuClick}
|
|
21
|
-
className="text-white hover:bg-teal-600 md:hidden"
|
|
25
|
+
className="text-white hover:bg-teal-600 aria-expanded:bg-teal-600 aria-expanded:text-white md:hidden"
|
|
22
26
|
aria-label="Toggle menu"
|
|
27
|
+
aria-expanded={isNavOpen}
|
|
28
|
+
aria-controls={isNavOpen ? "mobile-nav" : undefined}
|
|
23
29
|
>
|
|
24
|
-
<Menu className="size-6" />
|
|
30
|
+
<Menu className="size-6" aria-hidden />
|
|
25
31
|
</Button>
|
|
26
32
|
<Logo />
|
|
27
33
|
</div>
|
|
@@ -30,7 +36,7 @@ export function TopBar({ onMenuClick }: TopBarProps) {
|
|
|
30
36
|
trigger={(user) => (
|
|
31
37
|
<Button
|
|
32
38
|
variant="ghost"
|
|
33
|
-
className="h-10 gap-2 rounded-lg px-3 text-base text-white hover:bg-teal-
|
|
39
|
+
className="h-10 gap-2 rounded-lg px-3 text-base text-white hover:bg-teal-800 hover:text-white"
|
|
34
40
|
aria-label="User menu"
|
|
35
41
|
>
|
|
36
42
|
<Avatar className="size-7 bg-teal-300 text-teal-900">
|
|
@@ -39,7 +45,7 @@ export function TopBar({ onMenuClick }: TopBarProps) {
|
|
|
39
45
|
</AvatarFallback>
|
|
40
46
|
</Avatar>
|
|
41
47
|
<span className="hidden font-medium md:inline">{user.name.toUpperCase()}</span>
|
|
42
|
-
<ChevronDown className="hidden size-4 md:block" />
|
|
48
|
+
<ChevronDown className="hidden size-4 md:block" aria-hidden />
|
|
43
49
|
</Button>
|
|
44
50
|
)}
|
|
45
51
|
guestContent={
|
|
@@ -49,8 +55,8 @@ export function TopBar({ onMenuClick }: TopBarProps) {
|
|
|
49
55
|
size="icon"
|
|
50
56
|
className="rounded-full bg-white text-teal-700 hover:bg-teal-50 md:h-9 md:w-auto md:gap-2 md:rounded-lg md:px-4"
|
|
51
57
|
>
|
|
52
|
-
<Link to={ROUTES.LOGIN.PATH}>
|
|
53
|
-
<User className="size-5 md:size-4" />
|
|
58
|
+
<Link to={ROUTES.LOGIN.PATH} aria-label="Sign Up / Sign In">
|
|
59
|
+
<User className="size-5 md:size-4" aria-hidden="true" />
|
|
54
60
|
<span className="hidden md:inline">Sign Up / Sign In</span>
|
|
55
61
|
</Link>
|
|
56
62
|
</Button>
|
|
@@ -2,7 +2,7 @@ import { Link, useLocation } from "react-router";
|
|
|
2
2
|
import { Home, Search, BarChart3, Wrench, Phone, type LucideIcon } from "lucide-react";
|
|
3
3
|
import { useAuth } from "@/features/authentication/context/AuthContext";
|
|
4
4
|
import { useTenantAccess } from "@/context/TenantAccessContext";
|
|
5
|
-
import { useMemo } from "react";
|
|
5
|
+
import { useEffect, useMemo } from "react";
|
|
6
6
|
|
|
7
7
|
interface VerticalNavItem {
|
|
8
8
|
path: string;
|
|
@@ -29,6 +29,15 @@ export function VerticalNav({ isOpen = false, onClose }: VerticalNavProps) {
|
|
|
29
29
|
const { isAuthenticated } = useAuth();
|
|
30
30
|
const { hasTenantRecord } = useTenantAccess();
|
|
31
31
|
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!isOpen) return;
|
|
34
|
+
const onKey = (e: KeyboardEvent) => {
|
|
35
|
+
if (e.key === "Escape") onClose?.();
|
|
36
|
+
};
|
|
37
|
+
document.addEventListener("keydown", onKey);
|
|
38
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
39
|
+
}, [isOpen, onClose]);
|
|
40
|
+
|
|
32
41
|
const visibleItems = useMemo(
|
|
33
42
|
() =>
|
|
34
43
|
navItems.filter((item) => {
|
|
@@ -63,6 +72,7 @@ export function VerticalNav({ isOpen = false, onClose }: VerticalNavProps) {
|
|
|
63
72
|
}`}
|
|
64
73
|
title={item.label}
|
|
65
74
|
aria-label={item.label}
|
|
75
|
+
aria-current={active ? "page" : undefined}
|
|
66
76
|
>
|
|
67
77
|
<Icon className="size-6 shrink-0" aria-hidden />
|
|
68
78
|
<span className="text-center text-xs font-medium leading-tight">{item.label}</span>
|
|
@@ -89,6 +99,7 @@ export function VerticalNav({ isOpen = false, onClose }: VerticalNavProps) {
|
|
|
89
99
|
* same time, and two navigation landmarks sharing one name is ambiguous.
|
|
90
100
|
*/}
|
|
91
101
|
<nav
|
|
102
|
+
id="mobile-nav"
|
|
92
103
|
className="fixed left-0 top-0 bottom-0 z-50 flex w-24 flex-col border-r border-gray-200 bg-white py-8"
|
|
93
104
|
aria-label="Mobile navigation"
|
|
94
105
|
>
|
|
@@ -29,14 +29,21 @@ export default function MaintenanceRequestList({
|
|
|
29
29
|
onClose={() => setSelectedRequest(null)}
|
|
30
30
|
/>
|
|
31
31
|
)}
|
|
32
|
-
{loading &&
|
|
32
|
+
{loading && (
|
|
33
|
+
<div role="status" aria-live="polite" aria-atomic="true">
|
|
34
|
+
<SkeletonListRows count={skeletonCount} />
|
|
35
|
+
<span className="sr-only">Loading maintenance requests…</span>
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
33
38
|
{error && (
|
|
34
39
|
<p className="py-4 text-sm text-destructive" role="alert">
|
|
35
40
|
{error}
|
|
36
41
|
</p>
|
|
37
42
|
)}
|
|
38
43
|
{!loading && !error && requests.length === 0 && (
|
|
39
|
-
<div className="py-8 text-center text-gray-500">
|
|
44
|
+
<div role="status" className="py-8 text-center text-gray-500">
|
|
45
|
+
{emptyMessage}
|
|
46
|
+
</div>
|
|
40
47
|
)}
|
|
41
48
|
{!loading &&
|
|
42
49
|
!error &&
|
|
@@ -23,6 +23,16 @@ export default function MaintenanceRequestListItem({
|
|
|
23
23
|
: (request.Name?.value ?? "—");
|
|
24
24
|
const title = request.Description__c?.value?.trim() || request.Name?.value?.trim() || "—";
|
|
25
25
|
|
|
26
|
+
const rawStatus = request.Status__c?.value ?? null;
|
|
27
|
+
const statusLabel =
|
|
28
|
+
rawStatus === "New"
|
|
29
|
+
? "Needs Action"
|
|
30
|
+
: rawStatus === "In Progress"
|
|
31
|
+
? "In Progress"
|
|
32
|
+
: rawStatus === "Resolved"
|
|
33
|
+
? "Resolved"
|
|
34
|
+
: rawStatus;
|
|
35
|
+
|
|
26
36
|
const handleClick = useCallback(() => {
|
|
27
37
|
onClick?.(request);
|
|
28
38
|
}, [onClick, request]);
|
|
@@ -48,7 +58,14 @@ export default function MaintenanceRequestListItem({
|
|
|
48
58
|
tabIndex={isClickable ? 0 : undefined}
|
|
49
59
|
aria-label={
|
|
50
60
|
isClickable
|
|
51
|
-
?
|
|
61
|
+
? [
|
|
62
|
+
`View details for ${title !== "—" ? title : "maintenance request"}`,
|
|
63
|
+
issueType,
|
|
64
|
+
addressFirstPart !== "—" ? addressFirstPart : null,
|
|
65
|
+
statusLabel ? `status ${statusLabel}` : null,
|
|
66
|
+
]
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.join(", ")
|
|
52
69
|
: undefined
|
|
53
70
|
}
|
|
54
71
|
>
|
|
@@ -5,13 +5,13 @@ import { Popover } from "radix-ui";
|
|
|
5
5
|
import { Search, ChevronDown } from "lucide-react";
|
|
6
6
|
|
|
7
7
|
const SEARCH_INPUT_CLASS =
|
|
8
|
-
"h-10 rounded-full border-2 border-primary
|
|
8
|
+
"h-10 rounded-full border-2 border-primary bg-background px-4 text-sm outline-none transition-colors placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2";
|
|
9
9
|
|
|
10
10
|
const FILTER_PILL_CLASS =
|
|
11
|
-
"inline-flex h-10 items-center gap-1.5 rounded-full border-2 border-primary
|
|
11
|
+
"inline-flex h-10 items-center gap-1.5 rounded-full border-2 border-primary bg-background px-4 text-sm font-medium text-foreground outline-none transition-colors hover:bg-primary/10 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2";
|
|
12
12
|
|
|
13
13
|
const SAVE_BUTTON_CLASS =
|
|
14
|
-
"mt-3 w-full rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary
|
|
14
|
+
"mt-3 w-full rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2";
|
|
15
15
|
|
|
16
16
|
const DEFAULT_PRICE_MIN = 0;
|
|
17
17
|
const DEFAULT_PRICE_MAX = 100_000;
|
|
@@ -161,7 +161,7 @@ export default function PropertySearchFilters({
|
|
|
161
161
|
Price range
|
|
162
162
|
</p>
|
|
163
163
|
<div className="flex items-center gap-2">
|
|
164
|
-
<div className="flex flex-1 items-center rounded-full border-2 border-primary
|
|
164
|
+
<div className="flex flex-1 items-center rounded-full border-2 border-primary bg-background focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2">
|
|
165
165
|
<span className="pl-4 text-sm text-muted-foreground">$</span>
|
|
166
166
|
<input
|
|
167
167
|
type="number"
|
|
@@ -176,7 +176,7 @@ export default function PropertySearchFilters({
|
|
|
176
176
|
/>
|
|
177
177
|
</div>
|
|
178
178
|
<span className="text-muted-foreground">–</span>
|
|
179
|
-
<div className="flex flex-1 items-center rounded-full border-2 border-primary
|
|
179
|
+
<div className="flex flex-1 items-center rounded-full border-2 border-primary bg-background focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2">
|
|
180
180
|
<span className="pl-4 text-sm text-muted-foreground">$</span>
|
|
181
181
|
<input
|
|
182
182
|
type="number"
|
|
@@ -230,7 +230,7 @@ export default function PropertySearchFilters({
|
|
|
230
230
|
Number of bedrooms
|
|
231
231
|
</p>
|
|
232
232
|
<div
|
|
233
|
-
className="flex
|
|
233
|
+
className="flex rounded-full border-2 border-primary"
|
|
234
234
|
role="group"
|
|
235
235
|
aria-label="Number of bedrooms"
|
|
236
236
|
>
|
|
@@ -239,7 +239,7 @@ export default function PropertySearchFilters({
|
|
|
239
239
|
key={value}
|
|
240
240
|
type="button"
|
|
241
241
|
onClick={() => onBedroomsChange(bedrooms === value ? null : value)}
|
|
242
|
-
className={`min-w-[4rem] flex-1 border-r border-primary
|
|
242
|
+
className={`min-w-[4rem] flex-1 border-r border-primary px-4 py-2 text-sm font-medium transition-colors first:rounded-l-full last:rounded-r-full last:border-r-0 outline-none focus-visible:relative focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ${
|
|
243
243
|
bedrooms === value
|
|
244
244
|
? "bg-primary text-primary-foreground"
|
|
245
245
|
: "bg-background text-primary hover:bg-primary/10"
|
|
@@ -301,7 +301,7 @@ export default function PropertySearchFilters({
|
|
|
301
301
|
key={value}
|
|
302
302
|
type="button"
|
|
303
303
|
onClick={() => onSortChange(value)}
|
|
304
|
-
className={`rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors ${
|
|
304
|
+
className={`rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ${
|
|
305
305
|
sortBy === value
|
|
306
306
|
? "bg-primary text-primary-foreground"
|
|
307
307
|
: "bg-background text-foreground hover:bg-muted"
|
|
@@ -15,7 +15,7 @@ const IMPERIAL_REGIONS = new Set(["US", "LR", "MM"]);
|
|
|
15
15
|
const MIN_TODAY_TILES = 3;
|
|
16
16
|
|
|
17
17
|
/** WMO weather codes → short label (Open-Meteo) */
|
|
18
|
-
const WEATHER_LABELS: Record<number, string> = {
|
|
18
|
+
export const WEATHER_LABELS: Record<number, string> = {
|
|
19
19
|
0: "Clear",
|
|
20
20
|
1: "Mainly clear",
|
|
21
21
|
2: "Partly cloudy",
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { useSearchParams, Link } from "react-router";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ChangeEvent,
|
|
8
|
+
type SubmitEvent,
|
|
9
|
+
} from "react";
|
|
3
10
|
import { Button } from "../components/ui/button";
|
|
4
11
|
import { Input } from "../components/ui/input";
|
|
5
12
|
import { Label } from "../components/ui/label";
|
|
@@ -70,6 +77,7 @@ async function createContactForUser(): Promise<string | null> {
|
|
|
70
77
|
function ApplicationSkeleton() {
|
|
71
78
|
return (
|
|
72
79
|
<div className="mx-auto max-w-[900px]" role="status">
|
|
80
|
+
<h1 className="sr-only">Apply for a property</h1>
|
|
73
81
|
<Skeleton className="mb-4 h-4 w-28" />
|
|
74
82
|
|
|
75
83
|
<Card className="mb-6 flex gap-4 rounded-2xl border border-border p-6 shadow-sm">
|
|
@@ -135,6 +143,11 @@ export default function Application() {
|
|
|
135
143
|
const [submitting, setSubmitting] = useState(false);
|
|
136
144
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
137
145
|
const [submittedName, setSubmittedName] = useState<string | null>(null);
|
|
146
|
+
const successHeadingRef = useRef<HTMLHeadingElement>(null);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (submittedName) successHeadingRef.current?.focus();
|
|
150
|
+
}, [submittedName]);
|
|
138
151
|
|
|
139
152
|
const handleSubmit = useCallback(
|
|
140
153
|
async (e: SubmitEvent<HTMLFormElement>) => {
|
|
@@ -170,9 +183,16 @@ export default function Application() {
|
|
|
170
183
|
|
|
171
184
|
if (submittedName) {
|
|
172
185
|
return (
|
|
173
|
-
<div className="mx-auto max-w-[900px]">
|
|
186
|
+
<div className="mx-auto max-w-[900px]" role="status">
|
|
187
|
+
<h1 className="sr-only">Application submitted</h1>
|
|
174
188
|
<Card className="mb-6 rounded-2xl border border-border p-6 shadow-sm">
|
|
175
|
-
<h2
|
|
189
|
+
<h2
|
|
190
|
+
ref={successHeadingRef}
|
|
191
|
+
tabIndex={-1}
|
|
192
|
+
className="mb-2 text-2xl font-semibold text-foreground"
|
|
193
|
+
>
|
|
194
|
+
Application submitted
|
|
195
|
+
</h2>
|
|
176
196
|
<p className="text-sm text-muted-foreground">
|
|
177
197
|
Your application
|
|
178
198
|
{propertyName ? (
|
|
@@ -208,6 +228,7 @@ export default function Application() {
|
|
|
208
228
|
|
|
209
229
|
return (
|
|
210
230
|
<div className="mx-auto max-w-[900px]">
|
|
231
|
+
<h1 className="sr-only">Apply for a property</h1>
|
|
211
232
|
<div className="mb-4">
|
|
212
233
|
<Link
|
|
213
234
|
to={propertyId ? `/property/${propertyId}` : "/properties"}
|
|
@@ -226,7 +247,7 @@ export default function Application() {
|
|
|
226
247
|
</div>
|
|
227
248
|
<div className="min-w-0 flex-1">
|
|
228
249
|
<h2 className="mb-1.5 text-2xl font-semibold text-foreground">
|
|
229
|
-
{propertyName ?? "
|
|
250
|
+
{propertyName ?? "Application details"}
|
|
230
251
|
</h2>
|
|
231
252
|
<p className="text-sm text-muted-foreground">
|
|
232
253
|
{propertyAddress ??
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback, type SubmitEvent } from "react";
|
|
1
|
+
import { useState, useCallback, useEffect, useRef, type SubmitEvent } from "react";
|
|
2
2
|
import { Link } from "react-router";
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { Input } from "@/components/ui/input";
|
|
@@ -13,23 +13,31 @@ import { useAsyncData } from "@/hooks/useAsyncData";
|
|
|
13
13
|
import type { UserInfo } from "@/api/leads/leadApi";
|
|
14
14
|
|
|
15
15
|
function SuccessCard() {
|
|
16
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
containerRef.current?.focus();
|
|
19
|
+
}, []);
|
|
16
20
|
return (
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
<div ref={containerRef} tabIndex={-1} role="status" aria-live="polite" className="outline-none">
|
|
22
|
+
<Card>
|
|
23
|
+
<CardHeader>
|
|
24
|
+
<CardTitle role="heading" aria-level={2} className="text-xl">
|
|
25
|
+
Message sent
|
|
26
|
+
</CardTitle>
|
|
27
|
+
</CardHeader>
|
|
28
|
+
<CardContent className="space-y-4">
|
|
29
|
+
<p className="text-muted-foreground">
|
|
30
|
+
Thank you for reaching out. We've received your message and will get back to you soon.
|
|
31
|
+
</p>
|
|
32
|
+
<Button asChild variant="outline">
|
|
33
|
+
<Link to="/">Back to Home</Link>
|
|
34
|
+
</Button>
|
|
35
|
+
<Button asChild>
|
|
36
|
+
<Link to="/contact">Send another message</Link>
|
|
37
|
+
</Button>
|
|
38
|
+
</CardContent>
|
|
39
|
+
</Card>
|
|
40
|
+
</div>
|
|
33
41
|
);
|
|
34
42
|
}
|
|
35
43
|
|
|
@@ -60,13 +68,19 @@ function ContactForm({
|
|
|
60
68
|
return (
|
|
61
69
|
<Card className="relative">
|
|
62
70
|
{loading && (
|
|
63
|
-
<div
|
|
64
|
-
|
|
71
|
+
<div
|
|
72
|
+
role="status"
|
|
73
|
+
aria-live="polite"
|
|
74
|
+
className="absolute inset-0 z-10 flex items-center justify-center rounded-[inherit] bg-muted/60"
|
|
75
|
+
>
|
|
76
|
+
<Loader2 className="size-8 animate-spin text-muted-foreground" aria-hidden />
|
|
65
77
|
<span className="sr-only">Loading contact form…</span>
|
|
66
78
|
</div>
|
|
67
79
|
)}
|
|
68
80
|
<CardHeader>
|
|
69
|
-
<CardTitle className="text-lg">
|
|
81
|
+
<CardTitle role="heading" aria-level={2} className="text-lg">
|
|
82
|
+
Send a message
|
|
83
|
+
</CardTitle>
|
|
70
84
|
</CardHeader>
|
|
71
85
|
<CardContent>
|
|
72
86
|
<form
|
|
@@ -152,7 +166,7 @@ function ContactForm({
|
|
|
152
166
|
Something went wrong. Please try again later.
|
|
153
167
|
</p>
|
|
154
168
|
)}
|
|
155
|
-
<Button type="submit" disabled={submitting} className="bg-teal-
|
|
169
|
+
<Button type="submit" disabled={submitting} className="bg-teal-700 hover:bg-teal-800">
|
|
156
170
|
{submitting ? "Sending…" : "Send message"}
|
|
157
171
|
</Button>
|
|
158
172
|
</form>
|
|
@@ -14,6 +14,7 @@ export default function Dashboard() {
|
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
16
|
<div className="mx-auto flex max-w-[1100px] flex-col gap-6 lg:flex-row">
|
|
17
|
+
<h1 className="sr-only">Dashboard</h1>
|
|
17
18
|
<div className="min-w-0 flex-1">
|
|
18
19
|
<Card className="border-gray-200 p-6 shadow-sm">
|
|
19
20
|
<div className="mb-6 flex items-center justify-between">
|
|
@@ -212,7 +212,7 @@ export default function Home() {
|
|
|
212
212
|
<Button
|
|
213
213
|
type="submit"
|
|
214
214
|
size="lg"
|
|
215
|
-
className="min-h-12 shrink-0 cursor-pointer rounded-xl bg-primary px-8 font-medium text-primary-foreground shadow-md transition-colors duration-200 hover:bg-primary/90"
|
|
215
|
+
className="min-h-12 shrink-0 cursor-pointer rounded-xl bg-primary px-8 font-medium text-primary-foreground shadow-md transition-colors duration-200 hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2"
|
|
216
216
|
>
|
|
217
217
|
Find Home
|
|
218
218
|
</Button>
|
|
@@ -250,7 +250,10 @@ export default function Home() {
|
|
|
250
250
|
amenitiesMap={amenitiesMap}
|
|
251
251
|
/>
|
|
252
252
|
) : (
|
|
253
|
-
<div
|
|
253
|
+
<div
|
|
254
|
+
role="status"
|
|
255
|
+
className="rounded-xl bg-white/60 px-6 py-10 text-center text-muted-foreground"
|
|
256
|
+
>
|
|
254
257
|
<p>No featured properties at the moment.</p>
|
|
255
258
|
<Button asChild className="mt-3">
|
|
256
259
|
<Link to="/properties">Browse all properties</Link>
|
|
@@ -292,7 +295,7 @@ export default function Home() {
|
|
|
292
295
|
href="tel:18005550120"
|
|
293
296
|
className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-foreground transition-colors duration-200 hover:underline"
|
|
294
297
|
>
|
|
295
|
-
<Phone className="size-4" />
|
|
298
|
+
<Phone className="size-4" aria-hidden />
|
|
296
299
|
Talk to an Agent 1.800.555.0120
|
|
297
300
|
</a>
|
|
298
301
|
</div>
|
|
@@ -357,13 +360,13 @@ export default function Home() {
|
|
|
357
360
|
</Button>
|
|
358
361
|
<Button asChild variant="outline" size="lg" className="rounded-full gap-2">
|
|
359
362
|
<Link to="/contact">
|
|
360
|
-
<MessageCircle className="size-4" />
|
|
363
|
+
<MessageCircle className="size-4" aria-hidden />
|
|
361
364
|
Contact
|
|
362
365
|
</Link>
|
|
363
366
|
</Button>
|
|
364
367
|
<Button asChild variant="ghost" size="lg" className="rounded-full gap-2">
|
|
365
368
|
<Link to="/contact">
|
|
366
|
-
<HelpCircle className="size-4" />
|
|
369
|
+
<HelpCircle className="size-4" aria-hidden />
|
|
367
370
|
Help
|
|
368
371
|
</Link>
|
|
369
372
|
</Button>
|
|
@@ -435,14 +438,14 @@ export default function Home() {
|
|
|
435
438
|
value={footerEmail}
|
|
436
439
|
onChange={(e: ChangeEvent<HTMLInputElement>) => setFooterEmail(e.target.value)}
|
|
437
440
|
disabled={newsletterSubmitting}
|
|
438
|
-
className="max-w-xs border-teal-600 bg-teal-900/50 text-white placeholder:text-teal-300"
|
|
441
|
+
className="max-w-xs border-teal-600 bg-teal-900/50 text-white placeholder:text-teal-300 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-teal-800"
|
|
439
442
|
aria-label="Email for updates"
|
|
440
443
|
/>
|
|
441
444
|
<Button
|
|
442
445
|
type="submit"
|
|
443
446
|
size="icon"
|
|
444
447
|
aria-label="Subscribe to newsletter"
|
|
445
|
-
className="shrink-0 bg-teal-600 hover:bg-teal-700"
|
|
448
|
+
className="shrink-0 bg-teal-600 hover:bg-teal-700 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-teal-800"
|
|
446
449
|
disabled={newsletterSubmitting}
|
|
447
450
|
>
|
|
448
451
|
<Send className="size-4" aria-hidden />
|
|
@@ -457,7 +460,7 @@ export default function Home() {
|
|
|
457
460
|
? "sr-only"
|
|
458
461
|
: newsletterMessage.type === "success"
|
|
459
462
|
? "text-teal-200"
|
|
460
|
-
: "text-red-
|
|
463
|
+
: "text-red-200"
|
|
461
464
|
}
|
|
462
465
|
>
|
|
463
466
|
{newsletterMessage?.text ?? ""}
|
|
@@ -58,6 +58,7 @@ const PAGINATION_CONFIG: PaginationConfig = {
|
|
|
58
58
|
function MaintenanceSkeleton() {
|
|
59
59
|
return (
|
|
60
60
|
<div className="mx-auto max-w-[900px]" role="status">
|
|
61
|
+
<h1 className="sr-only">Maintenance</h1>
|
|
61
62
|
<Card className="mb-6 rounded-2xl shadow-md">
|
|
62
63
|
<CardHeader>
|
|
63
64
|
<Skeleton className="h-6 w-1/3" />
|
|
@@ -173,8 +174,8 @@ export default function Maintenance() {
|
|
|
173
174
|
setPriority("Standard");
|
|
174
175
|
setDateRequested(new Date().toISOString().slice(0, 10));
|
|
175
176
|
setRefreshCounter((c) => c + 1);
|
|
176
|
-
} catch
|
|
177
|
-
setSubmitError(
|
|
177
|
+
} catch {
|
|
178
|
+
setSubmitError("Something went wrong. Please try again later.");
|
|
178
179
|
} finally {
|
|
179
180
|
setSubmitting(false);
|
|
180
181
|
}
|
|
@@ -186,9 +187,12 @@ export default function Maintenance() {
|
|
|
186
187
|
|
|
187
188
|
return (
|
|
188
189
|
<div className="mx-auto max-w-[900px]">
|
|
190
|
+
<h1 className="sr-only">Maintenance</h1>
|
|
189
191
|
<Card className="mb-6 rounded-2xl shadow-md">
|
|
190
192
|
<CardHeader>
|
|
191
|
-
<CardTitle className="text-2xl text-primary">
|
|
193
|
+
<CardTitle role="heading" aria-level={2} className="text-2xl text-primary">
|
|
194
|
+
New maintenance request
|
|
195
|
+
</CardTitle>
|
|
192
196
|
</CardHeader>
|
|
193
197
|
<CardContent className="space-y-4">
|
|
194
198
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
@@ -303,11 +307,11 @@ export default function Maintenance() {
|
|
|
303
307
|
</div>
|
|
304
308
|
{submitError && (
|
|
305
309
|
<p className="text-sm text-destructive" role="alert">
|
|
306
|
-
|
|
310
|
+
{submitError}
|
|
307
311
|
</p>
|
|
308
312
|
)}
|
|
309
313
|
{submitSuccess && (
|
|
310
|
-
<p className="text-sm text-green-
|
|
314
|
+
<p className="text-sm text-green-700" role="status">
|
|
311
315
|
Request submitted. It will appear in the list below.
|
|
312
316
|
</p>
|
|
313
317
|
)}
|
package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertyDetails.tsx
CHANGED
|
@@ -39,6 +39,7 @@ function formatDate(val: string | null): string {
|
|
|
39
39
|
function PropertyDetailsSkeleton() {
|
|
40
40
|
return (
|
|
41
41
|
<div className="mx-auto max-w-[900px]" role="status">
|
|
42
|
+
<h1 className="sr-only">Property details</h1>
|
|
42
43
|
<Skeleton className="mb-4 h-4 w-32" />
|
|
43
44
|
|
|
44
45
|
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
@@ -141,6 +142,7 @@ export default function PropertyDetails() {
|
|
|
141
142
|
if (error) {
|
|
142
143
|
return (
|
|
143
144
|
<div className="mx-auto max-w-[900px]">
|
|
145
|
+
<h1 className="sr-only">Property details</h1>
|
|
144
146
|
<div className="mb-4">
|
|
145
147
|
<Link to="/properties" className="text-sm text-primary no-underline hover:underline">
|
|
146
148
|
← Back to listings
|
|
@@ -148,7 +150,9 @@ export default function PropertyDetails() {
|
|
|
148
150
|
</div>
|
|
149
151
|
<Card className="rounded-2xl border border-border shadow-sm">
|
|
150
152
|
<CardContent className="pt-6">
|
|
151
|
-
<p className="text-destructive"
|
|
153
|
+
<p className="text-destructive" role="alert">
|
|
154
|
+
Something went wrong. Please try again later.
|
|
155
|
+
</p>
|
|
152
156
|
</CardContent>
|
|
153
157
|
</Card>
|
|
154
158
|
</div>
|
|
@@ -158,6 +162,7 @@ export default function PropertyDetails() {
|
|
|
158
162
|
if (!property && id) {
|
|
159
163
|
return (
|
|
160
164
|
<div className="mx-auto max-w-[900px]">
|
|
165
|
+
<h1 className="sr-only">Property details</h1>
|
|
161
166
|
<div className="mb-4">
|
|
162
167
|
<Link to="/properties" className="text-sm text-primary no-underline hover:underline">
|
|
163
168
|
← Back to listings
|
|
@@ -165,7 +170,9 @@ export default function PropertyDetails() {
|
|
|
165
170
|
</div>
|
|
166
171
|
<Card className="rounded-2xl border border-border shadow-sm">
|
|
167
172
|
<CardContent className="pt-6">
|
|
168
|
-
<p className="text-destructive"
|
|
173
|
+
<p className="text-destructive" role="status">
|
|
174
|
+
Listing not found.
|
|
175
|
+
</p>
|
|
169
176
|
</CardContent>
|
|
170
177
|
</Card>
|
|
171
178
|
</div>
|
|
@@ -253,25 +260,25 @@ export default function PropertyDetails() {
|
|
|
253
260
|
<span className="text-xl font-semibold text-primary-foreground">
|
|
254
261
|
{property?.Bedrooms__c?.value ?? "—"}
|
|
255
262
|
</span>
|
|
256
|
-
<span className="text-xs text-primary-foreground
|
|
263
|
+
<span className="text-xs text-primary-foreground">Bedrooms</span>
|
|
257
264
|
</div>
|
|
258
265
|
<div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
|
|
259
266
|
<span className="text-xl font-semibold text-primary-foreground">
|
|
260
267
|
{property?.Bathrooms__c?.value ?? "—"}
|
|
261
268
|
</span>
|
|
262
|
-
<span className="text-xs text-primary-foreground
|
|
269
|
+
<span className="text-xs text-primary-foreground">Baths</span>
|
|
263
270
|
</div>
|
|
264
271
|
<div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
|
|
265
272
|
<span className="text-xl font-semibold text-primary-foreground">
|
|
266
273
|
{property?.Sq_Ft__c?.value ?? "—"}
|
|
267
274
|
</span>
|
|
268
|
-
<span className="text-xs text-primary-foreground
|
|
275
|
+
<span className="text-xs text-primary-foreground">Square Feet</span>
|
|
269
276
|
</div>
|
|
270
277
|
<div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
|
|
271
278
|
<span className="text-xl font-semibold text-primary-foreground">
|
|
272
279
|
{listing?.Listing_Status__c?.value ?? "Now"}
|
|
273
280
|
</span>
|
|
274
|
-
<span className="text-xs text-primary-foreground
|
|
281
|
+
<span className="text-xs text-primary-foreground">Available</span>
|
|
275
282
|
</div>
|
|
276
283
|
</div>
|
|
277
284
|
{property?.Type__c?.value && (
|
|
@@ -287,7 +294,9 @@ export default function PropertyDetails() {
|
|
|
287
294
|
{costs.length > 0 && (
|
|
288
295
|
<Card className="mb-4 rounded-2xl border border-border shadow-sm">
|
|
289
296
|
<CardHeader>
|
|
290
|
-
<CardTitle className="text-base font-semibold">
|
|
297
|
+
<CardTitle role="heading" aria-level={2} className="text-base font-semibold">
|
|
298
|
+
Related costs
|
|
299
|
+
</CardTitle>
|
|
291
300
|
</CardHeader>
|
|
292
301
|
<CardContent>
|
|
293
302
|
<ul className="space-y-2">
|
|
@@ -322,7 +331,9 @@ export default function PropertyDetails() {
|
|
|
322
331
|
{features.length > 0 && (
|
|
323
332
|
<Card className="mb-4 rounded-2xl border border-border shadow-sm">
|
|
324
333
|
<CardHeader>
|
|
325
|
-
<CardTitle className="text-base font-semibold">
|
|
334
|
+
<CardTitle role="heading" aria-level={2} className="text-base font-semibold">
|
|
335
|
+
Features & amenities
|
|
336
|
+
</CardTitle>
|
|
326
337
|
</CardHeader>
|
|
327
338
|
<CardContent>
|
|
328
339
|
<div className="flex flex-wrap gap-1.5">
|
package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx
CHANGED
|
@@ -339,6 +339,7 @@ export default function PropertySearch() {
|
|
|
339
339
|
|
|
340
340
|
return (
|
|
341
341
|
<div className="flex h-[calc(100vh-4rem)] min-h-[500px] flex-col">
|
|
342
|
+
<h1 className="sr-only">Property Search</h1>
|
|
342
343
|
<PropertySearchFilters
|
|
343
344
|
searchQuery={searchQuery}
|
|
344
345
|
onSearchQueryChange={setSearchQuery}
|
|
@@ -380,7 +381,12 @@ export default function PropertySearch() {
|
|
|
380
381
|
{searchQuery.trim() ? ` matching "${searchQuery.trim()}"` : ""}
|
|
381
382
|
</h2>
|
|
382
383
|
<div className="flex flex-wrap items-center gap-2">
|
|
383
|
-
<div
|
|
384
|
+
<div
|
|
385
|
+
role="status"
|
|
386
|
+
aria-live="polite"
|
|
387
|
+
aria-atomic="true"
|
|
388
|
+
className="text-sm text-muted-foreground"
|
|
389
|
+
>
|
|
384
390
|
{apiUnavailable ? (
|
|
385
391
|
"Placeholder (API unavailable)"
|
|
386
392
|
) : resultsLoading ? (
|
|
@@ -402,7 +408,12 @@ export default function PropertySearch() {
|
|
|
402
408
|
)}
|
|
403
409
|
</div>
|
|
404
410
|
</div>
|
|
405
|
-
<div
|
|
411
|
+
<div
|
|
412
|
+
className="flex-1 overflow-y-auto p-4"
|
|
413
|
+
tabIndex={0}
|
|
414
|
+
role="region"
|
|
415
|
+
aria-label="Property listings"
|
|
416
|
+
>
|
|
406
417
|
{apiUnavailable ? (
|
|
407
418
|
<PropertySearchPlaceholder message="Search is temporarily unavailable." />
|
|
408
419
|
) : resultsLoading ? (
|
|
@@ -412,12 +423,12 @@ export default function PropertySearch() {
|
|
|
412
423
|
))}
|
|
413
424
|
</div>
|
|
414
425
|
) : results.length === 0 ? (
|
|
415
|
-
<div className="py-12 text-center">
|
|
426
|
+
<div role="status" className="py-12 text-center">
|
|
416
427
|
<p className="mb-2 font-medium">No results found</p>
|
|
417
428
|
<p className="text-sm text-muted-foreground">Try adjusting search or filters</p>
|
|
418
429
|
</div>
|
|
419
430
|
) : visibleResults.length === 0 && mapBounds != null ? (
|
|
420
|
-
<div className="py-12 text-center">
|
|
431
|
+
<div role="status" className="py-12 text-center">
|
|
421
432
|
<p className="mb-2 font-medium">No listings in this map area</p>
|
|
422
433
|
<p className="text-sm text-muted-foreground">
|
|
423
434
|
Pan or zoom to see results, or clear the map filter
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
--accent-foreground: oklch(0.21 0.02 260);
|
|
155
155
|
/* Border / input: soft teal-tinted gray */
|
|
156
156
|
--border: oklch(0.91 0.02 192);
|
|
157
|
-
--input: oklch(0.
|
|
157
|
+
--input: oklch(0.62 0.03 192);
|
|
158
158
|
--ring: oklch(0.52 0.09 192);
|
|
159
159
|
/* Slightly larger radius for soft, friendly feel (screenshots use 16–24px) */
|
|
160
160
|
--radius: 0.75rem;
|
package/dist/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/ui-bundle-template-base-sfdx-project",
|
|
3
|
-
"version": "11.56.
|
|
3
|
+
"version": "11.56.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@salesforce/ui-bundle-template-base-sfdx-project",
|
|
9
|
-
"version": "11.56.
|
|
9
|
+
"version": "11.56.4",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"fast-xml-parser": "^5.9.3",
|
package/dist/package.json
CHANGED