-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
2709484
commit df18491
Showing
5 changed files
with
257 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
src/stories/components/calendar/CourseCellColorPicker.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof CourseCellColorPicker>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof CourseCellColorPicker>; | ||
|
||
function CourseCellColorPickerWithState() { | ||
const [, setSelectedColor] = useState<ThemeColor | null>(null); | ||
const [isInvertColorsToggled, setIsInvertColorsToggled] = useState<boolean>(false); | ||
return ( | ||
<CourseCellColorPicker | ||
setSelectedColor={setSelectedColor} | ||
isInvertColorsToggled={isInvertColorsToggled} | ||
setIsInvertColorsToggled={setIsInvertColorsToggled} | ||
/> | ||
); | ||
} | ||
|
||
export const Default: Story = { | ||
render: CourseCellColorPickerWithState, | ||
}; |
38 changes: 38 additions & 0 deletions
38
src/views/components/calendar/CalendarCourseCellColorPicker/ColorPatch.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<button | ||
className='h-5.5 w-5.5 p-0 transition-all duration-200 hover:scale-110 btn' | ||
style={{ backgroundColor: color }} | ||
onClick={handleClick} | ||
> | ||
{isSelected && <CheckIcon className='h-5 w-5 color-white' />} | ||
</button> | ||
); | ||
} |
145 changes: 145 additions & 0 deletions
145
src/views/components/calendar/CalendarCourseCellColorPicker/CourseCellColorPicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string, string[]>( | ||
baseColors.map((baseColor: string) => [ | ||
theme.colors[baseColor][BaseColorNum], | ||
Array.from({ length: 6 }, (_, index) => theme.colors[baseColor][StartingShadeIndex + ShadeIncrement * index]), | ||
]) | ||
); | ||
|
||
const hexCodeToBaseColor = new Map<string, string>( | ||
Array.from(colorPatchColors.entries()).flatMap(([baseColor, shades]) => shades.map(shade => [shade, baseColor])) | ||
); | ||
|
||
/** | ||
* Props for the CourseCellColorPicker component. | ||
*/ | ||
export interface CourseCellColorPickerProps { | ||
setSelectedColor: React.Dispatch<React.SetStateAction<string | null>>; | ||
isInvertColorsToggled: boolean; | ||
setIsInvertColorsToggled: React.Dispatch<React.SetStateAction<boolean>>; | ||
} | ||
|
||
/** | ||
* @param {CourseCellColorPickerProps} props - the props for the component | ||
* @param {React.Dispatch<React.SetStateAction<string | null>>} 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<React.SetStateAction<boolean>>} 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<string | null>(null); | ||
* const [isInvertColorsToggled, setIsInvertColorsToggled] = useState<boolean>(false); | ||
* return ( | ||
* <CourseCellColorPicker | ||
* setSelectedColor={setSelectedColor} | ||
* isInvertColorsToggled={isInvertColorsToggled} | ||
* setIsInvertColorsToggled={setIsInvertColorsToggled} | ||
* /> | ||
* ); | ||
* ``` | ||
* | ||
* @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<string>( | ||
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 ( | ||
<div className='inline-flex flex-col border border-1 border-ut-offwhite rounded-1 p-1.25'> | ||
<div className='grid grid-cols-6 gap-1'> | ||
{Array.from(colorPatchColors.keys()).map(baseColor => ( | ||
<ColorPatch | ||
color={baseColor} | ||
isSelected={baseColor === selectedBaseColor} | ||
handleSetSelectedColor={handleSelectColorPatch} | ||
/> | ||
))} | ||
<div className='col-span-3 flex items-center justify-center overflow-hidden'> | ||
<HexColorEditor hexCode={hexCode} setHexCode={setHexCode} /> | ||
</div> | ||
<button | ||
className='h-5.5 w-5.5 bg-ut-black p-0 transition-all duration-200 hover:scale-110 btn' | ||
onClick={() => setIsInvertColorsToggled(prev => !prev)} | ||
> | ||
{isInvertColorsToggled ? ( | ||
<InvertColorsIcon className='h-3.5 w-3.5 color-white' /> | ||
) : ( | ||
<InvertColorsOffIcon className='h-3.5 w-3.5 color-white' /> | ||
)} | ||
</button> | ||
</div> | ||
{hexCodeToBaseColor.has(hexCodeWithHash) && ( | ||
<> | ||
<Divider orientation='horizontal' size='100%' className='my-1' /> | ||
<div className='grid grid-cols-6 gap-1'> | ||
{colorPatchColors | ||
.get(selectedBaseColor) | ||
?.map(shadeColor => ( | ||
<ColorPatch | ||
color={shadeColor} | ||
isSelected={shadeColor === hexCodeWithHash} | ||
handleSetSelectedColor={handleSelectColorPatch} | ||
/> | ||
))} | ||
</div> | ||
</> | ||
)} | ||
</div> | ||
); | ||
} |
46 changes: 46 additions & 0 deletions
46
src/views/components/calendar/CalendarCourseCellColorPicker/HexColorEditor.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<React.SetStateAction<string>>; | ||
} | ||
|
||
/** | ||
* 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<React.SetStateAction<string>>} 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 ( | ||
<> | ||
<div | ||
style={{ backgroundColor: previewColor }} | ||
className='h-5.5 w-5.25 flex items-center justify-center rounded-l-1' | ||
> | ||
<TagIcon className='h-4 w-4 text-color-white' /> | ||
</div> | ||
<div className='h-5.5 w-[53px] flex flex-1 items-center justify-center border-b border-r border-t rounded-br rounded-tr p-1.25'> | ||
<input | ||
type='text' | ||
maxLength={6} | ||
className='w-full border-none bg-transparent font-size-2.75 font-normal outline-none focus:outline-none' | ||
value={hexCode} | ||
onChange={e => setHexCode(e.target.value)} | ||
/> | ||
</div> | ||
</> | ||
); | ||
} |