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:
committed by
GitHub
parent
5493c63f18
commit
f036d409e6
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
38
src/stories/components/WhatsNewPopup.stories.tsx
Normal file
38
src/stories/components/WhatsNewPopup.stories.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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's New!</Text>
|
||||
<OutwardArrowIcon className='h-4 w-4' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CalendarFooter />
|
||||
|
||||
148
src/views/components/common/WhatsNewPopup.tsx
Normal file
148
src/views/components/common/WhatsNewPopup.tsx
Normal 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;
|
||||
}
|
||||
46
src/views/hooks/useWhatsNew.tsx
Normal file
46
src/views/hooks/useWhatsNew.tsx
Normal 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'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;
|
||||
}
|
||||
Reference in New Issue
Block a user