Skip to content

Commit

Permalink
feat: inline course colour picker
Browse files Browse the repository at this point in the history
* Fixes #108
  • Loading branch information
pl4nty committed Jan 26, 2022
1 parent d3bb50d commit 6078826
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 10 deletions.
12 changes: 9 additions & 3 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ let App = () => {
hiddenOccurrences.filter(([module]) => selectedModules.map(({ id }) => id).includes(module))
)

// Map of event IDs to colours that override the event's default colour
const [overrideColours, setOverrideColours] = useState(JSON.parse(localStorage.getItem('overrideColours')) || {})
useEffect(() => localStorage.setItem('overrideColours', JSON.stringify(overrideColours)), [overrideColours])

// Update query string parameters and calendar events whenever anything changes
useEffect(() => {
const api = calendar.current.getApi()
Expand Down Expand Up @@ -160,14 +164,16 @@ let App = () => {
}
}

const [h, s, l] = stringToColor(id)

// Add currently visible events to the calendar
api.addEventSource({
id,
color: stringToColor(id),
color: overrideColours[id] || `hsl(${h}, ${s}%, ${l}%)`,
events: parseEvents(eventsForModule, year, session, id)
})
})
}, [timetableData, year, session, selectedModules, calendar, timeZone, m, modules, specifiedOccurrences, hiddenOccurrences])
}, [overrideColours, timetableData, year, session, selectedModules, calendar, timeZone, m, modules, specifiedOccurrences, hiddenOccurrences])

// Remove specified events for modules that have been removed
useEffect(() => {
Expand Down Expand Up @@ -235,7 +241,7 @@ let App = () => {
return <Container fluid>
<h2 className="mt-2">ANU Timetable</h2>

<Toolbar API={API} ref={calendar} state={state} />
<Toolbar API={API} ref={calendar} state={state} {...{overrideColours, setOverrideColours}} />

<Calendar ref={calendar} state={state} />

Expand Down
33 changes: 27 additions & 6 deletions src/Toolbar.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { forwardRef } from 'react'
import { forwardRef, useState } from 'react'

import { InputGroup, Dropdown, DropdownButton } from 'react-bootstrap'
import { InputGroup, Dropdown, DropdownButton, Form } from 'react-bootstrap'
import { Token, Typeahead } from 'react-bootstrap-typeahead'
import TimezoneSelect from 'react-timezone-select'

import Export from './Export'
import { hslToHex, stringToColor } from './utils'

export default forwardRef(({ API, state: {
export default forwardRef(({ API, overrideColours, setOverrideColours, state: {
timeZone, year, session, sessions, timetableData, modules, selectedModules, darkMode,
setTimeZone, setYear, setSession, setSessions, setTimetableData, setModules, setSelectedModules,
} }, calendar) => {
Expand All @@ -23,6 +24,8 @@ export default forwardRef(({ API, state: {
setSession(e)
}

const [isColourPending, setIsColourPending] = useState(false)

return <>
<InputGroup className="mb-2">
<DropdownButton
Expand Down Expand Up @@ -63,13 +66,31 @@ export default forwardRef(({ API, state: {
onRemove={props.onRemove}
option={option}
tabIndex={props.tabIndex}
href={`http://programsandcourses.anu.edu.au/${year}/course/${option.id}`}
>
<a
<Form inline><Form.Label className='m-0 mr-1'><a
href={`http://programsandcourses.anu.edu.au/${year}/course/${option.id}`}
target={"_blank"}
rel={"noreferrer"}
>{option.id}</a> {/** use id (eg COMP1130) instead of label to save space */}
>{option.id}</a></Form.Label> {/** use id (eg COMP1130) instead of label to save space */}
<Form.Control
id={option.id}
custom
type='color'
defaultValue={overrideColours[option.id] || hslToHex(...stringToColor(option.id))}
title="Customise course colour"
size='sm'
className='rounded-circle border-0 p-0 w-auto h-auto'
// triggers expensive rerender, so rate-limit with a timeout
onChange={e => {
if (!isColourPending) {
setTimeout(() => {
setOverrideColours({...overrideColours, [option.id]: e.target.value })
setIsColourPending(false)
}, 1000)
setIsColourPending(true)
}
}}
/></Form>
</Token>}
/>

Expand Down
15 changes: 15 additions & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,18 @@ body[data-theme="dark"] {
background-color: #090a0a;
}
}

// colour picker
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-color-swatch {
padding: 0.4rem;
border-radius: 50%;
}

// prevent colour change (click) on search bar selected item
.rbt-token-active {
background-color: #e7f4ff;
color: #007bff;
}
15 changes: 14 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,24 @@ export const stringToColor = str => {
if (!(str in stringColorMap)) {
const hue = hues[Object.keys(stringColorMap).length % hues.length]
const offset = 60; // configurable
stringColorMap[str] = `hsl(${(hue + offset) % 360}, 100%, 30%)`;
stringColorMap[str] = [(hue + offset) % 360, 100, 30]; // static saturation and lightness
}
return stringColorMap[str]
}

// https://stackoverflow.com/a/44134328
// TODO: refactor stringToColor so this isn't necessary
export const hslToHex = (h, s, l) => {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
};
return `#${f(0)}${f(8)}${f(4)}`;
}

// hardcode to semester 1 or 2 as users usually want them
// allows app to function even if /sessions endpoint is down
export const getInitialState = () => {
Expand Down

0 comments on commit 6078826

Please sign in to comment.