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 all 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
2 changes: 1 addition & 1 deletion src/views/components/calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function Calendar(): JSX.Element {
<div className='h-full w-full flex flex-col'>
<CalendarHeader
onSidebarToggle={() => {
setShowSidebar(!showSidebar);
setShowSidebar(prev => !prev);
}}
/>
<div className='h-full flex overflow-auto pl-3'>
Expand Down
195 changes: 146 additions & 49 deletions src/views/components/calendar/CalendarHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,186 @@
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 { LargeLogo } from '@views/components/common/LogoIcon';
import { ExtensionRootWrapper, styleResetClass } from '@views/components/common/ExtensionRoot/ExtensionRoot';
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, useMemo, 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;
}

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);

const [activeSchedule] = useSchedules();
const secondaryActionContainerRef = useRef<HTMLDivElement | null>(null);
const [isDisplayingPrimaryActionsText, setIsDisplayingPrimaryActionsText] = useState(true);
const [isDisplayingSecondaryActionsText, setIsDisplayingSecondaryActionsText] = useState(true);

useEffect(() => {
initSettings().then(({ enableCourseStatusChips, enableDataRefreshing }) => {
setEnableCourseStatusChips(enableCourseStatusChips);
setEnableDataRefreshing(enableDataRefreshing);
});
const resizeObserver = useMemo(
() =>
new ResizeObserver(([entry]) => {
if (!entry) return;

const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
setEnableCourseStatusChips(newValue);
// console.log('enableCourseStatusChips', newValue);
});
const width = Math.round(entry.contentRect.width);

const l2 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => {
setEnableDataRefreshing(newValue);
// console.log('enableDataRefreshing', newValue);
});
if (
// Collapses the primary action section
isDisplayingPrimaryActionsText &&
width < SECONDARY_ACTIONS_WITH_TEXT_WIDTH
) {
setIsDisplayingPrimaryActionsText(false);
return;
}

return () => {
OptionsStore.removeListener(l1);
OptionsStore.removeListener(l2);
};
}, []);
if (
// Expands the primary action section if there is enough room for it to expand
!isDisplayingPrimaryActionsText &&
width - SECONDARY_ACTIONS_WITH_TEXT_WIDTH >=
PRIMARY_ACTION_WITH_TEXT_WIDTH - PRIMARY_ACTION_WITHOUT_TEXT_WIDTH
) {
setIsDisplayingPrimaryActionsText(true);
return;
}

// Contracts the secondary action section
if (isDisplayingSecondaryActionsText && width < SECONDARY_ACTIONS_WITH_TEXT_WIDTH) {
setIsDisplayingSecondaryActionsText(false);
return;
}

// Expands the secondary action section if there is enough room for it to expand
if (!isDisplayingSecondaryActionsText && width >= SECONDARY_ACTIONS_WITH_TEXT_WIDTH) {
setIsDisplayingSecondaryActionsText(true);
}
}),
[isDisplayingPrimaryActionsText, isDisplayingSecondaryActionsText]
);

useEffect(() => {
if (!secondaryActionContainerRef.current) return;

resizeObserver.observe(secondaryActionContainerRef.current);

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

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='minimal'
icon={Sidebar}
color='ut-gray'
color='ut-black'
onClick={onSidebarToggle}
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'>
<div className='min-w-[10.9375rem] screenshot:transform-origin-left screenshot:scale-120'>
<ScheduleTotalHoursAndCourses
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='minimal' icon={GearSix} color='theme-black' onClick={handleOpenOptions} />
<Divider className='border-theme-offwhite1' size='1.75rem' orientation='vertical' />
<div className='flex flex-shrink-0 items-center gap-5'>
<Button variant='minimal' color='ut-black' icon={PlusCircle} className='flex-shrink-0'>
{isDisplayingPrimaryActionsText && <Text variant='small'>Quick Add</Text>}
</Button>
<Button variant='minimal' 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='minimal' 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 className='border-theme-offwhite1' size='1.75rem' orientation='vertical' />
<div ref={secondaryActionContainerRef} className='mr-5 flex flex-1 items-center justify-end gap-5'>
<Button variant='minimal' color='ut-black' icon={BookmarkSimple}>
{isDisplayingSecondaryActionsText && <Text variant='small'>Bookmarks</Text>}
</Button>
<Button variant='minimal' 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' | 'minimal';
size?: 'regular' | 'small' | 'mini';
Expand All @@ -24,6 +25,7 @@ interface Props {
*/
export function Button({
className,
ref,
style,
variant = 'filled',
size = 'regular',
Expand All @@ -42,6 +44,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