Skip to content

Commit 727a0ed

Browse files
Merge pull request #4586 from OneCommunityGlobal/Manvitha-Phase-3-Feedback-Form
Sharadha taking over for Manvitha phase 3 - feedback form
2 parents cfb3cc9 + d810d36 commit 727a0ed

5 files changed

Lines changed: 484 additions & 1 deletion

File tree

src/components/CommunityPortal/Activities/ActivitiesPage.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1+
import React, { useState } from 'react';
12
import RegistrationForm from './RegistrationForm';
23
import ResourceMonitoring from './ResourceMonitoring';
34
import LatestRegistration from './LatestRegistration';
45
import MyEvent from './MyEvent';
6+
import ActivityFeedbackModal from '../ActivityFeedbackForm';
57
import styles from './styles.module.css';
68

79
function ActivitiesPage() {
10+
const [showFeedback, setShowFeedback] = useState(false);
11+
812
return (
913
<div className={`${styles.activitiesPage}`}>
1014
<header className={`${styles.header}`}>
1115
<h1 className={styles.headerTitle}>Event Registrations</h1>
16+
17+
<button className={styles.feedbackBtn} onClick={() => setShowFeedback(true)}>
18+
Give Feedback
19+
</button>
1220
</header>
1321

1422
<ResourceMonitoring />
@@ -21,6 +29,7 @@ function ActivitiesPage() {
2129
<LatestRegistration />
2230
<MyEvent />
2331
</div>
32+
{showFeedback && <ActivityFeedbackModal onClose={() => setShowFeedback(false)} />}
2433
</div>
2534
);
2635
}

src/components/CommunityPortal/Activities/styles.module.css

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@
2121
text-align: center;
2222
}
2323

