diff --git a/.github/workflows/pull_request_test.yml b/.github/workflows/pull_request_test.yml index 9f6010e85f..9c81fb35d0 100644 --- a/.github/workflows/pull_request_test.yml +++ b/.github/workflows/pull_request_test.yml @@ -19,18 +19,18 @@ jobs: run: | # Get list of changed CSS files in this PR changed_files=$(git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD | grep '\.css$' || true) - + if [ -z "$changed_files" ]; then echo "No CSS files changed in this PR" exit 0 fi - + echo "Changed CSS files:" echo "$changed_files" - + # Check if any of the changed files are disallowed disallowed=$(echo "$changed_files" | grep -vE '(\.module\.css$|index\.css$)' || true) - + if [ -n "$disallowed" ]; then echo "❌ Disallowed CSS file detected! Only '.module.css' (or 'index.css') files are permitted." echo "The following files violate the CSS Module policy:" @@ -43,16 +43,17 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: + - uses: actions/checkout@v4 + with: fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'yarn' - - name: Install Dependencies - run: yarn install --frozen-lockfile - - name: Run Unit Tests for Changed Files Only - run: NODE_OPTIONS="--max-old-space-size=4096" yarn run test:changed - - name: Run Lint - run: yarn run lint \ No newline at end of file + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Clear Yarn Cache + run: yarn cache clean + - name: Install Dependencies + run: yarn install --frozen-lockfile + - name: Run Unit Tests for Changed Files Only + run: NODE_OPTIONS="--max-old-space-size=4096" yarn run test:changed + - name: Run Lint + run: yarn run lint diff --git a/src/components/CommunityPortal/Activities/activityId/Activity.jsx b/src/components/CommunityPortal/Activities/activityId/Activity.jsx deleted file mode 100644 index 565a6bb375..0000000000 --- a/src/components/CommunityPortal/Activities/activityId/Activity.jsx +++ /dev/null @@ -1,278 +0,0 @@ -import { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import CommentSection from './CommentSection/CommentSection'; -import ActivityFAQs from './ActivityFAQs'; -import styles from './Activity.module.css'; - -const data = { - eventName: 'Event Name', - eventType: 'Event or Course', - eventLocation: 'San Francisco, CA 94108', - eventLink: 'https://devforum.zoom.us', - eventDate: 'Monday, Sept 2', - eventTime: '9:00 AM - 11:00 AM', - eventOrganizer: 'Alex Brain', - eventCapacity: '7/20', - eventRating: 4, - eventStatus: 'Activated', - eventStatusClass: 'status-active', - eventDescription: - 'this is activity comments. this is activity comments. this is activity comments. this is activity comments. this is activity comments.', - eventParticipates: [ - { id: 1, name: 'Summer' }, - { id: 2, name: 'Jimmy' }, - ], - eventComments: [ - { - id: 1, - name: 'Summer', - comment: - 'this is activity comments. this is activity comments. this is activity comments. this is activity comments. this is activity comments.', - time: '2 hours ago', - }, - { - id: 2, - name: 'Jason', - comment: - 'this is activity comments. this is activity comments. this is activity comments. this is activity comments. this is activity comments.', - time: '2 hours ago', - }, - { - id: 3, - name: 'Jimmy', - comment: - 'this is activity comments. this is activity comments. this is activity comments. this is activity comments. this is activity comments.', - time: '2 hours ago', - }, - ], - eventFAQs: [ - { id: 1, question: 'What is the event about?', answer: 'This is a sample answer to the FAQ.' }, - { id: 2, question: 'How do I register?', answer: 'This is a sample answer to the FAQ.' }, - ], -}; - -function getCalendarDays(year, month) { - const first = new Date(year, month, 1); - const last = new Date(year, month + 1, 0); - const daysInMonth = last.getDate(); - const startOffset = first.getDay(); - const totalCells = Math.ceil((startOffset + daysInMonth) / 7) * 7; - const days = []; - for (let i = 0; i < startOffset; i++) days.push(null); - for (let d = 1; d <= daysInMonth; d++) days.push(d); - while (days.length < totalCells) days.push(null); - return days; -} - -function Activity({ initialTab }) { - const [tab, setTab] = useState(initialTab || 'Description'); - const [event, setEvent] = useState(data); - const now = new Date(); - const calendarYear = now.getFullYear(); - const calendarMonth = now.getMonth(); - const todayDate = now.getDate(); - const monthLabel = now.toLocaleString('default', { month: 'long', year: 'numeric' }); - const calendarDays = getCalendarDays(calendarYear, calendarMonth); - - const handleTabClick = tabName => { - setTab(tabName); - }; - - useEffect(() => { - setEvent(data); - }, [data]); - - useEffect(() => { - if (initialTab) setTab(initialTab); - }, [initialTab]); - - return ( -
-
-
-
Participated
-
-

