@salesforce/webapp-template-app-react-sample-b2x-experimental 1.96.0 → 1.97.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/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
+ # [1.97.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.96.0...v1.97.0) (2026-03-12)
7
+
8
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
9
+
10
+
11
+
12
+
13
+
6
14
  # [1.96.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.95.0...v1.96.0) (2026-03-12)
7
15
 
8
16
  **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
@@ -15,8 +15,8 @@
15
15
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@salesforce/sdk-data": "^1.96.0",
19
- "@salesforce/webapp-experimental": "^1.96.0",
18
+ "@salesforce/sdk-data": "^1.97.0",
19
+ "@salesforce/webapp-experimental": "^1.97.0",
20
20
  "@tailwindcss/vite": "^4.1.17",
21
21
  "@tanstack/react-form": "^1.28.5",
22
22
  "@types/leaflet": "^1.9.21",
@@ -43,7 +43,7 @@
43
43
  "@graphql-eslint/eslint-plugin": "^4.1.0",
44
44
  "@graphql-tools/utils": "^11.0.0",
45
45
  "@playwright/test": "^1.49.0",
46
- "@salesforce/vite-plugin-webapp-experimental": "^1.96.0",
46
+ "@salesforce/vite-plugin-webapp-experimental": "^1.97.0",
47
47
  "@testing-library/jest-dom": "^6.6.3",
48
48
  "@testing-library/react": "^16.1.0",
49
49
  "@testing-library/user-event": "^14.5.2",
