diff --git a/package-lock.json b/package-lock.json index e5427d0b..26698306 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "react-scroll": "^1.8.0", "react-table": "^7.6.3", "react-test-renderer": "^16.14.0", + "react-toastify": "^8.1.0", "react-toggle": "^4.1.1", "react-tooltip": "^3.11.2", "reactstrap": "^8.8.1", @@ -5517,6 +5518,15 @@ "node": ">=6" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -18670,6 +18680,19 @@ "scheduler": "^0.19.1" } }, + "node_modules/react-toastify": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-8.1.0.tgz", + "integrity": "sha512-M+Q3rTmEw/53Csr7NsV/YnldJe4c7uERcY7Tma9mvLU98QT2VhIkKwjBzzxZkJRk/oBKyUAtkyMjMgO00hx6gQ==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-toggle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.1.1.tgz", @@ -28206,6 +28229,11 @@ "shallow-clone": "^3.0.0" } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -38755,6 +38783,14 @@ "scheduler": "^0.19.1" } }, + "react-toastify": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-8.1.0.tgz", + "integrity": "sha512-M+Q3rTmEw/53Csr7NsV/YnldJe4c7uERcY7Tma9mvLU98QT2VhIkKwjBzzxZkJRk/oBKyUAtkyMjMgO00hx6gQ==", + "requires": { + "clsx": "^1.1.1" + } + }, "react-toggle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.1.1.tgz", diff --git a/package.json b/package.json index 0f3e6a60..af81a817 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-scroll": "^1.8.0", "react-table": "^7.6.3", "react-test-renderer": "^16.14.0", + "react-toastify": "^8.1.0", "react-toggle": "^4.1.1", "react-tooltip": "^3.11.2", "reactstrap": "^8.8.1", diff --git a/src/components/Table/index.js b/src/components/Table/index.js index 39125efa..ecddea86 100644 --- a/src/components/Table/index.js +++ b/src/components/Table/index.js @@ -23,6 +23,8 @@ import { FaRandom, FaQuestionCircle, } from 'react-icons/fa'; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; import { DefaultColumnFilter, SelectDifficultyColumnFilter, @@ -30,7 +32,6 @@ import { SelectCheckedColumnFilter, } from './filters'; import { Event } from '../Shared/Tracking'; - import questions, { updated } from '../../data'; import 'react-toggle/style.css'; @@ -41,6 +42,7 @@ const iconPath = `${process.env.PUBLIC_URL}/static/icons/`; const Table = () => { const [resetCount, setResetCount] = useState(0); + let checkedList = JSON.parse(localStorage.getItem('checked')) || new Array(questions.length).fill(false); @@ -50,11 +52,11 @@ const Table = () => { new Array(questions.length).fill(''); /* If the user has previously visited the website, then an array in - LocalStorage would exist of a certain length which corresponds to which - questions they have/have not completed. In the event that we add new questions - to the list, then we would need to resize and copy the existing 'checked' - array before updating it in LocalStorage in order to transfer their saved - progress. */ + LocalStorage would exist of a certain length which corresponds to which + questions they have/have not completed. In the event that we add new questions + to the list, then we would need to resize and copy the existing 'checked' + array before updating it in LocalStorage in order to transfer their saved + progress. */ if (checkedList.length !== questions.length) { const resizedCheckedList = new Array(questions.length).fill(false); @@ -77,6 +79,22 @@ const Table = () => { window.localStorage.setItem('checkedAt', JSON.stringify(checkedAtList)); } + let importantList = + JSON.parse(localStorage.getItem('importantProblems')) || + new Array(questions.length).fill(false); + + if (importantList.length !== questions.length) { + const resizedImportantList = new Array(questions.length).fill(false); + for (let i = 0; i < importantList.length; i += 1) { + resizedImportantList[i] = importantList[i]; + } + importantList = resizedImportantList; + window.localStorage.setItem( + 'importantProblems', + JSON.stringify(importantList), + ); + } + const filteredByCheckbox = () => { const checkbox = localStorage.getItem('checkbox') || ''; return questions.filter(question => { @@ -113,6 +131,13 @@ const Table = () => { const [showPatterns, setShowPatterns] = useState( JSON.parse(localStorage.getItem('showPatterns')) || new Array(1).fill(true), ); + const savedImportant = JSON.parse(localStorage.getItem('importantProblems')); + const [important, setImportant] = useState( + savedImportant && savedImportant.length === questions.length + ? savedImportant + : new Array(questions.length).fill(false), + ); + const [starAnimation, setStarAnimation] = useState({}); useEffect(() => { window.localStorage.setItem('checked', JSON.stringify(checked)); @@ -126,6 +151,10 @@ const Table = () => { window.localStorage.setItem('showPatterns', JSON.stringify(showPatterns)); }, [showPatterns]); + useEffect(() => { + window.localStorage.setItem('importantProblems', JSON.stringify(important)); + }, [important]); + const defaultColumn = React.useMemo( () => ({ Filter: DefaultColumnFilter, @@ -171,7 +200,7 @@ const Table = () => { totalValue={totalDifficultyCount.Total} label={() => `${difficultyCount.Total} / - ${totalDifficultyCount.Total}` + ${totalDifficultyCount.Total}` } labelPosition={0} labelStyle={{ @@ -481,7 +510,67 @@ const Table = () => { }, Filter: SelectColumnFilter, }, + /* eslint-disable react/prop-types */ { + Header: '⭐', + accessor: 'important', + disableSortBy: true, + disableFilters: true, + Cell: ({ row }) => { + const id = Number(row?.original?.id); + if (Number.isNaN(id)) return '❌'; + + const handleToggle = () => { + const updatedImportant = [...important]; + updatedImportant[id] = !updatedImportant[id]; + setImportant(updatedImportant); + toast( + updatedImportant[id] + ? 'Marked as Important' + : 'Removed from Important', + { + type: updatedImportant[id] ? 'success' : 'info', + autoClose: 1200, + hideProgressBar: true, + position: 'bottom-center', + }, + ); + // Trigger animation + setStarAnimation(prev => ({ ...prev, [id]: true })); + setTimeout(() => { + setStarAnimation(prev => ({ ...prev, [id]: false })); + }, 400); + }; + + const handleKeyPress = e => { + if (e.key === 'Enter' || e.key === ' ') { + handleToggle(); + } + }; + + return ( + + {important[id] ? '⭐' : '☆'} + + ); + }, + }, // Optional + /* eslint-enable react/prop-types */ { Header: 'Last Solved On', accessor: 'LastSolvedOn', disableSortBy: true, @@ -498,7 +587,7 @@ const Table = () => { }, ], // eslint-disable-next-line - [resetCount], + [resetCount, important], ); const { @@ -539,10 +628,50 @@ const Table = () => { useSortBy, ); + const [showOnlyStarred, setShowOnlyStarred] = useState(false); + + useEffect(() => { + // Always start from the full questions list + let filtered = filteredByCheckbox(); + if (showOnlyStarred) { + filtered = filtered.filter(q => important[q.id]); + } + setData(filtered); + // eslint-disable-next-line + }, [showOnlyStarred, important, checked, resetCount]); + return ( + + + {/* Minimal Show Only Starred Button */} +
+ +
+ {headerGroups.map(headerGroup => ( diff --git a/src/components/Table/styles.scss b/src/components/Table/styles.scss index 5c566b27..ad5327bf 100644 --- a/src/components/Table/styles.scss +++ b/src/components/Table/styles.scss @@ -123,3 +123,25 @@ body.dark-mode .modal-content { text-wrap: nowrap; } } + +.star-animate { + animation: pop-star 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} + +@keyframes pop-star { + 0% { + transform: scale(1) rotate(0deg); + color: #ffd700; + filter: drop-shadow(0 0 0 #ffd700); + } + 60% { + transform: scale(1.5) rotate(-15deg); + color: #ffd700; + filter: drop-shadow(0 0 8px #ffd700); + } + 100% { + transform: scale(1) rotate(0deg); + color: #ffd700; + filter: drop-shadow(0 0 0 #ffd700); + } +}