Skip to content

Commit

Permalink
Add form to color picker (#32)
Browse files Browse the repository at this point in the history
Also update button behaviour so that only manual section assignment or
color assignment is toggled at once. Renaming and color assignment are
deliberately left simultaneous in the case of a custom activity.

---------

Co-authored-by: Felix Prasanna <[email protected]>
  • Loading branch information
fprasx and fprasx authored Mar 8, 2023
1 parent 2180b58 commit 3bad537
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 28 deletions.
100 changes: 72 additions & 28 deletions src/components/ActivityButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,30 @@ import {
Select,
Text,
} from "@chakra-ui/react";
import { ComponentProps, FormEvent, useState } from "react";
import { ComponentProps, FormEvent, useEffect, useRef, useState } from "react";
import { HexColorPicker } from "react-colorful";

import { Activity, NonClass, Timeslot } from "../lib/activity";
import { Class, LockOption, SectionLockOption, Sections } from "../lib/class";
import { textColor, canonicalizeColor } from "../lib/colors";
import { WEEKDAY_STRINGS, TIMESLOT_STRINGS, Slot } from "../lib/dates";
import { State } from "../lib/state";

import { ColorButton } from "./SelectedActivities";

/**
* A button that toggles the active value, and is outlined if active, solid
* if not.
*/
function ToggleButton(
props: ComponentProps<"button"> & {
active: boolean;
setActive: (value: boolean) => void;
handleClick: () => void;
}
) {
const { children, active, setActive, ...otherProps } = props;
const { children, active, handleClick, ...otherProps } = props;
return (
<Button
{...otherProps}
onClick={() => setActive(!active)}
onClick={handleClick}
variant={active ? "outline" : "solid"}
>
{children}
Expand All @@ -61,10 +60,7 @@ function ClassManualOption(props: {
})();

return (
<Radio
isChecked={isChecked}
onChange={() => state.lockSection(secs, sec)}
>
<Radio isChecked={isChecked} onChange={() => state.lockSection(secs, sec)}>
{label}
</Radio>
);
Expand All @@ -82,12 +78,7 @@ function ClassManualSections(props: { cls: Class; state: State }) {
<FormLabel>{secs.name}</FormLabel>
<Flex direction="column">
{options.map((sec, i) => (
<ClassManualOption
key={i}
secs={secs}
sec={sec}
state={state}
/>
<ClassManualOption key={i} secs={secs} sec={sec} state={state} />
))}
</Flex>
</FormControl>
Expand All @@ -114,17 +105,57 @@ function ActivityColor(props: {
};
const onCancel = onHide;
const onConfirm = () => {
state.setBackgroundColor(activity, color);
// Try to set new color to input but fall back to old color
const canon = canonicalizeColor(input);
state.setBackgroundColor(activity, canon ? canon : color);
onHide();
};

const [input, setInput] = useState<string>("");
const inputElement = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (inputElement.current) {
inputElement.current.focus();
}
}, []);

const isError = input !== "" ? canonicalizeColor(input) === undefined : false;

const handleSubmit = (e: FormEvent) => {
e.preventDefault();
const canon = canonicalizeColor(input);
if (canon) {
setColor(canon);
setInput("");
}
}

return (
<Flex gap={2}>
<HexColorPicker color={color} onChange={setColor} />
<Flex direction="column" gap={2}>
<ColorButton color={color} style={{ cursor: "default" }}>
{activity.buttonName}
</ColorButton>
<form
onSubmit={handleSubmit}
onBlur={handleSubmit}
>
<Input
// Wide enough to hold everything, but keeps buttons below small
width={"12ch"}
ref={inputElement}
style={{ backgroundColor: color }}
placeholder={color}
_placeholder={{
color: textColor(color),
opacity: 0.6,
}}
focusBorderColor={isError ? "crimson" : "green.300"}
color={textColor(color)}
value={input}
onChange={(e) => {
setInput(e.target.value);
}}
/>
</form>
<Button onClick={onReset}>Reset</Button>
<Button onClick={onCancel}>Cancel</Button>
<Button onClick={onConfirm}>Confirm</Button>
Expand All @@ -136,7 +167,6 @@ function ActivityColor(props: {
/** Buttons in class description to add/remove class, and lock sections. */
export function ClassButtons(props: { cls: Class; state: State }) {
const { cls, state } = props;

const [showManual, setShowManual] = useState(false);
const [showColors, setShowColors] = useState(false);
const isSelected = state.isSelectedActivity(cls);
Expand All @@ -148,12 +178,24 @@ export function ClassButtons(props: { cls: Class; state: State }) {
{isSelected ? "Remove class" : "Add class"}
</Button>
{isSelected && (
<ToggleButton active={showManual} setActive={setShowManual}>
<ToggleButton
active={showManual}
handleClick={() => {
setShowManual(!showManual);
setShowColors(false); // untoggle colors
}}
>
Edit sections
</ToggleButton>
)}
{isSelected && (
<ToggleButton active={showColors} setActive={setShowColors}>
<ToggleButton
active={showColors}
handleClick={() => {
setShowColors(!showColors);
setShowManual(false); // untoggle manual section assignment
}}
>
Edit color
</ToggleButton>
)}
Expand Down Expand Up @@ -238,10 +280,7 @@ function NonClassAddTime(props: { activity: NonClass; state: State }) {
/**
* Buttons in non-class description to rename it, or add/edit/remove timeslots.
*/
export function NonClassButtons(props: {
activity: NonClass;
state: State;
}) {
export function NonClassButtons(props: { activity: NonClass; state: State }) {
const { activity, state } = props;

const isSelected = state.isSelectedActivity(activity);
Expand Down Expand Up @@ -287,7 +326,12 @@ export function NonClassButtons(props: {
</Button>
<Button onClick={onRename}>Rename activity</Button>
{isSelected && (
<ToggleButton active={showColors} setActive={setShowColors}>
<ToggleButton
active={showColors}
handleClick={() => {
setShowColors(!showColors);
}}
>
Edit color
</ToggleButton>
)}
Expand Down
21 changes: 21 additions & 0 deletions src/lib/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,26 @@ export function textColor(color: string): string {
return brightness > 128 ? "#000000" : "#ffffff";
}

/** Return a standard #AABBCC representation from an input color */
export function canonicalizeColor(code: string): string | undefined {
code = code.trim();
let fiveSix = code.match(/^#?[0-9a-f]{5,6}$/gi);
if (fiveSix) {
return code.startsWith("#") ? code : `#${code}`;
}
let triplet = code.match(/^#?[0-9a-f]{3}$/gi);
if (triplet) {
const expanded =
code.slice(-3, -2) +
code.slice(-3, -2) +
code.slice(-2, -1) +
code.slice(-2, -1) +
code.slice(-1) +
code.slice(-1);
return code.startsWith("#") ? expanded : `#${expanded}`;
}
return undefined;
}

/** The Google calendar background color. */
export const CALENDAR_COLOR = "#DB5E45";

0 comments on commit 3bad537

Please sign in to comment.