diff --git a/client/package.json b/client/package.json index 7a26de8..832a710 100644 --- a/client/package.json +++ b/client/package.json @@ -8,6 +8,7 @@ "axios": "^1.7.9", "highlight.js": "^11.11.1", "katex": "^0.16.21", + "lucide-react": "^0.487.0", "mongodb": "6.12.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/client/src/components/main/homePage/notification/index.css b/client/src/components/main/homePage/notification/index.css index 2d0c2eb..bd23f13 100644 --- a/client/src/components/main/homePage/notification/index.css +++ b/client/src/components/main/homePage/notification/index.css @@ -18,6 +18,12 @@ margin: 0; } +.section-title-off { + font-size: 1.25rem; + color: var(--home-text); + margin-top: 1rem; +} + .notification-box { padding-top: 0.7rem; } diff --git a/client/src/components/main/homePage/notification/index.tsx b/client/src/components/main/homePage/notification/index.tsx index 845a566..5486672 100644 --- a/client/src/components/main/homePage/notification/index.tsx +++ b/client/src/components/main/homePage/notification/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import useNotifications from '../../../../hooks/useNotification'; import './index.css'; @@ -6,6 +6,23 @@ import './index.css'; const NotificationView = () => { const navigate = useNavigate(); const { notifications, viewNotification, clearNotifications } = useNotifications(); + const [notificationsOn, setNotificationsOn] = useState(true); + + useEffect(() => { + const saved = localStorage.getItem('notificationsEnabled'); + setNotificationsOn(saved ? JSON.parse(saved) : true); + }, []); + + if (!notificationsOn) { + return ( +
+
+

Notification Catch-up

+
+

Notifications are turned off.

