Skip to content

Commit

Permalink
Add "Remember selection" checkbox (#146)
Browse files Browse the repository at this point in the history
* Add "Remember selection" checkbox

* Make (de)select all into a button link

---------

Co-authored-by: Jean-Philippe Green <[email protected]>
  • Loading branch information
PooSham and Jean-Philippe Green authored Nov 6, 2024
1 parent eeeddcf commit c328661
Show file tree
Hide file tree
Showing 23 changed files with 650 additions and 640 deletions.
929 changes: 479 additions & 450 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"autofix": "eslint . --fix && stylelint \"**/*.{css,scss}\" --fix && prettier . --write",
"build": "turbo run build",
"test": "npm run test -w @cwfc/app",
"test-update": "npm run test -w @cwfc/app -- -u",
"watch": "turbo run watch"
},
"devDependencies": {
Expand Down
17 changes: 12 additions & 5 deletions packages/app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import appStyle from './App.module.scss';
import { EventSeriesModal } from './EventSeriesModal';
import { Footer } from './Footer';
import { Header } from './Header';
import { Calendar } from './calendar/Calendar';
import { EventSelection } from './event-selection/EventSelection';
import { stateContext } from './state';
import { useEffect, useState } from 'react';
import { WCJ } from './types';
import { canStoreSelection } from './services/cookies';

type Props = WCJ.Context & {
parent: HTMLElement;
Expand All @@ -16,9 +14,13 @@ type Props = WCJ.Context & {

export function App(props: Props) {
const [eventModal, setEventModal] = useState<string>();
const [rememberSelection, setRememberSelection] = useState<boolean>(
props.rememberSelection,
);
const [checkedEvents, setCheckedEvents] = useCheckedEvents(
props.events.map((x) => x.id),
props.selectedEventIds,
rememberSelection,
);

return (
Expand All @@ -28,15 +30,16 @@ export function App(props: Props) {
events: props.events,
checkedEvents,
eventModal,
rememberSelection,
}}
>
<Header />
<div className={appStyle.contentWrapper}>
<div className={appStyle.eventSelection} data-testid="event-selection">
<EventSelection
isLoading={props.isLoading}
setCheckedEvents={setCheckedEvents}
setEventModal={setEventModal}
setRememberSelection={setRememberSelection}
/>
</div>
<div className={appStyle.calendar} data-testid="calendar">
Expand All @@ -62,6 +65,7 @@ export function App(props: Props) {
export function useCheckedEvents(
allEventIds: string[],
initialSelection: string[],
rememberSelection: boolean,
) {
// This is a hook to store the user selection events into localstorage to keep it over multiple sessions.
// Since new events can be added from one session to another, and we want the user to be able to see the new event,
Expand All @@ -72,13 +76,16 @@ export function useCheckedEvents(

// When checkedEvents is changed, put it in localStorage
useEffect(() => {
if (canStoreSelection()) {
if (rememberSelection) {
const uncheckedEvents = allEventIds.filter(
(eventId) => !checkedEvents.includes(eventId),
);
localStorage.setItem('uncheckedEvents', JSON.stringify(uncheckedEvents));
localStorage.setItem('rememberSelection', 'true');
} else {
localStorage.setItem('rememberSelection', 'false');
}
}, [allEventIds, checkedEvents]);
}, [allEventIds, checkedEvents, rememberSelection]);

return [checkedEvents, setCheckedEvents] as const;
}
4 changes: 3 additions & 1 deletion packages/app/src/AppInit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,16 @@ function AppContent(props: Props & { rootRef: HTMLElement | null }) {
} else if (!data) {
return <h3>Didn&apos;t get any data</h3>;
} else if (props.rootRef) {
const { events, categories, selectedEventIds } = initContext(data);
const { events, categories, selectedEventIds, rememberSelection } =
initContext(data);
return (
<App
events={events}
categories={categories}
selectedEventIds={selectedEventIds}
parent={props.rootRef}
isLoading={isValidating}
rememberSelection={rememberSelection}
/>
);
}
Expand Down
14 changes: 0 additions & 14 deletions packages/app/src/Header.module.scss

This file was deleted.

45 changes: 0 additions & 45 deletions packages/app/src/Header.tsx

This file was deleted.

10 changes: 7 additions & 3 deletions packages/app/src/event-selection/EventGroup.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
.panel {
overflow: hidden;
background: var(--cw-color-light);
max-height: 0;

height: 0;
max-height: 100%;
transition: height 0.25s ease;
&.expanded {
max-height: 100%;
height: auto;

// For browsers that support calc-size, the next line will be used for height. This helps the transition work
height: calc-size(auto, size);
}
}
31 changes: 30 additions & 1 deletion packages/app/src/event-selection/EventSelection.module.scss
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
.top {
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
align-items: center;
background-color: var(--cw-color-dark);
justify-content: space-between;
width: 100%;
min-height: 64px;
padding: 8px;
gap: 8px;
}

.actionButtons {
display: flex;
height: 64px;
flex-shrink: 0;
flex-direction: row;
align-items: center;
gap: 16px;
justify-content: center;
}

.rememberSelection {
display: flex;
align-items: center;
color: #fff;
background-color: var(--cw-color-dark);
text-wrap: wrap;
line-height: 0;
outline: none;
border: none;
border-radius: 0;
justify-content: center;
}

.rememberSelectionLabel {
padding: 4px;
}

.eventGroupList {
Expand All @@ -15,6 +43,7 @@
overflow: auto;
background-color: #fff;
border-right: 1px solid rgb(0 0 0);
scrollbar-width: thin;
}

.loader {
Expand Down
56 changes: 44 additions & 12 deletions packages/app/src/event-selection/EventSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ import { stateContext } from '../state';
import { EventGroup } from './EventGroup';
import style from './EventSelection.module.scss';
import { useCallback, useContext } from 'react';
import checkedImg from '../assets/checkbox-checked.svg';
import uncheckedImg from '../assets/checkbox-unchecked.svg';

type Props = {
isLoading: boolean;
setCheckedEvents: (events: string[]) => void;
setEventModal: (eventId: string) => void;
setRememberSelection: (value: boolean) => void;
};

export function EventSelection(props: Props) {
const { events, categories, checkedEvents } = useContext(stateContext);
const { events, categories, checkedEvents, rememberSelection } =
useContext(stateContext);

const setEventCheckboxState = useCallback(
(eventId: string, check: boolean) => {
Expand Down Expand Up @@ -49,21 +53,49 @@ export function EventSelection(props: Props) {

return (
<>
<div className={style.actionButtons}>
<Button
<div className={style.top}>
<button
className={style.rememberSelection}
onClick={() => {
props.setCheckedEvents(events.map((event) => event.id));
props.setRememberSelection(!rememberSelection);
}}
>
Select all
</Button>
<Button
onClick={() => {
props.setCheckedEvents([]);
onKeyUp={(e) => {
if (['Enter', 'Space'].includes(e.code)) {
e.stopPropagation();
e.preventDefault();
props.setRememberSelection(!rememberSelection);
}
}}
>
Deselect all
</Button>
<div
role="checkbox"
aria-checked={rememberSelection}
tabIndex={0}
className={style.checkbox}
>
{rememberSelection ? (
<img src={checkedImg} width={16} height={16} alt="☑" />
) : (
<img src={uncheckedImg} width={16} height={16} alt="☐" />
)}
</div>
<div className={style.rememberSelectionLabel}>Remember selection</div>
</button>
<div className={style.actionButtons}>
<Button
type="link"
size="sm"
onClick={() => {
if (checkedEvents.length === events.length) {
props.setCheckedEvents([]);
} else {
props.setCheckedEvents(events.map((event) => event.id));
}
}}
>
{checkedEvents.length === events.length ? 'Deselect' : 'Select'} all
</Button>
</div>
</div>
<div className={style.eventGroupList} role="list">
{categories.map((categoryId) => (
Expand Down
9 changes: 9 additions & 0 deletions packages/app/src/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:host {
min-height: calc(100vh - 128px);
font-size: 16px;

* {
font-family: Raleway, ui-sans-serif, system-ui;
box-sizing: border-box;
}
}
1 change: 1 addition & 0 deletions packages/app/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './index.scss';
import AppInit from './AppInit';
import { WpCwfc } from '@cwfc/shared';
import { StrictMode } from 'react';
Expand Down
22 changes: 15 additions & 7 deletions packages/app/src/services/cogwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,27 @@ export function initContext(cwEvents: MaybeArray<CW.Event>): WCJ.Context {
const categories = Array.from(new Set(events.map((event) => event.category)));

let selectedEventIds: string[] = [];
const uncheckedEventsStr = localStorage.getItem('uncheckedEvents');
const allEventIds = events.map((event) => event.id);
const rememberSelection =
localStorage.getItem('rememberSelection') !== 'false';
if (rememberSelection) {
const uncheckedEventsStr = localStorage.getItem('uncheckedEvents');

const uncheckedEvents = uncheckedEventsStr
? (JSON.parse(uncheckedEventsStr) as string[])
: [];
selectedEventIds = events
.map((event) => event.id)
.filter((eventId) => !uncheckedEvents.includes(eventId));
const uncheckedEvents = uncheckedEventsStr
? (JSON.parse(uncheckedEventsStr) as string[])
: [];
selectedEventIds = allEventIds.filter(
(eventId) => !uncheckedEvents.includes(eventId),
);
} else {
selectedEventIds = allEventIds;
}

return {
events,
categories,
selectedEventIds,
rememberSelection,
};
}

Expand Down
33 changes: 0 additions & 33 deletions packages/app/src/services/cookies.ts

This file was deleted.

Loading

0 comments on commit c328661

Please sign in to comment.