Merge branch 'Longhorn-Developers:main' into master

This commit is contained in:
2024-05-21 21:46:06 -05:00
committed by GitHub
35 changed files with 9640 additions and 8046 deletions

View File

@@ -15,7 +15,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9
- name: Install dependencies
run: pnpm install
@@ -34,7 +34,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9
- name: Install dependencies
run: pnpm install

View File

@@ -15,7 +15,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9
- name: Install dependencies
run: pnpm install

View File

@@ -13,7 +13,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9
- name: Install dependencies
run: pnpm install

View File

@@ -15,7 +15,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9
- name: Install dependencies
run: pnpm install

View File

@@ -2,7 +2,13 @@ import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-designs'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-designs',
'@storybook/test',
'@chromatic-com/storybook',
],
framework: {
name: '@storybook/react-vite',
options: {

View File

@@ -1,10 +1,10 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { Preview } from '@storybook/react';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import React from 'react';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
@@ -166,4 +166,13 @@ globalThis.chrome = {
},
} as typeof chrome;
// set updatedAt dates to be fixed
UserScheduleStore.get('schedules').then(schedules => {
schedules.forEach(schedule => {
schedule.updatedAt = new Date('2024-01-01 12:00').getTime();
});
UserScheduleStore.set('schedules', schedules);
});
export default preview;

5
chromatic.config.json Normal file
View File

@@ -0,0 +1,5 @@
{
"onlyChanged": true,
"projectId": "Project:65c5172964f36dcf207985bf",
"zip": true
}

View File

@@ -24,7 +24,7 @@
"prepare": "husky"
},
"dependencies": {
"@headlessui/react": "^1.7.18",
"@headlessui/react": "^2.0.3",
"@hello-pangea/dnd": "^16.5.0",
"@unocss/vite": "^0.58.6",
"@vitejs/plugin-react": "^4.2.1",
@@ -35,33 +35,34 @@
"html-to-image": "^1.11.11",
"husky": "^9.0.11",
"nanoid": "^5.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.71.1",
"sql.js": "1.10.2"
},
"devDependencies": {
"@chromatic-com/storybook": "^1.4.0",
"@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.6.2",
"@commitlint/types": "^19.0.3",
"@crxjs/vite-plugin": "2.0.0-beta.21",
"@iconify-json/bi": "^1.1.23",
"@iconify-json/material-symbols": "^1.1.73",
"@iconify-json/ri": "^1.1.20",
"@iconify-json/bi": "^1.1.23",
"@storybook/addon-designs": "^7.0.9",
"@storybook/addon-essentials": "^7.6.17",
"@storybook/addon-links": "^7.6.17",
"@storybook/blocks": "^7.6.17",
"@storybook/react": "^7.6.17",
"@storybook/react-vite": "^7.6.17",
"@storybook/test": "^7.6.17",
"@storybook/addon-designs": "^8.0.1",
"@storybook/addon-essentials": "^8.1.1",
"@storybook/addon-links": "^8.1.1",
"@storybook/blocks": "^8.1.1",
"@storybook/react": "^8.1.1",
"@storybook/react-vite": "^8.1.1",
"@storybook/test": "^8.1.1",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@types/chrome": "^0.0.260",
"@types/node": "^20.11.24",
"@types/chrome": "^0.0.268",
"@types/node": "^20.12.12",
"@types/prompts": "^2.4.9",
"@types/react": "^18.2.61",
"@types/react-dom": "^18.2.19",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@types/semver": "^7.5.8",
"@types/sql.js": "^1.4.9",
"@typescript-eslint/eslint-plugin": "^6.21.0",
@@ -76,7 +77,7 @@
"@vitejs/plugin-react-swc": "^3.6.0",
"@vitest/coverage-v8": "^1.3.1",
"@vitest/ui": "^1.3.1",
"chromatic": "^10.9.6",
"chromatic": "^11.3.5",
"cssnano": "^6.0.5",
"cssnano-preset-advanced": "^6.0.5",
"dotenv": "^16.4.5",
@@ -96,12 +97,12 @@
"eslint-plugin-react-prefer-function-component": "^3.3.0",
"eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-storybook": "^0.6.15",
"eslint-plugin-storybook": "^0.8.0",
"path": "^0.12.7",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"react-dev-utils": "^12.0.1",
"storybook": "^7.6.17",
"storybook": "^8.1.1",
"typescript": "^5.4.3",
"unocss": "^0.58.6",
"unocss-preset-primitives": "0.0.2-beta.0",

16637
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from '@views/components/common/Button';
import DialogProvider, { usePrompt } from '@views/components/common/DialogProvider/DialogProvider';
import Text from '@views/components/common/Text/Text';
import React, { useState } from 'react';
import MaterialSymbolsExpandAllRoundedIcon from '~icons/material-symbols/expand-all-rounded';
const meta = {
title: 'Components/Common/DialogProvider',
component: DialogProvider,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
args: {},
argTypes: {},
} satisfies Meta<typeof DialogProvider>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { children: undefined },
render: () => (
<DialogProvider>
<InnerComponent />
</DialogProvider>
),
};
const InnerComponent = () => {
const showDialog = usePrompt();
const myShow = () => {
showDialog({
title: 'Dialog Title',
description: 'Dialog Description',
// eslint-disable-next-line react/no-unstable-nested-components
buttons: close => (
<Button variant='filled' color='ut-burntorange' onClick={close}>
Close
</Button>
),
});
};
return (
<Button variant='filled' color='ut-burntorange' icon={MaterialSymbolsExpandAllRoundedIcon} onClick={myShow}>
Open Dialog
</Button>
);
};
export const FiveDialogs: Story = {
args: { children: undefined },
render: () => (
<DialogProvider>
<Text variant='p'>They&apos;ll open with 100ms delay</Text>
<FiveDialogsInnerComponent />
</DialogProvider>
),
};
const FiveDialogsInnerComponent = () => {
const showDialog = usePrompt();
const myShow = () => {
for (let i = 0; i < 5; i++) {
setTimeout(
() =>
showDialog({
title: `Dialog #${i}`,
description:
'Deleting Main Schedule is permanent and will remove all added courses from that schedule.',
// eslint-disable-next-line react/no-unstable-nested-components
buttons: close => (
<Button variant='filled' color='ut-burntorange' onClick={close}>
Close
</Button>
),
}),
100 * i
);
}
};
return (
<Button variant='filled' color='ut-burntorange' icon={MaterialSymbolsExpandAllRoundedIcon} onClick={myShow}>
Open Dialogs
</Button>
);
};
export const NestedDialogs: Story = {
args: { children: undefined },
render: () => (
<DialogProvider>
<NestedDialogsInnerComponent />
</DialogProvider>
),
};
const NestedDialogsInnerComponent = () => {
const showDialog = usePrompt();
const myShow = () => {
showDialog({
title: 'Dialog Title',
description: 'Dialog Description',
// eslint-disable-next-line react/no-unstable-nested-components
buttons: close => (
<>
<NestedDialogsInnerComponent />
<Button variant='filled' color='ut-burntorange' onClick={close}>
Close
</Button>
</>
),
});
};
return (
<Button variant='filled' color='ut-burntorange' icon={MaterialSymbolsExpandAllRoundedIcon} onClick={myShow}>
Open Next Dialog
</Button>
);
};
export const DialogWithOnClose: Story = {
args: { children: undefined },
render: () => (
<DialogProvider>
<DialogWithOnCloseInnerComponent />
</DialogProvider>
),
};
const DialogWithOnCloseInnerComponent = () => {
const showDialog = usePrompt();
const [timesClosed, setTimesClosed] = useState(0);
const myShow = () => {
showDialog({
title: 'Dialog Title',
description: 'Dialog Description',
// eslint-disable-next-line react/no-unstable-nested-components
buttons: close => (
<Button variant='filled' color='ut-burntorange' onClick={close}>
Close
</Button>
),
onClose: () => {
setTimesClosed(prev => prev + 1);
},
});
};
return (
<>
<h1>
You closed the button below {timesClosed} {timesClosed === 1 ? 'time' : 'times'}
</h1>
<Button variant='filled' color='ut-burntorange' icon={MaterialSymbolsExpandAllRoundedIcon} onClick={myShow}>
Open Dialog
</Button>
</>
);
};

