diff --git a/src/pages/calendar/CalendarMain.tsx b/src/pages/calendar/CalendarMain.tsx index 56837873..4fcce1bb 100644 --- a/src/pages/calendar/CalendarMain.tsx +++ b/src/pages/calendar/CalendarMain.tsx @@ -3,6 +3,7 @@ import Calendar from '@views/components/calendar/Calendar'; import DialogProvider from '@views/components/common/DialogProvider/DialogProvider'; import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'; import { MigrationDialog } from '@views/components/common/MigrationDialog'; +import { WhatsNewDialog } from '@views/components/common/WhatsNewPopup'; import SentryProvider from '@views/contexts/SentryContext'; import { MessageListener } from 'chrome-extension-toolkit'; import useKC_DABR_WASM from 'kc-dabr-wasm'; @@ -34,6 +35,7 @@ export default function CalendarMain() { + diff --git a/src/shared/storage/ExtensionStore.ts b/src/shared/storage/ExtensionStore.ts index cd12660c..197b52e4 100644 --- a/src/shared/storage/ExtensionStore.ts +++ b/src/shared/storage/ExtensionStore.ts @@ -8,11 +8,14 @@ interface IExtensionStore { version: string; /** When was the last update */ lastUpdate: number; + /** The last version of the "What's New" popup that was shown to the user */ + lastWhatsNewPopupVersion: number; } export const ExtensionStore = createLocalStore({ version: chrome.runtime.getManifest().version, lastUpdate: Date.now(), + lastWhatsNewPopupVersion: 0, }); debugStore({ ExtensionStore }); diff --git a/src/stories/components/WhatsNewPopup.stories.tsx b/src/stories/components/WhatsNewPopup.stories.tsx new file mode 100644 index 00000000..03472305 --- /dev/null +++ b/src/stories/components/WhatsNewPopup.stories.tsx @@ -0,0 +1,38 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from '@views/components/common/Button'; +import DialogProvider from '@views/components/common/DialogProvider/DialogProvider'; +import WhatsNewPopup from '@views/components/common/WhatsNewPopup'; +import useWhatsNewPopUp from '@views/hooks/useWhatsNew'; +import React from 'react'; + +const meta = { + title: 'Components/Common/WhatsNewPopup', + component: WhatsNewPopup, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + render: () => ( + + + + ), +}; + +const InnerComponent = () => { + const handleOnClick = useWhatsNewPopUp(); + + return ( + + ); +}; diff --git a/src/views/components/calendar/Calendar.tsx b/src/views/components/calendar/Calendar.tsx index c106918b..0715cf75 100644 --- a/src/views/components/calendar/Calendar.tsx +++ b/src/views/components/calendar/Calendar.tsx @@ -13,6 +13,7 @@ import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalog import { CalendarContext } from '@views/contexts/CalendarContext'; import useCourseFromUrl from '@views/hooks/useCourseFromUrl'; import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSchedule'; +import useWhatsNewPopUp from '@views/hooks/useWhatsNew'; import { MessageListener } from 'chrome-extension-toolkit'; import clsx from 'clsx'; import React, { useEffect, useState } from 'react'; @@ -34,6 +35,7 @@ export default function Calendar(): JSX.Element { const [showPopup, setShowPopup] = useState(course !== null); const [showSidebar, setShowSidebar] = useState(true); + const showWhatsNewDialog = useWhatsNewPopUp(); useEffect(() => { const listener = new MessageListener({ @@ -99,19 +101,34 @@ export default function Calendar(): JSX.Element { {/* */} - { - event.preventDefault(); - openReportWindow(); - }} - > - Send us Feedback! - - + diff --git a/src/views/components/common/WhatsNewPopup.tsx b/src/views/components/common/WhatsNewPopup.tsx new file mode 100644 index 00000000..db6ee9d0 --- /dev/null +++ b/src/views/components/common/WhatsNewPopup.tsx @@ -0,0 +1,148 @@ +import type { IconProps } from '@phosphor-icons/react'; +import { CloudX, Copy, Exam, MapPinArea, Palette } from '@phosphor-icons/react'; +import { ExtensionStore } from '@shared/storage/ExtensionStore'; +import Text from '@views/components/common/Text/Text'; +import useWhatsNewPopUp from '@views/hooks/useWhatsNew'; +import React, { useEffect, useState } from 'react'; + +/** + * This is the version of the 'What's New' features popup. + * + * It is used to check if the popup has already been shown to the user or not + * + * It should be incremented every time the "What's New" popup is updated. + */ +const WHATSNEW_POPUP_VERSION = 1; + +const WHATSNEW_VIDEO_URL = 'https://cdn.longhorns.dev/whats-new-v2.1.2.mp4'; + +type Feature = { + id: string; + icon: React.ForwardRefExoticComponent; + title: string | JSX.Element; + description: string; +}; + +const NEW_FEATURES = [ + { + id: 'custom-course-colors', + icon: Palette, + title: 'Custom Course Colors', + description: 'Paint your schedule in your favorite color theme', + }, + { + id: 'quick-copy', + icon: Copy, + title: 'Quick Copy', + description: 'Quickly copy a course unique number to your clipboard', + }, + { + id: 'updated-grades', + icon: Exam, + title: 'Updated Grades', + description: 'Fall 2024 grades are now available in the grade distribution', + }, + { + id: 'ut-map', + icon: MapPinArea, + title: ( +
+ UTRP Map + + BETA + +
+ ), + description: 'Find directions to your classes with our beta map feature in the settings page', + }, +] as const satisfies readonly Feature[]; + +/** + * WhatsNewPopupContent component. + * + * This component displays the content of the WhatsNew dialog. + * It shows the new features that have been added to the extension. + * + * @returns A JSX of WhatsNewPopupContent component. + */ +export default function WhatsNewPopupContent(): JSX.Element { + const [videoError, setVideoError] = useState(false); + + return ( +
+
+
+ {NEW_FEATURES.map(({ id, icon: Icon, title, description }) => ( +
+ +
+ + {title} + + + {description} + +
+
+ ))} +
+
+ {videoError ? ( +
+
+ + + Failed to load video. Please try again later. + +
+
+ ) : ( + + )} +
+
+
+ ); +} + +/** + * WhatsNewDialog component. + * + * This component is responsible for checking if the extension has already been updated + * and if so, it displays the WhatsNew dialog. Then it updates the state to show that the + * dialog has been shown. + * + * @returns An empty fragment. + * + * @remarks + * The component uses the `useWhatsNew` hook to show the WhatsNew dialog and the + * `useEffect` hook to perform the check on component mount. It also uses the `ExtensionStore` + * to view the state of the dialog. + */ +export function WhatsNewDialog(): null { + const showPopUp = useWhatsNewPopUp(); + + useEffect(() => { + const checkUpdate = async () => { + const version = await ExtensionStore.get('lastWhatsNewPopupVersion'); + if (version !== WHATSNEW_POPUP_VERSION) { + await ExtensionStore.set('lastWhatsNewPopupVersion', WHATSNEW_POPUP_VERSION); + showPopUp(); + } + }; + + checkUpdate(); + + // This is on purpose + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return null; +} diff --git a/src/views/hooks/useWhatsNew.tsx b/src/views/hooks/useWhatsNew.tsx new file mode 100644 index 00000000..d803cc87 --- /dev/null +++ b/src/views/hooks/useWhatsNew.tsx @@ -0,0 +1,46 @@ +import { Button } from '@views/components/common/Button'; +import Text from '@views/components/common/Text/Text'; +import WhatsNewPopupContent from '@views/components/common/WhatsNewPopup'; +import { useDialog } from '@views/contexts/DialogContext'; +import React from 'react'; + +import { LogoIcon } from '../components/common/LogoIcon'; +import useChangelog from './useChangelog'; + +/** + * Custom hook that provides a function to display a what's new dialog. + * + * @returns A function that, when called, shows a dialog with the changelog. + */ +export default function useWhatsNewPopUp(): () => void { + const showDialog = useDialog(); + const showChangeLog = useChangelog(); + const { version } = chrome.runtime.getManifest(); + + const showPopUp = () => { + showDialog(close => ({ + className: 'w-[830px] flex flex-col items-center gap-spacing-7 p-spacing-8', + title: ( +
+ + + What's New in UT Registration Plus + +
+ ), + description: , + buttons: ( +
+ + +
+ ), + })); + }; + + return showPopUp; +}