+
+ ); + } return (
diff --git a/client/src/components/profileSettings/index.tsx b/client/src/components/profileSettings/index.tsx index 9944c15..fcd39e1 100644 --- a/client/src/components/profileSettings/index.tsx +++ b/client/src/components/profileSettings/index.tsx @@ -5,6 +5,7 @@ import useProfileSettings from '../../hooks/useProfileSettings'; import UserContributionsComponent from './userContributionsComponent'; import ShortcutsComponent from './shortcutsComponent'; import ToggleDarkLightComponent from './toggleDarkLightComponent'; +import ToggleNotification from './toggleNotification'; const ProfileSettings: React.FC = () => { const { @@ -59,6 +60,7 @@ const ProfileSettings: React.FC = () => { {userData ? ( <> +

General Information

Username: {userData.username} @@ -110,10 +112,10 @@ const ProfileSettings: React.FC = () => { {userData.dateJoined ? new Date(userData.dateJoined).toLocaleDateString() : 'N/A'}

- - + + {/* ---- Reset Password Section ---- */} {canEditProfile && ( <> diff --git a/client/src/components/profileSettings/toggleDarkLightComponent/index.css b/client/src/components/profileSettings/toggleDarkLightComponent/index.css index 0b16e1d..03c8a10 100644 --- a/client/src/components/profileSettings/toggleDarkLightComponent/index.css +++ b/client/src/components/profileSettings/toggleDarkLightComponent/index.css @@ -1,9 +1,9 @@ :root { - --dark-text: #292929; - --light-text: #f9f8f8; - --dark-background: #2f4550; - --light-background: #586f7c; - --accent: #b8dbd9; + --dark-text: #f9f8f8; + --light-text: #292929; + --dark-background: #13091b; + --light-background: #efe2fc; + --accent: #be9fdb; --button-border: #b8dbd9; } @@ -12,7 +12,7 @@ --dark-background: #13091b; --light-background: #efe2fc; --accent: #be9fdb; - --comment:#be9fdb; + --comment: #be9fdb; --header: #1429af21; --container: #281738; --nav-bar: #372a43; @@ -27,16 +27,91 @@ --dark-background: #f3f3ed; --light-background: #d8d8d8; --accent: #380666; - --comment:white; + --comment: white; --header: #8896f16d; --container: #DDCDEC; --nav-bar: #d4aef8; --button: #4d2a6d; --button-hover: #9d81b7; - --notification: rgb(236, 236, 236); + --notification: rgb(236, 236, 236); --home-text: #32206C; } +.theme-flatly { + --text: #495057; + --dark-background: #f8f9fa; + --light-background: #f8f9fa; + --accent: #0d6efd; + --comment: #0d6efd; + --header: #e9ecef; + --container: #f8f9fa; + --nav-bar: #f8f9fa; + --button: #0d6efd; + --button-hover: #0d6efd; +} + +.theme-darkly { + --text: #ffffff; + --dark-background: #343a40; + --light-background: #343a40; + --accent: #6610f2; + --comment: #6610f2; + --header: #343a40; + --container: #343a40; + --nav-bar: #343a40; + --button: #6610f2; + --button-hover: #6610f2; +} + +.theme-minty { + --text: #2e3a34; + --dark-background: #eaf0e1; + --light-background: #eaf0e1; + --accent: #4fa3d1; + --comment: #4fa3d1; + --header: #eaf0e1; + --container: #eaf0e1; + --nav-bar: #eaf0e1; + --button: #83c5be; + --button-hover: #83c5be; +} + +.theme-quartz { + --text: #212529; + --dark-background: #f8f9fa; + --light-background: #f8f9fa; + --accent: #7952b3; + --comment: #7952b3; + --header: #f8f9fa; + --container: #f8f9fa; + --nav-bar: #f8f9fa; + --button: #7952b3; + --button-hover: #7952b3; +} + +.theme-colorblind-mode { + --primary-color: #000000; + --background-color-light: #f5f5f5; + --text-color: #000000; + --success-color: #3366cc; + --warning-color: #ff9933; + --danger-color: #cc0000; + --button-background: #3366cc; + /* --button-text: #ffffff; */ + --link-color: #3366cc; + --border-color: #ced4da; +} + +.colorblind-mode { + --text: var(--text-color); + --dark-background: var(--background-color-light); + --light-background: var(--background-color-light); + --button: var(--button-background); + --button-hover: var(--button-background); + --link: var(--link-color); + --border: var(--border-color); +} + html, #root { background-color: var(--dark-background); @@ -98,3 +173,143 @@ nav { .toggle-switch.light .toggle-thumb { transform: translateX(30px); } + +:root { + --text-small: 0.8rem; + --text-medium: 1rem; + --text-large: 1.2rem; +} + +body { + font-size: var(--text-medium); +} + +.theme-small { + font-size: var(--text-small); +} + +.theme-large { + font-size: var(--text-large); +} + +.settings-container { + padding: 10px; + background-color: var(--background); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + max-width: 250px; + font-family: 'Arial', sans-serif; + transition: all 0.3s ease; +} + +.settings-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.settings-header h2 { + font-size: 1em; /* Smaller header */ + color: var(--primary-color); + margin: 0; +} + +.collapse-button { + background-color: #f1f1f1; + border: none; + padding: 5px 10px; + cursor: pointer; + font-size: 0.9em; + color: var(--primary-color); + border-radius: 4px; + transition: transform 0.2s ease; +} + +.collapse-button:hover { + transform: scale(1.1); +} + +.settings-content { + display: flex; + flex-direction: column; +} + +.settings-item { + margin-bottom: 12px; + padding-bottom: 12px; /* Space between items */ + border-bottom: 1px solid var(--border-color); /* Divider between items */ +} + +.settings-item:last-child { + border-bottom: none; /* Remove the bottom border from the last item */ +} + + +.setting-label { + font-size: 1em; + margin-bottom: 5px; + color: var(--secondary-color); + margin: 10px; +} + +.setting-select { + padding: 6px; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--input-background); + font-size: 0.9em; /* Smaller font size */ +} + +.switch { + position: relative; + display: inline-block; + width: 45px; + height: 22px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.4s; + border-radius: 34px; +} + +.slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + border-radius: 50%; + left: 4px; + bottom: 4px; + background-color: white; + transition: 0.4s; +} + +input:checked + .slider { + background-color: #4CAF50; +} + +input:checked + .slider:before { + transform: translateX(23px); /* Adjusted for smaller switch */ +} + +/* Colors */ +:root { + --primary-color: #333; + --secondary-color: #555; + --background: #fff; + --input-background: #f1f1f1; + --border-color: #ddd; +} diff --git a/client/src/components/profileSettings/toggleDarkLightComponent/index.tsx b/client/src/components/profileSettings/toggleDarkLightComponent/index.tsx index 742fcd2..c1ca73c 100644 --- a/client/src/components/profileSettings/toggleDarkLightComponent/index.tsx +++ b/client/src/components/profileSettings/toggleDarkLightComponent/index.tsx @@ -1,31 +1,140 @@ import React, { useState, useEffect } from 'react'; -import { setTheme } from '../../../tool'; +import { setTheme, setTextSize, setColorblindMode } from '../../../tool'; import './index.css'; function ToggleDarkLightComponent() { const [togClass, setTogClass] = useState('dark'); - - const handleOnClick = () => { - const currentTheme = localStorage.getItem('theme'); - const newTheme = currentTheme === 'theme-dark' ? 'theme-light' : 'theme-dark'; - setTheme(newTheme); - setTogClass(newTheme === 'theme-dark' ? 'dark' : 'light'); - }; + const [textClass, setTextClass] = useState(localStorage.getItem('text-size') || 'medium'); + const [colorblindClass, setColorblindClass] = useState( + localStorage.getItem('colorblind-mode') === 'true', + ); + const [isCollapsed, setIsCollapsed] = useState(false); useEffect(() => { - const currentTheme = localStorage.getItem('theme'); - if (currentTheme === 'theme-dark') { - setTogClass('dark'); + const currentTheme = localStorage.getItem('theme') || 'theme-dark'; + setTogClass(currentTheme); + setTextClass(localStorage.getItem('text-size') || 'medium'); + setColorblindClass(localStorage.getItem('colorblind-mode') === 'true'); + document.documentElement.className = currentTheme; + + if (textClass === 'small') { + document.body.classList.add('theme-small'); + document.body.classList.remove('theme-medium', 'theme-large'); + } else if (textClass === 'large') { + document.body.classList.add('theme-large'); + document.body.classList.remove('theme-medium', 'theme-small'); + } else { + document.body.classList.add('theme-medium'); + document.body.classList.remove('theme-small', 'theme-large'); + } + + if (colorblindClass) { + document.documentElement.classList.add('colorblind-mode'); } else { - setTogClass('light'); + document.documentElement.classList.remove('colorblind-mode'); } - }, []); + }, [colorblindClass, textClass]); + + const handleTextSizeChange = (e: React.ChangeEvent) => { + const newSize = e.target.value; + setTextSize(newSize); + setTextClass(newSize); + localStorage.setItem('text-size', newSize); + }; + + const handleColorblindModeChange = (e: React.ChangeEvent) => { + const isChecked = e.target.checked; + setColorblindMode(String(isChecked)); + setColorblindClass(isChecked); + localStorage.setItem('colorblind-mode', isChecked.toString()); + const currentTheme = localStorage.getItem('theme') || 'theme-dark'; + document.documentElement.classList.toggle('colorblind-mode', isChecked); + document.documentElement.classList.add(currentTheme); + }; + + const handleLightModeChange = (e: React.ChangeEvent) => { + const isChecked = e.target.checked; + setTogClass(isChecked ? 'theme-light' : 'theme-dark'); + document.documentElement.className = isChecked ? 'theme-light' : 'theme-dark'; + localStorage.setItem('theme', isChecked ? 'theme-light' : 'theme-dark'); + }; + + const toggleCollapse = () => { + setIsCollapsed(prevState => !prevState); + }; return ( -
-
-
+
+
+

Settings

+
+ + {!isCollapsed && ( +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ )}
); } diff --git a/client/src/components/profileSettings/toggleNotification/index.css b/client/src/components/profileSettings/toggleNotification/index.css new file mode 100644 index 0000000..e33b257 --- /dev/null +++ b/client/src/components/profileSettings/toggleNotification/index.css @@ -0,0 +1,39 @@ +.toggle-container { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.toggle-switch { + width: 60px; + height: 30px; + border-radius: 30px; + background-color: #ccc; + position: relative; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.toggle-switch.off { + background-color: #372a43; +} + +.toggle-switch.on { + background-color: #be9fdb; +} + +.toggle-thumb { + width: 26px; + height: 26px; + background-color: white; + border-radius: 50%; + position: absolute; + top: 2px; + left: 2px; + transition: transform 0.3s ease; +} + +.toggle-switch.on .toggle-thumb { + transform: translateX(30px); +} diff --git a/client/src/components/profileSettings/toggleNotification/index.tsx b/client/src/components/profileSettings/toggleNotification/index.tsx new file mode 100644 index 0000000..94e0913 --- /dev/null +++ b/client/src/components/profileSettings/toggleNotification/index.tsx @@ -0,0 +1,32 @@ +import React, { useState, useEffect } from 'react'; +import './index.css'; + +function ToggleNotification() { + const [notificationsEnabled, setNotificationsEnabled] = useState(true); + + const handleOnClick = () => { + const newValue = !notificationsEnabled; + localStorage.setItem('notificationsEnabled', JSON.stringify(newValue)); + setNotificationsEnabled(newValue); + }; + + useEffect(() => { + const savedValue = localStorage.getItem('notificationsEnabled'); + if (savedValue !== null) { + setNotificationsEnabled(JSON.parse(savedValue)); + } + }, []); + + return ( +
+

Notifications

+
+
+
+
+ ); +} + +export default ToggleNotification; diff --git a/client/src/tool/index.tsx b/client/src/tool/index.tsx index 795cf43..a798e55 100644 --- a/client/src/tool/index.tsx +++ b/client/src/tool/index.tsx @@ -42,6 +42,48 @@ const keepTheme = () => { } }; +/** + * Helper function to set the website's text size. + * @param textSize - The string object which identifies the text size. + */ +const setTextSize = (textSize: string): void => { + localStorage.setItem('textSize', textSize); + document.documentElement.className = textSize; +}; + +/** + * Helper function to store a user's text size so it is kept when browser reopened. + */ +const keepTextSize = (): void => { + const textSize = localStorage.getItem('textSize'); + if (textSize) { + setTextSize(textSize); + } else { + setTextSize('medium'); + } +}; + +/** + * Helper function to set the website's colorblind mode. + * @param colorblindMode - The string object which identifies the colorblind mode. + */ +const setColorblindMode = (colorblindMode: string): void => { + localStorage.setItem('colorblindMode', colorblindMode); + document.documentElement.className = colorblindMode; +}; + +/** + * Helper function to store a user's colorblind mode so it is kept when browser reopened. + */ +const keepColorblindMode = (): void => { + const colorblindMode = localStorage.getItem('colorblindMode'); + if (colorblindMode) { + setColorblindMode(colorblindMode); + } else { + setColorblindMode('false'); + } +}; + /** * Helper function to format the day of the month with leading zero if necessary. * It returns a string representing the day of the month with a leading zero if it's less than 10. @@ -155,4 +197,14 @@ const handleHyperlink = (text: string) => { return
{content}
; }; -export { getMetaData, setTheme, keepTheme, handleHyperlink, validateHyperlink }; +export { + getMetaData, + setTheme, + keepTheme, + setTextSize, + keepTextSize, + setColorblindMode, + keepColorblindMode, + handleHyperlink, + validateHyperlink, +}; diff --git a/package-lock.json b/package-lock.json index cc40e71..c11a6aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,8 @@ "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", - "remark-math": "^6.0.0" + "remark-math": "^6.0.0", + "ts-node": "^10.9.2" } }, "client": { @@ -39,6 +40,7 @@ "axios": "^1.7.9", "highlight.js": "^11.11.1", "katex": "^0.16.21", + "lucide-react": "^0.487.0", "mongodb": "6.12.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -2239,7 +2241,6 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -2252,7 +2253,6 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -4696,28 +4696,24 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/babel__core": { @@ -6651,7 +6647,6 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "devOptional": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -8535,7 +8530,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true, "license": "MIT" }, "node_modules/cross-spawn": { @@ -9416,7 +9410,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -16602,6 +16595,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.487.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.487.0.tgz", + "integrity": "sha512-aKqhOQ+YmFnwq8dWgGjOuLc8V1R9/c/yOd+zDY4+ohsR2Jo05lSGc3WsstYPIzcTpeosN7LoCkLReUUITvaIvw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -16642,7 +16644,6 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true, "license": "ISC" }, "node_modules/makeerror": { @@ -27528,7 +27529,6 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "devOptional": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -27572,7 +27572,6 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true, "license": "MIT" }, "node_modules/tsconfig-paths": { @@ -28170,7 +28169,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -29202,7 +29200,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/package.json b/package.json index 7726711..fea9aa6 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", - "remark-math": "^6.0.0" + "remark-math": "^6.0.0", + "ts-node": "^10.9.2" } }