Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add my starred classes #121

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 72 additions & 23 deletions src/components/ClassTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@
type IRowNode,
type ColDef,
} from "ag-grid-community";
import { Box, Group, Flex, Image, Input } from "@chakra-ui/react";
import { Box, Group, Flex, Input } from "@chakra-ui/react";
import React, { useEffect, useMemo, useRef, useState } from "react";

import { LuPlus, LuMinus, LuSearch } from "react-icons/lu";
import { LuPlus, LuMinus, LuSearch, LuStar } from "react-icons/lu";

import { InputGroup } from "./ui/input-group";
import { Button, LabelledButton } from "./ui/button";
import { useColorMode } from "./ui/color-mode";
import { Button } from "./ui/button";

import { Class, DARK_IMAGES, Flags, getFlagImg } from "../lib/class";
import { Class, Flags, getFlagImg } from "../lib/class";
import { classNumberMatch, classSort, simplifyString } from "../lib/utils";
import { State } from "../lib/state";
import { TSemester } from "../lib/dates";
Expand Down Expand Up @@ -201,10 +200,13 @@
);
}

type FilterGroup = Array<[keyof Flags | "fits", string, string?]>;
type FilterGroup = Array<
[keyof Flags | "fits" | "starred", string, React.ReactNode?]
>;

