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(ui): calendar header redesign #479

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/stories/components/calendar/CalendarHeader.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const meta = {
layout: 'centered',
},
tags: ['autodocs'],
args: {
showSidebar: true,
},
} satisfies Meta<typeof CalendarHeader>;

export default meta;
Expand Down
3 changes: 2 additions & 1 deletion src/views/components/calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ export default function Calendar(): JSX.Element {
<CalendarContext.Provider value>
<div className='h-full w-full flex flex-col'>
<CalendarHeader
showSidebar={showSidebar}
onSidebarToggle={() => {
setShowSidebar(!showSidebar);
setShowSidebar(prev => !prev);
}}
/>
<div className='h-full flex overflow-auto pl-3'>
Expand Down
194 changes: 148 additions & 46 deletions src/views/components/calendar/CalendarHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,191 @@
import { GearSix, Sidebar } from '@phosphor-icons/react';
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
import {
BookmarkSimple,
CalendarDots,
Export,
FilePng,
FileText,
MapPinArea,
PlusCircle,
SelectionPlus,
Sidebar,
} from '@phosphor-icons/react';
import { saveAsCal, saveCalAsPng } from '@views/components/calendar/utils';
import { Button } from '@views/components/common/Button';
import CourseStatus from '@views/components/common/CourseStatus';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
import Divider from '@views/components/common/Divider';
import { ExtensionRootWrapper, styleResetClass } from '@views/components/common/ExtensionRoot/ExtensionRoot';
import { LargeLogo } from '@views/components/common/LogoIcon';
import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotalHoursAndCourses';
import Text from '@views/components/common/Text/Text';
import useSchedules from '@views/hooks/useSchedules';
import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript';
import React, { useEffect, useState } from 'react';
import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';

/**
* Opens the options page in a new tab.
* @returns A promise that resolves when the options page is opened.
*/
const handleOpenOptions = async (): Promise<void> => {
const url = chrome.runtime.getURL('/options.html');
await openTabFromContentScript(url);
};

interface CalendarHeaderProps {
onSidebarToggle?: () => void;
showSidebar: boolean;
}

const SECONDARY_ACTIONS_WITH_TEXT_WIDTH = 274; // in px
const PRIMARY_ACTION_WITH_TEXT_WIDTH = 405; // in px
const PRIMARY_ACTION_WITHOUT_TEXT_WIDTH = 160; // in px

/**
* Renders the header component for the calendar.
* @returns The JSX element representing the calendar header.
*/
export default function CalendarHeader({ onSidebarToggle }: CalendarHeaderProps): JSX.Element {
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
const [_enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);

export default function CalendarHeader({ onSidebarToggle, showSidebar }: CalendarHeaderProps): JSX.Element {
const [activeSchedule] = useSchedules();
const secondaryActionContainerRef = useRef<HTMLDivElement | null>(null);
const [{ isDisplayingPrimaryActionsText, isDisplayingSecondaryActionsText }, setIsDisplayingText] = useState({
isDisplayingPrimaryActionsText: true,
isDisplayingSecondaryActionsText: true,
});

useEffect(() => {
initSettings().then(({ enableCourseStatusChips, enableDataRefreshing }) => {
setEnableCourseStatusChips(enableCourseStatusChips);
setEnableDataRefreshing(enableDataRefreshing);
});
if (!secondaryActionContainerRef.current) return;

const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
setEnableCourseStatusChips(newValue);
// console.log('enableCourseStatusChips', newValue);
});
const resizeObserver = new ResizeObserver(([entry]) => {
if (!entry) return;

const width = Math.round(entry.contentRect.width);

if (
width < SECONDARY_ACTIONS_WITH_TEXT_WIDTH &&
isDisplayingPrimaryActionsText &&
isDisplayingSecondaryActionsText
) {
setIsDisplayingText({ isDisplayingSecondaryActionsText: true, isDisplayingPrimaryActionsText: false });
return;
}

if (
isDisplayingSecondaryActionsText &&
width - SECONDARY_ACTIONS_WITH_TEXT_WIDTH >=
PRIMARY_ACTION_WITH_TEXT_WIDTH - PRIMARY_ACTION_WITHOUT_TEXT_WIDTH
) {
setIsDisplayingText({ isDisplayingSecondaryActionsText: true, isDisplayingPrimaryActionsText: true });
return;
}

const l2 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => {
setEnableDataRefreshing(newValue);
// console.log('enableDataRefreshing', newValue);
if (width < SECONDARY_ACTIONS_WITH_TEXT_WIDTH && isDisplayingSecondaryActionsText) {
setIsDisplayingText({ isDisplayingSecondaryActionsText: false, isDisplayingPrimaryActionsText: false });
return;
}

if (width >= SECONDARY_ACTIONS_WITH_TEXT_WIDTH && !isDisplayingSecondaryActionsText) {
setIsDisplayingText({ isDisplayingSecondaryActionsText: true, isDisplayingPrimaryActionsText: false });
}
});

return () => {
OptionsStore.removeListener(l1);
OptionsStore.removeListener(l2);
};
}, []);
resizeObserver.observe(secondaryActionContainerRef.current);

return () => resizeObserver.disconnect();
}, [isDisplayingPrimaryActionsText, isDisplayingSecondaryActionsText]);