@@ -0,0 +1,221 @@
1
+ import { useState } from "react";
2
+ import { Card } from "@/components/ui/card";
3
+ import { Skeleton } from "@/components/ui/skeleton";
4
+ import { useWeather, type WeatherCurrent, type WeatherHour } from "@/hooks/useWeather";
5
+ import {
6
+ Sun,
7
+ Cloud,
8
+ CloudSun,
9
+ CloudRain,
10
+ CloudSnow,
11
+ CloudLightning,
12
+ CloudFog,
13
+ CloudDrizzle,
14
+ Wind,
15
+ Droplets,
16
+ type LucideIcon,
17
+ } from "lucide-react";
18
+
19
+ type ForecastTab = "today" | "tomorrow" | "next3days";
20
+
21
+ const FORECAST_TABS: { key: ForecastTab; label: string }[] = [
22
+ { key: "today", label: "Today" },
23
+ { key: "tomorrow", label: "Tomorrow" },
24
+ { key: "next3days", label: "Next 3 Days" },
25
+ ];
26
+
27
+ function getWeatherIcon(code: number, className = "h-16 w-16 text-gray-400") {
28
+ const props = { className, "aria-hidden": true as const };
29
+ if (code <= 1) return <Sun {...props} />;
30
+ if (code === 2) return <CloudSun {...props} />;
31
+ if (code === 3) return <Cloud {...props} />;
32
+ if (code === 45 || code === 48) return <CloudFog {...props} />;
33
+ if (code >= 51 && code <= 55) return <CloudDrizzle {...props} />;
34
+ if (code >= 61 && code <= 65) return <CloudRain {...props} />;
35
+ if (code >= 71 && code <= 77) return <CloudSnow {...props} />;
36
+ if (code >= 80 && code <= 82) return <CloudRain {...props} />;
37
+ if (code >= 85 && code <= 86) return <CloudSnow {...props} />;
38
+ if (code >= 95) return <CloudLightning {...props} />;
39
+ return <Sun {...props} />;
40
+ }
41
+
42
+ const Divider = () => <div className="my-5 border-t border-gray-200" />;
43
+
44
+ function WeatherSkeleton() {
45
+ return (
46
+ <div className="mt-5 space-y-5" aria-hidden="true">
47
+ <Skeleton className="h-4 w-32" />
48
+
49
+ <div className="flex items-center justify-between">
50
+ <div className="space-y-2">
51
+ <Skeleton className="h-4 w-20" />
52
+ <Skeleton className="h-14 w-32" />
53
+ </div>
54
+ <Skeleton className="h-20 w-20 rounded-full" />
55
+ </div>
56
+
57
+ <div className="border-t border-gray-200" />
58
+
59
+ <div className="grid grid-cols-3 gap-2">
60
+ {[0, 1, 2].map((i) => (
61
+ <div key={i} className="flex flex-col items-center gap-1.5">
62
+ <Skeleton className="h-5 w-5 rounded-full" />
63
+ <Skeleton className="h-4 w-14" />
64
+ <Skeleton className="h-3 w-10" />
65
+ </div>
66
+ ))}
67
+ </div>
68
+
69
+ <div className="border-t border-gray-200" />
70
+
71
+ <div className="flex gap-6">
72
+ <Skeleton className="h-4 w-12" />
73
+ <Skeleton className="h-4 w-16" />
74
+ <Skeleton className="h-4 w-20" />
75
+ </div>
76
+
77
+ <div className="flex gap-3">
78
+ {[0, 1, 2, 3].map((i) => (
79
+ <div
80
+ key={i}
81
+ className="flex w-[70px] flex-col items-center gap-1.5 rounded-2xl border border-gray-100 px-3 py-3"
82
+ >
83
+ <Skeleton className="h-3 w-10" />
84
+ <Skeleton className="h-5 w-5 rounded-full" />
85
+ <Skeleton className="h-4 w-8" />
86
+ </div>
87
+ ))}
88
+ </div>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ function StatItem({
94
+ icon: Icon,
95
+ value,
96
+ label,
97
+ }: {
98
+ icon: LucideIcon;
99
+ value: string;
100
+ label: string;
101
+ }) {
102
+ return (
103
+ <div className="flex flex-col items-center gap-1">
104
+ <Icon className="h-5 w-5 text-gray-400" />
105
+ <p className="text-base font-semibold text-primary">{value}</p>
106
+ <p className="text-xs text-muted-foreground">{label}</p>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ function CurrentConditions({ current }: { current: WeatherCurrent }) {
112
+ return (
113
+ <>
114
+ <p className="text-sm text-muted-foreground">
115
+ {new Date().toLocaleDateString("en-US", {
116
+ day: "numeric",
117
+ month: "long",
118
+ year: "numeric",
119
+ })}
120
+ </p>
121
+
122
+ <div className="mt-2 flex items-center justify-between">
123
+ <div>
124
+ <p className="text-base text-foreground">{current.description}</p>
125
+ <p className="text-6xl font-bold tracking-tight text-foreground">
126
+ {current.tempF}
127
+ <span className="align-top text-3xl">°</span>
128
+ <span className="text-4xl">F</span>
129
+ </p>
130
+ </div>
131
+ {getWeatherIcon(current.weatherCode, "h-20 w-20 text-gray-400")}
132
+ </div>
133
+
134
+ <Divider />
135
+
136
+ <div className="grid grid-cols-3 gap-2 text-center">
137
+ <StatItem icon={Wind} value={`${current.windSpeedMph} mph`} label="Wind" />
138
+ <StatItem icon={Droplets} value={`${current.humidity}%`} label="Humidity" />
139
+ <StatItem icon={CloudRain} value={`${current.precipitationProbability}%`} label="Rain" />
140
+ </div>
141
+ </>
142
+ );
143
+ }
144
+
145
+ function ForecastTabs({
146
+ activeTab,
147
+ onTabChange,
148
+ }: {
149
+ activeTab: ForecastTab;
150
+ onTabChange: (tab: ForecastTab) => void;
151
+ }) {
152
+ return (
153
+ <div className="flex gap-6">
154
+ {FORECAST_TABS.map(({ key, label }) => (
155
+ <button key={key} onClick={() => onTabChange(key)} className="flex flex-col items-center">
156
+ <span
157
+ className={`text-sm font-semibold ${activeTab === key ? "text-foreground" : "text-muted-foreground"}`}
158
+ >
159
+ {label}
160
+ </span>
161
+ {activeTab === key && <span className="mt-1 h-1.5 w-1.5 rounded-full bg-foreground" />}
162
+ </button>
163
+ ))}
164
+ </div>
165
+ );
166
+ }
167
+
168
+ function HourlyForecast({ hours }: { hours: WeatherHour[] }) {
169
+ if (hours.length === 0) return null;
170
+
171
+ return (
172
+ <div className="mt-4 flex gap-3 overflow-x-auto pb-1">
173
+ {hours.map((h) => (
174
+ <div
175
+ key={h.time}
176
+ className="flex min-w-[70px] flex-col items-center gap-1.5 rounded-2xl border border-gray-100 bg-gray-50/80 px-3 py-3"
177
+ >
178
+ <p className="whitespace-nowrap text-xs text-muted-foreground">{h.time}</p>
179
+ {getWeatherIcon(h.weatherCode, "h-5 w-5 text-gray-500")}
180
+ <p className="text-base font-semibold text-foreground">{h.tempF}°</p>
181
+ </div>
182
+ ))}
183
+ </div>
184
+ );
185
+ }
186
+
187
+ export function WeatherWidget() {
188
+ const { data: weather, loading, error } = useWeather();
189
+ const [activeTab, setActiveTab] = useState<ForecastTab>("today");
190
+
191
+ const activeHourly = !weather
192
+ ? []
193
+ : activeTab === "tomorrow"
194
+ ? weather.tomorrowHourly
195
+ : activeTab === "next3days"
196
+ ? weather.next3DaysHourly
197
+ : weather.todayHourly;
198
+
199
+ return (
200
+ <Card className="rounded-2xl border-0 p-6 shadow-md">
201
+ <h2 className="text-lg font-semibold text-primary">Weather</h2>
202
+
203
+ {loading && <WeatherSkeleton />}
204
+
205
+ {error && (
206
+ <p className="mt-4 text-sm text-destructive" role="alert">
207
+ {error}
208
+ </p>
209
+ )}
210
+
211
+ {!loading && !error && weather && (
212
+ <div className="mt-5">
213
+ <CurrentConditions current={weather.current} />
214
+ <Divider />
215
+ <ForecastTabs activeTab={activeTab} onTabChange={setActiveTab} />
216
+ <HourlyForecast hours={activeHourly} />
217
+ </div>
218
+ )}
219
+ </Card>
220
+ );
221
+ }
@@ -49,16 +49,21 @@ export interface WeatherCurrent {
49
49
  humidity: number;
50
50
  windSpeedKmh: number;
51
51
  windSpeedMph: number;
52
+ weatherCode: number;
53
+ precipitationProbability: number;
52
54
  }
53
55
 
54
56
  export interface WeatherHour {
55
- time: string; // ISO or "h:mm a"
57
+ time: string;
56
58
  tempF: number;
59
+ weatherCode: number;
57
60
  }
58
61
 
59
62
  export interface WeatherData {
60
63
  current: WeatherCurrent;
61
- hourly: WeatherHour[];
64
+ todayHourly: WeatherHour[];
65
+ tomorrowHourly: WeatherHour[];
66
+ next3DaysHourly: WeatherHour[];
62
67
  timezone: string;
63
68
  }
64
69
 
@@ -70,9 +75,9 @@ async function fetchWeather(lat: number, lng: number): Promise<WeatherData> {
70
75
  "current",
71
76
  "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m",
72
77
  );
73
- url.searchParams.set("hourly", "temperature_2m");
78
+ url.searchParams.set("hourly", "temperature_2m,weather_code,precipitation_probability");
74
79
  url.searchParams.set("timezone", "America/Los_Angeles");
75
- url.searchParams.set("forecast_days", "1");
80
+ url.searchParams.set("forecast_days", "4");
76
81
 
77
82
  const res = await fetch(url.toString());
78
83
  if (!res.ok) throw new Error(`Weather failed: ${res.status}`);
@@ -83,7 +88,12 @@ async function fetchWeather(lat: number, lng: number): Promise<WeatherData> {
83
88
  weather_code?: number;
84
89
  wind_speed_10m?: number;
85
90
  };
86
- hourly?: { time?: string[]; temperature_2m?: (number | null)[] };
91
+ hourly?: {
92
+ time?: string[];
93
+ temperature_2m?: (number | null)[];
94
+ weather_code?: (number | null)[];
95
+ precipitation_probability?: (number | null)[];
96
+ };
87
97
  timezone?: string;
88
98
  };
89
99
 
@@ -94,38 +104,65 @@ async function fetchWeather(lat: number, lng: number): Promise<WeatherData> {
94
104
  const windMph = Math.round(windKmh * 0.621371 * 10) / 10;
95
105
  const code = cur.weather_code ?? 0;
96
106
 
97
- const hourly: WeatherHour[] = [];
98
107
  const times = data.hourly?.time ?? [];
99
108
  const temps = data.hourly?.temperature_2m ?? [];
109
+ const hourlyCodes = data.hourly?.weather_code ?? [];
110
+ const precipProbs = data.hourly?.precipitation_probability ?? [];
100
111
  const now = new Date();
101
- for (let i = 0; i < Math.min(12, times.length); i++) {
112
+
113
+ let currentPrecipProb = 0;
114
+ for (let i = 0; i < times.length; i++) {
102
115
  const t = times[i];
103
- const temp = temps[i];
104
- if (t && temp != null) {
105
- const d = new Date(t);
106
- if (d >= now) {
107
- hourly.push({
108
- time: d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }),
109
- tempF: celsiusToFahrenheit(temp),
110
- });
111
- }
116
+ if (t && new Date(t) >= now) {
117
+ currentPrecipProb = precipProbs[i] ?? 0;
118
+ break;
112
119
  }
113
120
  }
114
- // If no future hours, use first 4 from response
115
- if (hourly.length === 0) {
116
- for (let i = 0; i < Math.min(4, times.length); i++) {
117
- const t = times[i];
118
- const temp = temps[i];
119
- if (t && temp != null) {
120
- hourly.push({
121
- time: new Date(t).toLocaleTimeString("en-US", {
122
- hour: "numeric",
123
- minute: "2-digit",
124
- hour12: true,
125
- }),
126
- tempF: celsiusToFahrenheit(temp),
127
- });
128
- }
121
+
122
+ const todayDateStr = now.toISOString().split("T")[0];
123
+ const tomorrowDate = new Date(now);
124
+ tomorrowDate.setDate(tomorrowDate.getDate() + 1);
125
+ const tomorrowDateStr = tomorrowDate.toISOString().split("T")[0];
126
+
127
+ const todayHourly: WeatherHour[] = [];
128
+ const tomorrowHourly: WeatherHour[] = [];
129
+ const next3DaysHourly: WeatherHour[] = [];
130
+
131
+ for (let i = 0; i < times.length; i++) {
132
+ const t = times[i];
133
+ const temp = temps[i];
134
+ if (!t || temp == null) continue;
135
+
136
+ const d = new Date(t);
137
+ const dateStr = t.split("T")[0];
138
+ const wc = hourlyCodes[i] ?? 0;
139
+ const langCode = navigator.language ?? "en-US";
140
+
141
+ if (dateStr === todayDateStr && d > now) {
142
+ todayHourly.push({
143
+ time: d.toLocaleTimeString(langCode, { hour: "numeric" }),
144
+ tempF: celsiusToFahrenheit(temp),
145
+ weatherCode: wc,
146
+ });
147
+ }
148
+
149
+ if (dateStr === tomorrowDateStr && d.getHours() % 3 === 0) {
150
+ tomorrowHourly.push({
151
+ time: d.toLocaleTimeString(langCode, { hour: "numeric" }),
152
+ tempF: celsiusToFahrenheit(temp),
153
+ weatherCode: wc,
154
+ });
155
+ }
156
+
157
+ if (dateStr > todayDateStr && d.getHours() % 6 === 0) {
158
+ next3DaysHourly.push({
159
+ time:
160
+ d.toLocaleDateString(langCode, { weekday: "short" }) +
161
+ " " +
162
+ d.toLocaleTimeString(langCode, { hour: "numeric" }),
163
+ tempF: celsiusToFahrenheit(temp),
164
+ weatherCode: wc,
165
+ });
129
166
  }
130
167
  }
131
168
 
@@ -136,8 +173,12 @@ async function fetchWeather(lat: number, lng: number): Promise<WeatherData> {
136
173
  humidity,
137
174
  windSpeedKmh: windKmh,
138
175
  windSpeedMph: windMph,
176
+ weatherCode: code,
177
+ precipitationProbability: currentPrecipProb,
139
178
  },
140
- hourly: hourly.slice(0, 6),
179
+ todayHourly,
180
+ tomorrowHourly,
181
+ next3DaysHourly,
141
182
  timezone: data.timezone ?? "America/Los_Angeles",
142
183
  };
143
184
  }
@@ -1,11 +1,10 @@
1
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
1
+ import { Card, CardContent } from "@/components/ui/card";
2
2
  import { Link } from "react-router";
3
3
  import { MaintenanceRequestIcon } from "@/components/MaintenanceRequestIcon";
4
- import { useWeather } from "@/hooks/useWeather";
4
+ import { WeatherWidget } from "@/components/WeatherWidget";
5
5
  import { useMaintenanceRequests } from "@/hooks/useMaintenanceRequests";
6
6
 
7
7
  export default function Dashboard() {
8
- const { data: weather, loading: weatherLoading, error: weatherError } = useWeather();
9
8
  const {
10
9
  requests: maintenanceRequests,
11
10
  loading: maintenanceLoading,
@@ -19,7 +18,7 @@ export default function Dashboard() {
19
18
  <div className="min-w-0 flex-1">
20
19
  <Card className="border-gray-200 p-6 shadow-sm">
21
20
  <div className="mb-6 flex items-center justify-between">
22
- <h2 className="text-lg font-semibold uppercase tracking-wide text-primary">
21
+ <h2 className="text-lg font-semibold tracking-wide text-primary">
23
22
  Maintenance Requests
24
23
  </h2>
25
24
  <Link
@@ -89,57 +88,9 @@ export default function Dashboard() {
89
88
  </CardContent>
90
89
  </Card>
91
90
  </div>
92
- {/* Weather: stays visible and clear */}
91
+ {/* Weather */}
93
92
  <div className="w-full shrink-0 lg:w-[320px]">
94
- <Card className="rounded-2xl shadow-md">
95
- <CardHeader>
96
- <CardTitle className="text-primary">Weather</CardTitle>
97
- <p className="text-sm text-muted-foreground">
98
- {new Date().toLocaleDateString("en-US", {
99
- weekday: "short",
100
- month: "short",
101
- day: "numeric",
102
- year: "numeric",
103
- })}
104
- </p>
105
- </CardHeader>
106
- <CardContent className="space-y-4">
107
- {weatherLoading && <p className="text-sm text-muted-foreground">Loading weather…</p>}
108
- {weatherError && (
109
- <p className="text-sm text-destructive" role="alert">
110
- {weatherError}
111
- </p>
112
- )}
113
- {!weatherLoading && !weatherError && weather && (
114
- <>
115
- <p className="text-base text-foreground">{weather.current.description}</p>
116
- <p className="text-4xl font-bold text-foreground">{weather.current.tempF}°F</p>
117
- <div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
118
- <span>{weather.current.windSpeedMph} mph Wind</span>
119
- <span>{weather.current.humidity}% Humidity</span>
120
- </div>
121
- <div className="flex gap-2 border-b border-border pb-2">
122
- <span className="border-b-2 border-primary pb-1 text-sm font-semibold text-primary">
123
- Today
124
- </span>
125
- </div>
126
- {weather.hourly.length > 0 && (
127
- <div className="flex flex-wrap gap-4">
128
- {weather.hourly.map((h) => (
129
- <div
130
- key={h.time}
131
- className="min-w-[60px] rounded-xl bg-muted/50 p-2 text-center"
132
- >
133
- <p className="text-xs text-muted-foreground">{h.time}</p>
134
- <p className="text-base font-semibold text-foreground">{h.tempF}°</p>
135
- </div>
136
- ))}
137
- </div>
138
- )}
139
- </>
140
- )}
141
- </CardContent>
142
- </Card>
93
+ <WeatherWidget />
143
94
  </div>
144
95
  </div>
145
96
  );
@@ -59,7 +59,7 @@ export default function Maintenance() {
59
59
  Type__c: type.trim() || undefined,
60
60
  Priority__c: priority,
61
61
  Status__c: "New",
62
- Scheduled__c: dateRequested || undefined,
62
+ Scheduled__c: dateRequested ? new Date(dateRequested).toISOString() : undefined,
63
63
  });
64
64
  setSubmitSuccess(true);
65
65
  setTitle("");
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
3
- "version": "1.96.0",
3
+ "version": "1.97.0",
4
4
  "description": "Base SFDX project template",
5
5
  "private": true,
6
6
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-app-react-sample-b2x-experimental",
3
- "version": "1.96.0",
3
+ "version": "1.97.0",
4
4
  "description": "B2C sample app template with app shell",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",