/** List of top filter IDs and their displayed names. */
const CLASS_FLAGS_1: FilterGroup = [
["starred", "My starred", <LuStar fill="currentColor" />],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "Starred", no my

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but honestly whatever

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is this where we want this to be? like, before even the "HASS" filter?

seems too important for a feature that i'm not sure will get much usage

i think it should be at least at the end of this row, and possibly even hidden

["hass", "HASS"],
["cih", "CI-H"],
["fits", "Fits schedule"],
Expand Down Expand Up @@ -245,7 +247,9 @@
const { setFlagsFilter, state, updateFilter } = props;

// Map from flag to whether it's on.
const [flags, setFlags] = useState<Map<keyof Flags | "fits", boolean>>(() => {
const [flags, setFlags] = useState<
Map<keyof Flags | "fits" | "starred", boolean>
>(() => {
const result = new Map();
for (const flag of CLASS_FLAGS) {
result.set(flag, false);
Expand All @@ -262,7 +266,7 @@
state.fitsScheduleCallback = () => flags.get("fits") && updateFilter();
}, [state, flags, updateFilter]);

const onChange = (flag: keyof Flags | "fits", value: boolean) => {
const onChange = (flag: keyof Flags | "fits" | "starred", value: boolean) => {
const newFlags = new Map(flags);
newFlags.set(flag, value);
setFlags(newFlags);
Expand All @@ -275,39 +279,39 @@
newFlags.forEach((value, flag) => {
if (value && flag === "fits" && !state.fitsSchedule(cls)) {
result = false;
} else if (value && flag !== "fits" && !cls.flags[flag]) {
} else if (
value &&
flag === "starred" &&
!state.isClassStarred(cls.number)
) {
result = false;
} else if (
value &&
flag !== "fits" &&
flag !== "starred" &&
!cls.flags[flag]
) {
result = false;
}
});
return result;
});
};

const { colorMode } = useColorMode();

const renderGroup = (group: FilterGroup) => {
return (
<Group attached colorPalette="orange" wrap="wrap">
{group.map(([flag, label, image]) => {
const checked = flags.get(flag);
return image ? (
<LabelledButton
<Button
key={flag}
onClick={() => onChange(flag, !checked)}
title={label}
variant={checked ? "solid" : "outline"}
portalled
>
<Image
src={image}
alt={label}
filter={
colorMode === "dark" && DARK_IMAGES.includes(flag ?? "")
? "invert()"
: ""
}
/>
</LabelledButton>
{image}
</Button>
Comment on lines +313 to +314
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: why was this removed? what does this look like on dark mode?

) : (
<Button
key={flag}
Expand Down Expand Up @@ -342,6 +346,32 @@
);
}

const StarButton = ({
classNumber,
state,
onStarToggle,
}: {
classNumber: string;
state: State;
onStarToggle?: () => void;
}) => {
const isStarred = state.isClassStarred(classNumber);
return (
<Button
onClick={(e) => {
e.stopPropagation();
state.toggleStarClass(classNumber);
onStarToggle?.();
}}
variant="ghost"
size="sm"
aria-label={isStarred ? "Unstar class" : "Star class"}
>
<LuStar fill={isStarred ? "currentColor" : "none"} />
</Button>
);
};

/** The table of all classes, along with searching and filtering with flags. */
export function ClassTable(props: {
classes: Map<string, Class>;
Expand Down Expand Up @@ -373,6 +403,25 @@
...sortProps,
};
return [
{
headerName: "",
field: "number",
maxWidth: 49,
cellRenderer: (params: { value: string; data: ClassTableRow }) => (
<StarButton
classNumber={params.value}
state={state}
onStarToggle={() => {
gridRef.current?.api?.refreshCells({
force: true,
columns: ["number"],
});
}}
/>
),
sortable: false,
cellStyle: { padding: 0 },
},
{
field: "number",
headerName: "Class",
Expand Down Expand Up @@ -401,7 +450,7 @@
},
{ field: "name", sortable: false, flex: 1 },
];
}, [state.term.semester]);

Check warning on line 453 in src/components/ClassTable.tsx

View workflow job for this annotation

GitHub Actions / Build

React Hook useMemo has a missing dependency: 'state'. Either include it or remove the dependency array

Check warning on line 453 in src/components/ClassTable.tsx

View workflow job for this annotation

GitHub Actions / Build

React Hook useMemo has a missing dependency: 'state'. Either include it or remove the dependency array

const defaultColDef: ColDef<ClassTableRow, string> = useMemo(() => {
return {
Expand Down
31 changes: 31 additions & 0 deletions src/lib/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export class State {
conflicts: number = 0;
/** Browser-specific saved state. */
store: Store;
/** Set of starred class numbers */
private starredClasses: Set<string> = new Set();
Comment on lines +26 to +27
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move this to below the other private fields


// The following are React state, so should be private. Even if we pass the
// State object to React components, they shouldn't be looking at these
Expand Down Expand Up @@ -68,6 +70,12 @@ export class State {
this.classes.set(number, new Class(cls, this.colorScheme));
});
this.initState();

// Load starred classes from storage
const storedStarred = this.store.get("starredClasses");
if (storedStarred) {
this.starredClasses = new Set(storedStarred);
}
Comment on lines +73 to +78
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move this to initState

}

/** All activities. */
Expand Down Expand Up @@ -401,4 +409,27 @@ export class State {
this.loadSave(this.saves[0]!.id);
}
}

/** Star or unstar a class */
toggleStarClass(classNumber: string): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: i'm not sure if we want this method to use classNumber, rather than cls. all the other methods that take in classes always take them in by class, and classNumber is nearly always only used for internal representations

if (this.starredClasses.has(classNumber)) {
this.starredClasses.delete(classNumber);
} else {
this.starredClasses.add(classNumber);
}
this.store.set("starredClasses", Array.from(this.starredClasses));
this.updateState();
}

/** Check if a class is starred */
isClassStarred(classNumber: string): boolean {
return this.starredClasses.has(classNumber);
}

/** Get all starred classes */
getStarredClasses(): Class[] {
return Array.from(this.starredClasses)
.map((number) => this.classes.get(number))
.filter((cls): cls is Class => cls !== undefined);
}
Comment on lines +412 to +434
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move these under the state management methods

Comment on lines +429 to +434
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove unused function

}
1 change: 1 addition & 0 deletions src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Preferences, Save } from "./schema";

export type TermStore = {
saves: Save[];
starredClasses: string[]; // Array of class numbers that are starred
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: i kind of wonder whether we should be putting this as under TermStore or GlobalStore, though i think TermStore makes more sense

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's use the jsdoc style to comment this

[saveId: string]: unknown[];
};

Expand Down
Loading