return (
<div className='flex items-center gap-5 overflow-x-auto overflow-y-hidden border-b border-ut-offwhite px-7 py-4 md:overflow-x-hidden'>
<div className='flex items-center gap-5 overflow-x-auto overflow-y-hidden border-b border-ut-offwhite py-5 pl-6 md:overflow-x-hidden'>
<Button
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Remove the offwhite border since the toolbar/header will no longer have a bottom border.

variant='single'
icon={Sidebar}
color='ut-gray'
onClick={onSidebarToggle}
Copy link
Member

@IsaDavRod IsaDavRod Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Change the button color to theme-black for now (Ethan's Calendar Sidebar PR already has it black too)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, yes, adding more buttons would break it. I can try to find workarounds, though. I have a few ideas.

className='screenshot:hidden'
className='flex-shrink-0 screenshot:hidden'
/>
<LargeLogo />
<Divider className='mx-2 self-center md:mx-4' size='2.5rem' orientation='vertical' />
<div className='flex-1 screenshot:transform-origin-left screenshot:scale-120'>
{showSidebar && (
<>
<LargeLogo className='flex-shrink-0' />
<Divider className='mx-2 flex-shrink-0 self-center md:mx-4' size='2.5rem' orientation='vertical' />
</>
)}
Copy link
Member

@IsaDavRod IsaDavRod Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Since the logo is moved to the calendar sidebar, we can remove it from the header. This divider would also be removed.


<div className='min-w-0 screenshot:transform-origin-left screenshot:scale-120'>
<ScheduleTotalHoursAndCourses
Copy link
Member

@IsaDavRod IsaDavRod Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • This container should have a minimm width of 10.9375rem to prevent the following overlap:
    screenshot 2025-01-02 at 18 32 04@2x

scheduleName={activeSchedule.name}
totalHours={activeSchedule.hours}
totalCourses={activeSchedule.courses.length}
/>
</div>
<div className='hidden flex-row items-center justify-end gap-6 screenshot:hidden lg:flex'>
{enableCourseStatusChips && (
<>
<CourseStatus status='WAITLISTED' size='mini' />
<CourseStatus status='CLOSED' size='mini' />
<CourseStatus status='CANCELLED' size='mini' />
</>
)}

{/* <Button variant='single' icon={UndoIcon} color='ut-black' />
<Button variant='single' icon={RedoIcon} color='ut-black' /> */}
<Button variant='single' icon={GearSix} color='theme-black' onClick={handleOpenOptions} />

<Divider size='2.5rem' orientation='vertical' />
<div className='flex flex-shrink-0 items-center gap-5'>
Copy link
Member

@IsaDavRod IsaDavRod Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Change divider height/size to 1.75rem and use the theme-offwhite1 color (assuming your color PR is merged in main)

<Button variant='single' color='ut-black' icon={PlusCircle} className='flex-shrink-0'>
{isDisplayingPrimaryActionsText && <Text variant='small'>Quick Add</Text>}
</Button>
<Button variant='single' color='ut-black' icon={SelectionPlus} className='flex-shrink-0'>
{isDisplayingPrimaryActionsText && <Text variant='small'>Add Block</Text>}
</Button>
<DialogProvider>
<Menu>
<MenuButton className='h-fit bg-transparent p-0'>
<Button variant='single' color='ut-black' icon={Export} className='flex-shrink-0'>
{isDisplayingPrimaryActionsText && <Text variant='small'>Export</Text>}
</Button>
</MenuButton>

<MenuItems
as={ExtensionRootWrapper}
className={clsx([
styleResetClass,
'w-42 cursor-pointer origin-top-right rounded bg-white p-1 text-black shadow-lg transition border border-ut-offwhite focus:outline-none',
'data-[closed]:(opacity-0 scale-95)',
'data-[enter]:(ease-out-expo duration-150)',
'data-[leave]:(ease-out duration-50)',
'mt-2',
])}
transition
anchor='bottom start'
>
<MenuItem>
<Text
onClick={() => saveCalAsPng()}
as='button'
variant='small'
className='w-full flex items-center gap-2 rounded bg-transparent p-2 text-left data-[focus]:bg-gray-200/40'
>
<FilePng className='h-4 w-4' />
Save as .png
</Text>
</MenuItem>
<MenuItem>
<Text
as='button'
onClick={saveAsCal}
variant='small'
className='w-full flex items-center gap-2 rounded bg-transparent p-2 text-left data-[focus]:bg-gray-200/40'
>
<CalendarDots className='h-4 w-4' />
Save as .cal
</Text>
</MenuItem>
<MenuItem>
<Text
as='button'
variant='small'
className='w-full flex items-center gap-2 rounded bg-transparent p-2 text-left data-[focus]:bg-gray-200/40'
>
<FileText className='h-4 w-4' />
Export Unique IDs
</Text>
</MenuItem>
</MenuItems>
</Menu>
</DialogProvider>
</div>
<Divider size='2.5rem' orientation='vertical' />
<div ref={secondaryActionContainerRef} className='mr-5 flex flex-1 items-center justify-end gap-5'>
Copy link
Member

@IsaDavRod IsaDavRod Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Change divider height/size to 1.75rem and use the theme-offwhite1 color (assuming your color PR is merged in main)

<Button variant='single' color='ut-black' icon={BookmarkSimple}>
{isDisplayingSecondaryActionsText && <Text variant='small'>Bookmarks</Text>}
</Button>
<Button variant='single' color='ut-black' icon={MapPinArea}>
{isDisplayingSecondaryActionsText && <Text variant='small'>UT Map</Text>}
</Button>
</div>
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions src/views/components/common/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from 'react';

interface Props {
className?: string;
ref?: React.ForwardedRef<HTMLButtonElement>;
style?: React.CSSProperties;
variant: 'filled' | 'outline' | 'single';
onClick?: () => void;
Expand All @@ -23,6 +24,7 @@ interface Props {
*/
export function Button({
className,
ref,
style,
variant,
onClick,
Expand All @@ -40,6 +42,7 @@ export function Button({

return (
<button
ref={ref}
style={
{
...style,
Expand Down
6 changes: 3 additions & 3 deletions src/views/components/common/ScheduleTotalHoursAndCourses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export default function ScheduleTotalHoursAndCourses({
totalCourses,
}: ScheduleTotalHoursAndCoursesProps): JSX.Element {
return (
<div className='min-w-full w-0 items-center whitespace-nowrap'>
<Text className='truncate text-ut-burntorange normal-case!' variant='h1' as='span'>
<div className='flex flex-col gap-1'>
<Text className='w-full truncate text-ut-burntorange normal-case!' variant='h1' as='span'>
{scheduleName}
</Text>
<div className='flex flex-row items-center gap-2.5 text-theme-black'>
<div className='flex flex-shrink-0 flex-row items-center gap-2.5 whitespace-nowrap text-theme-black'>
<div className='flex flex-row items-center gap-1.25 text-theme-black'>
<Text variant='h3' as='span' className='capitalize screenshot:inline sm:inline'>
{totalHours}
Expand Down
26 changes: 26 additions & 0 deletions src/views/hooks/useScreenSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect, useState } from 'react';

/**
* Retrieves the current screen size using a debounced event listener callback
*/
export function useScreenSize() {
const [screenSize, setScreenSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});

useEffect(() => {
function handleResize() {
setScreenSize({
width: window.innerWidth,
height: window.innerHeight,
});
}

window.addEventListener('resize', handleResize);

return () => window.removeEventListener('resize', handleResize);
}, []);

return screenSize;
}
Loading