Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement a What's New prompt #539

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions src/pages/background/events/onUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export default async function onUpdate() {
await ExtensionStore.set({
version: chrome.runtime.getManifest().version,
lastUpdate: Date.now(),
newFeaturesDialogShown: false,
});
}
2 changes: 2 additions & 0 deletions src/pages/calendar/CalendarMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -34,6 +35,7 @@ export default function CalendarMain() {
<ExtensionRoot className='h-full w-full'>
<DialogProvider>
<MigrationDialog />
<WhatsNewDialog />
<Calendar />
</DialogProvider>
</ExtensionRoot>
Expand Down
3 changes: 3 additions & 0 deletions src/shared/storage/ExtensionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ interface IExtensionStore {
version: string;
/** When was the last update */
lastUpdate: number;
/** User was shown the new features dialog */
newFeaturesDialogShown: boolean;
}

export const ExtensionStore = createLocalStore<IExtensionStore>({
version: chrome.runtime.getManifest().version,
lastUpdate: Date.now(),
newFeaturesDialogShown: false,
});

debugStore({ ExtensionStore });
38 changes: 38 additions & 0 deletions src/stories/components/WhatsNewPopup.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 useWhatsNew 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 = useWhatsNew();

return (
<Button color='ut-burntorange' onClick={handleOnClick}>
Open Dialog
</Button>
);
};
107 changes: 107 additions & 0 deletions src/views/components/common/WhatsNewPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { LockKey, Palette, PlusCircle, SelectionPlus } from '@phosphor-icons/react';
import { ExtensionStore } from '@shared/storage/ExtensionStore';
import Text from '@views/components/common/Text/Text';
import useWhatsNew from '@views/hooks/useWhatsNew';
import React, { useEffect } from 'react';

/**
* 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 newFeatures = [
{
id: 'custom-course-colors',
icon: Palette,
title: 'Custom Course Colors',
description: 'Paint your schedule in your favorite color theme',
},
{
id: 'custom-blocks',
icon: SelectionPlus,
title: 'Custom Blocks',
description: 'Reserve time for personal or work commitments',
},
{
id: 'quick-add',
icon: PlusCircle,
title: 'Quick Add',
description: 'Quickly add a course with the unique number and skip the course schedule',
},
{
id: 'course-statuses',
icon: LockKey,
title: 'Course Statuses',
description: 'Know when a course is waitlisted, closed, or cancelled',
},
];

return (
<div className='w-full flex flex-row justify-between'>
<div className='w-full flex flex-row justify-between'>
<div className='w-[277px] flex flex-col items-center gap-spacing-6'>
{newFeatures.map(({ id, icon: Icon, title, description }) => (
<div key={id} className='w-full flex items-center justify-between gap-spacing-5'>
<Icon width='32' height='32' className='text-ut-burntorange' />
<div className='w-full flex flex-col gap-spacing-2'>
<Text variant='h4' className='text-ut-burntorange font-bold!'>
{title}
</Text>
<Text variant='p' className='text-ut-black'>
{description}
</Text>
</div>
</div>
))}
</div>
<img
// TODO: Replace with actual image/video
src='https://placehold.co/464x315'
alt='UT Registration Plus Demo'
className='border-ut-theme-offwhite1 max-w-[464px] w-full border rounded object-cover'
/>
</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(): JSX.Element {
const showPopUp = useWhatsNew();

useEffect(() => {
const checkUpdate = async () => {
const shown = await ExtensionStore.get('newFeaturesDialogShown');
if (!shown) {
ExtensionStore.set('newFeaturesDialogShown', true);
showPopUp();
}
};

checkUpdate();

// This is on purpose
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// (not actually a useless fragment)
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
47 changes: 47 additions & 0 deletions src/views/hooks/useWhatsNew.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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';
import useVersion from './useVersion';

/**
* 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 useWhatsNew(): () => void {
const showDialog = useDialog();
const showChangeLog = useChangelog();
const version = useVersion() || 'v2.1.0';

const handleOnClick = () => {
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: (
<>
<Button onClick={showChangeLog} variant='minimal' color='ut-black'>
Read Changelog {version}
</Button>
<Button onClick={close} color='ut-burntorange'>
Get started
</Button>
</>
),
}));
};

return handleOnClick;
}