From 7aad9a1f1cabc1634f8333f0b8df5c0703d758e6 Mon Sep 17 00:00:00 2001 From: DreamBot706 <187842058+DreamBot706@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:22:32 +0530 Subject: [PATCH 01/11] fix(client): fix year navigation redirect and synchronize active year in browse screen --- .../browse/components/year-info/index.jsx | 20 ++++++- client/src/screens/browse/index.jsx | 56 +++++++++++++++++-- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/client/src/screens/browse/components/year-info/index.jsx b/client/src/screens/browse/components/year-info/index.jsx index 081de71f..48983fa0 100644 --- a/client/src/screens/browse/components/year-info/index.jsx +++ b/client/src/screens/browse/components/year-info/index.jsx @@ -16,6 +16,7 @@ import { import { ConfirmDialog } from "./confirmDialog"; import { ConfirmDelDialog } from "./confirmDelDialog"; import { getSubtreeFileCount } from "../../../../utils/folderUtils"; +import { useNavigate } from "react-router-dom"; const YearInfo = ({ isBR, @@ -24,6 +25,7 @@ const YearInfo = ({ currYear, }) => { const dispatch = useDispatch(); + const navigate = useNavigate(); const [showConfirm, setShowConfirm] = useState(false); const [showConfirmDel, setShowConfirmDel] = useState(false); const [newYearName, setNewYearName] = useState(""); @@ -109,6 +111,10 @@ const YearInfo = ({ dispatch(ClearFolderHistory()); dispatch(RefreshCurrentFolder()); + if (courseCode && nextYearFolder?._id) { + navigate(`/browse/${courseCode}/${nextYearFolder._id}`); + } + toast.success(`Year "${yearName}" added`); } catch (error) { toast.error("Failed to add year."); @@ -157,6 +163,12 @@ const YearInfo = ({ dispatch(ClearFolderHistory()); dispatch(RefreshCurrentFolder()); + if (courseCode && nextYearFolder?._id) { + navigate(`/browse/${courseCode}/${nextYearFolder._id}`); + } else if (courseCode) { + navigate(`/browse/${courseCode}`); + } + toast.success("Year deleted successfully!"); setShowConfirmDel(false); } catch (err) { @@ -185,11 +197,17 @@ const YearInfo = ({ { + dispatch(ClearFolderHistory()); dispatch( - ChangeCurrentYearData(idx, course[idx].children) + ChangeCurrentYearData(idx, course[idx]?.children || []) ); dispatch(ChangeFolder(course[idx])); dispatch(RefreshCurrentFolder()); + if (courseCode && course[idx]?._id) { + navigate(`/browse/${courseCode}/${course[idx]._id}`); + } else if (courseCode) { + navigate(`/browse/${courseCode}`); + } }} >
diff --git a/client/src/screens/browse/index.jsx b/client/src/screens/browse/index.jsx index b679a6c9..f144c4f3 100644 --- a/client/src/screens/browse/index.jsx +++ b/client/src/screens/browse/index.jsx @@ -159,12 +159,23 @@ const BrowseScreen = () => { const defaultYear = fetchedData.children[defaultYearIndex]; let activeFolder = folderId ? findFolderById(fetchedData.children, folderId) : null; - if (defaultYear && defaultYear.children) { + let activeYearIndex = defaultYearIndex; + if (folderId && activeFolder) { + const matchedIdx = fetchedData.children.findIndex( + (y) => y._id === activeFolder._id || findFolderById(y.children, activeFolder._id) + ); + if (matchedIdx !== -1) { + activeYearIndex = matchedIdx; + } + } + + const activeYear = fetchedData.children[activeYearIndex]; + if (activeYear && activeYear.children) { dispatch( - ChangeCurrentYearData(defaultYearIndex, defaultYear.children || []) + ChangeCurrentYearData(activeYearIndex, activeYear.children || []) ); dispatch(ClearFolderHistory()); // Clear history when starting with a new course/year - dispatch(ChangeFolder(activeFolder || defaultYear)); + dispatch(ChangeFolder(activeFolder || activeYear)); } } } else { @@ -196,12 +207,23 @@ const BrowseScreen = () => { const defaultYear = fetchedData.children[defaultYearIndex]; let activeFolder = folderId ? findFolderById(fetchedData.children, folderId) : null; - if (defaultYear && defaultYear.children) { + let activeYearIndex = defaultYearIndex; + if (folderId && activeFolder) { + const matchedIdx = fetchedData.children.findIndex( + (y) => y._id === activeFolder._id || findFolderById(y.children, activeFolder._id) + ); + if (matchedIdx !== -1) { + activeYearIndex = matchedIdx; + } + } + + const activeYear = fetchedData.children[activeYearIndex]; + if (activeYear && activeYear.children) { dispatch( - ChangeCurrentYearData(defaultYearIndex, defaultYear.children || []) + ChangeCurrentYearData(activeYearIndex, activeYear.children || []) ); dispatch(ClearFolderHistory()); // Clear history when starting with a new course/year - dispatch(ChangeFolder(activeFolder || defaultYear)); + dispatch(ChangeFolder(activeFolder || activeYear)); } } } else { @@ -223,11 +245,33 @@ const BrowseScreen = () => { const matched = findFolderById(currCourse, folderId); if (matched) { + const matchedYearIndex = currCourse.findIndex( + (y) => y._id === matched._id || findFolderById(y.children, matched._id) + ); + if (matchedYearIndex !== -1 && matchedYearIndex !== currYear) { + dispatch( + ChangeCurrentYearData( + matchedYearIndex, + currCourse[matchedYearIndex]?.children || [] + ) + ); + } dispatch(ChangeFolder(matched)); } else { fetchFolder(folderId, code) .then((freshFolder) => { if (freshFolder && freshFolder._id) { + const matchedYearIndex = currCourse.findIndex( + (y) => y._id === freshFolder._id || findFolderById(y.children, freshFolder._id) + ); + if (matchedYearIndex !== -1 && matchedYearIndex !== currYear) { + dispatch( + ChangeCurrentYearData( + matchedYearIndex, + currCourse[matchedYearIndex]?.children || [] + ) + ); + } dispatch(ChangeFolder(freshFolder)); } else { toast.error("Folder not found!"); From 72e56c2be525c9425ed93131ae3c1cc8c692c5c3 Mon Sep 17 00:00:00 2001 From: DreamBot706 <187842058+DreamBot706@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:30:07 +0530 Subject: [PATCH 02/11] feat(ui): add tactile physics, spring modals, seamless course updates without reload, and smooth transitions --- .../navbar/components/logo/styles.scss | 11 +++- .../navbar/components/navlink/styles.scss | 31 +++++++++- .../navbar/components/searchbar/styles.scss | 28 +++++++++- .../components/browsefolder/styles.scss | 9 ++- .../browse/components/collapsible/styles.scss | 17 +++++- .../components/file-display/styles.scss | 16 +++++- .../components/sectionC/styles.scss | 35 +++++++++--- .../components/sectionC/styles.scss | 35 +++++++++--- .../components/button/styles.scss | 18 ++++-- .../components/contributionbanner/styles.scss | 22 +++++++- .../dashboard/components/coursecard/index.jsx | 12 +++- .../components/coursecard/styles.scss | 40 ++++++++++--- .../dashboard/components/examcard/styles.scss | 11 ++++ .../components/examschedule/index.jsx | 7 ++- .../components/examschedule/styles.scss | 35 ++++++++++-- client/src/screens/dashboard/index.jsx | 56 ++++++++++++++----- client/src/screens/dashboard/styles.scss | 15 +++++ .../components/searchcoursemodal/styles.scss | 35 +++++++++--- 18 files changed, 360 insertions(+), 73 deletions(-) diff --git a/client/src/components/navbar/components/logo/styles.scss b/client/src/components/navbar/components/logo/styles.scss index af3f1993..b90ed540 100644 --- a/client/src/components/navbar/components/logo/styles.scss +++ b/client/src/components/navbar/components/logo/styles.scss @@ -1,6 +1,15 @@ -.logo{ +.logo { font-weight: bold; font-size: 2rem; font-family: 'Black'; cursor: pointer; + display: inline-block; + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + + &:hover { + transform: scale(1.04); + } + &:active { + transform: scale(0.96); + } } \ No newline at end of file diff --git a/client/src/components/navbar/components/navlink/styles.scss b/client/src/components/navbar/components/navlink/styles.scss index 2c6fc4d0..4653fb51 100644 --- a/client/src/components/navbar/components/navlink/styles.scss +++ b/client/src/components/navbar/components/navlink/styles.scss @@ -1,10 +1,37 @@ .nav-link { - font-family: 'bold'; + font-family: 'Bold'; font-size: 1.1rem; margin-left: 2rem; cursor: pointer; + position: relative; + display: inline-block; + color: #000; + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), + color 0.2s ease; + + &::after { + content: ""; + position: absolute; + bottom: -4px; + left: 0; + width: 100%; + height: 2px; + background-color: #000; + transform: scaleX(0); + transform-origin: right; + transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1); + } &:hover { - scale: 1.1; + transform: translateY(-2px); + + &::after { + transform: scaleX(1); + transform-origin: left; + } + } + + &:active { + transform: translateY(0px) scale(0.96); } } \ No newline at end of file diff --git a/client/src/components/navbar/components/searchbar/styles.scss b/client/src/components/navbar/components/searchbar/styles.scss index 02afa289..e00ee637 100644 --- a/client/src/components/navbar/components/searchbar/styles.scss +++ b/client/src/components/navbar/components/searchbar/styles.scss @@ -46,14 +46,27 @@ position: absolute; width: 518px; height: auto; - min-height: 100px; + min-height: 80px; max-height: 400px; background-color: #fff; - z-index: 2; + z-index: 20; margin-left: 21px; color: #222; padding: 10px 20px; overflow-y: hidden; + border-radius: 4px; + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2); + animation: searchSlideIn 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards; + + p { + transition: transform 0.15s ease, background-color 0.15s ease; + border-radius: 3px; + + &:hover { + transform: translateX(4px); + } + } + .code { margin-right: 1rem; font-family: "Bold"; @@ -63,6 +76,17 @@ font-family: "Bold"; } } + +@keyframes searchSlideIn { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} @media screen and (max-width: 1240px) { width: 500px; .search-results { diff --git a/client/src/screens/browse/components/browsefolder/styles.scss b/client/src/screens/browse/components/browsefolder/styles.scss index 0509057f..52b4bb4e 100644 --- a/client/src/screens/browse/components/browsefolder/styles.scss +++ b/client/src/screens/browse/components/browsefolder/styles.scss @@ -4,12 +4,17 @@ margin: 10px; z-index: 2; cursor: pointer; - transition: 200ms ease; + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), filter 0.2s ease; background: url(./folder.svg), none; background-position: center; background-repeat: no-repeat; &:hover { - transform: translateY(-2px); + transform: translateY(-4px); + filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.12)); + } + &:active { + transform: translateY(0px) scale(0.97); + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.08)); } @media (max-width: 768px) { width: calc(50% - 4px); // Reduced gap for larger folders diff --git a/client/src/screens/browse/components/collapsible/styles.scss b/client/src/screens/browse/components/collapsible/styles.scss index 930f8ef6..d37ed7f3 100644 --- a/client/src/screens/browse/components/collapsible/styles.scss +++ b/client/src/screens/browse/components/collapsible/styles.scss @@ -4,6 +4,12 @@ .main{ cursor: pointer; display: flex; + transition: background-color 0.15s ease; + + &:hover { + background-color: rgba(0, 0, 0, 0.03); + } + .color{ height: 100px; width: 12px; @@ -27,7 +33,7 @@ } } .arrow{ - transition: 200ms; + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); transform: rotateZ(-90deg); width: 20px; height: 20px; @@ -47,12 +53,17 @@ } } .collapsible-content{ - display: block; + max-height: 1200px; + opacity: 1; border-bottom: 1px solid rgba(0, 0, 0, 0.33); } } .collapsible-content{ - display: none; + max-height: 0; + opacity: 0; + overflow: hidden; + transition: max-height 0.3s cubic-bezier(0.16, 1, 0.3, 1), + opacity 0.25s ease; } } \ No newline at end of file diff --git a/client/src/screens/browse/components/file-display/styles.scss b/client/src/screens/browse/components/file-display/styles.scss index 87b61540..b7010ca1 100644 --- a/client/src/screens/browse/components/file-display/styles.scss +++ b/client/src/screens/browse/components/file-display/styles.scss @@ -3,8 +3,12 @@ height: 220px; background-color: white; border: 1px solid rgba(0, 0, 0, 0.32); + border-radius: 4px; margin-right: 14px; margin-bottom: 15px; + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.2s ease, + border-color 0.2s ease; &.verified { border: 2px solid #4caf50; } @@ -60,6 +64,14 @@ background-repeat: no-repeat; background-position: center; background-size: contain; + transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1); + + &:hover { + transform: scale(1.2); + } + &:active { + transform: scale(0.9); + } } } .view { @@ -137,10 +149,12 @@ } &:hover { border: 1px solid #000; + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12); .img-preview { .top, .view { - opacity: 100; + opacity: 1; } } } diff --git a/client/src/screens/contributions/components/sectionC/styles.scss b/client/src/screens/contributions/components/sectionC/styles.scss index e613f371..7af13b99 100644 --- a/client/src/screens/contributions/components/sectionC/styles.scss +++ b/client/src/screens/contributions/components/sectionC/styles.scss @@ -6,15 +6,34 @@ left: 0; right: 0; bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: 2; - display: none; - + background-color: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + z-index: 999; + display: flex; justify-content: center; align-items: center; -} + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1), + visibility 0.25s cubic-bezier(0.16, 1, 0.3, 1), + backdrop-filter 0.25s ease; -.show { - display: block; - display: flex; + .wrapper { + transform: scale(0.93) translateY(16px); + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), + box-shadow 0.3s ease; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + } + + &.show { + opacity: 1; + visibility: visible; + pointer-events: auto; + + .wrapper { + transform: scale(1) translateY(0); + } + } } \ No newline at end of file diff --git a/client/src/screens/dashboard/components/addcoursemodal/components/sectionC/styles.scss b/client/src/screens/dashboard/components/addcoursemodal/components/sectionC/styles.scss index 4a3b822e..38945580 100644 --- a/client/src/screens/dashboard/components/addcoursemodal/components/sectionC/styles.scss +++ b/client/src/screens/dashboard/components/addcoursemodal/components/sectionC/styles.scss @@ -6,15 +6,34 @@ left: 0; right: 0; bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: 2; - display: none; - + background-color: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + z-index: 999; + display: flex; justify-content: center; align-items: center; -} + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1), + visibility 0.25s cubic-bezier(0.16, 1, 0.3, 1), + backdrop-filter 0.25s ease; -.show { - display: block; - display: flex; + .wrapper { + transform: scale(0.93) translateY(16px); + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), + box-shadow 0.3s ease; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + } + + &.show { + opacity: 1; + visibility: visible; + pointer-events: auto; + + .wrapper { + transform: scale(1) translateY(0); + } + } } \ No newline at end of file diff --git a/client/src/screens/dashboard/components/contributionbanner/components/button/styles.scss b/client/src/screens/dashboard/components/contributionbanner/components/button/styles.scss index 07730598..a82861d3 100644 --- a/client/src/screens/dashboard/components/contributionbanner/components/button/styles.scss +++ b/client/src/screens/dashboard/components/contributionbanner/components/button/styles.scss @@ -1,6 +1,6 @@ -.contributebutton{ +.contributebutton { color: #fff; background-color: #000; margin: 5px; @@ -8,8 +8,18 @@ font-family: 'Bold'; font-size: 1.2rem; cursor: pointer; - transition: 200ms ease; - &:hover{ - transform: scale(1.02); + border-radius: 4px; + box-shadow: 4px 4px 0 #000; + transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.18s ease; + + &:hover { + transform: translate(-3px, -3px); + box-shadow: 7px 7px 0 #000; + } + + &:active { + transform: translate(2px, 2px); + box-shadow: 1px 1px 0 #000; } } \ No newline at end of file diff --git a/client/src/screens/dashboard/components/contributionbanner/styles.scss b/client/src/screens/dashboard/components/contributionbanner/styles.scss index 5da7d08e..15fcf005 100644 --- a/client/src/screens/dashboard/components/contributionbanner/styles.scss +++ b/client/src/screens/dashboard/components/contributionbanner/styles.scss @@ -50,6 +50,7 @@ $medium: 1250px; background-repeat: no-repeat; background-position: center; width: 100%; + animation: wingBobUp 3s ease-in-out infinite alternate; } .downwings { @@ -58,6 +59,7 @@ $medium: 1250px; background-repeat: no-repeat; background-position: center; width: 100%; + animation: wingBobDown 3s ease-in-out infinite alternate; } } } @@ -90,4 +92,22 @@ $medium: 1250px; } } -#contribute-info {} \ No newline at end of file +#contribute-info {} + +@keyframes wingBobUp { + from { + transform: translateY(0); + } + to { + transform: translateY(-5px); + } +} + +@keyframes wingBobDown { + from { + transform: translateY(0); + } + to { + transform: translateY(5px); + } +} \ No newline at end of file diff --git a/client/src/screens/dashboard/components/coursecard/index.jsx b/client/src/screens/dashboard/components/coursecard/index.jsx index 90aa1e1b..2c619317 100644 --- a/client/src/screens/dashboard/components/coursecard/index.jsx +++ b/client/src/screens/dashboard/components/coursecard/index.jsx @@ -6,7 +6,7 @@ import { IsCourseAvailable } from "../../../../api/Search"; import { DeleteCourseAPI } from "../../../../api/User"; import { toast } from "react-toastify"; import { ConfirmDialog } from "./ConfirmDialog"; -const CourseCard = ({ code, color, name, type, setClicked, isReadOnly }) => { +const CourseCard = ({ code, color, name, type, setClicked, isReadOnly, onCourseRemoved }) => { const [isAvailable, setIsAvailable] = useState(true); const [showConfirm, setShowConfirm] = useState(false); const [isRemoving, setIsRemoving] = useState(false); @@ -26,8 +26,14 @@ const CourseCard = ({ code, color, name, type, setClicked, isReadOnly }) => { if (isRemoving) return; try { setIsRemoving(true); - const resp = await DeleteCourseAPI(code); - location.reload(); + await DeleteCourseAPI(code); + setIsRemoving(false); + setShowConfirm(false); + if (onCourseRemoved) { + onCourseRemoved(code); + } else { + location.reload(); + } } catch (error) { toast.error("Something went wrong!"); setIsRemoving(false); diff --git a/client/src/screens/dashboard/components/coursecard/styles.scss b/client/src/screens/dashboard/components/coursecard/styles.scss index cfc8574f..73cee69b 100644 --- a/client/src/screens/dashboard/components/coursecard/styles.scss +++ b/client/src/screens/dashboard/components/coursecard/styles.scss @@ -4,10 +4,12 @@ background-color: #fff; padding: 10px; margin: 10px; - transition: 150ms ease; + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.2s ease; position: relative; cursor: pointer; flex-shrink: 0; + border-radius: 4px; .card-content { display: flex; flex-direction: column; @@ -26,16 +28,23 @@ background-position: center; cursor: pointer; opacity: 0; - transition: opacity 0.2s ease-in-out; + transform: scale(0.7); + transition: opacity 0.2s ease-in-out, + transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); z-index: 3; border-radius: 50%; border: none; display: flex; align-items: center; justify-content: center; + + &:hover { + transform: scale(1.2) rotate(90deg) !important; + } } &:hover .remove-course { opacity: 1; + transform: scale(1); } .code { background-color: #000; @@ -61,12 +70,21 @@ &.ADD { background-color: #000; - background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='5' ry='5' stroke='white' strokeWidth='2' stroke-dasharray='6%2c 14' stroke-dashoffset='0' strokeLinecap='square'/%3e%3c/svg%3e"); border-radius: 5px; + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + + &:hover { + transform: translateY(-3px) scale(1.02); + } + &:active { + transform: translateY(0px) scale(0.97); + } + .content { i { margin: 20px; + transition: transform 0.2s ease; } p { font-size: 1.1rem; @@ -80,21 +98,25 @@ color: #fff; margin: auto; } + + &:hover .content i { + transform: scale(1.15) rotate(90deg); + } } &.true { &:hover { - transform: scale(1.05); + transform: translateY(-4px); + box-shadow: 0 8px 0 rgba(0, 0, 0, 0.95); + } + &:active { + transform: translateY(-1px) scale(0.98); + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.95); } } &.false { background-color: #636363 !important; cursor: not-allowed !important; } - &:hover { - .remove-course { - display: unset; - } - } @media screen and (max-width: 768px) { width: calc(50% - 10px); diff --git a/client/src/screens/dashboard/components/examcard/styles.scss b/client/src/screens/dashboard/components/examcard/styles.scss index ab41a2a3..29357292 100644 --- a/client/src/screens/dashboard/components/examcard/styles.scss +++ b/client/src/screens/dashboard/components/examcard/styles.scss @@ -10,9 +10,20 @@ flex-shrink: 0; justify-content: space-around; + border-radius: 4px; + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.25s ease; + cursor: default; + + &:hover { + transform: translateY(-4px); + box-shadow: 0 8px 0 rgba(0, 0, 0, 0.95); + } + .ndays { p { font-size: 3.5rem; + transition: transform 0.2s ease; } } .name { diff --git a/client/src/screens/dashboard/components/examschedule/index.jsx b/client/src/screens/dashboard/components/examschedule/index.jsx index 069f27a5..63f0adf3 100644 --- a/client/src/screens/dashboard/components/examschedule/index.jsx +++ b/client/src/screens/dashboard/components/examschedule/index.jsx @@ -112,12 +112,15 @@ const ExamScheduleWidget = () => { {/* State 4: Populated chronological schedule */} {!isLoading && !hasError && scheduledExams.length > 0 && ( -
+
{scheduledExams.map((exam, index) => (
{exam.code} diff --git a/client/src/screens/dashboard/components/examschedule/styles.scss b/client/src/screens/dashboard/components/examschedule/styles.scss index 19465f39..e35d77af 100644 --- a/client/src/screens/dashboard/components/examschedule/styles.scss +++ b/client/src/screens/dashboard/components/examschedule/styles.scss @@ -22,7 +22,10 @@ color: #fff; cursor: pointer; text-align: center; - transition: 150ms ease; + border-radius: 2px; + transition: background-color 0.2s cubic-bezier(0.16, 1, 0.3, 1), + color 0.2s ease, + transform 0.15s ease; user-select: none; &.active { @@ -31,7 +34,11 @@ } &:not(.active):hover { - background-color: #222; + background-color: #2a2a2a; + } + + &:active { + transform: scale(0.96); } } } @@ -53,13 +60,22 @@ margin: 10px; position: relative; flex-shrink: 0; + border-radius: 4px; display: flex; flex-direction: column; justify-content: space-between; - transition: 150ms ease; + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.2s ease; + animation: examCardFadeSlide 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; &:hover { - transform: scale(1.04); + transform: translateY(-4px); + box-shadow: 0 8px 0 rgba(0, 0, 0, 0.95); + } + + &:active { + transform: translateY(-1px) scale(0.98); + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.95); } .card-top { @@ -246,3 +262,14 @@ } } } + +@keyframes examCardFadeSlide { + from { + opacity: 0; + transform: translateY(14px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} diff --git a/client/src/screens/dashboard/index.jsx b/client/src/screens/dashboard/index.jsx index 1716724a..b2d3565f 100644 --- a/client/src/screens/dashboard/index.jsx +++ b/client/src/screens/dashboard/index.jsx @@ -21,9 +21,9 @@ import { useEffect, useState } from "react"; import { getColors } from "../../utils/colors"; import { LoadCourses } from "../../actions/filebrowser_actions"; import Contributions from "../contributions"; -import { AddNewCourseLocal, ClearLocalCourses } from "../../actions/user_actions"; +import { AddNewCourseLocal, ClearLocalCourses, LoginUser } from "../../actions/user_actions"; import AddCourseModal from "./components/addcoursemodal"; -import { AddNewCourseAPI, GetExamDates } from "../../api/User"; +import { AddNewCourseAPI, GetExamDates, getUser } from "../../api/User"; import { toast } from "react-toastify"; import { clearLegacySessionLocalCoursesCache, readAllCoursesCache } from "../../utils/frontendCache"; @@ -46,29 +46,54 @@ const Dashboard = () => { const contributionHandler = (event) => { const collection = document.getElementsByClassName("contri"); const contributionSection = collection[0]; - contributionSection.classList.add("show"); + if (contributionSection) contributionSection.classList.add("show"); }; const addCourseModalShowHandler = (event) => { const collection = document.getElementsByClassName("add_modal"); const contributionSection = collection[0]; - contributionSection.classList.add("show"); + if (contributionSection) contributionSection.classList.add("show"); }; const handleAddCourse = async ({ code, name }) => { try { - const found = user.user?.courses?.find( - (course) => course.code.toLowerCase() === code.toLowerCase() - ); + const found = + user.user?.courses?.find( + (course) => course.code.toLowerCase() === code.toLowerCase() + ) || + user.user?.readOnly?.find( + (course) => course.code.toLowerCase() === code.toLowerCase() + ); if (found) { toast.info("Course already exists."); return; } - await AddNewCourseAPI(code, name); - location.reload(); + const res = await AddNewCourseAPI(code, name); + if (res?.data?.readOnly) { + dispatch(LoginUser({ ...user.user, readOnly: res.data.readOnly })); + } else { + const fresh = await getUser(); + if (fresh?.data) { + dispatch(LoginUser(fresh.data)); + } + } + toast.success(`Course ${code.toUpperCase()} added to Others!`); + + const collection = document.getElementsByClassName("add_modal"); + const modal = collection[0]; + if (modal) modal.classList.remove("show"); } catch (error) { + toast.error("Failed to add course."); } }; + const handleCourseRemoved = (removedCode) => { + const updatedReadOnly = (user.user?.readOnly || []).filter( + (c) => c.code?.replaceAll(" ", "").toLowerCase() !== removedCode?.replaceAll(" ", "").toLowerCase() + ); + dispatch(LoginUser({ ...user.user, readOnly: updatedReadOnly })); + toast.success(`Course ${removedCode.toUpperCase()} removed`); + }; + useEffect(() => { clearLegacySessionLocalCoursesCache(); dispatch(ClearLocalCourses()); @@ -162,12 +187,13 @@ const Dashboard = () => {
{user.user.readOnly.map((course, index) => ( handleClick(course.code)} isReadOnly={true} + onCourseRemoved={handleCourseRemoved} /> ))} @@ -202,12 +228,12 @@ const Dashboard = () => {
{showPrevious && ( - <> +
{user.user.previousCourses.map((semesterGroup, semIndex) => (
toggleSemester(semIndex)}> - + { />
{openSemesters[semIndex] && ( - <> +
{semesterGroup.courses.map((course, index) => ( @@ -230,11 +256,11 @@ const Dashboard = () => { /> ))}
- +
)}
))} - +
)} )} diff --git a/client/src/screens/dashboard/styles.scss b/client/src/screens/dashboard/styles.scss index 0134bb1f..0abac41a 100644 --- a/client/src/screens/dashboard/styles.scss +++ b/client/src/screens/dashboard/styles.scss @@ -26,6 +26,21 @@ gap: 0; } +.previous-courses-wrapper { + animation: fadeInSlide 0.25s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +@keyframes fadeInSlide { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + .fav-container { display: flex; flex-wrap: wrap; diff --git a/client/src/screens/landing/components/searchcoursemodal/styles.scss b/client/src/screens/landing/components/searchcoursemodal/styles.scss index 739f922d..136621c6 100644 --- a/client/src/screens/landing/components/searchcoursemodal/styles.scss +++ b/client/src/screens/landing/components/searchcoursemodal/styles.scss @@ -46,15 +46,34 @@ left: 0; right: 0; bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: 2; - display: none; - + background-color: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + z-index: 999; + display: flex; justify-content: center; align-items: center; -} + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1), + visibility 0.25s cubic-bezier(0.16, 1, 0.3, 1), + backdrop-filter 0.25s ease; -.show { - display: block; - display: flex; + .wrapper { + transform: scale(0.93) translateY(16px); + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), + box-shadow 0.3s ease; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + } + + &.show { + opacity: 1; + visibility: visible; + pointer-events: auto; + + .wrapper { + transform: scale(1) translateY(0); + } + } } \ No newline at end of file From a666dcc98f9474f322d63306602b1d5d014d53f3 Mon Sep 17 00:00:00 2001 From: DreamBot706 <187842058+DreamBot706@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:05:41 +0530 Subject: [PATCH 03/11] fix(ui): restore navbar links, smooth year transitions, add folder dialog effects, and clean exam card styling --- .../navbar/components/navlink/styles.scss | 5 +- client/src/components/navbar/styles.scss | 4 + .../components/browsefolder/confirmDialog.jsx | 9 +- .../components/folder-info/confirmDialog.jsx | 21 ++--- .../browse/components/folder-info/styles.scss | 64 +++++++++++-- .../components/year-info/confirmDelDialog.jsx | 9 +- .../components/year-info/confirmDialog.jsx | 22 ++--- client/src/screens/browse/index.jsx | 5 +- client/src/screens/browse/styles.scss | 92 ++++++++++++++++++- client/src/screens/contributions/styles.scss | 12 +++ .../components/examschedule/styles.scss | 9 +- 11 files changed, 198 insertions(+), 54 deletions(-) diff --git a/client/src/components/navbar/components/navlink/styles.scss b/client/src/components/navbar/components/navlink/styles.scss index 4653fb51..7c0b6e37 100644 --- a/client/src/components/navbar/components/navlink/styles.scss +++ b/client/src/components/navbar/components/navlink/styles.scss @@ -5,7 +5,7 @@ cursor: pointer; position: relative; display: inline-block; - color: #000; + color: #fff; transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), color 0.2s ease; @@ -16,13 +16,14 @@ left: 0; width: 100%; height: 2px; - background-color: #000; + background-color: #FECF6F; transform: scaleX(0); transform-origin: right; transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1); } &:hover { + color: #FECF6F; transform: translateY(-2px); &::after { diff --git a/client/src/components/navbar/styles.scss b/client/src/components/navbar/styles.scss index f6c33ab6..d2813518 100644 --- a/client/src/components/navbar/styles.scss +++ b/client/src/components/navbar/styles.scss @@ -142,6 +142,10 @@ nav { text-decoration: none; white-space: nowrap; // Prevent text wrapping + &::after { + display: none; + } + &:hover { background-color: #fecf6f; color: #2c3e50; diff --git a/client/src/screens/browse/components/browsefolder/confirmDialog.jsx b/client/src/screens/browse/components/browsefolder/confirmDialog.jsx index 6fdf9401..a4064372 100644 --- a/client/src/screens/browse/components/browsefolder/confirmDialog.jsx +++ b/client/src/screens/browse/components/browsefolder/confirmDialog.jsx @@ -73,8 +73,13 @@ const ConfirmDialog = ({ isOpen, onConfirm, onCancel, isLoading = false }) => { if (!isOpen) return null; return ( -
-
+
{ + if (e.target === e.currentTarget) onCancel(); + }} + > +
Delete

Are you sure?

diff --git a/client/src/screens/browse/components/folder-info/confirmDialog.jsx b/client/src/screens/browse/components/folder-info/confirmDialog.jsx index a6d5c1e5..efbb2b23 100644 --- a/client/src/screens/browse/components/folder-info/confirmDialog.jsx +++ b/client/src/screens/browse/components/folder-info/confirmDialog.jsx @@ -2,20 +2,6 @@ import React from "react"; import { useState } from "react"; import Wrapper from "../../../contributions/components/wrapper"; -const styles = { - overlay: { - position: "fixed", - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: "rgba(0, 0, 0, 0.4)", - display: "flex", - alignItems: "center", - justifyContent: "center", - zIndex: 1000, - }, -}; const ConfirmDialog = ({ show, @@ -31,7 +17,12 @@ const ConfirmDialog = ({ if (!show) return null; return ( -

+
{ + if (e.target === e.currentTarget) onCancel(); + }} + >
📁 Add Folder
diff --git a/client/src/screens/browse/components/folder-info/styles.scss b/client/src/screens/browse/components/folder-info/styles.scss index d921de10..31ce7c59 100644 --- a/client/src/screens/browse/components/folder-info/styles.scss +++ b/client/src/screens/browse/components/folder-info/styles.scss @@ -23,6 +23,7 @@ display: flex; justify-content: flex-start; align-items: center; + animation: folderNameSlideIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) both; .folder-name { font-family: "Bold"; @@ -87,13 +88,16 @@ background-color: #000; border: none; cursor: pointer; - transition: all 120ms ease; + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.2s ease, + background-color 0.2s ease; min-width: 80px; .icon { width: 20px; height: 20px; margin-bottom: 4px; + transition: transform 0.2s ease; &.download-icon { background: url(./assets/download.svg); @@ -116,24 +120,38 @@ } &:hover { - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); + + .icon { + transform: scale(1.1); + } + } + + &:active { + transform: translateY(0px) scale(0.96); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); } &:disabled { opacity: 0.6; cursor: not-allowed; + transform: none !important; + box-shadow: none !important; &:hover { - transform: none; - box-shadow: none; + transform: none !important; + box-shadow: none !important; + .icon { + transform: none; + } } } &.download { background-color: #000; // Black for download &:hover { - background-color: #333; + background-color: #222; } } @@ -141,7 +159,7 @@ background-color: #000; &:hover { - background-color: #333; + background-color: #222; } } @@ -210,16 +228,48 @@ .addfolderbutton { flex: 1; + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.2s ease, + background-color 0.2s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + &:active { + transform: translateY(0px) scale(0.97); + } + + &.false { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; + box-shadow: none !important; + } } .cancelbutton { background-color: #535353 !important; + color: #fff; } .submitbutton { background-color: #fecf6f !important; + color: #000; } #bottommarginneeded { margin-bottom: 2rem; } + +@keyframes folderNameSlideIn { + 0% { + opacity: 0; + transform: translateX(-8px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} diff --git a/client/src/screens/browse/components/year-info/confirmDelDialog.jsx b/client/src/screens/browse/components/year-info/confirmDelDialog.jsx index 22a5b068..ee2b144f 100644 --- a/client/src/screens/browse/components/year-info/confirmDelDialog.jsx +++ b/client/src/screens/browse/components/year-info/confirmDelDialog.jsx @@ -74,8 +74,13 @@ const ConfirmDelDialog = ({ isOpen, onConfirm, onCancel, isLoading = false }) => if (!isOpen) return null; return ( -
-
+
{ + if (e.target === e.currentTarget) onCancel(); + }} + > +
Delete

Are you sure?

diff --git a/client/src/screens/browse/components/year-info/confirmDialog.jsx b/client/src/screens/browse/components/year-info/confirmDialog.jsx index 687d5438..4d5c1baa 100644 --- a/client/src/screens/browse/components/year-info/confirmDialog.jsx +++ b/client/src/screens/browse/components/year-info/confirmDialog.jsx @@ -3,21 +3,6 @@ import Yroptions from "./year-options"; import { useState } from "react"; import Wrapper from "../../../contributions/components/wrapper"; -const styles = { - overlay: { - position: "fixed", - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: "rgba(0, 0, 0, 0.4)", - display: "flex", - alignItems: "center", - justifyContent: "center", - zIndex: 1000, - }, -}; - const ConfirmDialog = ({ show, yearName = "", @@ -30,7 +15,12 @@ const ConfirmDialog = ({ if (!show) return null; return ( -

+
{ + if (e.target === e.currentTarget) onCancel(); + }} + >
📁 Add Year
diff --git a/client/src/screens/browse/index.jsx b/client/src/screens/browse/index.jsx index f144c4f3..9330d636 100644 --- a/client/src/screens/browse/index.jsx +++ b/client/src/screens/browse/index.jsx @@ -466,7 +466,7 @@ const BrowseScreen = () => { />
-
+
{canGoBack && ( )} {!folderData ? ( -
{HeaderText}
+
{HeaderText}
) : folderData?.childType === "File" ? ( folderData?.children?.length === 0 ? ( -

No files available.

+

No files available.

) : ( { /> ) ) : folderData?.children?.length === 0 ? ( -
+

No folders available.

) : ( @@ -576,10 +576,10 @@ const BrowseScreen = () => { )}
{!folderData ? ( -
{HeaderText}
+
{HeaderText}
) : folderData?.childType === "File" ? ( folderData?.children?.length === 0 ? ( -

No files available.

+

No files available.

) : ( { /> ) ) : folderData?.children?.length === 0 ? ( -
+

No folders available.

) : ( diff --git a/client/src/screens/browse/styles.scss b/client/src/screens/browse/styles.scss index 470c7937..9ae45f22 100644 --- a/client/src/screens/browse/styles.scss +++ b/client/src/screens/browse/styles.scss @@ -54,8 +54,11 @@ display: flex; flex-wrap: wrap; } - .empty-message { + .empty-message, + .empty-folder { font-size: 1.2rem; + animation: folderItemSlideIn 0.26s cubic-bezier(0.16, 1, 0.3, 1); + will-change: opacity, transform; } &::-webkit-scrollbar { display: none; @@ -353,12 +356,15 @@ align-content: flex-start; // Align rows to top } - .empty-message { + .empty-message, + .empty-folder { font-size: 1.1rem; text-align: center; padding: 20px; color: #666; width: 100%; // Full width for empty message + animation: folderItemSlideIn 0.26s cubic-bezier(0.16, 1, 0.3, 1); + will-change: opacity, transform; } } .mobile-back-btn-circular { @@ -533,6 +539,16 @@ } } +@keyframes folderItemSlideIn { + 0% { + opacity: 0; + transform: translateX(-10px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} .confirm-dialog-overlay { position: fixed; From bd699fb720e7210abacebeab50d322790f2c6dc9 Mon Sep 17 00:00:00 2001 From: DreamBot706 <187842058+DreamBot706@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:39:00 +0530 Subject: [PATCH 08/11] fix(ui): eliminate network stutter on year navigation and smooth year pill transition --- client/src/screens/browse/components/year-info/index.jsx | 1 - client/src/screens/browse/index.jsx | 1 - client/src/screens/browse/styles.scss | 6 ++++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/screens/browse/components/year-info/index.jsx b/client/src/screens/browse/components/year-info/index.jsx index 48983fa0..abc42103 100644 --- a/client/src/screens/browse/components/year-info/index.jsx +++ b/client/src/screens/browse/components/year-info/index.jsx @@ -202,7 +202,6 @@ const YearInfo = ({ ChangeCurrentYearData(idx, course[idx]?.children || []) ); dispatch(ChangeFolder(course[idx])); - dispatch(RefreshCurrentFolder()); if (courseCode && course[idx]?._id) { navigate(`/browse/${courseCode}/${course[idx]._id}`); } else if (courseCode) { diff --git a/client/src/screens/browse/index.jsx b/client/src/screens/browse/index.jsx index 8ecb9c2c..d1c608c8 100644 --- a/client/src/screens/browse/index.jsx +++ b/client/src/screens/browse/index.jsx @@ -410,7 +410,6 @@ const BrowseScreen = () => { dispatch(ClearFolderHistory()); // Clear folder history when changing years dispatch(ChangeCurrentYearData(selectedYearIndex, selectedYear.children)); dispatch(ChangeFolder(selectedYear)); - dispatch(RefreshCurrentFolder()); if (selectedYear._id) { navigate(`/browse/${currCourseCode}/${selectedYear._id}`); } else { diff --git a/client/src/screens/browse/styles.scss b/client/src/screens/browse/styles.scss index 9ae45f22..de1dd764 100644 --- a/client/src/screens/browse/styles.scss +++ b/client/src/screens/browse/styles.scss @@ -85,15 +85,17 @@ display: flex; align-items: center; justify-content: space-between; + box-shadow: 0 0 0 transparent; transition: background-color 0.2s cubic-bezier(0.16, 1, 0.3, 1), color 0.2s cubic-bezier(0.16, 1, 0.3, 1), - padding-left 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), + padding-left 0.2s cubic-bezier(0.16, 1, 0.3, 1), + box-shadow 0.2s cubic-bezier(0.16, 1, 0.3, 1), transform 0.15s ease; &:hover:not(.selected) { background-color: rgba(254, 207, 111, 0.12); color: #fecf6f; - padding-left: 25px; + padding-left: 24px; } &:active { From 05aefb93d8f0b3744860863923c80ab7516b4725 Mon Sep 17 00:00:00 2001 From: DreamBot706 <187842058+DreamBot706@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:50:01 +0530 Subject: [PATCH 09/11] feat(ui): refine animation timing to 0.35s easeOutQuart with micro-stagger for silky smooth transitions --- .../src/screens/browse/components/browsefolder/index.jsx | 7 ++++++- .../src/screens/browse/components/browsefolder/styles.scss | 4 ++-- .../collapsible/components/file-controller/index.jsx | 4 ++-- .../src/screens/browse/components/file-display/index.jsx | 3 ++- .../src/screens/browse/components/file-display/styles.scss | 4 ++-- .../src/screens/browse/components/folder-info/styles.scss | 4 ++-- client/src/screens/browse/index.jsx | 6 ++++-- client/src/screens/browse/styles.scss | 6 +++--- 8 files changed, 23 insertions(+), 15 deletions(-) diff --git a/client/src/screens/browse/components/browsefolder/index.jsx b/client/src/screens/browse/components/browsefolder/index.jsx index aac9f54b..2fead7ef 100644 --- a/client/src/screens/browse/components/browsefolder/index.jsx +++ b/client/src/screens/browse/components/browsefolder/index.jsx @@ -17,6 +17,7 @@ const BrowseFolder = ({ folderData, parentFolder, isMobileView = false, + index = 0, }) => { const dispatch = useDispatch(); const navigate = useNavigate(); @@ -95,7 +96,11 @@ const BrowseFolder = ({ return ( <> -
onClick(folderData)}> +
onClick(folderData)} + style={{ animationDelay: `${Math.min(index * 30, 150)}ms` }} + >

{""}

diff --git a/client/src/screens/browse/components/browsefolder/styles.scss b/client/src/screens/browse/components/browsefolder/styles.scss index e90293fe..792fa22a 100644 --- a/client/src/screens/browse/components/browsefolder/styles.scss +++ b/client/src/screens/browse/components/browsefolder/styles.scss @@ -5,7 +5,7 @@ z-index: 2; cursor: pointer; transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), filter 0.2s ease; - animation: folderItemSlideIn 0.26s cubic-bezier(0.16, 1, 0.3, 1); + animation: folderItemSlideIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; will-change: opacity, transform; background: url(./folder.svg), none; background-position: center; @@ -252,7 +252,7 @@ @keyframes folderItemSlideIn { 0% { opacity: 0; - transform: translateX(-10px); + transform: translateX(-14px); } 100% { opacity: 1; diff --git a/client/src/screens/browse/components/collapsible/components/file-controller/index.jsx b/client/src/screens/browse/components/collapsible/components/file-controller/index.jsx index 493db02f..b7624305 100644 --- a/client/src/screens/browse/components/collapsible/components/file-controller/index.jsx +++ b/client/src/screens/browse/components/collapsible/components/file-controller/index.jsx @@ -11,8 +11,8 @@ const FileController = ({ files, code, isMobileView = false }) => { return file.isVerified; }); - return visibleFiles.map((file) => ( - + return visibleFiles.map((file, idx) => ( + )); }; diff --git a/client/src/screens/browse/components/file-display/index.jsx b/client/src/screens/browse/components/file-display/index.jsx index aff19e78..a2d74b4b 100644 --- a/client/src/screens/browse/components/file-display/index.jsx +++ b/client/src/screens/browse/components/file-display/index.jsx @@ -19,7 +19,7 @@ import FileRename from "./components/FileRename.jsx"; import { getFileDownloadLink } from "../../../../api/File"; import { fetchFolder } from "../../../../api/Folder.js"; -const FileDisplay = ({ file, path, code, isMobileView = false }) => { +const FileDisplay = ({ file, path, code, isMobileView = false, index = 0 }) => { const user = useSelector((state) => state.user?.user); const fileSize = formatFileSize(file.size); @@ -201,6 +201,7 @@ const FileDisplay = ({ file, path, code, isMobileView = false }) => {
{

No folders available.

) : ( - folderData?.children.map((folder) => ( + folderData?.children.map((folder, idx) => ( { folderData={folder} parentFolder={folderData} isMobileView={isMobile} + index={idx} /> )) )} @@ -590,7 +591,7 @@ const BrowseScreen = () => {

No folders available.

) : ( - folderData?.children.map((folder) => ( + folderData?.children.map((folder, idx) => ( { subject={currCourseCode || (folder.courses ? folder.courses[0] : folder.course)} folderData={folder} parentFolder={folderData} + index={idx} /> )) )} diff --git a/client/src/screens/browse/styles.scss b/client/src/screens/browse/styles.scss index de1dd764..f1e7b476 100644 --- a/client/src/screens/browse/styles.scss +++ b/client/src/screens/browse/styles.scss @@ -57,7 +57,7 @@ .empty-message, .empty-folder { font-size: 1.2rem; - animation: folderItemSlideIn 0.26s cubic-bezier(0.16, 1, 0.3, 1); + animation: folderItemSlideIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; will-change: opacity, transform; } &::-webkit-scrollbar { @@ -365,7 +365,7 @@ padding: 20px; color: #666; width: 100%; // Full width for empty message - animation: folderItemSlideIn 0.26s cubic-bezier(0.16, 1, 0.3, 1); + animation: folderItemSlideIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; will-change: opacity, transform; } } @@ -544,7 +544,7 @@ @keyframes folderItemSlideIn { 0% { opacity: 0; - transform: translateX(-10px); + transform: translateX(-14px); } 100% { opacity: 1; From cc7443befe2c72bfe008945151891a4623521dcf Mon Sep 17 00:00:00 2001 From: DreamBot706 <187842058+DreamBot706@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:21:39 +0530 Subject: [PATCH 10/11] fix: resolve review bugs, state handling, and animation performance issues --- client/src/actions/user_actions.js | 9 +++++ client/src/index.css | 11 ++++++ client/src/reducers/user_reducer.js | 12 +++++-- .../components/browsefolder/confirmDialog.jsx | 12 ------- .../components/browsefolder/styles.scss | 18 ++-------- .../components/file-display/styles.scss | 7 ++-- .../browse/components/folder-info/styles.scss | 1 - .../components/year-info/confirmDelDialog.jsx | 12 ------- client/src/screens/browse/index.jsx | 36 ++++++------------- client/src/screens/browse/styles.scss | 19 ++++++---- client/src/screens/dashboard/index.jsx | 16 +++++---- client/src/utils/folderUtils.js | 25 +++++++++++++ 12 files changed, 93 insertions(+), 85 deletions(-) diff --git a/client/src/actions/user_actions.js b/client/src/actions/user_actions.js index 995a4b07..8c5af0fd 100644 --- a/client/src/actions/user_actions.js +++ b/client/src/actions/user_actions.js @@ -51,3 +51,12 @@ export const ClearLocalCourses = () => { type: "CLEAR_LOCAL_COURSES", }; }; + +export const UpdateReadOnlyCourses = (readOnly) => { + return { + type: "UPDATE_READONLY_COURSES", + payload: { + readOnly, + }, + }; +}; diff --git a/client/src/index.css b/client/src/index.css index 73102e35..fd1d0abb 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -42,3 +42,14 @@ font-size: 14px; } } + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/client/src/reducers/user_reducer.js b/client/src/reducers/user_reducer.js index 14a40e8d..29dced07 100644 --- a/client/src/reducers/user_reducer.js +++ b/client/src/reducers/user_reducer.js @@ -15,8 +15,16 @@ const UserReducer = ( return { ...state, loggedIn: true, - user: action.payload.user, - favourites: action.payload.user.favourites, + user: { ...state.user, ...action.payload.user }, + favourites: action.payload.user?.favourites ?? state.favourites ?? [], + }; + case "UPDATE_READONLY_COURSES": + return { + ...state, + user: { + ...state.user, + readOnly: action.payload.readOnly, + }, }; case "LOG_OUT": return { ...state, loggedIn: false }; diff --git a/client/src/screens/browse/components/browsefolder/confirmDialog.jsx b/client/src/screens/browse/components/browsefolder/confirmDialog.jsx index a4064372..053a89e6 100644 --- a/client/src/screens/browse/components/browsefolder/confirmDialog.jsx +++ b/client/src/screens/browse/components/browsefolder/confirmDialog.jsx @@ -2,18 +2,6 @@ import React from "react"; import cross from "./cross.svg"; const styles = { - overlay: { - position: "fixed", - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: "rgba(0, 0, 0, 0.4)", - display: "flex", - alignItems: "center", - justifyContent: "center", - zIndex: 1000, - }, dialog: { backgroundColor: "#fff", padding: "25px 30px", diff --git a/client/src/screens/browse/components/browsefolder/styles.scss b/client/src/screens/browse/components/browsefolder/styles.scss index 792fa22a..02d32636 100644 --- a/client/src/screens/browse/components/browsefolder/styles.scss +++ b/client/src/screens/browse/components/browsefolder/styles.scss @@ -5,17 +5,16 @@ z-index: 2; cursor: pointer; transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), filter 0.2s ease; - animation: folderItemSlideIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; - will-change: opacity, transform; + animation: folderItemSlideIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) backwards; background: url(./folder.svg), none; background-position: center; background-repeat: no-repeat; &:hover { - transform: translateY(-4px) !important; + transform: translateY(-4px); filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.12)); } &:active { - transform: translateY(0px) scale(0.97) !important; + transform: translateY(0px) scale(0.97); filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.08)); } @media (max-width: 768px) { @@ -248,14 +247,3 @@ } } } - -@keyframes folderItemSlideIn { - 0% { - opacity: 0; - transform: translateX(-14px); - } - 100% { - opacity: 1; - transform: translateX(0); - } -} diff --git a/client/src/screens/browse/components/file-display/styles.scss b/client/src/screens/browse/components/file-display/styles.scss index a33ac417..11c14090 100644 --- a/client/src/screens/browse/components/file-display/styles.scss +++ b/client/src/screens/browse/components/file-display/styles.scss @@ -9,8 +9,7 @@ transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease, border-color 0.2s ease; - animation: fileItemSlideIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; - will-change: opacity, transform; + animation: fileItemSlideIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) backwards; &.verified { border: 2px solid #4caf50; } @@ -151,7 +150,7 @@ } &:hover { border: 1px solid #000; - transform: translateY(-4px) !important; + transform: translateY(-4px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12); .img-preview { .top, @@ -162,7 +161,7 @@ } &:active { - transform: translateY(0px) scale(0.97) !important; + transform: translateY(0px) scale(0.97); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); } diff --git a/client/src/screens/browse/components/folder-info/styles.scss b/client/src/screens/browse/components/folder-info/styles.scss index 2ee6b82d..d1131204 100644 --- a/client/src/screens/browse/components/folder-info/styles.scss +++ b/client/src/screens/browse/components/folder-info/styles.scss @@ -24,7 +24,6 @@ justify-content: flex-start; align-items: center; animation: folderNameSlideIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; - will-change: opacity, transform; .folder-name { font-family: "Bold"; diff --git a/client/src/screens/browse/components/year-info/confirmDelDialog.jsx b/client/src/screens/browse/components/year-info/confirmDelDialog.jsx index ee2b144f..88773572 100644 --- a/client/src/screens/browse/components/year-info/confirmDelDialog.jsx +++ b/client/src/screens/browse/components/year-info/confirmDelDialog.jsx @@ -2,18 +2,6 @@ import React from "react"; import cross from "../browsefolder/cross.svg"; const styles = { - overlay: { - position: "fixed", - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: "rgba(0, 0, 0, 0.4)", - display: "flex", - alignItems: "center", - justifyContent: "center", - zIndex: 1000, - }, dialog: { backgroundColor: "#fff", padding: "25px 30px", diff --git a/client/src/screens/browse/index.jsx b/client/src/screens/browse/index.jsx index 686936f7..e1728869 100644 --- a/client/src/screens/browse/index.jsx +++ b/client/src/screens/browse/index.jsx @@ -18,7 +18,6 @@ import { ChangeFolder, LoadCourses, UpdateCourses, - RefreshCurrentFolder, PushFolderHistory, PopFolderHistory, ClearFolderHistory, @@ -29,7 +28,7 @@ import { getUser } from "../../api/User"; import { useParams } from "react-router-dom"; import { getCourse } from "../../api/Course"; import { fetchFolder } from "../../api/Folder"; -import { getSubtreeFileCount } from "../../utils/folderUtils"; +import { getSubtreeFileCount, findFolderById, findYearIndexForFolder } from "../../utils/folderUtils"; import { toast } from "react-toastify"; import { useNavigate } from "react-router-dom"; import Share from "../share"; @@ -115,17 +114,6 @@ const BrowseScreen = () => { } }, []); - const findFolderById = (folders, id) => { - if (!folders || !Array.isArray(folders)) return null; - for (const folder of folders) { - if (folder._id === id) return folder; - if (folder.children?.length) { - const result = findFolderById(folder.children, id); - if (result) return result; - } - } - return null; - }; useEffect(() => { if (loading || !code) { @@ -161,8 +149,9 @@ const BrowseScreen = () => { let activeYearIndex = defaultYearIndex; if (folderId && activeFolder) { - const matchedIdx = fetchedData.children.findIndex( - (y) => y._id === activeFolder._id || findFolderById(y.children, activeFolder._id) + const matchedIdx = findYearIndexForFolder( + fetchedData.children, + activeFolder._id ); if (matchedIdx !== -1) { activeYearIndex = matchedIdx; @@ -209,8 +198,9 @@ const BrowseScreen = () => { let activeYearIndex = defaultYearIndex; if (folderId && activeFolder) { - const matchedIdx = fetchedData.children.findIndex( - (y) => y._id === activeFolder._id || findFolderById(y.children, activeFolder._id) + const matchedIdx = findYearIndexForFolder( + fetchedData.children, + activeFolder._id ); if (matchedIdx !== -1) { activeYearIndex = matchedIdx; @@ -245,9 +235,7 @@ const BrowseScreen = () => { const matched = findFolderById(currCourse, folderId); if (matched) { - const matchedYearIndex = currCourse.findIndex( - (y) => y._id === matched._id || findFolderById(y.children, matched._id) - ); + const matchedYearIndex = findYearIndexForFolder(currCourse, matched._id); if (matchedYearIndex !== -1 && matchedYearIndex !== currYear) { dispatch( ChangeCurrentYearData( @@ -261,9 +249,7 @@ const BrowseScreen = () => { fetchFolder(folderId, code) .then((freshFolder) => { if (freshFolder && freshFolder._id) { - const matchedYearIndex = currCourse.findIndex( - (y) => y._id === freshFolder._id || findFolderById(y.children, freshFolder._id) - ); + const matchedYearIndex = findYearIndexForFolder(currCourse, freshFolder._id); if (matchedYearIndex !== -1 && matchedYearIndex !== currYear) { dispatch( ChangeCurrentYearData( @@ -476,7 +462,7 @@ const BrowseScreen = () => { )} {!folderData ? ( -
{HeaderText}
+
{HeaderText}
) : folderData?.childType === "File" ? ( folderData?.children?.length === 0 ? (

No files available.

@@ -550,7 +536,7 @@ const BrowseScreen = () => { user.user?.previousCourses?.length > 0 && user.user?.previousCourses?.map((semesterGroup, semIdx) => (
-
+
Semester {semesterGroup.semester} ({semesterGroup.year})
{semesterGroup.courses.map((course, idx) => ( diff --git a/client/src/screens/browse/styles.scss b/client/src/screens/browse/styles.scss index f1e7b476..a93171cb 100644 --- a/client/src/screens/browse/styles.scss +++ b/client/src/screens/browse/styles.scss @@ -43,6 +43,16 @@ font-size: 1.2rem; background-color: #fecf6f; } + .semester-subheading { + font-size: 0.85em; + margin-top: 12px; + margin-bottom: 4px; + padding: 0 20px; + color: #666; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.5px; + } } .middle { flex: 6; @@ -58,7 +68,6 @@ .empty-folder { font-size: 1.2rem; animation: folderItemSlideIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; - will-change: opacity, transform; } &::-webkit-scrollbar { display: none; @@ -88,14 +97,13 @@ box-shadow: 0 0 0 transparent; transition: background-color 0.2s cubic-bezier(0.16, 1, 0.3, 1), color 0.2s cubic-bezier(0.16, 1, 0.3, 1), - padding-left 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s cubic-bezier(0.16, 1, 0.3, 1), - transform 0.15s ease; + transform 0.2s cubic-bezier(0.16, 1, 0.3, 1); &:hover:not(.selected) { background-color: rgba(254, 207, 111, 0.12); color: #fecf6f; - padding-left: 24px; + transform: translateX(4px); } &:active { @@ -366,7 +374,6 @@ color: #666; width: 100%; // Full width for empty message animation: folderItemSlideIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; - will-change: opacity, transform; } } .mobile-back-btn-circular { @@ -571,13 +578,11 @@ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); border-radius: 8px; animation: confirmWrapperSpringIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; - will-change: transform, opacity; } .confirm-modal-box { animation: confirmWrapperSpringIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3) !important; - will-change: transform, opacity; } } diff --git a/client/src/screens/dashboard/index.jsx b/client/src/screens/dashboard/index.jsx index b2d3565f..019f3aac 100644 --- a/client/src/screens/dashboard/index.jsx +++ b/client/src/screens/dashboard/index.jsx @@ -21,7 +21,7 @@ import { useEffect, useState } from "react"; import { getColors } from "../../utils/colors"; import { LoadCourses } from "../../actions/filebrowser_actions"; import Contributions from "../contributions"; -import { AddNewCourseLocal, ClearLocalCourses, LoginUser } from "../../actions/user_actions"; +import { AddNewCourseLocal, ClearLocalCourses, LoginUser, UpdateReadOnlyCourses } from "../../actions/user_actions"; import AddCourseModal from "./components/addcoursemodal"; import { AddNewCourseAPI, GetExamDates, getUser } from "../../api/User"; import { toast } from "react-toastify"; @@ -69,11 +69,11 @@ const Dashboard = () => { } const res = await AddNewCourseAPI(code, name); if (res?.data?.readOnly) { - dispatch(LoginUser({ ...user.user, readOnly: res.data.readOnly })); + dispatch(UpdateReadOnlyCourses(res.data.readOnly)); } else { const fresh = await getUser(); - if (fresh?.data) { - dispatch(LoginUser(fresh.data)); + if (fresh?.data?.readOnly) { + dispatch(UpdateReadOnlyCourses(fresh.data.readOnly)); } } toast.success(`Course ${code.toUpperCase()} added to Others!`); @@ -87,10 +87,12 @@ const Dashboard = () => { }; const handleCourseRemoved = (removedCode) => { + if (!removedCode) return; + const normalizedRemoved = removedCode.replace(/\s+/g, "").toLowerCase(); const updatedReadOnly = (user.user?.readOnly || []).filter( - (c) => c.code?.replaceAll(" ", "").toLowerCase() !== removedCode?.replaceAll(" ", "").toLowerCase() + (c) => c.code?.replace(/\s+/g, "").toLowerCase() !== normalizedRemoved ); - dispatch(LoginUser({ ...user.user, readOnly: updatedReadOnly })); + dispatch(UpdateReadOnlyCourses(updatedReadOnly)); toast.success(`Course ${removedCode.toUpperCase()} removed`); }; @@ -185,7 +187,7 @@ const Dashboard = () => {
- {user.user.readOnly.map((course, index) => ( + {(user.user?.readOnly || []).map((course, index) => ( { return 0; }; + +/** + * Recursively finds a folder by its _id within a nested folder structure. + */ +export const findFolderById = (folders, id) => { + if (!folders || !Array.isArray(folders) || !id) return null; + for (const folder of folders) { + if (folder?._id === id) return folder; + if (folder?.children?.length) { + const result = findFolderById(folder.children, id); + if (result) return result; + } + } + return null; +}; + +/** + * Finds the index of the year within years array that contains targetFolderId either as the year folder itself or in its subtree. + */ +export const findYearIndexForFolder = (years, targetFolderId) => { + if (!Array.isArray(years) || !targetFolderId) return -1; + return years.findIndex( + (y) => y?._id === targetFolderId || findFolderById(y?.children, targetFolderId) + ); +}; From 30c3e30618139c80738da8020b3f63880fd480f7 Mon Sep 17 00:00:00 2001 From: DreamBot706 <187842058+DreamBot706@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:54:33 +0530 Subject: [PATCH 11/11] fix(browse): remove year section scrollbar and isolate hover shift to inner title --- client/src/screens/browse/styles.scss | 30 ++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/client/src/screens/browse/styles.scss b/client/src/screens/browse/styles.scss index a93171cb..758fe428 100644 --- a/client/src/screens/browse/styles.scss +++ b/client/src/screens/browse/styles.scss @@ -16,11 +16,14 @@ } } .controller { + display: flex; + height: 92vh; // Keep original desktop height + overflow-x: hidden; + scrollbar-width: none; + -ms-overflow-style: none; &::-webkit-scrollbar { display: none; } - display: flex; - height: 92vh; // Keep original desktop height @media (max-width: 1024px) { height: 98vh; // Compensate for 2vh navbar } @@ -37,6 +40,11 @@ max-width: 300px; border-right: 1px solid rgba(0, 0, 0, 0.33); overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } .heading { padding: 10px 20px; font-family: "Bold"; @@ -58,6 +66,8 @@ flex: 6; border-right: 1px solid rgba(0, 0, 0, 0.33); overflow: scroll; + scrollbar-width: none; + -ms-overflow-style: none; .files { padding: 20px 20px; @@ -78,6 +88,13 @@ max-width: 200px; background-color: #000; color: #fff; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } .year-content { display: flex; flex-direction: column-reverse; @@ -97,13 +114,15 @@ box-shadow: 0 0 0 transparent; transition: background-color 0.2s cubic-bezier(0.16, 1, 0.3, 1), color 0.2s cubic-bezier(0.16, 1, 0.3, 1), - box-shadow 0.2s cubic-bezier(0.16, 1, 0.3, 1), - transform 0.2s cubic-bezier(0.16, 1, 0.3, 1); + box-shadow 0.2s cubic-bezier(0.16, 1, 0.3, 1); &:hover:not(.selected) { background-color: rgba(254, 207, 111, 0.12); color: #fecf6f; - transform: translateX(4px); + + .year-title-wrapper { + transform: translateX(4px); + } } &:active { @@ -114,6 +133,7 @@ display: flex; align-items: center; gap: 8px; + transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1); } .empty-indicator {