feat: add dining app promo (#598)
* feat: add DiningAppPromo component and integrate it into Calendar * feat: update WhatsNewPopup with new features and app download link * fix: remove outdated links * chore: run lint * chore: run prettier * feat: enhance DiningAppPromo with close button and integrate user preference for promo visibility * chore: run lint * chore: run check types * fix: correct promo visibility logic in Calendar component * feat: centralize app store URLs in appUrls.ts * chore: run lint * feat: integrate UT Dining promo image * chore: run lint * fix: update logo in WhatsNew popup to use LD icon * fix: convert URLs to URL objects for consistency * fix: update LD icon in WhatsNew popup to new version * fix: update description for Coffee Shops feature to clarify operating times * fix: rename promo state and storage key to showUTDiningPromo for clarity --------- Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
This commit is contained in:
BIN
src/assets/LD-icon-new.png
Normal file
BIN
src/assets/LD-icon-new.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 188 KiB |
@@ -21,6 +21,9 @@ export interface IOptionsStore {
|
|||||||
|
|
||||||
/** whether the calendar sidebar should be shown when the calendar is opened */
|
/** whether the calendar sidebar should be shown when the calendar is opened */
|
||||||
showCalendarSidebar: boolean;
|
showCalendarSidebar: boolean;
|
||||||
|
|
||||||
|
/** whether the promo should be shown */
|
||||||
|
showUTDiningPromo: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OptionsStore = createSyncStore<IOptionsStore>({
|
export const OptionsStore = createSyncStore<IOptionsStore>({
|
||||||
@@ -30,6 +33,7 @@ export const OptionsStore = createSyncStore<IOptionsStore>({
|
|||||||
enableDataRefreshing: false,
|
enableDataRefreshing: false,
|
||||||
alwaysOpenCalendarInNewTab: false,
|
alwaysOpenCalendarInNewTab: false,
|
||||||
showCalendarSidebar: true,
|
showCalendarSidebar: true,
|
||||||
|
showUTDiningPromo: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +49,7 @@ export const initSettings = async () =>
|
|||||||
enableDataRefreshing: await OptionsStore.get('enableDataRefreshing'),
|
enableDataRefreshing: await OptionsStore.get('enableDataRefreshing'),
|
||||||
alwaysOpenCalendarInNewTab: await OptionsStore.get('alwaysOpenCalendarInNewTab'),
|
alwaysOpenCalendarInNewTab: await OptionsStore.get('alwaysOpenCalendarInNewTab'),
|
||||||
showCalendarSidebar: await OptionsStore.get('showCalendarSidebar'),
|
showCalendarSidebar: await OptionsStore.get('showCalendarSidebar'),
|
||||||
|
showUTDiningPromo: await OptionsStore.get('showUTDiningPromo'),
|
||||||
}) satisfies IOptionsStore;
|
}) satisfies IOptionsStore;
|
||||||
|
|
||||||
// Clothing retailer right
|
// Clothing retailer right
|
||||||
|
|||||||
19
src/shared/util/appUrls.ts
Normal file
19
src/shared/util/appUrls.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* This file contains URLs for external applications and resources.
|
||||||
|
* Centralizing these URLs makes it easier to track, update, and manage them in a single place.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL to the UT Dining app on the App Store
|
||||||
|
*/
|
||||||
|
export const UT_DINING_APP_STORE_URL = new URL('https://apps.apple.com/us/app/ut-dining/id6743042002').toString();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL to the UT Dining app on the Google Play Store (currently not available)
|
||||||
|
*/
|
||||||
|
export const UT_DINING_GOOGLE_PLAY_URL = ''; // Placeholder for Google Play URL, Android app not available yet
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL to the promo image
|
||||||
|
*/
|
||||||
|
export const UT_DINING_PROMO_IMAGE_URL = new URL('https://cdn.longhorns.dev/ut-dining-advert1.png').toString();
|
||||||
14
src/stories/components/DiningAppPromo.stories.tsx
Normal file
14
src/stories/components/DiningAppPromo.stories.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import DiningAppPromo from '@views/components/calendar/DiningAppPromo';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/Calendar/DiningAppPromo',
|
||||||
|
component: DiningAppPromo,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof DiningAppPromo>;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof DiningAppPromo>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
@@ -27,6 +27,7 @@ import { Button } from '../common/Button';
|
|||||||
import { LargeLogo } from '../common/LogoIcon';
|
import { LargeLogo } from '../common/LogoIcon';
|
||||||
import Text from '../common/Text/Text';
|
import Text from '../common/Text/Text';
|
||||||
import CalendarFooter from './CalendarFooter';
|
import CalendarFooter from './CalendarFooter';
|
||||||
|
import DiningAppPromo from './DiningAppPromo';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calendar page component
|
* Calendar page component
|
||||||
@@ -41,6 +42,8 @@ export default function Calendar(): ReactNode {
|
|||||||
const [showPopup, setShowPopup] = useState<boolean>(course !== null);
|
const [showPopup, setShowPopup] = useState<boolean>(course !== null);
|
||||||
const showWhatsNewDialog = useWhatsNewPopUp();
|
const showWhatsNewDialog = useWhatsNewPopUp();
|
||||||
|
|
||||||
|
const [showUTDiningPromo, setShowUTDiningPromo] = useState<boolean>(false);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: showSidebar, isPending: isSidebarStatePending } = useQuery({
|
const { data: showSidebar, isPending: isSidebarStatePending } = useQuery({
|
||||||
queryKey: ['settings', 'showCalendarSidebar'],
|
queryKey: ['settings', 'showCalendarSidebar'],
|
||||||
@@ -82,12 +85,19 @@ export default function Calendar(): ReactNode {
|
|||||||
if (course) setShowPopup(true);
|
if (course) setShowPopup(true);
|
||||||
}, [course]);
|
}, [course]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Load the user's preference for the promo
|
||||||
|
OptionsStore.get('showUTDiningPromo').then(show => {
|
||||||
|
setShowUTDiningPromo(show);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (isSidebarStatePending) return null;
|
if (isSidebarStatePending) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CalendarContext.Provider value>
|
<CalendarContext.Provider value>
|
||||||
<div className='h-full w-full flex flex-col'>
|
<div className='h-full w-full flex flex-col'>
|
||||||
<div className='h-screen flex overflow-auto screenshot:calendar-target'>
|
<div className='screenshot:calendar-target h-screen flex overflow-auto'>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'py-spacing-6 relative h-full min-h-screen w-full flex flex-none flex-col justify-between overflow-clip whitespace-nowrap border-r border-ut-offwhite/50 shadow-[2px_0_10px,rgba(214_210_196_/_.1)] motion-safe:duration-300 motion-safe:ease-out-expo motion-safe:transition-[max-width] screenshot:hidden',
|
'py-spacing-6 relative h-full min-h-screen w-full flex flex-none flex-col justify-between overflow-clip whitespace-nowrap border-r border-ut-offwhite/50 shadow-[2px_0_10px,rgba(214_210_196_/_.1)] motion-safe:duration-300 motion-safe:ease-out-expo motion-safe:transition-[max-width] screenshot:hidden',
|
||||||
@@ -122,8 +132,16 @@ export default function Calendar(): ReactNode {
|
|||||||
<CalendarSchedules />
|
<CalendarSchedules />
|
||||||
<Divider orientation='horizontal' size='100%' />
|
<Divider orientation='horizontal' size='100%' />
|
||||||
<ResourceLinks />
|
<ResourceLinks />
|
||||||
<Divider orientation='horizontal' size='100%' />
|
|
||||||
{/* <TeamLinks /> */}
|
{/* <TeamLinks /> */}
|
||||||
|
<Divider orientation='horizontal' size='100%' />
|
||||||
|
{showUTDiningPromo && (
|
||||||
|
<DiningAppPromo
|
||||||
|
onClose={() => {
|
||||||
|
setShowUTDiningPromo(false);
|
||||||
|
OptionsStore.set('showUTDiningPromo', false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className='flex flex-col gap-spacing-3'>
|
<div className='flex flex-col gap-spacing-3'>
|
||||||
<a
|
<a
|
||||||
href={CRX_PAGES.REPORT}
|
href={CRX_PAGES.REPORT}
|
||||||
|
|||||||
69
src/views/components/calendar/DiningAppPromo.tsx
Normal file
69
src/views/components/calendar/DiningAppPromo.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { AppStoreLogo, ForkKnife, X as CloseIcon } from '@phosphor-icons/react';
|
||||||
|
import { UT_DINING_APP_STORE_URL, UT_DINING_GOOGLE_PLAY_URL } from '@shared/util/appUrls';
|
||||||
|
import { Button } from '@views/components/common/Button';
|
||||||
|
import Text from '@views/components/common/Text/Text';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface DiningAppPromoProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promotional component for the UT Dining app
|
||||||
|
*/
|
||||||
|
export default function DiningAppPromo({ onClose }: DiningAppPromoProps) {
|
||||||
|
return (
|
||||||
|
<div className='relative min-w-[16.25rem] w-full flex items-center gap-spacing-3 border border-ut-offwhite/50 rounded p-spacing-4'>
|
||||||
|
<div className='flex items-center justify-center'>
|
||||||
|
<ForkKnife className='h-6 w-6 text-ut-black' />
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col gap-spacing-1'>
|
||||||
|
<Text as='p' variant='small' className='whitespace-normal text-ut-black'>
|
||||||
|
Download our new{' '}
|
||||||
|
<a
|
||||||
|
href={UT_DINING_APP_STORE_URL}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
aria-label='UT Dining app'
|
||||||
|
className='text-ut-burntorange underline'
|
||||||
|
>
|
||||||
|
UT Dining app
|
||||||
|
</a>{' '}
|
||||||
|
to explore all dining options on campus!
|
||||||
|
</Text>
|
||||||
|
<div className='mt-spacing-2 flex items-center gap-spacing-2'>
|
||||||
|
<Text variant='mini' className='text-ut-black'>
|
||||||
|
Available on
|
||||||
|
</Text>
|
||||||
|
<a
|
||||||
|
href={UT_DINING_APP_STORE_URL}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
aria-label='Download on App Store'
|
||||||
|
className='text-theme-black transition-colors hover:text-ut-burntorange'
|
||||||
|
>
|
||||||
|
<AppStoreLogo className='h-4.5 w-4.5' />
|
||||||
|
</a>
|
||||||
|
{/* <a
|
||||||
|
href={UT_DINING_GOOGLE_PLAY_URL}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
aria-label='Download on Google Play'
|
||||||
|
className='text-theme-black hover:text-ut-burntorange transition-colors'
|
||||||
|
>
|
||||||
|
<GooglePlayLogo className='h-4.5 w-4.5' />
|
||||||
|
</a> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='minimal'
|
||||||
|
color='theme-black'
|
||||||
|
onClick={onClose}
|
||||||
|
className='absolute right-1 top-1 h-5 w-5 p-0'
|
||||||
|
icon={CloseIcon}
|
||||||
|
aria-label='Close dining app promo'
|
||||||
|
title='Close'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,10 +13,10 @@ interface LinkItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const links: LinkItem[] = [
|
const links: LinkItem[] = [
|
||||||
{
|
// {
|
||||||
text: "Spring '25 Course Schedule",
|
// text: "Spring '25 Course Schedule",
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/',
|
// url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/',
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
text: 'Course Schedule Archives',
|
text: 'Course Schedule Archives',
|
||||||
url: 'https://registrar.utexas.edu/schedules/archive',
|
url: 'https://registrar.utexas.edu/schedules/archive',
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ const links: LinkItem[] = [
|
|||||||
text: "Summer '25 Course Schedule",
|
text: "Summer '25 Course Schedule",
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20256/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20256/',
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
text: "Spring '25 Course Schedule",
|
// text: "Spring '25 Course Schedule",
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/',
|
// url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/',
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
text: 'Course Schedule Archives',
|
text: 'Course Schedule Archives',
|
||||||
url: 'https://registrar.utexas.edu/schedules/archive',
|
url: 'https://registrar.utexas.edu/schedules/archive',
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<DisclosureButton className='w-full flex items-center border-none bg-transparent px-3.5 py-2.5 text-left'>
|
<DisclosureButton className='w-full flex items-center border-none bg-transparent px-3.5 py-2.5 text-left'>
|
||||||
<div className='flex-1 min-w-0 overflow-hidden'>
|
<div className='min-w-0 flex-1 overflow-hidden'>
|
||||||
<Text
|
<Text
|
||||||
as='div'
|
as='div'
|
||||||
variant='h3'
|
variant='h3'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { IconProps } from '@phosphor-icons/react';
|
import type { IconProps } from '@phosphor-icons/react';
|
||||||
import { CloudX, Copy, Exam, MapPinArea, Palette } from '@phosphor-icons/react';
|
import { CloudX, Coffee, ForkKnife, MapTrifold, Storefront } from '@phosphor-icons/react';
|
||||||
import { ExtensionStore } from '@shared/storage/ExtensionStore';
|
import { ExtensionStore } from '@shared/storage/ExtensionStore';
|
||||||
|
import { UT_DINING_PROMO_IMAGE_URL } from '@shared/util/appUrls';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import useWhatsNewPopUp from '@views/hooks/useWhatsNew';
|
import useWhatsNewPopUp from '@views/hooks/useWhatsNew';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@@ -12,7 +13,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
*
|
*
|
||||||
* It should be incremented every time the "What's New" popup is updated.
|
* It should be incremented every time the "What's New" popup is updated.
|
||||||
*/
|
*/
|
||||||
const WHATSNEW_POPUP_VERSION = 1;
|
const WHATSNEW_POPUP_VERSION = 2;
|
||||||
|
|
||||||
const WHATSNEW_VIDEO_URL = 'https://cdn.longhorns.dev/whats-new-v2.1.2.mp4';
|
const WHATSNEW_VIDEO_URL = 'https://cdn.longhorns.dev/whats-new-v2.1.2.mp4';
|
||||||
|
|
||||||
@@ -25,35 +26,28 @@ type Feature = {
|
|||||||
|
|
||||||
const NEW_FEATURES = [
|
const NEW_FEATURES = [
|
||||||
{
|
{
|
||||||
id: 'custom-course-colors',
|
id: 'dining-halls-info',
|
||||||
icon: Palette,
|
icon: ForkKnife,
|
||||||
title: 'Custom Course Colors',
|
title: 'Dining Halls Info',
|
||||||
description: 'Paint your schedule in your favorite color theme',
|
description: 'See daily menus and nutritional deets for J2, JCL, and Kins',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'quick-copy',
|
id: 'coffee-shops',
|
||||||
icon: Copy,
|
icon: Coffee,
|
||||||
title: 'Quick Copy',
|
title: 'Coffee Shops',
|
||||||
description: 'Quickly copy a course unique number to your clipboard',
|
description: 'Need a Coffee Fix? Check operating times for your favorite campus cafes.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'updated-grades',
|
id: 'convenience-stores',
|
||||||
icon: Exam,
|
icon: Storefront,
|
||||||
title: 'Updated Grades',
|
title: 'Convenience Stores',
|
||||||
description: 'Fall 2024 grades are now available in the grade distribution',
|
description: 'Find hours for quick snacks and essentials on campus.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ut-map',
|
id: 'microwave-map',
|
||||||
icon: MapPinArea,
|
icon: MapTrifold,
|
||||||
title: (
|
title: 'Microwave Map',
|
||||||
<div className='flex flex-row items-center'>
|
description: 'Need to heat up your lunch? Find microwaves across campus!',
|
||||||
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[];
|
] as const satisfies readonly Feature[];
|
||||||
|
|
||||||
@@ -97,14 +91,11 @@ export default function WhatsNewPopupContent(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<video
|
<img
|
||||||
className='h-fit w-full flex items-center justify-center border border-ut-offwhite/50 rounded object-cover'
|
className='h-full w-full border border-ut-offwhite/50 rounded object-cover'
|
||||||
autoPlay
|
src={UT_DINING_PROMO_IMAGE_URL}
|
||||||
loop
|
alt='UT Dining Promo'
|
||||||
muted
|
/>
|
||||||
>
|
|
||||||
<source src={WHATSNEW_VIDEO_URL} type='video/mp4' onError={() => setVideoError(true)} />
|
|
||||||
</video>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { UT_DINING_APP_STORE_URL } from '@shared/util/appUrls';
|
||||||
import { Button } from '@views/components/common/Button';
|
import { Button } from '@views/components/common/Button';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import WhatsNewPopupContent from '@views/components/common/WhatsNewPopup';
|
import WhatsNewPopupContent from '@views/components/common/WhatsNewPopup';
|
||||||
@@ -7,6 +8,8 @@ import React from 'react';
|
|||||||
import { LogoIcon } from '../components/common/LogoIcon';
|
import { LogoIcon } from '../components/common/LogoIcon';
|
||||||
import useChangelog from './useChangelog';
|
import useChangelog from './useChangelog';
|
||||||
|
|
||||||
|
const LDIconURL = new URL('/src/assets/LD-icon-new.png', import.meta.url).href;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hook that provides a function to display a what's new dialog.
|
* Custom hook that provides a function to display a what's new dialog.
|
||||||
*
|
*
|
||||||
@@ -22,20 +25,26 @@ export default function useWhatsNewPopUp(): () => void {
|
|||||||
className: 'w-[830px] flex flex-col items-center gap-spacing-7 p-spacing-8',
|
className: 'w-[830px] flex flex-col items-center gap-spacing-7 p-spacing-8',
|
||||||
title: (
|
title: (
|
||||||
<div className='flex items-center justify-between gap-4'>
|
<div className='flex items-center justify-between gap-4'>
|
||||||
<LogoIcon width='48' height='48' />
|
<img src={LDIconURL} alt='LD Icon' className='h-12 w-12 rounded-lg' />
|
||||||
<Text variant='h1' className='text-theme-black'>
|
<Text variant='h1' className='text-theme-black'>
|
||||||
What's New in UT Registration Plus
|
Download our new UT Dining app!
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
description: <WhatsNewPopupContent />,
|
description: <WhatsNewPopupContent />,
|
||||||
buttons: (
|
buttons: (
|
||||||
<div className='flex flex-row items-end gap-spacing-4'>
|
<div className='flex flex-row items-end gap-spacing-4'>
|
||||||
<Button onClick={showChangeLog} variant='minimal' color='ut-black'>
|
<Button
|
||||||
Read Changelog v{version}
|
onClick={() => {
|
||||||
|
window.open(UT_DINING_APP_STORE_URL, '_blank');
|
||||||
|
}}
|
||||||
|
variant='minimal'
|
||||||
|
color='ut-black'
|
||||||
|
>
|
||||||
|
Download UT Dining on iOS
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={close} color='ut-burntorange'>
|
<Button onClick={close} color='ut-burntorange'>
|
||||||
Get started
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user