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/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..7c0b6e37 100644
--- a/client/src/components/navbar/components/navlink/styles.scss
+++ b/client/src/components/navbar/components/navlink/styles.scss
@@ -1,10 +1,38 @@
.nav-link {
- font-family: 'bold';
+ font-family: 'Bold';
font-size: 1.1rem;
margin-left: 2rem;
cursor: pointer;
+ position: relative;
+ display: inline-block;
+ color: #fff;
+ 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: #FECF6F;
+ transform: scaleX(0);
+ transform-origin: right;
+ transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
+ }
&:hover {
- scale: 1.1;
+ color: #FECF6F;
+ 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/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/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 34d67fef..29dced07 100644
--- a/client/src/reducers/user_reducer.js
+++ b/client/src/reducers/user_reducer.js
@@ -15,41 +15,44 @@ 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 };
case "UPDATE_FAVOURITES":
return { ...state, favourites: action.payload.favourites };
- case "ADD_COURSE_LOCAL":
- if (
- state.user?.courses?.find(
- (course) =>
- course.code.toLowerCase() === action.payload.course.code.toLowerCase()
- )
- )
- return state;
- if (
- state.user?.previousCourses?.flatMap((sem) => sem.courses)?.find(
- (course) =>
- course.code.toLowerCase() === action.payload.course.code.toLowerCase()
- )
- )
- return state;
- if (
- state.user?.readOnly?.find(
- (course) =>
- course.code.toLowerCase() === action.payload.course.code.toLowerCase()
- )
- )
- return state;
- if (state.localCourses?.find((course) => course.code === action.payload.course.code))
- return state;
+ case "ADD_COURSE_LOCAL": {
+ const incomingCode = action.payload.course?.code?.toString()?.replace(/\s+/g, "")?.toLowerCase();
+ if (!incomingCode) return state;
+
+ const matchesIncoming = (c) => c?.code?.toString()?.replace(/\s+/g, "")?.toLowerCase() === incomingCode;
+
+ if (state.user?.courses?.some(matchesIncoming)) return state;
+ if (state.user?.previousCourses?.flatMap((sem) => sem.courses || []).some(matchesIncoming)) return state;
+ if (state.user?.readOnly?.some(matchesIncoming)) return state;
+ if (state.localCourses?.some(matchesIncoming)) return state;
+
upsertLocalCourseCache(action.payload.course);
return { ...state, localCourses: [...state.localCourses, action.payload.course] };
- case "LOAD_LOCAL_COURSES":
- return { ...state, localCourses: [...state.localCourses, ...action.payload.courses] };
+ }
+ case "LOAD_LOCAL_COURSES": {
+ const existingCodes = new Set(
+ (state.localCourses || []).map((c) => c?.code?.toString()?.replace(/\s+/g, "")?.toLowerCase())
+ );
+ const newUnique = (action.payload.courses || []).filter(
+ (c) => c?.code && !existingCodes.has(c.code.toString().replace(/\s+/g, "").toLowerCase())
+ );
+ return { ...state, localCourses: [...state.localCourses, ...newUnique] };
+ }
case "CLEAR_LOCAL_COURSES":
return { ...state, localCourses: [] };
case "UPDATE_USER":
diff --git a/client/src/screens/browse/components/browsefolder/confirmDialog.jsx b/client/src/screens/browse/components/browsefolder/confirmDialog.jsx
index 6fdf9401..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",
@@ -73,8 +61,13 @@ const ConfirmDialog = ({ isOpen, onConfirm, onCancel, isLoading = false }) => {
if (!isOpen) return null;
return (
-
-
+
{
+ if (e.target === e.currentTarget) onCancel();
+ }}
+ >
+
Are you sure?
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 0509057f..02d32636 100644
--- a/client/src/screens/browse/components/browsefolder/styles.scss
+++ b/client/src/screens/browse/components/browsefolder/styles.scss
@@ -4,12 +4,18 @@
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;
+ 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(-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/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/collapsible/components/folder/styles.scss b/client/src/screens/browse/components/collapsible/components/folder/styles.scss
index 44b635ec..90105f52 100644
--- a/client/src/screens/browse/components/collapsible/components/folder/styles.scss
+++ b/client/src/screens/browse/components/collapsible/components/folder/styles.scss
@@ -18,15 +18,35 @@
.text-content{
width: 100%;
background-color: #fff;
+ padding: 0.3em;
+ border-radius: 4px;
+ transition: background-color 0.2s cubic-bezier(0.16, 1, 0.3, 1),
+ padding-left 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
+ transform 0.15s ease;
+
&.current{
background-color: #FECF6F;
}
- padding: 0.3em;
+ &:hover:not(.current) {
+ background-color: rgba(254, 207, 111, 0.2);
+ padding-left: 0.6em;
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+
.text{
font-size: 1rem;
font-family: 'Bold';
cursor: pointer;
+ transition: color 0.15s ease;
+
+ &:hover {
+ color: #000;
+ }
+
&.nobold{
font-family: 'Regular';
}
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/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 }) => {

+
{
+ if (e.target === e.currentTarget) onCancel();
+ }}
+ >
📁 Add Folder
diff --git a/client/src/screens/browse/components/folder-info/index.jsx b/client/src/screens/browse/components/folder-info/index.jsx
index bb110e84..5a7fb89a 100644
--- a/client/src/screens/browse/components/folder-info/index.jsx
+++ b/client/src/screens/browse/components/folder-info/index.jsx
@@ -218,7 +218,7 @@ const FolderInfo = ({
{path}
-
+
{name}
{currentFolder && (
diff --git a/client/src/screens/browse/components/folder-info/styles.scss b/client/src/screens/browse/components/folder-info/styles.scss
index d921de10..d1131204 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.35s cubic-bezier(0.22, 1, 0.36, 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(-14px);
+ }
+ 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..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",
@@ -74,8 +62,13 @@ const ConfirmDelDialog = ({ isOpen, onConfirm, onCancel, isLoading = false }) =>
if (!isOpen) return null;
return (
-
-
+
{
+ if (e.target === e.currentTarget) onCancel();
+ }}
+ >
+
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/components/year-info/index.jsx b/client/src/screens/browse/components/year-info/index.jsx
index 081de71f..abc42103 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,16 @@ 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..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) {
@@ -159,12 +147,24 @@ 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 = findYearIndexForFolder(
+ fetchedData.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 +196,24 @@ 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 = findYearIndexForFolder(
+ fetchedData.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 +235,29 @@ const BrowseScreen = () => {
const matched = findFolderById(currCourse, folderId);
if (matched) {
+ const matchedYearIndex = findYearIndexForFolder(currCourse, 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 = findYearIndexForFolder(currCourse, freshFolder._id);
+ if (matchedYearIndex !== -1 && matchedYearIndex !== currYear) {
+ dispatch(
+ ChangeCurrentYearData(
+ matchedYearIndex,
+ currCourse[matchedYearIndex]?.children || []
+ )
+ );
+ }
dispatch(ChangeFolder(freshFolder));
} else {
toast.error("Folder not found!");
@@ -366,7 +396,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 {
@@ -436,7 +465,7 @@ const BrowseScreen = () => {
{HeaderText}
) : folderData?.childType === "File" ? (
folderData?.children?.length === 0 ? (
-
No files available.
+
No files available.
) : (
{
/>
)
) : folderData?.children?.length === 0 ? (
-
+
) : (
- folderData?.children.map((folder) => (
+ folderData?.children.map((folder, idx) => (
{
folderData={folder}
parentFolder={folderData}
isMobileView={isMobile}
+ index={idx}
/>
))
)}
@@ -506,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) => (
@@ -522,7 +552,7 @@ const BrowseScreen = () => {
{folderData && (
{
)}
{!folderData ? (
-
{HeaderText}
+
{HeaderText}
) : folderData?.childType === "File" ? (
folderData?.children?.length === 0 ? (
-
No files available.
+
No files available.
) : (
{
/>
)
) : folderData?.children?.length === 0 ? (
-
+
) : (
- 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 7eb42b56..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,25 +40,44 @@
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";
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;
border-right: 1px solid rgba(0, 0, 0, 0.33);
overflow: scroll;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
.files {
padding: 20px 20px;
display: flex;
flex-wrap: wrap;
}
- .empty-message {
+ .empty-message,
+ .empty-folder {
font-size: 1.2rem;
+ animation: folderItemSlideIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both;
}
&::-webkit-scrollbar {
display: none;
@@ -66,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;
@@ -82,11 +111,29 @@
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),
+ 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;
+
+ .year-title-wrapper {
+ transform: translateX(4px);
+ }
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
.year-title-wrapper {
display: flex;
align-items: center;
gap: 8px;
+ transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.empty-indicator {
@@ -101,11 +148,14 @@
background: rgba(255, 255, 255, 0.05);
line-height: 1.2;
user-select: none;
+ transition: all 0.2s ease;
}
&.selected {
color: #000;
background-color: #fff;
+ font-family: "Bold";
+ box-shadow: -4px 0 0 #fecf6f;
.empty-indicator {
color: rgba(0, 0, 0, 0.4);
@@ -145,10 +195,16 @@
color: #888;
font-size: 0.9rem;
cursor: pointer;
- transition: color 0.2s ease-in-out;
+ display: inline-block;
+ transition: color 0.2s ease, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
&:hover {
- color: #bbb;
+ color: #fecf6f;
+ transform: translateX(3px);
+ }
+
+ &:active {
+ transform: translateX(1px) scale(0.97);
}
.text {
@@ -330,12 +386,14 @@
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.35s cubic-bezier(0.22, 1, 0.36, 1) both;
}
}
.mobile-back-btn-circular {
@@ -509,3 +567,62 @@
margin-bottom: 0;
}
}
+
+@keyframes folderItemSlideIn {
+ 0% {
+ opacity: 0;
+ transform: translateX(-14px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.confirm-dialog-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(5px);
+ -webkit-backdrop-filter: blur(5px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ animation: confirmOverlayFadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+
+ .wrapper {
+ 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;
+ }
+
+ .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;
+ }
+}
+
+@keyframes confirmOverlayFadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes confirmWrapperSpringIn {
+ from {
+ opacity: 0;
+ transform: scale(0.93) translateY(16px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
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/contributions/styles.scss b/client/src/screens/contributions/styles.scss
index 8468a9fc..533c3b3e 100644
--- a/client/src/screens/contributions/styles.scss
+++ b/client/src/screens/contributions/styles.scss
@@ -160,6 +160,18 @@
margin-top: 1rem;
font-family: 'bold';
cursor: pointer;
+ transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
+ box-shadow 0.2s ease,
+ background-color 0.2s ease;
+
+ &:hover:not(.false) {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }
+
+ &:active:not(.false) {
+ transform: translateY(0px) scale(0.97);
+ }
&.true {
background-color: #FECF6F;
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..18af4ac3 100644
--- a/client/src/screens/dashboard/components/examcard/styles.scss
+++ b/client/src/screens/dashboard/components/examcard/styles.scss
@@ -10,6 +10,12 @@
flex-shrink: 0;
justify-content: space-around;
+ transition: 150ms ease;
+
+ &:hover {
+ transform: scale(1.04);
+ }
+
.ndays {
p {
font-size: 3.5rem;
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..5640af7d 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);
}
}
}
diff --git a/client/src/screens/dashboard/index.jsx b/client/src/screens/dashboard/index.jsx
index 1716724a..019f3aac 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, UpdateReadOnlyCourses } 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,56 @@ 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(UpdateReadOnlyCourses(res.data.readOnly));
+ } else {
+ const fresh = await getUser();
+ if (fresh?.data?.readOnly) {
+ dispatch(UpdateReadOnlyCourses(fresh.data.readOnly));
+ }
+ }
+ 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) => {
+ if (!removedCode) return;
+ const normalizedRemoved = removedCode.replace(/\s+/g, "").toLowerCase();
+ const updatedReadOnly = (user.user?.readOnly || []).filter(
+ (c) => c.code?.replace(/\s+/g, "").toLowerCase() !== normalizedRemoved
+ );
+ dispatch(UpdateReadOnlyCourses(updatedReadOnly));
+ toast.success(`Course ${removedCode.toUpperCase()} removed`);
+ };
+
useEffect(() => {
clearLegacySessionLocalCoursesCache();
dispatch(ClearLocalCourses());
@@ -160,14 +187,15 @@ const Dashboard = () => {
- {user.user.readOnly.map((course, index) => (
+ {(user.user?.readOnly || []).map((course, index) => (
handleClick(course.code)}
isReadOnly={true}
+ onCourseRemoved={handleCourseRemoved}
/>
))}
@@ -202,12 +230,12 @@ const Dashboard = () => {
{showPrevious && (
- <>
+
{user.user.previousCourses.map((semesterGroup, semIndex) => (
toggleSemester(semIndex)}>
-
+
▼
{
/>
{openSemesters[semIndex] && (
- <>
+
{semesterGroup.courses.map((course, index) => (
@@ -230,11 +258,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
diff --git a/client/src/utils/folderUtils.js b/client/src/utils/folderUtils.js
index 2086c896..8f7fe561 100644
--- a/client/src/utils/folderUtils.js
+++ b/client/src/utils/folderUtils.js
@@ -24,3 +24,28 @@ export const getSubtreeFileCount = (folder) => {
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)
+ );
+};