feat: course color generation (#179)
* feat: course color generation * feat: add proper TS for hex colors * refactor: fix oklab and improve contrast ratios * fix: update HexColor type * refactor: update color switch point * refactor: color-related functions and types * fix: imports and TS issues * fix: imports and TS issues * chore: add no-restricted-syntax ForInStatement * chore(docs): add jsdoc --------- Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
This commit is contained in:
@@ -76,8 +76,8 @@ export default function Calendar(): JSX.Element {
|
||||
<CalendarFooter />
|
||||
</div>
|
||||
)}
|
||||
<div className='h-full min-w-3xl flex flex-grow flex-col overflow-y-auto' ref={calendarRef}>
|
||||
<div className='min-h-2xl flex-grow overflow-auto pl-2 pr-4 pt-2xl'>
|
||||
<div className='h-full min-w-4xl flex flex-grow flex-col overflow-y-auto' ref={calendarRef}>
|
||||
<div className='min-h-2xl flex-grow overflow-auto pl-2 pr-4 pt-6'>
|
||||
<CalendarGrid courseCells={courseCells} setCourse={setCourse} />
|
||||
</div>
|
||||
<CalendarBottomBar calendarRef={calendarRef} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { StatusType } from '@shared/types/Course';
|
||||
import { Status } from '@shared/types/Course';
|
||||
import type { CourseColors } from '@shared/util/colors';
|
||||
import type { CourseColors } from '@shared/types/ThemeColors';
|
||||
import { pickFontColor } from '@shared/util/colors';
|
||||
import Text from '@views/components/common/Text/Text';
|
||||
import clsx from 'clsx';
|
||||
@@ -51,7 +51,7 @@ export default function CalendarCourseCell({
|
||||
rightIcon = <CancelledIcon className='h-5 w-5' />;
|
||||
}
|
||||
|
||||
// whiteText based on secondaryColor
|
||||
// text-white or text-black based on secondaryColor
|
||||
const fontColor = pickFontColor(colors.primaryColor);
|
||||
|
||||
return (
|
||||
@@ -73,14 +73,19 @@ export default function CalendarCourseCell({
|
||||
>
|
||||
<Text
|
||||
variant='h1-course'
|
||||
as='p'
|
||||
className={clsx('leading-tight! truncate', {
|
||||
'-my-0.8': timeAndLocation,
|
||||
'-mt-0.8 -mb-0.2': timeAndLocation,
|
||||
'text-wrap': !timeAndLocation,
|
||||
})}
|
||||
>
|
||||
{courseDeptAndInstr}
|
||||
</Text>
|
||||
{timeAndLocation && <Text variant='h3-course'>{timeAndLocation}</Text>}
|
||||
{timeAndLocation && (
|
||||
<Text variant='h3-course' as='p'>
|
||||
{timeAndLocation}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{rightIcon && (
|
||||
<div
|
||||
|
||||
@@ -75,7 +75,7 @@ export const saveAsCal = async () => {
|
||||
console.log(icsDays);
|
||||
|
||||
// Assuming course has date started and ended, adapt as necessary
|
||||
const year = new Date().getFullYear(); // Example year, adapt accordingly
|
||||
// const year = new Date().getFullYear(); // Example year, adapt accordingly
|
||||
// Example event date, adapt startDate according to your needs
|
||||
const startDate = `20240101T${formattedStartTime}`;
|
||||
const endDate = `20240101T${formattedEndTime}`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ThemeColor } from '@shared/util/themeColors';
|
||||
import type { ThemeColor } from '@shared/types/ThemeColors';
|
||||
import { getThemeColorHexByName, getThemeColorRgbByName } from '@shared/util/themeColors';
|
||||
import Text from '@views/components/common/Text/Text';
|
||||
import clsx from 'clsx';
|
||||
|
||||
@@ -6,12 +6,15 @@ import React, { Fragment } from 'react';
|
||||
|
||||
import ExtensionRoot from '../ExtensionRoot/ExtensionRoot';
|
||||
|
||||
export interface _DialogProps {
|
||||
interface _DialogProps {
|
||||
className?: string;
|
||||
title?: JSX.Element;
|
||||
description?: JSX.Element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the Dialog component.
|
||||
*/
|
||||
export type DialogProps = _DialogProps & Omit<TransitionRootProps<typeof HDialog>, 'children'>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -69,21 +69,21 @@ function Item<T>(props: {
|
||||
* @example
|
||||
* <List draggableElements={elements} />
|
||||
*/
|
||||
function List<T>(props: ListProps<T>): JSX.Element {
|
||||
const [items, setItems] = useState(wrap(props.draggables, props.itemKey));
|
||||
function List<T>({ draggables, itemKey, children, onReordered, gap }: ListProps<T>): JSX.Element {
|
||||
const [items, setItems] = useState(wrap(draggables, itemKey));
|
||||
|
||||
const transformFunction = props.children;
|
||||
const transformFunction = children;
|
||||
|
||||
useEffect(() => {
|
||||
// check if the draggables content has *actually* changed
|
||||
if (
|
||||
props.draggables.length === items.length &&
|
||||
props.draggables.every((element, index) => props.itemKey(element) === items[index].id)
|
||||
draggables.length === items.length &&
|
||||
draggables.every((element, index) => itemKey(element) === items[index].id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setItems(wrap(props.draggables, props.itemKey));
|
||||
}, [props.draggables]);
|
||||
setItems(wrap(draggables, itemKey));
|
||||
}, [draggables, itemKey, items]);
|
||||
|
||||
const onDragEnd: OnDragEndResponder = useCallback(
|
||||
result => {
|
||||
@@ -94,9 +94,9 @@ function List<T>(props: ListProps<T>): JSX.Element {
|
||||
const reordered = reorder(items, result.source.index, result.destination.index);
|
||||
|
||||
setItems(reordered);
|
||||
props.onReordered(reordered.map(item => item.content));
|
||||
onReordered(reordered.map(item => item.content));
|
||||
},
|
||||
[items]
|
||||
[items, onReordered]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -131,12 +131,8 @@ function List<T>(props: ListProps<T>): JSX.Element {
|
||||
);
|
||||
}}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
style={{ marginBottom: `-${props.gap}px` }}
|
||||
>
|
||||
{provided => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} style={{ marginBottom: `-${gap}px` }}>
|
||||
{items.map((item, index) => (
|
||||
<Draggable key={item.id} draggableId={item.id.toString()} index={index}>
|
||||
{draggableProvided => (
|
||||
@@ -146,7 +142,7 @@ function List<T>(props: ListProps<T>): JSX.Element {
|
||||
style={{
|
||||
...draggableProvided.draggableProps.style,
|
||||
// if last item, don't add margin
|
||||
marginBottom: `${props.gap}px`,
|
||||
marginBottom: `${gap}px`,
|
||||
}}
|
||||
>
|
||||
{transformFunction(item.content, draggableProvided.dragHandleProps)}
|
||||
|
||||
@@ -2,6 +2,11 @@ import clsx from 'clsx';
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Renders the logo icon.
|
||||
* @param {SVGProps<SVGSVGElement>} props - The SVG props.
|
||||
* @returns {JSX.Element} The rendered logo icon.
|
||||
*/
|
||||
export function LogoIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
|
||||
return (
|
||||
<svg width='40' height='40' viewBox='0 0 40 40' fill='none' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
@@ -13,6 +18,12 @@ export function LogoIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the small logo.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.className - The class name for the logo container.
|
||||
* @returns {JSX.Element} The rendered small logo.
|
||||
*/
|
||||
export function SmallLogo({ className }: { className?: string }): JSX.Element {
|
||||
return (
|
||||
<div className={clsx('flex items-center gap-2', className)}>
|
||||
@@ -25,6 +36,12 @@ export function SmallLogo({ className }: { className?: string }): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the large logo.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.className - The class name for the logo container.
|
||||
* @returns {JSX.Element} The rendered large logo.
|
||||
*/
|
||||
export function LargeLogo({ className }: { className?: string }): JSX.Element {
|
||||
return (
|
||||
<div className={clsx('flex items-center gap-2', className)}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { background } from '@shared/messages';
|
||||
import type { Course } from '@shared/types/Course';
|
||||
import { Status } from '@shared/types/Course';
|
||||
import type { CourseColors } from '@shared/util/colors';
|
||||
import type { CourseColors } from '@shared/types/ThemeColors';
|
||||
import { pickFontColor } from '@shared/util/colors';
|
||||
import { StatusIcon } from '@shared/util/icons';
|
||||
import Text from '@views/components/common/Text/Text';
|
||||
@@ -31,7 +31,7 @@ export default function PopupCourseBlock({
|
||||
colors,
|
||||
dragHandleProps,
|
||||
}: PopupCourseBlockProps): JSX.Element {
|
||||
// whiteText based on secondaryColor
|
||||
// text-white or text-black based on secondaryColor
|
||||
const fontColor = pickFontColor(colors.primaryColor);
|
||||
const formattedUniqueId = course.uniqueId.toString().padStart(5, '0');
|
||||
|
||||
|
||||
@@ -28,15 +28,16 @@ export default function ScheduleListItem({ schedule, dragHandleProps, onClick }:
|
||||
const [editorValue, setEditorValue] = useState(schedule.name);
|
||||
|
||||
const editorRef = React.useRef<HTMLInputElement>(null);
|
||||
const { current: editor } = editorRef;
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current;
|
||||
|
||||
setEditorValue(schedule.name);
|
||||
|
||||
if (isEditing && editor) {
|
||||
editor.focus();
|
||||
editor.setSelectionRange(0, editor.value.length);
|
||||
}
|
||||
}, [isEditing, schedule.name, editor]);
|
||||
}, [isEditing, schedule.name, editorRef]);
|
||||
|
||||
const isActive = useMemo(() => activeSchedule.id === schedule.id, [activeSchedule, schedule]);
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import Description from './Description';
|
||||
import GradeDistribution from './GradeDistribution';
|
||||
import HeadingAndActions from './HeadingAndActions';
|
||||
|
||||
/**
|
||||
* Props for the CourseCatalogInjectedPopup component.
|
||||
*/
|
||||
export type CourseCatalogInjectedPopupProps = DialogProps & {
|
||||
course: Course;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Course } from '@shared/types/Course';
|
||||
import type { Distribution, LetterGrade } from '@shared/types/Distribution';
|
||||
import { extendedColors } from '@shared/util/themeColors';
|
||||
import { extendedColors } from '@shared/types/ThemeColors';
|
||||
import Spinner from '@views/components/common/Spinner/Spinner';
|
||||
import Text from '@views/components/common/Text/Text';
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { HexColor } from '@shared/types/Color';
|
||||
import type { Course, StatusType } from '@shared/types/Course';
|
||||
import type { CourseMeeting } from '@shared/types/CourseMeeting';
|
||||
import { colors } from '@shared/types/ThemeColors';
|
||||
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||
import type { CalendarCourseCellProps } from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell';
|
||||
|
||||
@@ -19,9 +21,9 @@ interface CalendarGridPoint {
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
interface componentProps {
|
||||
calendarCourseCellProps: CalendarCourseCellProps;
|
||||
}
|
||||
// interface componentProps {
|
||||
// calendarCourseCellProps: CalendarCourseCellProps;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Return type of useFlattenedCourseSchedule
|
||||
@@ -35,6 +37,9 @@ export interface CalendarGridCourse {
|
||||
totalColumns?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a flattened course schedule.
|
||||
*/
|
||||
export interface FlattenedCourseSchedule {
|
||||
courseCells: CalendarGridCourse[];
|
||||
activeSchedule?: UserSchedule;
|
||||
@@ -96,15 +101,16 @@ export function useFlattenedCourseSchedule(): FlattenedCourseSchedule {
|
||||
} satisfies FlattenedCourseSchedule;
|
||||
}
|
||||
|
||||
// Helper function to extract and format basic course information
|
||||
/**
|
||||
* Function to extract and format basic course information
|
||||
*/
|
||||
function extractCourseInfo(course: Course) {
|
||||
const {
|
||||
status,
|
||||
department,
|
||||
instructors,
|
||||
schedule: { meetings },
|
||||
} = course;
|
||||
const courseDeptAndInstr = `${department} ${instructors[0].lastName}`;
|
||||
const courseDeptAndInstr = `${course.department} ${course.number} – ${course.instructors[0].lastName}`;
|
||||
|
||||
return { status, courseDeptAndInstr, meetings, course };
|
||||
}
|
||||
|
||||
@@ -131,8 +137,8 @@ function processAsyncCourses({
|
||||
courseDeptAndInstr,
|
||||
status,
|
||||
colors: {
|
||||
primaryColor: 'ut-gray',
|
||||
secondaryColor: 'ut-gray',
|
||||
primaryColor: colors.ut.gray as HexColor,
|
||||
secondaryColor: colors.ut.gray as HexColor,
|
||||
},
|
||||
},
|
||||
course,
|
||||
@@ -149,8 +155,9 @@ function processInPersonMeetings(meeting: CourseMeeting, { courseDeptAndInstr, s
|
||||
const normalizingTimeFactor = 720;
|
||||
const time = meeting.getTimeString({ separator: '-', capitalize: true });
|
||||
const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`;
|
||||
let normalizedStartTime = startTime >= midnightIndex ? startTime - normalizingTimeFactor : startTime;
|
||||
let normalizedEndTime = endTime >= midnightIndex ? endTime - normalizingTimeFactor : endTime;
|
||||
const normalizedStartTime = startTime >= midnightIndex ? startTime - normalizingTimeFactor : startTime;
|
||||
const normalizedEndTime = endTime >= midnightIndex ? endTime - normalizingTimeFactor : endTime;
|
||||
|
||||
return days.map(day => ({
|
||||
calendarGridPoint: {
|
||||
dayIndex: dayToNumber[day],
|
||||
@@ -162,8 +169,8 @@ function processInPersonMeetings(meeting: CourseMeeting, { courseDeptAndInstr, s
|
||||
timeAndLocation,
|
||||
status,
|
||||
colors: {
|
||||
primaryColor: 'ut-orange',
|
||||
secondaryColor: 'ut-orange',
|
||||
primaryColor: colors.ut.orange as HexColor,
|
||||
secondaryColor: colors.ut.orange as HexColor,
|
||||
},
|
||||
},
|
||||
course,
|
||||
|
||||
@@ -70,10 +70,19 @@ export default function useSchedules(): [active: UserSchedule, schedules: UserSc
|
||||
return [activeSchedule, schedules];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active schedule.
|
||||
* @returns The active schedule.
|
||||
*/
|
||||
export function getActiveSchedule(): UserSchedule {
|
||||
return schedulesCache[activeIndexCache] ?? errorSchedule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the old schedule with the new schedule.
|
||||
* @param oldSchedule - The old schedule to be replaced.
|
||||
* @param newSchedule - The new schedule to replace the old schedule.
|
||||
*/
|
||||
export async function replaceSchedule(oldSchedule: UserSchedule, newSchedule: UserSchedule) {
|
||||
const schedules = await UserScheduleStore.get('schedules');
|
||||
let oldIndex = schedules.findIndex(s => s.id === oldSchedule.id);
|
||||
|
||||
@@ -5,7 +5,6 @@ import CourseCatalogMain from './components/CourseCatalogMain';
|
||||
import PopupMain from './components/PopupMain';
|
||||
import getSiteSupport, { SiteSupport } from './lib/getSiteSupport';
|
||||
import render from './lib/react';
|
||||
import colors from './styles/colors.module.scss';
|
||||
|
||||
const support = getSiteSupport(window.location.href);
|
||||
console.log('support:', support);
|
||||
|
||||
Reference in New Issue
Block a user