Skip to content

Commit 4f585be

Browse files
authored
chore: added feedback card to map (#1765)
* chore: added feedback card to map * chore: sonar qube fixes * chore: fix mobile layout
1 parent 13042e3 commit 4f585be

6 files changed

Lines changed: 196 additions & 7 deletions

File tree

public/frontend/src/components/layout/NavigationDrawer.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ describe('NavigationDrawer', () => {
7272
it('renders feedback link correctly', async () => {
7373
await renderWithRouter(<NavigationDrawer {...defaultProps} />);
7474

75-
const feedbackLink = screen.getByText('Share feedback');
75+
const feedbackLink = screen.getByText('Website feedback');
7676
expect(feedbackLink).toBeInTheDocument();
7777
expect(feedbackLink.closest('a')).toHaveAttribute(
7878
'href',
@@ -124,7 +124,7 @@ describe('NavigationDrawer', () => {
124124
<NavigationDrawer {...defaultProps} onClose={mockOnClose} />,
125125
);
126126

127-
const feedbackLink = screen.getByText('Share feedback');
127+
const feedbackLink = screen.getByText('Website feedback');
128128
fireEvent.click(feedbackLink);
129129

130130
expect(mockOnClose).toHaveBeenCalledTimes(1);
@@ -170,12 +170,12 @@ describe('NavigationDrawer', () => {
170170

171171
await renderWithRouter(<NavigationDrawer {...defaultProps} />);
172172

173-
const feedbackLink = screen.getByText('Share feedback');
173+
const feedbackLink = screen.getByText('Website feedback');
174174
fireEvent.click(feedbackLink);
175175

176176
expect(mockTrackClickEvent).toHaveBeenCalledWith({
177177
category: 'Mobile Navigation',
178-
name: 'Hamburger Menu - Share feedback',
178+
name: 'Hamburger Menu - Website feedback',
179179
});
180180
});
181181

public/frontend/src/components/layout/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const HEADER_LINKS: HeaderLink[] = [
2929
},
3030
{
3131
url: EXTERNAL_LINKS.FEEDBACK_FORM,
32-
label: 'Share feedback',
32+
label: 'Website feedback',
3333
isExternal: true,
3434
},
3535
];

public/frontend/src/components/search-map/SearchMap.scss

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,54 @@
5353
padding-bottom: 1em;
5454
}
5555

56+
.feedback-card {
57+
color: $colorWhite;
58+
font-size: 12.5px;
59+
line-height: 20px;
60+
background-color: $colorBlue;
61+
position: fixed;
62+
bottom: 0;
63+
left: 50%;
64+
transform: translateX(-50%);
65+
width: 360px;
66+
height: 127px;
67+
box-sizing: border-box;
68+
text-align: left;
69+
margin-bottom: 1em;
70+
padding: 16px 16px 16px 20px;
71+
72+
@media (max-width: $mdBreakpoint) {
73+
width: 310px;
74+
height: 150px;
75+
}
76+
77+
.close-button {
78+
position: absolute;
79+
top: 8px;
80+
right: 12px;
81+
background: transparent;
82+
border: none;
83+
color: inherit;
84+
font-size: 20px;
85+
line-height: 1;
86+
cursor: pointer;
87+
padding: 4px;
88+
opacity: 0.8;
89+
transition: opacity 0.2s ease;
90+
91+
&:hover {
92+
opacity: 1;
93+
}
94+
}
95+
96+
.btn-thin {
97+
padding: 1px 0;
98+
height: 32px;
99+
font-size: 11px;
100+
line-height: 1;
101+
}
102+
}
103+
56104
.search-map-controls {
57105
position: fixed;
58106
top: 70px;

public/frontend/src/components/search-map/SearchMap.test.tsx

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
2-
import { forwardRef, useImperativeHandle } from 'react';
2+
import { act, forwardRef, useImperativeHandle } from 'react';
33
import { screen, fireEvent } from '@testing-library/react';
44
import { renderWithRouter } from '@/test-utils';
55
import SearchMap from '@/components/search-map/SearchMap';
66
import * as hooks from '@/components/search-map/hooks/useMapFocus';
77
import { trackClickEvent, trackEvent } from '@shared/utils';
88
import { selectedWildfireIcon } from './styles/icons';
99
import { useFeatureSelection } from './hooks';
10+
import Cookies from 'js-cookie';
1011

1112
vi.mock('@tanstack/react-router', async () => {
1213
const actual = await vi.importActual('@tanstack/react-router');
@@ -199,6 +200,7 @@ describe('SearchMap', () => {
199200
beforeEach(() => {
200201
vi.spyOn(Storage.prototype, 'setItem');
201202
vi.clearAllMocks();
203+
Cookies.remove('hidemap-feedback-card');
202204
mockUseMapFocus.mockReturnValue({
203205
isMapFocusLoading: false,
204206
mapCenter: [0, 0],
@@ -215,6 +217,13 @@ describe('SearchMap', () => {
215217
},
216218
writable: true,
217219
});
220+
221+
vi.useFakeTimers({ shouldAdvanceTime: true });
222+
});
223+
224+
afterEach(() => {
225+
vi.runOnlyPendingTimers(); // Flush remaining timers
226+
vi.useRealTimers(); // Reset back to real time
218227
});
219228

220229
it('renders main components', async () => {
@@ -250,6 +259,71 @@ describe('SearchMap', () => {
250259
expect(screen.getByRole('dialog')).toBeDefined();
251260
});
252261

262+
it('shows feedback card after two minutes', async () => {
263+
await renderWithRouter(
264+
<SearchMap
265+
ids={[]}
266+
totalCount={0}
267+
props={{
268+
style: { visibility: 'visible' },
269+
}}
270+
/>,
271+
);
272+
273+
act(() => {
274+
vi.advanceTimersByTime(120_000);
275+
});
276+
277+
// The card should be present in the DOM
278+
expect(screen.getByText("We'd love to hear from you")).toBeInTheDocument();
279+
});
280+
281+
it('should set a cookie when the close button is clicked', async () => {
282+
const setCookieSpy = vi.spyOn(Cookies, 'set');
283+
await renderWithRouter(
284+
<SearchMap
285+
ids={[]}
286+
totalCount={0}
287+
props={{
288+
style: { visibility: 'visible' },
289+
}}
290+
/>,
291+
);
292+
293+
act(() => {
294+
vi.advanceTimersByTime(120_000);
295+
});
296+
297+
const button = screen.getByTestId('feedback-card-close-button');
298+
fireEvent.click(button);
299+
expect(setCookieSpy).toHaveBeenCalledWith('hidemap-feedback-card', 'true', {
300+
expires: 30,
301+
});
302+
});
303+
304+
it('should set a cookie when the survey button is clicked', async () => {
305+
const setCookieSpy = vi.spyOn(Cookies, 'set');
306+
await renderWithRouter(
307+
<SearchMap
308+
ids={[]}
309+
totalCount={0}
310+
props={{
311+
style: { visibility: 'visible' },
312+
}}
313+
/>,
314+
);
315+
316+
act(() => {
317+
vi.advanceTimersByTime(120_000);
318+
});
319+
320+
const button = screen.getByTestId('feedback-card-survey-button');
321+
fireEvent.click(button);
322+
expect(setCookieSpy).toHaveBeenCalledWith('hidemap-feedback-card', 'true', {
323+
expires: 30,
324+
});
325+
});
326+
253327
it('can interact with disclaimer modal', async () => {
254328
await renderWithRouter(
255329
<SearchMap

public/frontend/src/components/search-map/SearchMap.tsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from '@/components/search-map/hooks';
2222
import MapLayersPanel from '@/components/search-map/MapLayersPanel';
2323
import { selectedWildfireIcon } from '@/components/search-map/styles/icons';
24+
import feedBackIcon from '@/images/icons/feedback-icon.svg';
2425
import searchResultsStore from '@/store/searchResults';
2526
import {
2627
FireEvacuationPreview,
@@ -30,7 +31,7 @@ import {
3031
import FilterButtonLabel from '@/components/search-map/FilterButtonLabel';
3132
import FilterMenuSearchMap from '@/components/search/filters/FilterMenuSearchMap';
3233
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
33-
import { faSliders } from '@fortawesome/free-solid-svg-icons';
34+
import { faSliders, faExternalLink } from '@fortawesome/free-solid-svg-icons';
3435
import { Button, ProgressBar } from 'react-bootstrap';
3536
import { trackClickEvent } from '@shared/utils';
3637
import {
@@ -48,6 +49,7 @@ import {
4849
LEGACY_MAP_LINK,
4950
WILDFIRE_LOCATION_MIN_ZOOM,
5051
} from '@/components/search-map/constants';
52+
import { EXTERNAL_LINKS } from '@/constants/urls';
5153
import RecreationSuggestionForm from '@/components/recreation-suggestion-form/RecreationSuggestionForm';
5254
import type Feature from 'ol/Feature';
5355
import MapDisclaimerModal from '@/components/search-map/MapDisclaimerModal';
@@ -88,6 +90,7 @@ const SearchMap = (searchViewControlsProps: SearchViewControlsProps) => {
8890
useState(true);
8991
const [isWildfireLocationEnabled, setIsWildfireLocationEnabled] =
9092
useState(true);
93+
const [isFeedbackCardVisible, setIsFeedbackCardVisible] = useState(false);
9194
const selectedFilterCount = filterChips.length;
9295

9396
// Current search results and the focused resource (if any),
@@ -106,6 +109,15 @@ const SearchMap = (searchViewControlsProps: SearchViewControlsProps) => {
106109
const popupRef = useRef<HTMLDivElement | null>(null);
107110
const overlayRef = useRef<Overlay | null>(null);
108111

112+
const setFeedbackCardCookie = () => {
113+
Cookies.set('hidemap-feedback-card', 'true', { expires: 30 }); // expires in 1 month
114+
};
115+
116+
const closeFeedbackCard = () => {
117+
setIsFeedbackCardVisible(false);
118+
setFeedbackCardCookie();
119+
};
120+
109121
useEffect(() => {
110122
if (!popupRef.current || !mapRef.current) return;
111123
const overlay = new Overlay({
@@ -122,6 +134,18 @@ const SearchMap = (searchViewControlsProps: SearchViewControlsProps) => {
122134
};
123135
}, [mapRef, popupRef]);
124136

137+
// Show the feedback card after 2 minutes if the user hasn't dismissed it before
138+
useEffect(() => {
139+
const timer = setTimeout(() => {
140+
const hideFeedbackCard = Cookies.get('hidemap-feedback-card');
141+
if (!hideFeedbackCard) {
142+
setIsFeedbackCardVisible(true);
143+
}
144+
}, 120000);
145+
146+
return () => clearTimeout(timer);
147+
}, []);
148+
125149
const { layer: clusteredRecreationFeatureLayer, innerSource: pinSource } =
126150
useClusteredRecreationFeatureLayer(allRelevantIds, mapRef, {
127151
clusterOptions: CLUSTER_OPTIONS,
@@ -366,6 +390,16 @@ const SearchMap = (searchViewControlsProps: SearchViewControlsProps) => {
366390
}
367391
};
368392

393+
const handleFeedbackClick = () => {
394+
trackClickEvent({
395+
category: 'Feedback',
396+
action: 'Map',
397+
name: `Map - Feedback card`,
398+
});
399+
window.open(EXTERNAL_LINKS.FEEDBACK_FORM, '_blank', 'noopener,noreferrer');
400+
setFeedbackCardCookie();
401+
};
402+
369403
return (
370404
<div
371405
className="search-map-container d-flex flex-column vh-100"
@@ -404,6 +438,36 @@ const SearchMap = (searchViewControlsProps: SearchViewControlsProps) => {
404438
Link to legacy map
405439
</a>
406440
</div>
441+
{isFeedbackCardVisible && (
442+
<div className="feedback-card rounded-2">
443+
<button
444+
type="button"
445+
className="close-button"
446+
onClick={() => closeFeedbackCard()}
447+
aria-label="Close feedback card"
448+
data-testid="feedback-card-close-button"
449+
>
450+
&times;
451+
</button>
452+
<p className="mb-0">
453+
<img src={feedBackIcon} alt="Feedback" className="me-2" />
454+
<strong>We'd love to hear from you</strong>
455+
</p>
456+
<p className="mb-0 ms-4">
457+
Your feedback helps us improve your experience on our website.
458+
Survey takes 1-2 minutes.
459+
</p>
460+
<button
461+
type="button"
462+
className="btn btn-outline-light btn-sm w-100 btn-thin mt-2"
463+
onClick={handleFeedbackClick}
464+
data-testid="feedback-card-survey-button"
465+
>
466+
Take Survey{' '}
467+
<FontAwesomeIcon icon={faExternalLink} className="ms-1" />
468+
</button>
469+
</div>
470+
)}
407471
</div>
408472
<div className="search-map-controls">
409473
<div className="map-search-form">
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)