Skip to content

Commit

Permalink
feat: calendar-course-cell-color-picker (#157)
Browse files Browse the repository at this point in the history
* 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
abhinavchadaga authored Mar 16, 2024
1 parent 2709484 commit df18491
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 1 deletion.
1 change: 0 additions & 1 deletion src/shared/util/themeColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
28 changes: 28 additions & 0 deletions src/stories/components/calendar/CourseCellColorPicker.stories.tsx
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,
};
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>
);
}
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>
);
}
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>
</>
);
}

0 comments on commit df18491

Please sign in to comment.