diff --git a/apps/frontend/src/components/Class/Overview/index.tsx b/apps/frontend/src/components/Class/Overview/index.tsx
index c731dc8ed..0ba3c3041 100644
--- a/apps/frontend/src/components/Class/Overview/index.tsx
+++ b/apps/frontend/src/components/Class/Overview/index.tsx
@@ -1,24 +1,21 @@
import Details from "@/components/Details";
import useClass from "@/hooks/useClass";
-
import styles from "./Overview.module.scss";
+import AttendanceRequirements from "@/components/Detail";
export default function Overview() {
- const { class: _class } = useClass();
-
- return (
-
-
-
Description
-
- {_class.description ?? _class.course.description}
-
- {_class.course.requirements && (
- <>
-
Prerequisites
-
{_class.course.requirements}
- >
- )}
-
- );
-}
+ const { class: _class } = useClass();
+ return (
+
+
+
Description
+
+ {_class.description ?? _class.course.description}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/frontend/src/components/Class/Ratings/Ratings.module.scss b/apps/frontend/src/components/Class/Ratings/Ratings.module.scss
index 274516fff..b0e936a9b 100644
--- a/apps/frontend/src/components/Class/Ratings/Ratings.module.scss
+++ b/apps/frontend/src/components/Class/Ratings/Ratings.module.scss
@@ -8,7 +8,6 @@
border-radius: 8px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
border: 1px solid var(--border-color);
-
}
.ratingSection {
@@ -115,6 +114,7 @@
.ratingContent {
margin-top: 16px;
margin-left: 25%;
+ animation: slideDown 300ms ease forwards;
}
.statRow {
@@ -122,6 +122,9 @@
align-items: center;
gap: 12px;
margin-bottom: 8px;
+ opacity: 0;
+ animation: fadeIn 500ms ease forwards;
+ animation-delay: var(--delay);
&:last-child {
margin-bottom: 0;
@@ -132,6 +135,9 @@
color: var(--heading-color);
font-weight: 500;
text-align: center;
+ opacity: 0;
+ animation: fadeIn 300ms ease forwards;
+ animation-delay: calc(var(--delay) + 100ms);
}
.barContainer {
@@ -145,7 +151,8 @@
height: 100%;
background-color: var(--blue-500);
border-radius: 4px;
- transition: width 0.3s ease;
+ transition: width 1000ms cubic-bezier(0.4, 0, 0.2, 1);
+ width: 0;
}
}
@@ -154,6 +161,10 @@
color: var(--paragraph-color);
font-size: 14px;
text-align: right;
+ opacity: 0;
+ animation: fadeIn 300ms ease forwards;
+ animation-delay: calc(var(--delay) + 200ms);
+ transition: all 1000ms cubic-bezier(0.4, 0, 0.2, 1);
}
}
@@ -161,202 +172,34 @@
margin-bottom: 16px;
}
-.overlay {
- background-color: rgb(0 0 0 / 50%);
- position: fixed;
- inset: 0;
- animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
- z-index: 50; /* Ensure overlay is above other content */
-}
-
-
-.modal {
- background-color: var(--foreground-color);
- border-radius: 8px;
- box-shadow: 0 4px 32px rgb(0 0 0 / 25%);
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 90vw;
- max-width: 600px;
- max-height: 85vh;
- padding: 24px;
- animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
- overflow-y: auto;
- z-index: 51; /* Ensure modal is above the overlay */
-}
-
-.modalHeader {
- margin-bottom: 24px;
- text-align: left;
-}
-
-.modalTitle {
- color: var(--heading-color);
- font-size: 24px;
- font-weight: 500;
- margin-bottom: 4px;
-}
-
-.modalSubtitle {
- color: var(--paragraph-color);
- font-size: 16px;
-}
-
-.modalContent {
- margin-bottom: 24px;
-}
-
-.ratingQuestion {
- margin-bottom: 24px;
-
- h3 {
- color: var(--heading-color);
- font-size: 16px;
- font-weight: 500;
- margin-bottom: 16px;
- }
-}
-
-.ratingScale {
- display: flex;
- align-items: center;
- gap: 16px;
- margin-top: 8px;
-
- span {
- color: var(--paragraph-color);
- font-size: 14px;
- min-width: 80px;
- }
-}
-
-.ratingButtons {
- display: flex;
- gap: 8px;
- flex-grow: 1;
- justify-content: center;
-}
-
-.ratingButton {
- width: 40px;
- height: 40px;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- background: none;
- color: var(--heading-color);
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s ease;
-
- &:hover {
- background-color: var(--background-hover-color);
- }
-
- &[data-state='checked'] {
- background-color: var(--blue-500);
- border-color: var(--blue-500);
- color: white;
- }
-}
-
-.radioGroup {
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-top: 8px;
-
- label {
- display: flex;
- align-items: center;
- gap: 8px;
- color: var(--paragraph-color);
- font-size: 14px;
- cursor: pointer;
-
- input {
- width: 16px;
- height: 16px;
- }
- }
-}
-
-.modalFooter {
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- border-top: 1px solid var(--border-color);
- padding-top: 24px;
-}
-
-@keyframes overlayShow {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
-}
-
-@keyframes contentShow {
+@keyframes slideDown {
from {
opacity: 0;
- transform: translate(-50%, -48%) scale(0.96);
+ transform: translateY(-20px);
}
to {
opacity: 1;
- transform: translate(-50%, -50%) scale(1);
- }
-}
-
-.ratingButton {
- width: 40px;
- height: 40px;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- background: none;
- color: var(--heading-color);
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s ease;
-
- &:hover {
- background-color: var(--background-hover-color);
- border-color: var(--blue-500);
- }
-
- &.selected {
- background-color: var(--blue-500);
- border-color: var(--blue-500);
- color: white;
- }
-
- &:focus {
- outline: none;
- box-shadow: 0 0 0 2px var(--blue-200);
+ transform: translateY(0);
}
}
-@keyframes contentSlide {
+@keyframes fadeIn {
from {
opacity: 0;
- transform: translateY(-10px);
+ transform: translateX(-10px);
}
to {
opacity: 1;
- transform: translateY(0);
+ transform: translateX(0);
}
}
-@keyframes fadeIn {
+@keyframes barFill {
from {
- opacity: 0;
- transform: translateX(-10px);
+ transform: scaleX(0);
}
to {
- opacity: 1;
- transform: translateX(0);
+ transform: scaleX(1);
}
}
@@ -400,8 +243,10 @@
height: 100%;
background-color: var(--blue-500);
border-radius: 4px;
- transition: width 1000ms cubic-bezier(0.4, 0, 0.2, 1);
width: 0;
+ transform-origin: left;
+ transition: width 600ms cubic-bezier(0.4, 0, 0.2, 1);
+ will-change: width;
}
}
@@ -413,7 +258,6 @@
opacity: 0;
animation: fadeIn 300ms ease forwards;
animation-delay: calc(var(--delay) + 200ms);
- transition: all 1000ms cubic-bezier(0.4, 0, 0.2, 1);
}
}
@@ -437,13 +281,4 @@
opacity: 1;
transform: translateX(0);
}
-}
-
-@keyframes barFill {
- from {
- transform: scaleX(0);
- }
- to {
- transform: scaleX(1);
- }
}
\ No newline at end of file
diff --git a/apps/frontend/src/components/Class/Ratings/index.tsx b/apps/frontend/src/components/Class/Ratings/index.tsx
index 030a8f8b0..71f2844d7 100644
--- a/apps/frontend/src/components/Class/Ratings/index.tsx
+++ b/apps/frontend/src/components/Class/Ratings/index.tsx
@@ -1,10 +1,22 @@
import React, { useState, useContext, useEffect } from 'react';
import { NavArrowDown } from 'iconoir-react';
import * as Tooltip from "@radix-ui/react-tooltip";
-import * as Dialog from "@radix-ui/react-dialog";
import { Container, Button } from "@repo/theme";
-import styles from './Ratings.module.scss';
+import { UserFeedbackModal } from '@/components/UserFeedbackModal';
import ClassContext from "@/contexts/ClassContext";
+import styles from './Ratings.module.scss';
+
+interface TooltipContentProps {
+ title: string;
+ description: string;
+}
+
+const TooltipContent: React.FC = ({ title, description }) => (
+
+
{title}
+
{description}
+
+);
interface RatingDetailProps {
title: string;
@@ -27,19 +39,22 @@ const RatingDetail: React.FC = ({
reviewCount
}) => {
const [isExpanded, setIsExpanded] = useState(true);
- const [shouldAnimate, setShouldAnimate] = useState(true);
+ const [shouldAnimate, setShouldAnimate] = useState(false);
- // Start animation slightly after expansion
useEffect(() => {
+ let timer: NodeJS.Timeout;
if (isExpanded) {
- const timer = setTimeout(() => {
- setShouldAnimate(true);
- }, 200); // Delay to match the slideDown animation
- return () => {
- clearTimeout(timer);
- setShouldAnimate(false);
- };
+ setShouldAnimate(false);
+ // Using requestAnimationFrame for smoother animation
+ requestAnimationFrame(() => {
+ timer = setTimeout(() => {
+ setShouldAnimate(true);
+ }, 50);
+ });
}
+ return () => {
+ if (timer) clearTimeout(timer);
+ };
}, [isExpanded]);
return (
@@ -92,7 +107,7 @@ const RatingDetail: React.FC = ({
className={styles.bar}
style={{
width: shouldAnimate ? `${stat.percentage}%` : '0%',
- transitionDelay: `${index * 100}ms`
+ transitionDelay: `${index * 60}ms`
}}
/>
@@ -107,122 +122,10 @@ const RatingDetail: React.FC = ({
);
};
-interface TooltipContentProps {
- title: string;
- description: string;
-}
-
-const TooltipContent: React.FC = ({ title, description }) => (
-
-
{title}
-
{description}
-
-);
-
-function RatingModal() {
+export default function Ratings() {
+ const [isModalOpen, setModalOpen] = useState(false);
const { class: currentClass } = useContext(ClassContext);
- const [ratings, setRatings] = useState({
- usefulness: 0,
- difficulty: 0,
- workload: 0
- });
-
- const handleRatingClick = (type: 'usefulness' | 'difficulty' | 'workload', value: number) => {
- setRatings(prev => ({
- ...prev,
- [type]: value,
- [type]: prev[type] === value ? 0 : value
- }));
- };
-
- const getRatingButtonClass = (type: 'usefulness' | 'difficulty' | 'workload', value: number) => {
- return `${styles.ratingButton} ${ratings[type] === value ? styles.selected : ''}`;
- };
-
- return (
-
-
-
-
-
- Rate Course
-
-
- {currentClass.subject} {currentClass.courseNumber} • {currentClass.semester} {currentClass.year}
-
-
-
-
-
-
1. How would you rate the usefulness of this course?
-
-
Not useful
-
- {[1, 2, 3, 4, 5].map((value) => (
- handleRatingClick('usefulness', value)}
- >
- {value}
-
- ))}
-
-
Very useful
-
-
-
-
-
2. How would you rate the difficulty of this course?
-
-
Very easy
-
- {[1, 2, 3, 4, 5].map((value) => (
- handleRatingClick('difficulty', value)}
- >
- {value}
-
- ))}
-
-
Very difficult
-
-
-
-
-
3. How would you rate the workload of this course?
-
-
Very light
-
- {[1, 2, 3, 4, 5].map((value) => (
- handleRatingClick('workload', value)}
- >
- {value}
-
- ))}
-
-
Very heavy
-
-
-
-
-
- Cancel
-
- Submit Rating
-
-
-
- );
-}
-
-export default function Ratings() {
const ratingsData = [
{
title: "Usefulness",
@@ -272,12 +175,7 @@ export default function Ratings() {
-
-
- Add a review
-
-
-
+ setModalOpen(true)}>Add a review
{ratingsData.map((ratingData) => (
@@ -287,6 +185,14 @@ export default function Ratings() {
/>
))}
+
+ setModalOpen(false)}
+ title="Rate Course"
+ subtitle={`${currentClass.subject} ${currentClass.courseNumber} • ${currentClass.semester} ${currentClass.year}`}
+ currentClass={currentClass}
+ />
);
diff --git a/apps/frontend/src/components/Detail/Detail.module.scss b/apps/frontend/src/components/Detail/Detail.module.scss
new file mode 100644
index 000000000..b0e009047
--- /dev/null
+++ b/apps/frontend/src/components/Detail/Detail.module.scss
@@ -0,0 +1,35 @@
+.label {
+ margin-top: 20px;
+ margin-bottom: 10px;
+ color: var(--label-color);
+ line-height: 1;
+}
+
+.description {
+ color: var(--paragraph-color);
+ font-size: 14px;
+ margin-top: 6px;
+ line-height: 1.5;
+}
+
+.attendanceRequirements {
+ .icon {
+ margin-right: 8px;
+ font-size: 1.2rem;
+ color: #6c757d;
+ vertical-align: middle;
+ }
+}
+
+.suggestEdit {
+ color: var(--blue-500);
+ text-decoration: none;
+ font-size: 0.9rem;
+ margin-top: 10px;
+ display: flex;
+ align-items: center;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
\ No newline at end of file
diff --git a/apps/frontend/src/components/Detail/index.tsx b/apps/frontend/src/components/Detail/index.tsx
new file mode 100644
index 000000000..34186d7bb
--- /dev/null
+++ b/apps/frontend/src/components/Detail/index.tsx
@@ -0,0 +1,52 @@
+import { useState } from "react";
+import { UserCircle, Camera } from "iconoir-react";
+import { UserFeedbackModal } from '@/components/UserFeedbackModal';
+import useClass from "@/hooks/useClass";
+import styles from "./Detail.module.scss";
+
+interface AttendanceRequirementsProps {
+ attendanceRequired: boolean | null;
+ lecturesRecorded: boolean | null;
+}
+
+export default function AttendanceRequirements({
+ attendanceRequired,
+ lecturesRecorded,
+}: AttendanceRequirementsProps) {
+ const [isModalOpen, setModalOpen] = useState(false);
+ const { class: currentClass } = useClass();
+
+ return (
+
+
Attendance Requirements
+
+
+
+ {attendanceRequired ? "Attendance Required" : "Attendance Not Required"}
+
+
+
+
+ {lecturesRecorded ? "Lectures Recorded" : "Lectures Not Recorded"}
+
+
{
+ e.preventDefault();
+ setModalOpen(true);
+ }}
+ >
+ Look inaccurate? Suggest an edit
+
+
+
setModalOpen(false)}
+ title="Suggest an edit"
+ subtitle={`${currentClass.subject} ${currentClass.courseNumber} • ${currentClass.semester} ${currentClass.year}`}
+ currentClass={currentClass}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/apps/frontend/src/components/UserFeedbackModal/AttendanceForm.tsx b/apps/frontend/src/components/UserFeedbackModal/AttendanceForm.tsx
new file mode 100644
index 000000000..10a91f63b
--- /dev/null
+++ b/apps/frontend/src/components/UserFeedbackModal/AttendanceForm.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import styles from './UserFeedbackModal.module.scss';
+import { ClassData } from './types';
+
+interface AttendanceFormProps {
+ currentClass: ClassData;
+}
+
+export function AttendanceForm({ currentClass }: AttendanceFormProps) {
+ return (
+
+
Attendance & Recording
+
+
+
+
+
+
+ );
+}
diff --git a/apps/frontend/src/components/UserFeedbackModal/RatingForm.tsx b/apps/frontend/src/components/UserFeedbackModal/RatingForm.tsx
new file mode 100644
index 000000000..f25151d67
--- /dev/null
+++ b/apps/frontend/src/components/UserFeedbackModal/RatingForm.tsx
@@ -0,0 +1,76 @@
+import React, { useState } from 'react';
+import styles from './UserFeedbackModal.module.scss';
+import { ClassData } from './types';
+
+interface RatingFormProps {
+ currentClass: ClassData;
+}
+
+export function RatingsForm({ currentClass }: RatingFormProps) {
+ const [ratings, setRatings] = useState({
+ usefulness: 0,
+ difficulty: 0,
+ workload: 0
+ });
+
+ const handleRatingClick = (type: keyof typeof ratings, value: number) => {
+ setRatings(prev => ({
+ ...prev,
+ [type]: prev[type] === value ? 0 : value
+ }));
+ };
+
+ const renderRatingScale = (
+ type: keyof typeof ratings,
+ question: string,
+ leftLabel: string,
+ rightLabel: string
+ ) => (
+
+
{question}
+
+
{leftLabel}
+
+ {[1, 2, 3, 4, 5].map((value) => (
+ handleRatingClick(type, value)}
+ type="button"
+ >
+ {value}
+
+ ))}
+
+
{rightLabel}
+
+
+ );
+
+ return (
+
+
Course Ratings
+
+ {renderRatingScale(
+ 'usefulness',
+ '1. How would you rate the usefulness of this course?',
+ 'Not useful',
+ 'Very useful'
+ )}
+
+ {renderRatingScale(
+ 'difficulty',
+ '2. How would you rate the difficulty of this course?',
+ 'Very easy',
+ 'Very difficult'
+ )}
+
+ {renderRatingScale(
+ 'workload',
+ '3. How would you rate the workload of this course?',
+ 'Very light',
+ 'Very heavy'
+ )}
+
+ );
+}
diff --git a/apps/frontend/src/components/UserFeedbackModal/UserFeedbackModal.module.scss b/apps/frontend/src/components/UserFeedbackModal/UserFeedbackModal.module.scss
new file mode 100644
index 000000000..33de85abe
--- /dev/null
+++ b/apps/frontend/src/components/UserFeedbackModal/UserFeedbackModal.module.scss
@@ -0,0 +1,278 @@
+.overlay {
+ background-color: rgb(0 0 0 / 60%);
+ position: fixed;
+ inset: 0;
+ animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
+ z-index: 50;
+}
+
+.modal {
+ background-color: var(--foreground-color);
+ border-radius: 8px;
+ box-shadow: 0 4px 32px rgb(0 0 0 / 25%);
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 90vw;
+ max-width: 600px;
+ max-height: 85vh;
+ padding: 24px;
+ animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
+ z-index: 51;
+}
+
+.modalHeader {
+ margin-bottom: 24px;
+ text-align: left;
+}
+
+.modalTitle {
+ color: var(--heading-color);
+ font-size: 24px;
+ font-weight: 500;
+ margin-bottom: 12px;
+}
+
+.subtitleRow {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.modalSubtitle {
+ color: var(--paragraph-color);
+ font-size: 16px;
+ margin: 0;
+}
+
+.modalContent {
+ margin-bottom: 0px;
+}
+
+.combinedForm {
+ max-height: calc(85vh - 220px);
+ overflow-y: auto;
+ padding-right: 12px;
+ margin-right: -12px;
+ margin-top:0;
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: var(--background-color);
+ border-radius: 3px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--label-color);
+ border-radius: 3px;
+ opacity: 0.8;
+
+ &:hover {
+ background: var(--paragraph-color);
+ }
+ }
+}
+
+.termSelect {
+ display: none;
+}
+
+.termDropdown {
+ padding: 4px 12px;
+ font-size: 14px;
+ font-weight: 400;
+ color: var(--heading-color);
+ background-color: var(--foreground-color);
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ outline: none;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 120px;
+
+ &:hover {
+ border-color: var(--blue-400);
+ }
+
+ &:focus {
+ border-color: var(--blue-500);
+ box-shadow: 0 0 0 2px var(--blue-200);
+ }
+}
+
+.modalFooter {
+ position: sticky;
+
+ margin-top: 32px;
+ padding-top: 24px;
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+}
+
+.sectionTitle {
+ color: var(--heading-color);
+ font-size: 18px;
+ font-weight: 500;
+ margin-bottom: 20px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.formGroup {
+ margin-bottom: 32px;
+
+ &:last-child {
+ margin-bottom: 40px;
+ }
+
+ h3, p {
+ color: var(--heading-color);
+ font-size: 16px;
+ font-weight: 500;
+ margin-bottom: 12px;
+ }
+}
+
+.ratingScale {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ margin-top: 8px;
+
+ span {
+ color: var(--paragraph-color);
+ font-size: 14px;
+ min-width: 80px;
+ }
+}
+
+.ratingButtons {
+ display: flex;
+ gap: 8px;
+ flex-grow: 1;
+ justify-content: center;
+}
+
+.ratingButton {
+ width: 40px;
+ height: 40px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background: none;
+ color: var(--heading-color);
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background-color: var(--background-hover-color);
+ border-color: var(--blue-500);
+ }
+
+ &.selected {
+ background-color: var(--blue-500);
+ border-color: var(--blue-500);
+ color: white;
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px var(--blue-200);
+ }
+}
+
+.radioOptions {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+
+ label {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ color: var(--paragraph-color);
+ font-size: 14px;
+ cursor: pointer;
+
+ input[type="radio"] {
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--border-color);
+ border-radius: 50%;
+ margin: 0;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:checked {
+ border-color: var(--blue-500);
+ background-color: var(--blue-500);
+ box-shadow: inset 0 0 0 3px var(--foreground-color);
+ }
+
+ &:hover:not(:checked) {
+ border-color: var(--blue-400);
+ }
+ }
+ }
+}
+
+.attendanceSection {
+ margin-top: 32px;
+ padding-top: 32px;
+ border-top: 1px solid var(--border-color);
+}
+
+@keyframes overlayShow {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes contentShow {
+ from {
+ opacity: 0;
+ transform: translate(-50%, -48%) scale(0.96);
+ }
+ to {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ }
+}
+
+.closeButton {
+ position: absolute;
+ right: 24px;
+ top: 24px;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ background: none;
+ color: var(--label-color);
+ cursor: pointer;
+ font-size: 18px;
+ padding: 0;
+ transition: color 0.2s ease;
+
+ &:hover {
+ color: var(--heading-color);
+ }
+
+ &:focus {
+ outline: none;
+ color: var(--heading-color);
+ }
+}
diff --git a/apps/frontend/src/components/UserFeedbackModal/UserFeedbackModal.tsx b/apps/frontend/src/components/UserFeedbackModal/UserFeedbackModal.tsx
new file mode 100644
index 000000000..0f03dcfcb
--- /dev/null
+++ b/apps/frontend/src/components/UserFeedbackModal/UserFeedbackModal.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import * as Dialog from "@radix-ui/react-dialog";
+import { Button } from "@repo/theme";
+import { RatingsForm } from './RatingForm';
+import { AttendanceForm } from './AttendanceForm';
+import { ClassData } from './types';
+import styles from './UserFeedbackModal.module.scss';
+
+interface UserFeedbackModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ title: string;
+ subtitle?: string;
+ currentClass: ClassData;
+}
+
+export function UserFeedbackModal({
+ isOpen,
+ onClose,
+ title,
+ subtitle,
+ currentClass
+}: UserFeedbackModalProps) {
+ return (
+
+
+
+
+
+ ✕
+
+
+
+ {title}
+
+
+
+ {currentClass.subject} {currentClass.courseNumber}
+
+
+ Fall 2024
+ testing
+ testing
+ {/* figure out to get all da terms */}
+ {/* change ratings based on terms */}
+
+
+
+
+
+
+
+
+ Cancel
+
+ Submit
+
+
+
+
+ );
+}
diff --git a/apps/frontend/src/components/UserFeedbackModal/index.ts b/apps/frontend/src/components/UserFeedbackModal/index.ts
new file mode 100644
index 000000000..f99cc2735
--- /dev/null
+++ b/apps/frontend/src/components/UserFeedbackModal/index.ts
@@ -0,0 +1,2 @@
+export { UserFeedbackModal } from './UserFeedbackModal';
+export type { ClassData } from './types';
\ No newline at end of file
diff --git a/apps/frontend/src/components/UserFeedbackModal/types.ts b/apps/frontend/src/components/UserFeedbackModal/types.ts
new file mode 100644
index 000000000..80a8701b2
--- /dev/null
+++ b/apps/frontend/src/components/UserFeedbackModal/types.ts
@@ -0,0 +1,6 @@
+export interface ClassData {
+ subject: string;
+ courseNumber: string;
+ semester: string;
+ year: string;
+}
diff --git a/apps/frontend/src/lib/api/classes.ts b/apps/frontend/src/lib/api/classes.ts
index 38f036e59..c6b4b7e96 100644
--- a/apps/frontend/src/lib/api/classes.ts
+++ b/apps/frontend/src/lib/api/classes.ts
@@ -165,6 +165,8 @@ export interface ISection {
startDate: string;
endDate: string;
exams: IExam[];
+ attendanceRequired: boolean;
+ lecturesRecorded: boolean;
}
export interface IReservation {