{event.eventType} / In-person or Remote

-

{event.eventName}

-

- Location: {event.eventLocation} -

-

- Link:{' '} - - {event.eventLink} - -

-
-
- Date -
- {event.eventDate} -
-
- Time -
- {event.eventTime} -
-
- Organizer -
- {event.eventOrganizer} -
-
- Capacity -
- {event.eventCapacity} -
-
- Overall Rating -
- {Array.from({ length: event.eventRating }, (_, i) => ( - - ))} - {Array.from({ length: 5 - event.eventRating }, (_, i) => ( - - ))} -
-
- Status -
- {event.eventStatus} -
-
- {event.eventParticipates && event.eventParticipates.length > 0 && ( -
- {event.eventParticipates.slice(0, 5).map((p, i) => ( - - {p.name ? p.name[0] : '?'} - - ))} - {event.eventParticipates.length > 5 && ( - +{event.eventParticipates.length - 5} - )} -
- )} -
- - -
-
-
-
{monthLabel}
- - - - - - - - - - - - - - {Array.from({ length: calendarDays.length / 7 }, (_, rowIndex) => { - const weekDays = calendarDays.slice(rowIndex * 7, rowIndex * 7 + 7); - const firstDayInRow = weekDays.find(d => d !== null); - const rowKey = - firstDayInRow === undefined - ? `week-pad-${calendarYear}-${calendarMonth}-${rowIndex * 7}` - : `week-${calendarYear}-${calendarMonth}-${firstDayInRow}`; - return ( - - {weekDays.map((day, cellOffset) => { - const isToday = day === todayDate; - const cellKey = - day === null - ? `pad-${calendarYear}-${calendarMonth}-${rowIndex * 7 + cellOffset}` - : `day-${calendarYear}-${calendarMonth}-${day}`; - return ( - - ); - })} - - ); - })} - -
SMTWTFS
- {day ?? '\u00A0'} -
-
-
- -
- - - - -
- {tab === 'Description' && ( -
-

{event.eventDescription}

-
- )} - {tab === 'Participates' && ( -
- {event.eventParticipates.map(participant => ( -
- - {participant.name[0]} - -
{participant.name}
-
- ))} -
- )} - {tab === 'FAQs' && ( -
- -
- )} - {tab === 'Comments' && } -
-
- ); -} - -Activity.propTypes = { - initialTab: PropTypes.string, -}; - -export default Activity; diff --git a/src/components/CommunityPortal/Activities/activityId/ActivityFeedback.jsx b/src/components/CommunityPortal/Activities/activityId/ActivityFeedback.jsx new file mode 100644 index 0000000000..dd8024e0b3 --- /dev/null +++ b/src/components/CommunityPortal/Activities/activityId/ActivityFeedback.jsx @@ -0,0 +1,308 @@ +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import CommentSection from './CommentSection/CommentSection'; +import Switch from '@mui/material/Switch'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Typography from '@mui/material/Typography'; +import Feedback from './FeedBackSection/Feedback'; +import { feedbackData } from './FeedbackData'; +import styles from './ActivityFeedback.module.css'; + +const data = { + eventName: 'Event Name', + eventType: 'Event or Course', + eventLocation: 'San Francisco, CA 94108', + eventLink: 'https://devforum.zoom.us', + eventDate: 'Monday, Sept 2', + eventTime: '9:00 AM - 11:00 AM', + eventOrganizer: 'Alex Brain', + eventCapacity: '7/20', + eventRating: 4, + eventStatus: 'Activated', + createdAt: '2025-09-20', + eventDescription: + 'this is activity comments. this is activity comments. this is activity comments. this is activity comments. this is activity comments.', + eventParticipates: [{ name: 'Summer' }, { name: 'Jimmy' }], + eventComments: [ + { + id: 1, + name: 'Summer', + comment: + 'this is activity comments. this is activity comments. this is activity comments. this is activity comments. this is activity comments.', + time: '2 hours ago', + }, + { + id: 2, + name: 'Jason', + comment: + 'this is activity comments. this is activity comments. this is activity comments. this is activity comments. this is activity comments.', + time: '2 hours ago', + }, + { + id: 3, + name: 'Jimmy', + comment: + 'this is activity comments. this is activity comments. this is activity comments. this is activity comments. this is activity comments.', + time: '2 hours ago', + }, + ], + eventFAQs: [ + { id: 1, question: 'What is the event about?', answer: 'This is a sample answer to the FAQ.' }, + { id: 2, question: 'How do I register?', answer: 'This is a sample answer to the FAQ.' }, + ], +}; + +const getAvatarColorClass = (name = '', index = 0, stylesRef) => { + let hash = 0; + for (let i = 0; i < name.length; i += 1) { + hash = (hash << 5) - hash + name.charCodeAt(i); + hash |= 0; + } + const isPurple = (hash + index) % 2 === 0; + return isPurple ? stylesRef.purple : stylesRef.blue; +}; + +function ActivityFeedback() { + const darkMode = useSelector(state => state.theme?.darkMode); + const [tab, setTab] = useState('Feedback'); + const [event, setEvent] = useState(data); + const [viewSelected, setViewSelected] = useState('Host'); + const [reviewsChecked, setReviewsChecked] = useState(true); + const [suggestionChecked, setSuggestionChecked] = useState(false); + const [feedbackList, setFeedbackList] = useState(feedbackData); + const [showFeedbackModal, setShowFeedbackModal] = useState(false); + const views = ['Host', 'Participant']; + + useEffect(() => { + setEvent(data); + }, []); + + return ( +
+
+
+
+
Participated
+ +
+
+
+

{event.eventType}

+

{event.eventName}

+

{event.eventLocation}

+ + {event.eventLink} + +
+ +
+ {views.map(sec => ( + + ))} +
+
+ +
+
+ Date +
+ {event.eventDate} +
+
+ Time +
+ {event.eventTime} +
+
+ Organizer +
+ {event.eventOrganizer} +
+
+ Capacity +
+ {event.eventCapacity} +
+
+ Overall Rating +
+ {Array.from({ length: event.eventRating }, (_, i) => ( + + ★ + + ))} + {Array.from({ length: 5 - event.eventRating }, (_, i) => ( + + ☆ + + ))} +
+
+ Status +
+ {event.eventStatus} +
+ + {viewSelected === 'Host' && ( +
+ setReviewsChecked(e.target.checked)} + color="primary" + /> + } + label={ + Allow Reviews + } + /> +
+ setSuggestionChecked(e.target.checked)} + color="primary" + /> + } + label={ + + Suggestions Only + + } + /> +
+ )} +
+ + {viewSelected !== 'Host' && ( +
+ +
+ )} +
+ +
+
September 2024
+ + + + {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map(day => ( + + ))} + + + + {[0, 1, 2, 3, 4].map(row => ( + + {Array.from({ length: 7 }).map((_, col) => { + const day = row * 7 + col + 1; + return ( + + ); + })} + + ))} + +
{day}
+ {day <= 31 ? day : ''} +
+
+
+ +
+ {['Description', 'Participates', 'Comments', 'FAQs', 'Feedback'].map(name => ( + + ))} +
+ + {tab === 'Description' && ( +
+

{event.eventDescription}

+
+ )} + + {tab === 'Participates' && ( +
+ {event.eventParticipates.map((p, i) => ( +
+ + {p.name[0]} + +
{p.name}
+
+ ))} +
+ )} + + {tab === 'FAQs' && ( +
+ {event.eventFAQs.map(faq => ( +
+

{faq.question}

+

{faq.answer}

+
+ ))} +
+ )} + + {tab === 'Comments' && ( + + )} + + {tab === 'Feedback' && viewSelected === 'Host' && ( + + )} + + {tab === 'Feedback' && viewSelected !== 'Host' && ( + + )} +
+
+
+ ); +} + +export default ActivityFeedback; diff --git a/src/components/CommunityPortal/Activities/activityId/ActivityFeedback.module.css b/src/components/CommunityPortal/Activities/activityId/ActivityFeedback.module.css new file mode 100644 index 0000000000..790cb9f15f --- /dev/null +++ b/src/components/CommunityPortal/Activities/activityId/ActivityFeedback.module.css @@ -0,0 +1,541 @@ +.activityContainer { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + background-color: #f3f5f9; + color: #0f172a; + min-height: 100vh; + box-sizing: border-box; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.activityEventCard { + border: 1px solid #e2e8f0; + border-radius: 16px; + padding: 20px; + font-family: Arial, sans-serif; + background-color: #ffffff; + max-width: 1100px; + width: 100%; + box-shadow: 0 14px 30px rgba(15, 23, 42, 0.08); + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; +} + +.activityEventHeader { + display: flex; + gap: 20px; + align-items: flex-start; +} + +.titlesAndViews { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 20px; +} + +.titles { + display: flex; + flex-direction: column; + gap: 4px; +} + +.viewToggler { + padding: 12px 0; + display: flex; + gap: 10px; +} + +.activityEventImage { + width: 180px; + height: 180px; + background: linear-gradient(145deg, #e2e8f0, #f8fafc); + text-align: center; + line-height: 180px; + font-weight: bold; + color: #475569; + border-radius: 12px; + border: 1px solid #e2e8f0; +} + +.activityEventDetails { + flex-grow: 2; +} + +.activityEventType { + font-size: 12px; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.6px; +} + +.activityEventTitle { + margin: 2px 0 0; + font-size: 24px; + color: #0f172a; +} + +.activityEventLocation { + font-size: 14px; + color: #475569; +} + +.activityEventLink { + color: #1d4ed8; + text-decoration: none; + font-size: 14px; +} + +.activityEventLink:hover { + text-decoration: underline; +} + +.activityEventInfo { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-top: 14px; + color: #1e293b; +} + +.activityEventInfo > div { + min-width: 120px; +} + +.activityEventInfo strong { + color: #0f172a; +} + +.activityCapacity { + color: #dc2626; + font-weight: 600; +} + +.activityStatusActive { + color: #16a34a; + font-weight: 700; +} + +.activityStar { + color: #f7c948; + margin-right: 2px; +} + +.activityStarEmpty { + color: #cbd5e1; + margin-right: 2px; +} + +.switchToggle { + color: #0f172a; + font-weight: 600; +} + +.activityEventButtons { + margin-top: 12px; +} + +.contactBtn { + padding: 8px 14px; + margin-right: 10px; + border: 1px solid #cbd5e1; + border-radius: 999px; + background-color: #f1f5f9; + color: #1e293b; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; +} + +.contactBtn:hover { + background-color: #e2e8f0; + border-color: #b8c9e3; + color: #1e3a8a; +} + +.activityCalendarBox { + min-width: 220px; + text-align: center; + background: #f8fafc; + border-radius: 12px; + border: 1px solid #e2e8f0; + padding: 10px 12px; +} + +.activityCalendarHeader { + font-weight: 700; + margin-bottom: 6px; + color: #1e293b; +} + +.activityCalendarTable { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} + +.activityCalendarTable th, +.activityCalendarTable td { + width: 14.2%; + padding: 4px; + border: 1px solid #e2e8f0; + color: #475569; +} + +.activityCalendarTable td:hover { + background: #eef2f7; + color: #1e3a8a; +} + +.activityCalendarTable th { + background: #eef2f7; + font-weight: 600; +} + +.activityActiveDate { + background-color: #f472b6; + border-radius: 50%; + color: #ffffff; + font-weight: 700; +} + +.activityEventTabs { + display: flex; + gap: 16px; + border-top: 1px solid #e2e8f0; + margin-top: 20px; + padding-top: 12px; +} + +.activityTab { + padding: 6px 12px; + cursor: pointer; + border: 1px solid transparent; + border-radius: 999px; + background: transparent; + color: #475569; + transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease; +} + +.activityTab:hover { + color: #1e3a8a; + background: #f1f5f9; + border-color: #c7d6ea; +} + +.activityTabActive { + border-bottom: 2px solid #1e3a8a; + font-weight: 700; + color: #1e3a8a; + border-radius: 0; + padding-bottom: 8px; +} + +.active { + background: #f1f5f9; + border-color: #c7d6ea; + color: #1e3a8a; + box-shadow: 0 6px 12px rgba(30, 58, 138, 0.12); +} + +.activityParticipatesSection { + margin-top: 20px; + display: flex; + flex-direction: row; + gap: 20px; +} + +.activityParticipant { + display: flex; + align-items: center; + gap: 8px; + color: #1e293b; +} + +.activityFaqsSection { + margin-top: 20px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.activityFaq { + padding: 12px 14px; + border-radius: 12px; + border: 1px solid #e2e8f0; + background: #f8fafc; +} + +.activityFaqTitle { + color: #0f172a; + margin: 0 0 6px; +} + +.activityFaqText { + color: #475569; + margin: 0; +} + +.activityEventDescription p { + color: #1e293b; + padding: 10px 0; + line-height: 1.6; +} + +.activityIcon { + width: 30px; + height: 30px; + border-radius: 50%; + color: #fff; + text-align: center; + line-height: 30px; + font-weight: bold; +} + +.purple { + background-color: #7c3aed; +} + +.blue { + background-color: #2563eb; +} + +@media (max-width: 800px) { + .activityEventCard { + max-width: 98vw; + padding: 12px; + } + + .activityEventHeader { + flex-direction: column; + gap: 12px; + align-items: stretch; + } + + .titlesAndViews { + flex-direction: column; + align-items: flex-start; + } + + .activityEventImage { + width: 120px; + height: 160px; + line-height: 120px; + font-size: 18px; + margin: 10px 0; + } + + .activityCalendarBox { + min-width: unset; + width: 100%; + margin-top: 10px; + } + + .activityEventDetails { + width: 100%; + } + + .activityEventInfo { + flex-direction: column; + gap: 8px; + } + + .activityEventTabs { + justify-content: center; + gap: 10px; + flex-wrap: wrap; + font-size: 15px; + } + + .activityParticipatesSection { + flex-direction: column; + gap: 10px; + } +} + +@media (max-width: 500px) { + .activityEventCard { + padding: 8px; + font-size: 14px; + } + + .activityEventTitle { + font-size: 18px; + } + + .activityEventImage { + height: 180px; + line-height: 80px; + font-size: 15px; + } + + .activityCalendarHeader { + font-size: 14px; + } + + .activityCalendarTable th, + .activityCalendarTable td { + padding: 2px; + font-size: 12px; + } + + .activityIcon { + width: 24px; + height: 24px; + line-height: 24px; + font-size: 13px; + } + + .activityEventTabs { + font-size: 13px; + justify-content: space-evenly; + gap: 0; + } + + .activityTab { + padding: 6px 6px; + } +} + +.darkMode .activityContainer { + background-color: #1b2a41; + color: #e2e8f0; +} + +.darkMode { + background-color: #1b2a41; + min-height: 100vh; +} + +.darkMode .activityEventCard { + border: 1px solid #2f425d; + background-color: #1f3048; + box-shadow: 0 18px 40px rgba(2, 8, 18, 0.5); +} + +.darkMode .activityEventImage { + background: linear-gradient(145deg, #243853, #2b3f5d); + color: #cbd5e1; + border-color: #2f425d; +} + +.darkMode .activityEventType { + color: #cbd5e1; +} + +.darkMode .activityEventTitle { + color: #f8fafc; +} + +.darkMode .activityEventLocation { + color: #e2e8f0; +} + +.darkMode .activityEventLink { + color: #93c5fd; +} + +.darkMode .activityEventInfo { + color: #e2e8f0; +} + +.darkMode .activityEventInfo strong { + color: #f8fafc; +} + +.darkMode .switchToggle { + color: #e2e8f0; +} + +.darkMode .contactBtn { + background-color: #243853; + border-color: #4b637f; + color: #e2e8f0; +} + +.darkMode .contactBtn:hover { + background-color: #2c4463; + border-color: #5a7698; + color: #f8fafc; +} + +.darkMode .activityCalendarBox { + background: #243853; + border-color: #2f425d; +} + +.darkMode .activityCalendarHeader { + color: #f8fafc; +} + +.darkMode .activityCalendarTable th, +.darkMode .activityCalendarTable td { + border-color: #2f425d; + color: #cbd5e1; +} + +.darkMode .activityCalendarTable th { + background: #1f2f48; +} + +.darkMode .activityCalendarTable td:hover { + background: #2c4463; + color: #f8fafc; +} + +.darkMode .activityActiveDate { + background-color: #f472b6; + color: #0b1220; +} + +.darkMode .activityEventTabs { + border-top-color: #2f425d; +} + +.darkMode .activityTab { + color: #cbd5e1; +} + +.darkMode .activityTab:hover { + color: #f8fafc; + background: #243853; + border-color: #4b637f; +} + +.darkMode .activityTabActive { + border-bottom-color: #93c5fd; + color: #93c5fd; +} + +.darkMode .active { + background: #243853; + border-color: #4b637f; + color: #f8fafc; + box-shadow: 0 10px 18px rgba(8, 18, 32, 0.35); +} + +.darkMode .activityParticipatesSection { + color: #e2e8f0; +} + +.darkMode .activityParticipant { + color: #e2e8f0; +} + +.darkMode .activityFaq { + background: #243853; + border-color: #2f425d; +} + +.darkMode .activityFaqTitle { + color: #f8fafc; +} + +.darkMode .activityFaqText { + color: #cbd5e1; +} + +.darkMode .activityEventDescription p { + color: #e2e8f0; +} diff --git a/src/components/CommunityPortal/Activities/activityId/CommentSection/CommentSection.jsx b/src/components/CommunityPortal/Activities/activityId/CommentSection/CommentSection.jsx index ef4e9d9368..96feab65de 100644 --- a/src/components/CommunityPortal/Activities/activityId/CommentSection/CommentSection.jsx +++ b/src/components/CommunityPortal/Activities/activityId/CommentSection/CommentSection.jsx @@ -1,21 +1,37 @@ import styles from './CommentSection.module.css'; +import { useSelector } from 'react-redux'; + +const getAvatarColorClass = (name = '', index = 0, stylesRef) => { + let hash = 0; + for (let i = 0; i < name.length; i += 1) { + hash = (hash << 5) - hash + name.charCodeAt(i); + hash |= 0; + } + const isPurple = (hash + index) % 2 === 0; + return isPurple ? stylesRef.purple : stylesRef.blue; +}; function CommentSection({ comments }) { + const darkMode = useSelector(state => state.theme?.darkMode); return ( -
-
- {comments.map(comment => ( -
-
+
+
+ {comments.map((comment, index) => ( +
+
{comment.name[0]}
-
+
{comment.comment} -
+
{comment.name} - {comment.time}
diff --git a/src/components/CommunityPortal/Activities/activityId/CommentSection/CommentSection.module.css b/src/components/CommunityPortal/Activities/activityId/CommentSection/CommentSection.module.css index 8f5563a587..a36a06dab4 100644 --- a/src/components/CommunityPortal/Activities/activityId/CommentSection/CommentSection.module.css +++ b/src/components/CommunityPortal/Activities/activityId/CommentSection/CommentSection.module.css @@ -1,36 +1,39 @@ -/* stylelint-disable */ -.commentText { - flex: 1; - background-color: #f4f4f4; - padding: 10px; - border-radius: 6px; -} - -.commentFooter { +.activityCommentFooter { font-size: 12px; color: gray; margin-top: 5px; + transition: color 0.3s ease; } -.comment { +.activityComment { display: flex; align-items: flex-start; gap: 10px; + transition: background-color 0.3s ease; } -.commentsSection { +.activityCommentsSection { margin-top: 20px; display: flex; flex-direction: column; gap: 20px; + transition: all 0.3s ease; } -.commentUser { +.activityCommentUser { display: flex; gap: 5px; } -.icon { +.activityCommentText { + flex: 1; + background-color: #f4f4f4; + padding: 10px; + border-radius: 6px; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.activityIcon { width: 30px; height: 30px; border-radius: 50%; @@ -38,6 +41,11 @@ text-align: center; line-height: 30px; font-weight: bold; + transition: transform 0.2s ease; +} + +.activityIcon:hover { + transform: scale(1.1); } .purple { @@ -48,33 +56,31 @@ background-color: #36f; } -@media (width <= 800px) { - .comment { +@media (max-width: 800px) { + .activityComment { flex-direction: column; gap: 5px; } - .commentsSection { + .activityCommentsSection { gap: 10px; padding: 15px; } } -/* Dark mode (body.dark-mode from theme) */ -:global(body.dark-mode) .commentsSection { - color: #e0e0e0; +.darkMode .activityCommentFooter { + color: #aaa; } -:global(body.dark-mode) .commentText { - background-color: #2a3a57; - color: #e0e0e0; - border: 1px solid #3b4a66; +.darkMode .activityComment { + background-color: transparent; } -:global(body.dark-mode) .commentFooter { - color: #b0b0b0; +.darkMode .activityCommentsSection { + background-color: transparent; } -:global(body.dark-mode) .commentFooter span { - color: #b0b0b0; +.darkMode .activityCommentText { + background-color: #3a506b; + color: #ffffff; } diff --git a/src/components/CommunityPortal/Activities/activityId/FeedBackSection/Feedback.jsx b/src/components/CommunityPortal/Activities/activityId/FeedBackSection/Feedback.jsx new file mode 100644 index 0000000000..cad408e059 --- /dev/null +++ b/src/components/CommunityPortal/Activities/activityId/FeedBackSection/Feedback.jsx @@ -0,0 +1,405 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import styles from './Feedback.module.css'; +import { FaSearch } from 'react-icons/fa'; +import { MdArrowUpward, MdArrowDownward } from 'react-icons/md'; +import { AiFillStar, AiOutlineStar } from 'react-icons/ai'; +import FeedbackModal from './FeedbackModal'; + +const nowISO = () => new Date().toISOString(); + +function Feedback({ + reviewsEnabled = true, + suggestionsOnly = false, + isHost = false, + eventCreatedAt = null, + showModal = false, + setShowModal = null, + feedbackList, + setFeedbackList, +}) { + // local list (in real app you'd fetch) + const [searchTerm, setSearchTerm] = useState(''); + const [filterBy, setFilterBy] = useState('date'); + const [sortOrder, setSortOrder] = useState('desc'); + const [visibilityFilter, setVisibilityFilter] = useState('all'); + const [showSuggestionsOnly, setShowSuggestionsOnly] = useState(false); + + // modal form state (participant) + const [modalOpen, setModalOpen] = useState(showModal); + const [modalRating, setModalRating] = useState(0); + const [modalComment, setModalComment] = useState(''); + const [modalSuggestionText, setModalSuggestionText] = useState(''); + const [modalPrivate, setModalPrivate] = useState(false); + const [visibleCount, setVisibleCount] = useState(2); + + // reflect incoming modal prop + useEffect(() => { + if (typeof showModal === 'boolean') setModalOpen(showModal); + }, [showModal]); + + useEffect(() => { + // If parent gave setShowModal, keep synchronized + if (setShowModal) { + setShowModal(modalOpen); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [modalOpen]); + + useEffect(() => { + // Placeholder for API call to fetch feedback data + // For now we use dummyFeedback above. + }, []); + + // dark mode for styling + const darkMode = useSelector(state => state.theme?.darkMode); + + // helper: determine if event was created within one month + const eventWithinFirstMonth = useMemo(() => { + if (!eventCreatedAt) return false; + const created = new Date(eventCreatedAt); + const oneMonthLater = new Date(created); + oneMonthLater.setMonth(oneMonthLater.getMonth() + 1); + return new Date() <= oneMonthLater; + }, [eventCreatedAt]); + + const handleSearch = e => setSearchTerm(e.target.value); + const handleFilterChange = e => setFilterBy(e.target.value); + const handleSortChange = () => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + + const renderStars = feedback => + Array.from({ length: 5 }, (_, i) => ( + + {i < (feedback.rating || 0) ? : } + + )); + + // Filtering & sorting for host view + const filteredFeedback = feedbackList + .filter(fb => { + // visibility filter + if (visibilityFilter !== 'all' && fb.visibility !== visibilityFilter) return false; + // search match + const q = searchTerm.trim().toLowerCase(); + if (!q) return true; + return ( + (fb.comment || '').toLowerCase().includes(q) || + (fb.name || '').toLowerCase().includes(q) || + (fb.rating !== null && String(fb.rating).includes(q)) + ); + }) + .sort((a, b) => { + if (filterBy === 'date') { + return sortOrder === 'asc' + ? new Date(a.date) - new Date(b.date) + : new Date(b.date) - new Date(a.date); + } + if (filterBy === 'rating') { + return sortOrder === 'asc' + ? (a.rating || 0) - (b.rating || 0) + : (b.rating || 0) - (a.rating || 0); + } + return 0; + }); + + // Participant submit handlers (local only) + const handleSubmitFeedback = () => { + if (!reviewsEnabled && !suggestionsOnly) return; // can't submit + const isSuggestion = suggestionsOnly; + const visibility = eventWithinFirstMonth ? 'host-only' : modalPrivate ? 'host-only' : 'public'; + const newItem = { + id: feedbackList.length + 1, + name: 'You', + rating: isSuggestion ? null : modalRating, + comment: isSuggestion ? modalSuggestionText || modalComment : modalComment, + date: nowISO().slice(0, 10), + visibility: isSuggestion ? 'suggestion' : visibility, + }; + setFeedbackList(prev => [newItem, ...prev]); + // reset modal + setModalComment(''); + setModalSuggestionText(''); + setModalRating(5); + setModalPrivate(false); + setModalOpen(false); + if (setShowModal) setShowModal(false); + }; + + const handleOpenModal = () => { + setModalOpen(true); + if (setShowModal) setShowModal(true); + }; + + // Host-only and suggestion lists + const suggestionList = feedbackList.filter(fb => fb.visibility === 'suggestion'); + + return ( +
+
+ {isHost && ( + <> +
+
+ + +
+ +
+ + + + + +
+
+ +
+ + +
+ + {showSuggestionsOnly ? ( +
+ {suggestionList.length === 0 ? ( +
No suggestions yet.
+ ) : ( + suggestionList.map(s => ( +
+ User +
+
+ {s.name} + {s.date} +
+

{s.comment}

+
+
+ )) + )} +
+ ) : ( +
+ {filteredFeedback.length === 0 ? ( +
No feedback matches your filters.
+ ) : ( + filteredFeedback.slice(0, visibleCount).map(feedback => ( +
+ User +
+
+ {feedback.name} + {feedback.date} + {/* Visibility badge */} + + {feedback.visibility === 'host-only' + ? 'Private' + : feedback.visibility === 'suggestion' + ? 'Suggestion' + : 'Public'} + +
+ + {/* rating */} + {feedback.rating !== null && ( +
{renderStars(feedback)}
+ )} + +

{feedback.comment}

+
+
+ )) + )} +
+ )} + {visibleCount < feedbackList.length && ( +
+ +
+ )} + + )} + + {/* Participant view / modal */} + {!isHost && ( + <> + {/* Notice if reviews disabled */} + {!reviewsEnabled && !suggestionsOnly && ( +
+ Reviews are currently disabled for this event. +
+ )} + + {/* Button to open modal if parent didn't provide one */} + {!modalOpen && (reviewsEnabled || suggestionsOnly) && ( +
+ +
+ )} + + {/* Modal-like simple panel (replace with your modal component if you have one) */} + {modalOpen && ( + { + setModalOpen(false); + if (setShowModal) setShowModal(false); + }} + onSubmit={handleSubmitFeedback} + show={modalOpen} + importantLabel={ + eventWithinFirstMonth + ? 'Your feedback is only visible to the host for the first month.' + : null + } + disableSubmit={ + (!suggestionsOnly && !modalComment && !modalRating) || + (suggestionsOnly && !modalSuggestionText) || + (!reviewsEnabled && !suggestionsOnly) + } + > + {suggestionsOnly ? ( +
+ +