diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1209ada3..484107d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,6 +217,9 @@ importers: semantic-ui-react: specifier: ^2.1.5 version: 2.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + toastify-js: + specifier: ^1.12.0 + version: 1.12.0 websoc-fuzzy-search: specifier: 1.0.1 version: 1.0.1 @@ -242,6 +245,9 @@ importers: '@types/react-twemoji': specifier: ^0.4.3 version: 0.4.3 + '@types/toastify-js': + specifier: ^1.12.3 + version: 1.12.3 '@typescript-eslint/eslint-plugin': specifier: ^7.8.0 version: 7.8.0(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) @@ -2393,6 +2399,9 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/toastify-js@1.12.3': + resolution: {integrity: sha512-9RjLlbAHMSaae/KZNHGv19VG4gcLIm3YjvacCXBtfMfYn26h76YP5oxXI8k26q4iKXCB9LNfv18lsoS0JnFPTg==} + '@types/use-sync-external-store@0.0.3': resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} @@ -5179,6 +5188,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toastify-js@1.12.0: + resolution: {integrity: sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -9101,6 +9113,8 @@ snapshots: '@types/node': 20.12.8 '@types/send': 0.17.4 + '@types/toastify-js@1.12.3': {} + '@types/use-sync-external-store@0.0.3': {} '@types/warning@3.0.3': {} @@ -12453,6 +12467,8 @@ snapshots: dependencies: is-number: 7.0.0 + toastify-js@1.12.0: {} + toidentifier@1.0.1: {} touch@3.1.1: {} diff --git a/site/package.json b/site/package.json index 1d038a05..09b07cec 100644 --- a/site/package.json +++ b/site/package.json @@ -28,6 +28,7 @@ "react-twemoji": "^0.5.0", "semantic-ui-css": "^2.5.0", "semantic-ui-react": "^2.1.5", + "toastify-js": "^1.12.0", "websoc-fuzzy-search": "1.0.1" }, "scripts": { @@ -57,6 +58,7 @@ "@types/react-google-recaptcha": "^2.1.9", "@types/react-transition-group": "^4.4.10", "@types/react-twemoji": "^0.4.3", + "@types/toastify-js": "^1.12.3", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "@vitejs/plugin-react": "^4.2.1", diff --git a/site/src/App.tsx b/site/src/App.tsx index a1eb90b3..6304d659 100644 --- a/site/src/App.tsx +++ b/site/src/App.tsx @@ -5,7 +5,7 @@ import 'bootstrap/dist/css/bootstrap.min.css'; import 'react-bootstrap-range-slider/dist/react-bootstrap-range-slider.css'; import './style/theme.scss'; import './App.scss'; - +import 'toastify-js/src/toastify.css'; import AppHeader from './component/AppHeader/AppHeader'; import ChangelogModal from './component/ChangelogModal/ChangelogModal'; import SearchPage from './pages/SearchPage'; diff --git a/site/src/component/Review/SubReview.tsx b/site/src/component/Review/SubReview.tsx index e0b60317..bb40439b 100644 --- a/site/src/component/Review/SubReview.tsx +++ b/site/src/component/Review/SubReview.tsx @@ -15,6 +15,7 @@ import ReviewForm from '../ReviewForm/ReviewForm'; import trpc from '../../trpc'; import { ReviewData } from '@peterportal/types'; import { useIsLoggedIn } from '../../hooks/isLoggedIn'; +import spawnToast from '../../helpers/toastify'; interface SubReviewProps { review: ReviewData; @@ -68,7 +69,7 @@ const SubReview: FC = ({ review, course, professor }) => { const vote = async (newVote: number) => { if (!isLoggedIn) { - alert('You must be logged in to vote.'); + spawnToast('You must be logged in to vote.', true); return; } updateScore(newVote); diff --git a/site/src/component/ReviewForm/ReviewForm.tsx b/site/src/component/ReviewForm/ReviewForm.tsx index 7994551a..19fe3d9d 100644 --- a/site/src/component/ReviewForm/ReviewForm.tsx +++ b/site/src/component/ReviewForm/ReviewForm.tsx @@ -25,6 +25,7 @@ import { ReviewTags, tags, } from '@peterportal/types'; +import spawnToast from '../../helpers/toastify'; import { useIsLoggedIn } from '../../hooks/isLoggedIn'; interface ReviewFormProps extends ReviewProps { @@ -62,13 +63,12 @@ const ReviewForm: FC = ({ const [anonymous, setAnonymous] = useState(reviewToEdit?.userDisplay === anonymousName); const [validated, setValidated] = useState(false); const { darkMode } = useContext(ThemeContext); - useEffect(() => { if (show) { // form opened // if not logged in, close the form if (!isLoggedIn) { - alert('You must be logged in to add a review!'); + spawnToast('You must be logged in to add a review!', true); closeForm(); } @@ -86,7 +86,7 @@ const ReviewForm: FC = ({ setSubmitted(true); dispatch(editReview(review as EditReviewSubmission)); } catch (e) { - alert((e as Error).message); + spawnToast((e as Error).message, true); } } else { try { @@ -94,7 +94,7 @@ const ReviewForm: FC = ({ setSubmitted(true); dispatch(addReview(res)); } catch (e) { - alert((e as Error).message); + spawnToast((e as Error).message, true); } } }; @@ -115,7 +115,7 @@ const ReviewForm: FC = ({ } // check if CAPTCHA is completed for new reviews (captcha omitted for editing) if (!editing && !captchaToken) { - alert('Please complete the CAPTCHA'); + spawnToast('Please complete the CAPTCHA', true); return; } const review = { @@ -153,7 +153,7 @@ const ReviewForm: FC = ({ newSelectedTags.push(tag); setSelectedTags(newSelectedTags); } else { - alert('Cannot select more than 3 tags'); + spawnToast('Cannot select more than 3 tags', true); } } }; diff --git a/site/src/helpers/toastify.ts b/site/src/helpers/toastify.ts new file mode 100644 index 00000000..c0431a68 --- /dev/null +++ b/site/src/helpers/toastify.ts @@ -0,0 +1,17 @@ +import Toastify from 'toastify-js'; + +export default function spawnToast(text: string, error = false, style?: Record, callback?: () => void) { + return Toastify({ + text, + duration: 3000, + close: true, + gravity: 'bottom', + position: 'right', + style: { + background: error ? '#D22B2B' : 'var(--peterportal-primary-color-1)', + fontWeight: 'bold', + ...style, + }, + onClick: callback, + }).showToast(); +} diff --git a/site/src/pages/RoadmapPage/Planner.tsx b/site/src/pages/RoadmapPage/Planner.tsx index c0eae1d2..8aefd961 100644 --- a/site/src/pages/RoadmapPage/Planner.tsx +++ b/site/src/pages/RoadmapPage/Planner.tsx @@ -20,6 +20,7 @@ import ImportTranscriptPopup from './ImportTranscriptPopup'; import { collapseAllPlanners, loadRoadmap, validatePlanner } from '../../helpers/planner'; import { Button, Modal } from 'react-bootstrap'; import trpc from '../../trpc'; +import spawnToast from '../../helpers/toastify'; import { useIsLoggedIn } from '../../hooks/isLoggedIn'; const Planner: FC = () => { @@ -30,7 +31,6 @@ const Planner: FC = () => { const allPlanData = useAppSelector(selectAllPlans); const transfers = useAppSelector((state) => state.roadmap.transfers); const [showSyncModal, setShowSyncModal] = useState(false); - const [missingPrerequisites, setMissingPrerequisites] = useState(new Set()); const roadmapStr = JSON.stringify({ planners: collapseAllPlanners(allPlanData).map((p) => ({ name: p.name, content: p.content })), // map to remove id attribute @@ -67,13 +67,13 @@ const Planner: FC = () => { trpc.roadmaps.save .mutate(roadmap) .then(() => { - alert(`Roadmap saved to your account!`); + spawnToast(`Roadmap saved to your account!`); }) .catch(() => { - alert('Roadmap saved locally! Login to save it to your account.'); + spawnToast('Roadmap saved locally! Login to save it to your account.'); }); } else { - alert('Roadmap saved locally! Login to save it to your account.'); + spawnToast('Roadmap saved locally! Login to save it to your account.'); } }; diff --git a/site/src/pages/RoadmapPage/RoadmapMultiplan.tsx b/site/src/pages/RoadmapPage/RoadmapMultiplan.tsx index a9692e32..f7b509a6 100644 --- a/site/src/pages/RoadmapPage/RoadmapMultiplan.tsx +++ b/site/src/pages/RoadmapPage/RoadmapMultiplan.tsx @@ -14,7 +14,7 @@ import './RoadmapMultiplan.scss'; import * as Icon from 'react-bootstrap-icons'; import { Button } from 'semantic-ui-react'; import { Button as Button2, Form, Modal } from 'react-bootstrap'; - +import spawnToast from '../../helpers/toastify'; interface RoadmapSelectableItemProps { plan: RoadmapPlan; index: number; @@ -54,7 +54,6 @@ const RoadmapMultiplan: FC = () => { const [delIdx, setDelIdx] = useState(-1); const [newPlanName, setNewPlanName] = useState(allPlans.plans[allPlans.currentPlanIndex].name); const [showDropdown, setShowDropdown] = useState(false); - const isDuplicateName = () => allPlans.plans.find((p) => p.name === newPlanName); // name: name of the plan, content: stores the content of plan @@ -74,8 +73,8 @@ const RoadmapMultiplan: FC = () => { }; const handleSubmitNewPlan = () => { - if (!newPlanName) return alert('Name cannot be empty'); - if (isDuplicateName()) return alert('A plan with that name already exists'); + if (!newPlanName) return spawnToast('Name cannot be empty', true); + if (isDuplicateName()) return spawnToast('A plan with that name already exists', true); setIsOpen(false); addNewPlan(newPlanName); const newIndex = allPlans.plans.length; @@ -84,8 +83,8 @@ const RoadmapMultiplan: FC = () => { }; const modifyPlanName = () => { - if (!newPlanName) return alert('Name cannot be empty'); - if (isDuplicateName()) return alert('A plan with that name already exists'); + if (!newPlanName) return spawnToast('Name cannot be empty', true); + if (isDuplicateName()) return spawnToast('A plan with that name already exists', true); dispatch(setPlanName({ index: editIdx, name: newPlanName })); setEditIdx(-1); }; diff --git a/site/src/store/slices/roadmapSlice.ts b/site/src/store/slices/roadmapSlice.ts index 652d5fc5..55937e54 100644 --- a/site/src/store/slices/roadmapSlice.ts +++ b/site/src/store/slices/roadmapSlice.ts @@ -12,7 +12,7 @@ import { } from '../../types/types'; import type { RootState } from '../store'; import { TransferData } from '@peterportal/types'; - +import spawnToast from '../../helpers/toastify'; // Define a type for the slice state interface RoadmapPlanState { // Store planner data @@ -167,7 +167,7 @@ export const roadmapSlice = createSlice({ // if year doesn't exist if (!currentYears.includes(startYear)) { - alert(`${startYear}-${startYear + 1} has not yet been added!`); + spawnToast(`${startYear}-${startYear + 1} has not yet been added!`, true); return; } @@ -178,7 +178,7 @@ export const roadmapSlice = createSlice({ // if duplicate quarter if (currentQuarters.includes(newQuarter.name)) { - alert(`${quarterDisplayNames[newQuarter.name]} has already been added to Year ${yearIndex}!`); + spawnToast(`${quarterDisplayNames[newQuarter.name]} has already been added to Year ${yearIndex}!`, true); return; } @@ -215,16 +215,18 @@ export const roadmapSlice = createSlice({ const newYear = action.payload.yearData.startYear; const currentNames = state.plans[state.currentPlanIndex].content.yearPlans.map((e) => e.name); const newName = action.payload.yearData.name; - // if duplicate year if (currentYears.includes(newYear)) { - alert(`${newYear}-${newYear + 1} has already been added as Year ${currentYears.indexOf(newYear) + 1}!`); + spawnToast( + `${newYear}-${newYear + 1} has already been added as Year ${currentYears.indexOf(newYear) + 1}!`, + true, + ); return; } // if duplicate name if (currentNames.includes(newName)) { const year = state.plans[state.currentPlanIndex].content.yearPlans[currentNames.indexOf(newName)].startYear; - alert(`${newName} already exists from ${year} - ${year + 1}!`); + spawnToast(`${newName} already exists from ${year} - ${year + 1}!`, true); return; } @@ -243,10 +245,9 @@ export const roadmapSlice = createSlice({ const currentYears = state.plans[state.currentPlanIndex].content.yearPlans.map((e) => e.startYear); const newYear = action.payload.startYear; const yearIndex = action.payload.index; - // if duplicate year if (currentYears.includes(newYear)) { - alert(`${newYear}-${newYear + 1} already exists as Year ${currentYears.indexOf(newYear) + 1}!`); + spawnToast(`${newYear}-${newYear + 1} already exists as Year ${currentYears.indexOf(newYear) + 1}!`, true); return; } @@ -270,11 +271,10 @@ export const roadmapSlice = createSlice({ const currentNames = state.plans[state.currentPlanIndex].content.yearPlans.map((e) => e.name); const newName = action.payload.name; const yearIndex = action.payload.index; - // if duplicate name if (currentNames.includes(newName)) { const year = state.plans[state.currentPlanIndex].content.yearPlans[yearIndex].startYear; - alert(`${newName} already exists from ${year} - ${year + 1}!`); + spawnToast(`${newName} already exists from ${year} - ${year + 1}!`, true); return; }