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(); + }} + > +
Delete

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(); + }} + > +
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/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 ? ( -
+

No folders available.

) : ( - 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 ? ( -
+

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 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) + ); +};