24+
.feedbackBtn {
25+
padding: 8px 18px;
26+
background: #1f6feb;
27+
color: #fff;
28+
border: none;
29+
border-radius: 6px;
30+
font-weight: 600;
31+
cursor: pointer;
32+
}
33+
34+
.feedbackBtn:hover {
35+
background: #1158c7;
36+
}
37+
2438
.resourceTitle {
2539
width: 100%;
2640
display: block;
@@ -422,4 +436,3 @@
422436
margin-top: 3px;
423437
font-size: 0.9em;
424438
}
425-
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import React, { useState, useRef, useEffect } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import styles from './FeedbackModal.module.css';
4+
5+
const ActivityFeedbackModal = ({ onClose }) => {
6+
const [rating, setRating] = useState(0);
7+
const [hover, setHover] = useState(0);
8+
const [comment, setComment] = useState('');
9+
const [moreDetails, setMoreDetails] = useState('');
10+
const [showMore, setShowMore] = useState(false);
11+
const [error, setError] = useState('');
12+
const [loading, setLoading] = useState(false);
13+
const [submitted, setSubmitted] = useState(false);
14+
15+
const darkMode = useSelector(state => state.theme.darkMode);
16+
17+
const modalRef = useRef(null);
18+
const errorRef = useRef(null);
19+
20+
useEffect(() => {
21+
const getFocusableElements = () =>
22+
Array.from(modalRef.current?.querySelectorAll('button, textarea, [role="button"]') || []);
23+
24+
modalRef.current?.focus();
25+
26+
const trap = e => {
27+
if (e.key !== 'Tab') return;
28+
const focusable = getFocusableElements();
29+
const firstEl = focusable[0];
30+
const lastEl = focusable[focusable.length - 1];
31+
32+
if (!firstEl || !lastEl) return;
33+
34+
if (e.shiftKey && document.activeElement === firstEl) {
35+
e.preventDefault();
36+
lastEl.focus();
37+
} else if (!e.shiftKey && document.activeElement === lastEl) {
38+
e.preventDefault();
39+
firstEl.focus();
40+
}
41+
};
42+
43+
const escClose = e => {
44+
if (e.key === 'Escape') onClose();
45+
};
46+
47+
document.addEventListener('keydown', trap);
48+
document.addEventListener('keydown', escClose);
49+
50+
return () => {
51+
document.removeEventListener('keydown', trap);
52+
document.removeEventListener('keydown', escClose);
53+
};
54+
}, [onClose]);
55+
56+
const handleSubmit = () => {
57+
if (!rating) {
58+
setError('Please select a rating.');
59+
setTimeout(() => errorRef.current?.scrollIntoView({ behavior: 'smooth' }), 100);
60+
return;
61+
}
62+
63+
setError('');
64+
setLoading(true);
65+
66+
setTimeout(() => {
67+
setSubmitted(true);
68+
setLoading(false);
69+
setTimeout(() => onClose(), 1200);
70+
}, 900);
71+
};
72+
73+
const FilledStar = (
74+
<svg viewBox="0 0 24 24" className={styles.starFilled}>
75+
<path d="M12 .587l3.668 7.568L24 9.748l-6 5.848L19.335 24 12 19.897 4.665 24 6 15.596 0 9.748l8.332-1.593z" />
76+
</svg>
77+
);
78+
79+
const EmptyStar = (
80+
<svg viewBox="0 0 24 24" className={styles.starEmpty}>
81+
<path
82+
d="M12 .587l3.668 7.568L24 9.748l-6 5.848L19.335 24 12 19.897 4.665 24 6 15.596 0 9.748l8.332-1.593z"
83+
strokeWidth="2"
84+
/>
85+
</svg>
86+
);
87+
88+
return (
89+
<div
90+
className={styles.overlay}
91+
role="presentation"
92+
onMouseDown={e => {
93+
if (e.target === e.currentTarget) onClose();
94+
}}
95+
>
96+
<div
97+
ref={modalRef}
98+
className={darkMode ? styles.modalDark : styles.modalLight}
99+
role="dialog"
100+
aria-modal="true"
101+
tabIndex={-1}
102+
>
103+
<button className={styles.closeBtn} onClick={onClose} aria-label="Close feedback form">
104+
105+
</button>
106+
107+
<h2 className={styles.heading}>Activity Feedback</h2>
108+
109+
{submitted && <div className={styles.success}>Feedback submitted!</div>}
110+
111+
<div className={styles.starContainer}>
112+
{[1, 2, 3, 4, 5].map(star => {
113+
const filled = star <= (hover || rating);
114+
return (
115+
<span
116+
key={star}
117+
className={styles.starWrapper}
118+
tabIndex={0}
119+
role="button"
120+
aria-label={`Rate ${star} stars`}
121+
onMouseEnter={() => setHover(star)}
122+
onMouseLeave={() => setHover(0)}
123+
onClick={() => {
124+
setRating(star);
125+
setError('');
126+
}}
127+
onKeyDown={e => {
128+
if (e.key === 'ArrowRight' && rating < 5) setRating(rating + 1);
129+
if (e.key === 'ArrowLeft' && rating > 1) setRating(rating - 1);
130+
if (e.key === 'Enter') setRating(star);
131+
}}
132+
>
133+
{filled ? FilledStar : EmptyStar}
134+
</span>
135+
);
136+
})}
137+
</div>
138+
139+
{error && (
140+
<div ref={errorRef} className={styles.error}>
141+
{error}
142+
</div>
143+
)}
144+
145+
<textarea
146+
className={styles.commentBox}
147+
maxLength={300}
148+
placeholder="Optional: Add your feedback"
149+
value={comment}
150+
onChange={e => setComment(e.target.value)}
151+
/>
152+
153+
<div className={comment.length > 250 ? styles.charCountWarning : styles.charCount}>
154+
{comment.length}/300
155+
</div>
156+
157+
<button className={styles.moreDetailsBtn} onClick={() => setShowMore(!showMore)}>
158+
{showMore ? 'Hide Additional Details' : 'Add More Details'}
159+
</button>
160+
161+
{showMore && (
162+
<textarea
163+
className={styles.moreDetailsBox}
164+
placeholder="Additional optional information..."
165+
value={moreDetails}
166+
onChange={e => setMoreDetails(e.target.value)}
167+
/>
168+
)}
169+
170+
<button
171+
className={rating ? styles.submitButton : styles.submitButtonDisabled}
172+
onClick={handleSubmit}
173+
aria-disabled={loading || !rating}
174+
>
175+
{loading ? 'Submitting...' : 'Submit Feedback'}
176+
</button>
177+
</div>
178+
</div>
179+
);
180+
};
181+
182+
export default ActivityFeedbackModal;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { render, screen, fireEvent } from '@testing-library/react';
2+
import '@testing-library/jest-dom';
3+
import { vi } from 'vitest';
4+
import { Provider } from 'react-redux';
5+
import { configureStore } from 'redux-mock-store';
6+
import ActivityFeedbackModal from './ActivityFeedbackForm';
7+
8+
const mockStore = configureStore([]);
9+
10+
vi.useFakeTimers();
11+
12+
describe('ActivityFeedbackModal', () => {
13+
const setup = ({ darkMode = false } = {}) => {
14+
const store = mockStore({
15+
theme: { darkMode },
16+
});
17+
18+
const onClose = vi.fn();
19+
20+
render(
21+
<Provider store={store}>
22+
<ActivityFeedbackModal onClose={onClose} />
23+
</Provider>,
24+
);
25+
26+
return { onClose };
27+
};
28+
29+
afterEach(() => {
30+
vi.clearAllTimers();
31+
});
32+
33+
test('renders modal title', () => {
34+
setup();
35+
expect(screen.getByText('Activity Feedback')).toBeInTheDocument();
36+
});
37+
38+
test('focuses the dialog when opened', () => {
39+
setup();
40+
expect(screen.getByRole('dialog')).toHaveFocus();
41+
});
42+
43+
test('uses dark modal styling when dark mode is enabled', () => {
44+
setup({ darkMode: true });
45+
expect(screen.getByRole('dialog').className).toContain('modalDark');
46+
});
47+
48+
test('requires rating before submission', () => {
49+
setup();
50+
fireEvent.click(screen.getByText('Submit Feedback'));
51+
expect(screen.getByText('Please select a rating.')).toBeInTheDocument();
52+
});
53+
54+
test('closes after successful submit', () => {
55+
const { onClose } = setup();
56+
57+
fireEvent.click(screen.getByLabelText('Rate 5 stars'));
58+
fireEvent.click(screen.getByText('Submit Feedback'));
59+
60+
vi.advanceTimersByTime(900);
61+
expect(screen.getByText('Feedback submitted!')).toBeInTheDocument();
62+
63+
vi.advanceTimersByTime(1200);
64+
expect(onClose).toHaveBeenCalled();
65+
});
66+
});

0 commit comments

Comments
 (0)