feat: implement a What's New prompt (#539)

* feat: create whats new initial component

* feat: create initial whats-new hook

* feat: create whats-new story

* feat: add button to open dialog in storybook

* feat: complete popup ui

* feat: add check for new updates or installs

* fix: fix linter issues

* fix: use proper features and add video

* fix: properly fetch version from manifest

* feat: add a link to open the popup

* fix: update spacing and features' content

* fix: update UTRP Map name

* fix: increase icon size and display version correctly

* feat: update the features video

* fix: update offwhite color

* fix: color typo

* fix: fixing colors again

* feat: use numbers instead of boolean

* fix: typo in import

* feat: add type safety to features array

* feat: cdn video url

* fix: delete mp4 video

* feat: handle video failure to load

* fix: make border outline tight to video

* feat: make design responsive

* fix: make features array readonly

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
Co-authored-by: Derek Chen <derex1987@gmail.com>
This commit is contained in:
Abdulrahman Alshahrani
2025-03-08 15:41:09 -06:00
committed by GitHub
parent 5493c63f18
commit f036d409e6
6 changed files with 267 additions and 13 deletions

View File

@@ -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() {
<ExtensionRoot className='h-full w-full'>
<DialogProvider>
<MigrationDialog />
<WhatsNewDialog />
<Calendar />
</DialogProvider>
</ExtensionRoot>

View File

@@ -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<IExtensionStore>({
version: chrome.runtime.getManifest().version,
lastUpdate: Date.now(),
lastWhatsNewPopupVersion: 0,
});
debugStore({ ExtensionStore });

View File

@@ -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<typeof WhatsNewPopup>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
render: () => (
<DialogProvider>
<InnerComponent />
</DialogProvider>
),
};
const InnerComponent = () => {
const handleOnClick = useWhatsNewPopUp();
return (
<Button color='ut-burntorange' onClick={handleOnClick}>
Open Dialog
</Button>
);
};

View File

@@ -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<boolean>(course !== null);
const [showSidebar, setShowSidebar] = useState<boolean>(true);
const showWhatsNewDialog = useWhatsNewPopUp();
useEffect(() => {
const listener = new MessageListener<CalendarTabMessages>({
@@ -99,19 +101,34 @@ export default function Calendar(): JSX.Element {
<ResourceLinks />
<Divider orientation='horizontal' size='100%' />
{/* <TeamLinks /> */}
<a
href={CRX_PAGES.REPORT}
className='flex items-center gap-spacing-2 text-ut-burntorange underline-offset-2 hover:underline'
target='_blank'
rel='noreferrer'
onClick={event => {
event.preventDefault();
openReportWindow();
}}
>
<Text variant='p'>Send us Feedback!</Text>
<OutwardArrowIcon className='h-4 w-4' />
</a>
<div className='flex flex-col gap-spacing-3'>
<a
href={CRX_PAGES.REPORT}
className='flex items-center gap-spacing-2 text-ut-burntorange underline-offset-2 hover:underline'
target='_blank'
rel='noreferrer'
onClick={event => {
event.preventDefault();
openReportWindow();
}}
>
<Text variant='p'>Send us Feedback!</Text>
<OutwardArrowIcon className='h-4 w-4' />
</a>
<a
href=''
className='flex items-center gap-spacing-2 text-ut-burntorange underline-offset-2 hover:underline'
target='_blank'
rel='noreferrer'
onClick={event => {
event.preventDefault();
showWhatsNewDialog();
}}
>
<Text variant='p'>What&apos;s New!</Text>
<OutwardArrowIcon className='h-4 w-4' />
</a>
</div>
</div>
<CalendarFooter />

View File

@@ -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<IconProps>;
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: (
<div className='flex flex-row items-center'>
UTRP Map
<span className='mx-2 border border-ut-burntorange rounded px-2 py-0.5 text-xs text-ut-burntorange font-medium'>
BETA
</span>
</div>
),
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 (
<div className='w-full flex flex-row justify-between'>
<div className='w-full flex flex-col-reverse items-center justify-between gap-spacing-7 md:flex-row'>
<div className='grid grid-cols-1 w-fit items-center gap-spacing-6 sm:grid-cols-2 md:w-[277px] md:flex md:flex-col md:flex-nowrap'>
{NEW_FEATURES.map(({ id, icon: Icon, title, description }) => (
<div key={id} className='w-full flex items-center justify-between gap-spacing-5'>
<Icon width='40' height='40' className='text-ut-burntorange' />
<div className='w-full flex flex-col gap-spacing-1'>
<Text variant='h4' className='text-ut-burntorange font-bold!'>
{title}
</Text>
<Text variant='p' className='text-ut-black'>
{description}
</Text>
</div>
</div>
))}
</div>
<div className='h-full max-w-[464px] w-full flex items-center justify-center'>
{videoError ? (
<div className='h-full w-full flex items-center justify-center border border-ut-offwhite/50 rounded'>
<div className='flex flex-col items-center justify-center p-spacing-2'>
<CloudX size={52} className='text-ut-black/50' />
<Text variant='h4' className='text-center text-ut-black/50'>
Failed to load video. Please try again later.
</Text>
</div>
</div>
) : (
<video
className='h-fit w-full flex items-center justify-center border border-ut-offwhite/50 rounded object-cover'
autoPlay
loop
muted
>
<source src={WHATSNEW_VIDEO_URL} type='video/mp4' onError={() => setVideoError(true)} />
</video>
)}
</div>
</div>
</div>
);
}
/**
* 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;
}

View File

@@ -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: (
<div className='flex items-center justify-between gap-4'>
<LogoIcon width='48' height='48' />
<Text variant='h1' className='text-theme-black'>
What&apos;s New in UT Registration Plus
</Text>
</div>
),
description: <WhatsNewPopupContent />,
buttons: (
<div className='flex flex-row items-end gap-spacing-4'>
<Button onClick={showChangeLog} variant='minimal' color='ut-black'>
Read Changelog v{version}
</Button>
<Button onClick={close} color='ut-burntorange'>
Get started
</Button>
</div>
),
}));
};
return showPopUp;
}