View File

@@ -38,23 +38,9 @@ const meta: Meta<typeof ScheduleDropdown> = {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
defaultOpen: {
control: {
type: 'boolean',
},
},
children: {
control: {
type: 'node',
},
},
},
render: (args: ScheduleDropdownProps) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [activeSchedule, schedules] = useSchedules();
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
console.log(activeSchedule);
}, [activeSchedule]);

View File

@@ -97,6 +97,7 @@ export const Default: Story = {
children: generateCourseBlocks,
itemKey: item => item.uniqueId,
gap: 12,
onReordered: () => {},
},
render: args => (
<div className='w-sm'>

View File

@@ -12,13 +12,6 @@ const meta = {
parameters: {
layout: 'centered',
},
argTypes: {
schedule: {
control: {
type: 'UserSchedule',
},
},
},
args: {
schedule: exampleSchedule,
},
@@ -30,7 +23,6 @@ type Story = StoryObj<typeof meta>;
export const Active: Story = {
render(args) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [activeSchedule] = useSchedules();
return (

View File

@@ -117,6 +117,7 @@ export const Default: Story = {
export const Empty: Story = {
args: {
courseCells: [],
setCourse: () => {},
},
render: props => (
<div className='outline-red outline w-292.5!'>

View File

@@ -135,5 +135,6 @@ export const Default: Story = {
args: {
saturdayClass: true,
courseCells: testData,
setCourse: () => {},
},
};

View File

@@ -15,7 +15,6 @@ const meta = {
},
tags: ['autodocs'],
render(args) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [isOpen, setIsOpen] = useState(args.open);
return <CourseCatalogInjectedPopup {...args} open={isOpen} onClose={() => setIsOpen(false)} />;

View File

@@ -2,7 +2,7 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"composite": true,
"lib": ["DOM", "es2021"],
"lib": ["DOM", "ESNext"],
"types": ["chrome", "node"]
}
}

View File

@@ -7,6 +7,7 @@ import CalendarHeader from '@views/components/calendar/CalenderHeader';
import ImportantLinks from '@views/components/calendar/ImportantLinks';
import Divider from '@views/components/common/Divider';
import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
import { CalendarContext } from '@views/contexts/CalendarContext';
import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSchedule';
import { MessageListener } from 'chrome-extension-toolkit';
import React, { useEffect, useState } from 'react';
@@ -64,6 +65,7 @@ export default function Calendar(): JSX.Element {
}, [course]);
return (
<CalendarContext.Provider value>
<div className='h-full w-full flex flex-col'>
<CalendarHeader
onSidebarToggle={() => {
@@ -72,8 +74,8 @@ export default function Calendar(): JSX.Element {
/>
<div className='h-full flex overflow-auto pl-3'>
{showSidebar && (
<div className='h-full flex flex-none flex-col justify-between pb-5 pl-4.5 screenshot:hidden'>
<div className='mb-3 h-full w-fit flex flex-col overflow-auto pb-2 pr-4 pt-5'>
<div className='h-full flex flex-none flex-col justify-between pb-5 screenshot:hidden'>
<div className='mb-3 h-full w-fit flex flex-col overflow-auto pb-2 pl-4.5 pr-4 pt-5'>
<CalendarSchedules />
<Divider orientation='horizontal' size='100%' className='my-5' />
<ImportantLinks />
@@ -83,7 +85,7 @@ export default function Calendar(): JSX.Element {
<CalendarFooter />
</div>
)}
<div className='h-full min-w-4xl flex flex-grow flex-col overflow-y-auto'>
<div className='h-full min-w-5xl flex flex-grow flex-col overflow-y-auto'>
<div className='min-h-2xl flex-grow overflow-auto pl-2 pr-4 pt-6 screenshot:min-h-xl'>
<CalendarGrid courseCells={courseCells} setCourse={setCourse} />
</div>
@@ -98,5 +100,6 @@ export default function Calendar(): JSX.Element {
afterLeave={() => setCourse(null)}
/>
</div>
</CalendarContext.Provider>
);
}

View File

@@ -12,15 +12,15 @@ import Link from '../common/Link';
*/
export default function CalendarFooter(): JSX.Element {
return (
<footer className='min-w-full w-0 space-y-2'>
<footer className='min-w-full w-0 pl-4.5 space-y-2'>
<div className='flex gap-2'>
<Link className='linkanimate' href='#'>
<Link className='linkanimate' href='https://www.instagram.com/longhorndevelopers'>
<InstagramIcon className='h-6 w-6' />
</Link>
<Link className='linkanimate' href='https://discord.gg/bVh9g6VFwB'>
<Link className='linkanimate' href='https://discord.gg/7pQDBGdmb7'>
<DiscordIcon className='h-6 w-6' />
</Link>
<Link className='linkanimate' href='https://github.com/Longhorn-Developers/UT-Registration-Plus'>
<Link className='linkanimate' href='https://github.com/Longhorn-Developers'>
<GithubIcon className='h-6 w-6' />
</Link>
</div>

View File

@@ -34,7 +34,7 @@ export default function CalendarHeader({ onSidebarToggle }: CalendarHeaderProps)
const [activeSchedule] = useSchedules();
return (
<div className='flex items-center gap-5 border-b border-ut-offwhite px-7 py-4'>
<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'>
<Button
variant='single'
icon={MenuIcon}
@@ -51,7 +51,7 @@ export default function CalendarHeader({ onSidebarToggle }: CalendarHeaderProps)
totalCourses={activeSchedule.courses.length}
/>
<div className='flex items-center gap-1 screenshot:hidden'>
<Text variant='mini' className='text-ut-gray font-normal'>
<Text variant='mini' className='text-nowrap text-ut-gray font-normal'>
DATA LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
</Text>
<button className='inline-block h-4 w-4 bg-transparent p-0 btn'>

View File

@@ -14,17 +14,17 @@ interface LinkItem {
}
const links: LinkItem[] = [
{
text: 'Feedback Form',
url: '#',
},
{
text: 'Apply to Longhorn Developers',
url: '#',
},
// {
// text: 'Feedback Form',
// url: '#',
// },
// {
// text: 'Apply to Longhorn Developers',
// url: '#',
// },
{
text: 'Become a Beta Tester',
url: 'https://discord.gg/bVh9g6VFwB',
url: 'https://forms.gle/Y9dmQAb1yzW5PRg48',
},
];

View File

@@ -1,5 +1,12 @@
import type { TransitionRootProps } from '@headlessui/react';
import { Dialog as HDialog, Transition } from '@headlessui/react';
import {
Description,
Dialog as HDialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import clsx from 'clsx';
import type { PropsWithChildren } from 'react';
import React, { Fragment } from 'react';
@@ -26,7 +33,7 @@ export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Eleme
return (
<Transition show={open} as={HDialog} {...rest}>
<ExtensionRoot>
<Transition.Child
<TransitionChild
as={Fragment}
enter='transition duration-300 motion-reduce:duration-150 ease-out'
enterFrom='opacity-0'
@@ -36,8 +43,8 @@ export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Eleme
leaveTo='opacity-0'
>
<div className={clsx('fixed inset-0 z-50 bg-slate-700/35')} />
</Transition.Child>
<Transition.Child
</TransitionChild>
<TransitionChild
as={Fragment}
enter='transition duration-375 motion-reduce:duration-0 ease-[cubic-bezier(0.05,0.4,0.2,1)]'
enterFrom='transform-gpu scale-95 opacity-0'
@@ -47,18 +54,18 @@ export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Eleme
leaveTo='transform-gpu scale-95 opacity-0'
>
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<HDialog.Panel
<DialogPanel
className={clsx(
'z-99 max-h-[90vh] flex flex-col overflow-y-auto border border-solid border-ut-offwhite rounded bg-white shadow-xl ml-[calc(100vw-100%)] mt-[calc(100vw-100%)]',
className
)}
>
{props.title && <HDialog.Title>{props.title}</HDialog.Title>}
{props.description && <HDialog.Description>{props.description}</HDialog.Description>}
{props.title && <DialogTitle as={Fragment}>{props.title}</DialogTitle>}
{props.description && <Description as={Fragment}>{props.description}</Description>}
{children}
</HDialog.Panel>
</DialogPanel>
</div>
</Transition.Child>
</TransitionChild>
</ExtensionRoot>
</Transition>
);

View File

@@ -0,0 +1,115 @@
import type { CloseWrapper, DialogInfo, ShowDialogFn } from '@views/contexts/DialogContext';
import { DialogContext, useDialog } from '@views/contexts/DialogContext';
import type { ReactNode } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import Dialog from '../Dialog';
import Text from '../Text/Text';
type DialogElement = (show: boolean) => ReactNode;
export interface PromptInfo extends Omit<DialogInfo, 'buttons' | 'className' | 'title' | 'description'> {
title: JSX.Element | string;
description: JSX.Element | string;
onClose?: () => void;
buttons: NonNullable<DialogInfo['buttons']>;
}
function unwrapCloseWrapper<T>(obj: T | CloseWrapper<T>, close: () => void): T {
if (typeof obj === 'function') {
return (obj as CloseWrapper<T>)(close);
}
return obj;
}
/**
* Hook to show prompt with default stylings.
*/
export function usePrompt(): (info: PromptInfo) => void {
const showDialog = useDialog();
return (info: PromptInfo) => {
showDialog({
...info,
title: (
<Text variant='h2' as='h1' className='text-theme-black'>
{info.title}
</Text>
),
description: (
<Text variant='p' as='p' className='text-ut-black'>
{info.description}
</Text>
),
className: 'max-w-[400px] flex flex-col gap-2.5 p-6.25',
});
};
}
// Unique ID counter is safe to be global
let nextId = 1;
/**
* Allows descendant to show dialogs via a function, handling animations and stacking.
*/
export default function DialogProvider(props: { children: ReactNode }): JSX.Element {
const dialogQueue = useRef<DialogElement[]>([]);
const [openDialog, setOpenDialog] = useState<DialogElement | undefined>();
const openRef = useRef<typeof openDialog>();
openRef.current = openDialog;
const [isOpen, setIsOpen] = useState(false);
const showDialog = useCallback<ShowDialogFn>(info => {
const id = nextId++;
const handleClose = () => {
setIsOpen(false);
};
const infoUnwrapped = unwrapCloseWrapper(info, handleClose);
const buttons = unwrapCloseWrapper(infoUnwrapped.buttons, handleClose);
const onLeave = () => {
setOpenDialog(undefined);
if (dialogQueue.current.length > 0) {
const newOpen = dialogQueue.current.pop();
setOpenDialog(() => newOpen);
setIsOpen(true);
}
infoUnwrapped.onClose?.();
};
const dialogElement = (show: boolean) => (
<Dialog
key={id}
onClose={handleClose}
afterLeave={onLeave}
title={infoUnwrapped.title}
description={infoUnwrapped.description}
appear
show={show}
className={infoUnwrapped.className}
>
<div className='mt-0.75 w-full flex justify-end gap-2.5'>{buttons}</div>
</Dialog>
);
if (openRef.current) {
dialogQueue.current.push(openRef.current);
}
setOpenDialog(() => dialogElement);
setIsOpen(true);
}, []);
return (
<DialogContext.Provider value={showDialog}>
{props.children}
{openDialog?.(isOpen)}
</DialogContext.Provider>
);
}

View File

@@ -13,4 +13,29 @@
.extensionRoot {
@apply font-sans h-full;
color: #303030;
[data-rfd-drag-handle-context-id=':r1:'] {
cursor: move;
}
}
::-webkit-scrollbar {
width: 14px;
height: 14px;
background: transparent;
}
::-webkit-scrollbar-thumb {
border: 3px solid #fff;
border-radius: 7px;
min-height: 40px;
box-shadow: none;
background: rgb(218, 220, 224);
}
:hover::-webkit-scrollbar-thumb,
::-webkit-scrollbar-thumb:hover {
background: rgb(189, 193, 198);
}
::-webkit-scrollbar-thumb:active {
background: rgb(128, 134, 139);
}

View File

@@ -42,12 +42,12 @@ export default function PopupCourseBlock({
};
return (
<button
<div
style={{
backgroundColor: colors.primaryColor,
}}
className={clsx(
'h-full w-full inline-flex items-center justify-center gap-1 rounded pr-3 cursor-pointer focusable text-left',
'h-full w-full inline-flex items-center justify-center gap-1 rounded pr-3 focusable cursor-pointer text-left',
className
)}
onClick={handleClick}
@@ -56,7 +56,7 @@ export default function PopupCourseBlock({
style={{
backgroundColor: colors.secondaryColor,
}}
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
className='flex items-center self-stretch rounded rounded-r-0 cursor-move!'
{...dragHandleProps}
>
<DragIndicatorIcon className='h-6 w-6 text-white' />
@@ -75,6 +75,6 @@ export default function PopupCourseBlock({
<StatusIcon status={course.status} className='h-5 w-5' />
</div>
)}
</button>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Dialog, Transition } from '@headlessui/react';
import { Dialog, Transition, TransitionChild } from '@headlessui/react';
import type { ReactElement } from 'react';
import React from 'react';
@@ -28,7 +28,7 @@ function PromptDialog({ isOpen, onClose, title, content, children }: PromptDialo
return (
<Transition appear show={isOpen} as={React.Fragment}>
<Dialog as='div' onClose={onClose} className='relative z-50'>
<Transition.Child
<TransitionChild
as={React.Fragment}
enter='ease-out duration-200'
enterFrom='opacity-0'
@@ -38,9 +38,9 @@ function PromptDialog({ isOpen, onClose, title, content, children }: PromptDialo
leaveTo='opacity-0'
>
<div className='fixed inset-0 bg-black bg-opacity-50' aria-hidden='true' />
</Transition.Child>
</TransitionChild>
<Transition.Child
<TransitionChild
as={React.Fragment}
enter='ease-out duration-200'
enterFrom='opacity-0 scale-95'
@@ -56,7 +56,7 @@ function PromptDialog({ isOpen, onClose, title, content, children }: PromptDialo
<div className='flex items-center justify-end gap-2'>{children}</div>
</Dialog.Panel>
</div>
</Transition.Child>
</TransitionChild>
</Dialog>
</Transition>
);

View File

@@ -1,4 +1,4 @@
import { Disclosure, Transition } from '@headlessui/react';
import { Disclosure, DisclosureButton, DisclosurePanel, Transition } from '@headlessui/react';
import Text from '@views/components/common/Text/Text';
import useSchedules from '@views/hooks/useSchedules';
import React from 'react';
@@ -25,7 +25,7 @@ export default function ScheduleDropdown(props: ScheduleDropdownProps) {
<Disclosure defaultOpen={props.defaultOpen}>
{({ open }) => (
<>
<Disclosure.Button 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'>
<Text as='div' variant='h4' className='mb-1 w-100% text-ut-burntorange'>
{(activeSchedule ? activeSchedule.name : 'Schedule').toUpperCase()}:
@@ -42,9 +42,10 @@ export default function ScheduleDropdown(props: ScheduleDropdownProps) {
<Text className='text-ut-burntorange text-2xl! font-normal!'>
{open ? <DropdownArrowDown /> : <DropdownArrowUp />}
</Text>
</Disclosure.Button>
</DisclosureButton>
<Transition
as='div'
className='contain-paint max-h-55 origin-top overflow-auto transition-all duration-400 ease-in-out-expo'
enterFrom='transform scale-98 opacity-0 max-h-0!'
enterTo='transform scale-100 opacity-100 max-h-55'
@@ -52,7 +53,7 @@ export default function ScheduleDropdown(props: ScheduleDropdownProps) {
leaveFrom='transform scale-100 opacity-100 max-h-55'
leaveTo='transform scale-98 opacity-0 max-h-0!'
>
<Disclosure.Panel className='px-3.5 pb-2.5 pt-2'>{props.children}</Disclosure.Panel>
<DisclosurePanel className='px-3.5 pb-2.5 pt-2'>{props.children}</DisclosurePanel>
</Transition>
</>
)}

View File

@@ -56,7 +56,9 @@ export default function Description({ course }: DescriptionProps): JSX.Element {
return (
<>
{status === LoadStatus.ERROR && (
<Text color='theme-red'>Please refresh the page and log back in using your UT EID and password.</Text>
<Text className='text-theme-red font-bold!'>
Please refresh the page and log back in using your UT EID and password.
</Text>
)}
{/* TODO (achadaga): would be nice to have a new spinner here */}
{status === LoadStatus.LOADING && <Spinner />}

View File

@@ -184,7 +184,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
};
return (
<div className='pb-[25px] pt-[12px]'>
<div className='pt-3'>
{/* TODO (achadaga): again would be nice to have an updated spinner */}
{status === DataStatus.LOADING && <Spinner />}
{status === DataStatus.NOT_FOUND && (
@@ -208,7 +208,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
Grade Distribution for {course.department} {course.number}
</Text>
<select
className='border border rounded-[4px] border-solid px-[12px] py-[8px]'
className='border border rounded border-solid px-3 py-2'
onChange={handleSelectSemester}
>
{Object.keys(distributions)

View File

@@ -7,6 +7,7 @@ import { Chip, flagMap } from '@views/components/common/Chip';
import Divider from '@views/components/common/Divider';
import Link from '@views/components/common/Link';
import Text from '@views/components/common/Text/Text';
import { useCalendar } from '@views/contexts/CalendarContext';
import React from 'react';
import Add from '~icons/material-symbols/add';
@@ -15,6 +16,7 @@ import CloseIcon from '~icons/material-symbols/close';
import Copy from '~icons/material-symbols/content-copy';
import Description from '~icons/material-symbols/description';
import Mood from '~icons/material-symbols/mood';
import OpenNewIcon from '~icons/material-symbols/open-in-new';
import Remove from '~icons/material-symbols/remove';
import Reviews from '~icons/material-symbols/reviews';
@@ -50,6 +52,7 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course;
const courseAdded = activeSchedule.courses.some(ourCourse => ourCourse.uniqueId === uniqueId);
const formattedUniqueId = uniqueId.toString().padStart(5, '0');
const isInCalendar = useCalendar();
const getInstructorFullName = (instructor: Instructor) => {
const { firstName = '', lastName = '' } = instructor;
@@ -85,9 +88,13 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
};
const handleOpenPastSyllabi = async () => {
// not specific to professor
const url = `https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/?year=&semester=&department=${department}&course_number=${courseNumber}&course_title=${courseName}&unique=&instructor_first=&instructor_last=&course_type=In+Residence&search=Search`;
for (const instructor of instructors) {
let { firstName = '', lastName = '' } = instructor;
firstName = capitalizeString(firstName);
lastName = capitalizeString(lastName);
const url = `https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/?year=&semester=&department=${department}&course_number=${courseNumber}&course_title=&unique=&instructor_first=${firstName}&instructor_last=${lastName}&course_type=In+Residence&search=Search`;
openNewTab({ url });
}
};
const handleAddOrRemoveCourse = async () => {
@@ -100,7 +107,7 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
};
return (
<div className='w-full px-2 pb-3 pt-6 text-ut-black'>
<div className='w-full px-2 pb-3 pt-5 text-ut-black'>
<div className='flex flex-col'>
<div className='flex items-center gap-1'>
<Text variant='h1' className='truncate text-theme-black'>
@@ -175,8 +182,16 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
<Button
variant='filled'
color='ut-burntorange'
icon={CalendarMonth}
onClick={() => background.switchToCalendarTab({})}
icon={isInCalendar ? OpenNewIcon : CalendarMonth}
onClick={() => {
if (isInCalendar) {
openNewTab({
url: course.url,
});
} else {
background.switchToCalendarTab({});
}
}}
/>
<Divider size='1.75rem' orientation='vertical' />
<Button variant='outline' color='ut-blue' icon={Reviews} onClick={handleOpenRateMyProf}>

View File

@@ -5,8 +5,8 @@ import { createPortal } from 'react-dom';
import styles from './RecruitmentBanner.module.scss';
const DISCORD_URL = 'https://discord.gg/qjcvgyVJbT';
const GITHUB_URL = 'https://github.com/sghsri/UT-Registration-Plus';
const DISCORD_URL = 'https://discord.gg/7pQDBGdmb7';
const GITHUB_URL = 'https://github.com/Longhorn-Developers/UT-Registration-Plus';
const RECRUIT_FROM_DEPARTMENTS = ['C S', 'ECE', 'MIS', 'CSE', 'EE', 'ITD'];

View File

@@ -0,0 +1,11 @@
import { createContext, useContext } from 'react';
/**
* Context for the calendar.
*/
export const CalendarContext = createContext(false);
/**
* @returns The calendar context.
*/
export const useCalendar = () => useContext(CalendarContext);

View File

@@ -0,0 +1,32 @@
import { createContext, useContext } from 'react';
/**
* Close wrapper
*/
export type CloseWrapper<T> = (close: () => void) => T;
/**
* Information about a dialog.
*/
export interface DialogInfo {
title?: JSX.Element;
description?: JSX.Element;
className?: string;
buttons?: JSX.Element | CloseWrapper<JSX.Element>;
onClose?: () => void;
}
/**
* Function to show a dialog.
*/
export type ShowDialogFn = (info: DialogInfo | CloseWrapper<DialogInfo>) => void;
/**
* Context for the dialog provider.
*/
export const DialogContext = createContext<ShowDialogFn>(() => {});
/**
* @returns The dialog context for showing dialogs.
*/
export const useDialog = () => useContext(DialogContext);

View File

@@ -122,11 +122,27 @@ export class CourseCatalogScraper {
/**
* Gets how many credit hours the course is worth
* @param number the course number, CS 314H
* @param courseNumber the course number, CS 314H
* @return the number of credit hours the course is worth
*/
getCreditHours(number: string): number {
return Number(number.split('')[0]);
getCreditHours(courseNumber: string): number {
let creditHours = Number(courseNumber.split('')[0]);
const lastChar = courseNumber.slice(-1);
// eslint-disable-next-line default-case
switch (lastChar) {
case 'A':
case 'B':
creditHours /= 2;
break;
case 'X':
case 'Y':
case 'Z':
creditHours /= 3;
break;
}
return creditHours;
}
/**

View File

@@ -10,7 +10,7 @@ export default defineConfig({
rules: [
[
'btn-transition',
{ transition: 'color 180ms, border-color 150ms, background-color 150ms, box-shadow 0ms, transform 50ms' },
{ transition: 'color 180ms, border-color 150ms, background-color 150ms, box-shadow 50ms, transform 50ms' },
],
[
'ring-offset-0',