From df1849180de2c793c10e3a49d2c862d236456ec2 Mon Sep 17 00:00:00 2001 From: Abhinav Chadaga Date: Sat, 16 Mar 2024 16:08:28 -0500 Subject: [PATCH] feat: calendar-course-cell-color-picker (#157) * feat: calendar-course-cell-color-picker done?? * fix: ensure hex code is lowercase * fix: make hex codes lower case * chore: convert px to rem in ColorPatch.tsx * fix: add functionality to the invert colors button * fix: some more lowercase stuff * fix: remove hardcoded color patch hex codes, remove hardcoded pixel values * chore: remove React.FC * chore: modify docs * fix: remove duplicate style * fix: used name over size specified classes * fix: grid over flex, elie feedback * refactor: use color strings instead of indices * refactor: remove console.log statements --- src/shared/util/themeColors.ts | 1 - .../CourseCellColorPicker.stories.tsx | 28 ++++ .../ColorPatch.tsx | 38 +++++ .../CourseCellColorPicker.tsx | 145 ++++++++++++++++++ .../HexColorEditor.tsx | 46 ++++++ 5 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 src/stories/components/calendar/CourseCellColorPicker.stories.tsx create mode 100644 src/views/components/calendar/CalendarCourseCellColorPicker/ColorPatch.tsx create mode 100644 src/views/components/calendar/CalendarCourseCellColorPicker/CourseCellColorPicker.tsx create mode 100644 src/views/components/calendar/CalendarCourseCellColorPicker/HexColorEditor.tsx diff --git a/src/shared/util/themeColors.ts b/src/shared/util/themeColors.ts index 67c08acc6..fe02968d9 100644 --- a/src/shared/util/themeColors.ts +++ b/src/shared/util/themeColors.ts @@ -12,7 +12,6 @@ export const colors = { offwhite: '#D6D2C4', concrete: '#95A5A6', red: '#B91C1C', // Not sure if this should be here, but it's used for remove course, and add course is ut-green - white: '#FFFFFF', }, theme: { red: '#AF2E2D', diff --git a/src/stories/components/calendar/CourseCellColorPicker.stories.tsx b/src/stories/components/calendar/CourseCellColorPicker.stories.tsx new file mode 100644 index 000000000..11e9129fa --- /dev/null +++ b/src/stories/components/calendar/CourseCellColorPicker.stories.tsx @@ -0,0 +1,28 @@ +import type { ThemeColor } from '@shared/util/themeColors'; +import type { Meta, StoryObj } from '@storybook/react'; +import CourseCellColorPicker from '@views/components/calendar/CalendarCourseCellColorPicker/CourseCellColorPicker'; +import React, { useState } from 'react'; + +const meta = { + title: 'Components/Calendar/CourseCellColorPicker', + component: CourseCellColorPicker, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +function CourseCellColorPickerWithState() { + const [, setSelectedColor] = useState(null); + const [isInvertColorsToggled, setIsInvertColorsToggled] = useState(false); + return ( + + ); +} + +export const Default: Story = { + render: CourseCellColorPickerWithState, +}; diff --git a/src/views/components/calendar/CalendarCourseCellColorPicker/ColorPatch.tsx b/src/views/components/calendar/CalendarCourseCellColorPicker/ColorPatch.tsx new file mode 100644 index 000000000..692f3077e --- /dev/null +++ b/src/views/components/calendar/CalendarCourseCellColorPicker/ColorPatch.tsx @@ -0,0 +1,38 @@ +import { getThemeColorHexByName } from '@shared/util/themeColors'; +import React from 'react'; + +import CheckIcon from '~icons/material-symbols/check'; + +/** + * Props for the ColorPatch component + */ +interface ColorPatchProps { + color: string; + isSelected: boolean; + handleSetSelectedColor: (color: string) => void; +} + +/** + * Renders a color patch square used in the CalendarCourseCellColorPicker component. + * + * @param {Object} props - The component props. + * @param {string} props.color - The color value (as a hex string with a hash prefix) to display in the patch. + * @param {boolean} props.isSelected - Indicates whether the patch is selected. + * @param {Function} props.handleSetSelectedColor - Function from parent component to control selection state of a patch. + * color is a hex string with a hash prefix. + * @returns {JSX.Element} The rendered color patch button. + */ +export default function ColorPatch({ color, isSelected, handleSetSelectedColor }: ColorPatchProps): JSX.Element { + const handleClick = () => { + handleSetSelectedColor(isSelected ? getThemeColorHexByName('ut-gray') : color); + }; + return ( + + ); +} diff --git a/src/views/components/calendar/CalendarCourseCellColorPicker/CourseCellColorPicker.tsx b/src/views/components/calendar/CalendarCourseCellColorPicker/CourseCellColorPicker.tsx new file mode 100644 index 000000000..bb3844fc2 --- /dev/null +++ b/src/views/components/calendar/CalendarCourseCellColorPicker/CourseCellColorPicker.tsx @@ -0,0 +1,145 @@ +import { getThemeColorHexByName } from '@shared/util/themeColors'; +import Divider from '@views/components/common/Divider/Divider'; +import React from 'react'; +import { theme } from 'unocss/preset-mini'; + +import InvertColorsIcon from '~icons/material-symbols/invert-colors'; +import InvertColorsOffIcon from '~icons/material-symbols/invert-colors-off'; + +import ColorPatch from './ColorPatch'; +import HexColorEditor from './HexColorEditor'; + +const baseColors = [ + 'slate', + 'gray', + 'stone', + 'red', + 'orange', + 'amber', + 'yellow', + 'lime', + 'green', + 'emerald', + 'teal', + 'cyan', + 'sky', + 'blue', + 'indigo', + 'violet', + 'purple', + 'fuchsia', + 'pink', + 'rose', +]; + +const BaseColorNum = 500; +const StartingShadeIndex = 200; +const ShadeIncrement = 100; + +const colorPatchColors = new Map( + baseColors.map((baseColor: string) => [ + theme.colors[baseColor][BaseColorNum], + Array.from({ length: 6 }, (_, index) => theme.colors[baseColor][StartingShadeIndex + ShadeIncrement * index]), + ]) +); + +const hexCodeToBaseColor = new Map( + Array.from(colorPatchColors.entries()).flatMap(([baseColor, shades]) => shades.map(shade => [shade, baseColor])) +); + +/** + * Props for the CourseCellColorPicker component. + */ +export interface CourseCellColorPickerProps { + setSelectedColor: React.Dispatch>; + isInvertColorsToggled: boolean; + setIsInvertColorsToggled: React.Dispatch>; +} + +/** + * @param {CourseCellColorPickerProps} props - the props for the component + * @param {React.Dispatch>} props.setSelectedColor - set state function passed down from the parent component + * @param {boolean} props.isInvertColorsToggled - boolean state passed down from the parent component that indicates whether the color picker is in invert colors mode + * @param {React.Dispatch>} props.setIsInvertColorsToggled - set state function passed down from the parent component to set invert colors mode + * that will be called when a color is selected. The user can set any valid hex color they want. + * + * @example + * ``` + * const [selectedColor, setSelectedColor] = useState(null); + * const [isInvertColorsToggled, setIsInvertColorsToggled] = useState(false); + * return ( + * + * ); + * ``` + * + * @returns {JSX.Element} - the color picker component that displays a color palette with a list of color patches. + * This component is available when a user hovers over a course cell in their calendar to + * color for the course cell. The user can set any valid hex color they want. + */ +export default function CourseCellColorPicker({ + setSelectedColor: setFinalColor, + isInvertColorsToggled, + setIsInvertColorsToggled, +}: CourseCellColorPickerProps): JSX.Element { + // hexCode mirrors contents of HexColorEditor which has no hash prefix + const [hexCode, setHexCode] = React.useState( + getThemeColorHexByName('ut-gray').slice(1).toLocaleLowerCase() + ); + const hexCodeWithHash = `#${hexCode}`; + const selectedBaseColor = hexCodeToBaseColor.get(hexCodeWithHash); + + const handleSelectColorPatch = (baseColor: string) => { + setHexCode(baseColor.slice(1).toLocaleLowerCase()); + }; + + React.useEffect(() => { + setFinalColor(hexCodeWithHash); + }, [hexCodeWithHash, setFinalColor]); + + return ( +
+
+ {Array.from(colorPatchColors.keys()).map(baseColor => ( + + ))} +
+ +
+ +
+ {hexCodeToBaseColor.has(hexCodeWithHash) && ( + <> + +
+ {colorPatchColors + .get(selectedBaseColor) + ?.map(shadeColor => ( + + ))} +
+ + )} +
+ ); +} diff --git a/src/views/components/calendar/CalendarCourseCellColorPicker/HexColorEditor.tsx b/src/views/components/calendar/CalendarCourseCellColorPicker/HexColorEditor.tsx new file mode 100644 index 000000000..a9f98d4d2 --- /dev/null +++ b/src/views/components/calendar/CalendarCourseCellColorPicker/HexColorEditor.tsx @@ -0,0 +1,46 @@ +import { getThemeColorHexByName } from '@shared/util/themeColors'; +import React from 'react'; + +import TagIcon from '~icons/material-symbols/tag'; + +/** + * Props for the HexColorEditor component + */ +export interface HexColorEditorProps { + hexCode: string; + setHexCode: React.Dispatch>; +} + +/** + * Utility component to allow the user to enter a valid hex color code + * + * @param {HexColorEditorProps} props - the props for the component + * @param {string} props.hexCode - the current hex color code displayed in this component. Note that this code does not + * include the leading '#' character since it is already included in the component. Passed down from the parent component. + * @param {React.Dispatch>} props.setHexCode - set state fn to control the hex color code from parent + * @returns {JSX.Element} - the hex color editor component + */ +export default function HexColorEditor({ hexCode, setHexCode }: HexColorEditorProps): JSX.Element { + const baseColor = React.useMemo(() => getThemeColorHexByName('ut-gray'), []); + const previewColor = hexCode.length === 6 ? `#${hexCode}` : baseColor; + + return ( + <> +
+ +
+
+ setHexCode(e.target.value)} + /> +
+ + ); +}