Skip to content

Commit e8fcea3

Browse files
committed
start work on audit (very much in progress...)
1 parent 5db7d16 commit e8fcea3

12 files changed

+1405
-998
lines changed

package.json

+4-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"check": "biome check --write ./src"
1717
},
1818
"dependencies": {
19-
"@ark-ui/solid": "^4.7.0",
19+
"@ark-ui/solid": "^4.8.0",
2020
"@fontsource-variable/inter": "^5.1.1",
2121
"@fontsource-variable/roboto-mono": "^5.1.1",
2222
"@solid-primitives/cookies": "^0.0.1",
@@ -26,9 +26,8 @@
2626
"@solidjs/meta": "^0.29.4",
2727
"@solidjs/router": "^0.15.2",
2828
"@solidjs/start": "^1.0.11",
29-
"localforage": "^1.10.0",
3029
"lucide-solid": "^0.469.0",
31-
"solid-js": "^1.9.3",
30+
"solid-js": "^1.9.4",
3231
"ua-parser-js": "^2.0.0",
3332
"vinxi": "^0.4.3",
3433
"vite-plugin-solid": "^2.11.0",
@@ -49,9 +48,9 @@
4948
"@testing-library/jest-dom": "^6.6.3",
5049
"@testing-library/user-event": "^14.5.2",
5150
"@types/node": "^22.10.5",
52-
"jsdom": "^25.0.1",
51+
"jsdom": "^26.0.0",
5352
"postcss": "^8.4.49",
54-
"typescript": "^5.7.2",
53+
"typescript": "^5.7.3",
5554
"typescript-eslint": "^8.19.1",
5655
"vitest": "^2.1.8"
5756
},

pnpm-lock.yaml

+930-976
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/Audit.tsx

+249-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,253 @@
1-
import type { Component } from "solid-js";
1+
import {
2+
type Component,
3+
For,
4+
Index,
5+
createDeferred,
6+
createEffect,
7+
createMemo,
8+
createSignal,
9+
on,
10+
} from "solid-js";
11+
import { Stack } from "styled-system/jsx";
212

3-
const Audit: Component = (props) => {
4-
return <div />;
13+
import { CheckIcon, ChevronsUpDownIcon, Store, XIcon } from "lucide-solid";
14+
import { Combobox, createListCollection } from "~/components/ui/combobox";
15+
import { IconButton } from "~/components/ui/icon-button";
16+
import { TagsInput } from "~/components/ui/tags-input";
17+
18+
import { useCombobox, useTagsInput } from "@ark-ui/solid";
19+
import { combobox } from "styled-system/recipes";
20+
import { useCourseDataContext } from "~/context/create";
21+
import type {
22+
CourseRequirements,
23+
CourseRequirementsWithKey,
24+
Reqs,
25+
} from "~/context/types";
26+
27+
const Audit: Component<{
28+
reqList: CourseRequirementsWithKey[];
29+
}> = (props) => {
30+
return (
31+
<Stack>
32+
<SelectProgram reqList={props.reqList} />
33+
</Stack>
34+
);
35+
};
36+
37+
const SelectProgram: Component<{
38+
reqList: CourseRequirementsWithKey[];
39+
}> = (props) => {
40+
// TODO: this entire component is hacky af, try to clean it up later
41+
42+
const [store, { removeReq, addReq }] = useCourseDataContext();
43+
const getCourses = createMemo(() => {
44+
const courses = props.reqList ?? [];
45+
courses.sort(sortCourses);
46+
47+
const courseItems = courses.map((course) => ({
48+
value: course.key,
49+
label: course["medium-title"],
50+
}));
51+
return courseItems;
52+
});
53+
const [items, setItems] = createSignal(getCourses());
54+
55+
const collection = createMemo(() =>
56+
createListCollection({
57+
items: getCourses(),
58+
}),
59+
);
60+
61+
const handleChange = (e: Combobox.InputValueChangeDetails) => {
62+
const filtered = getCourses().filter((item) =>
63+
item.label.toLowerCase().includes(e.inputValue.toLowerCase()),
64+
);
65+
setItems(filtered.length > 0 ? filtered : getCourses());
66+
};
67+
68+
const setSelected = (newReqs: string[]) => {
69+
// TODO: doesnt update components when changing activeRoad, need to fix.
70+
// if (currentReqs().length > newReqs.length) {
71+
// const diff = currentReqs().find((x) => !newReqs.includes(x));
72+
// if (diff) removeReq(diff);
73+
// } else if (currentReqs().length < newReqs.length) {
74+
// const newReq = newReqs[newReqs.length - 1];
75+
// addReq(newReq);
76+
// }
77+
78+
tagsInput()?.setValue(newReqs);
79+
comboboxInput()?.setValue(newReqs);
80+
81+
const currentReqs = store.roads[store.activeRoad].contents.coursesOfStudy;
82+
83+
for (const req of newReqs) {
84+
if (!currentReqs.includes(req)) addReq(req);
85+
}
86+
87+
for (const req of currentReqs) {
88+
if (!newReqs.includes(req)) removeReq(req);
89+
}
90+
};
91+
92+
const currentReqs = createMemo(
93+
() => store.roads[store.activeRoad].contents.coursesOfStudy,
94+
);
95+
96+
const tagsInput = useTagsInput({
97+
editable: false,
98+
value: currentReqs(),
99+
onValueChange: (e) => {
100+
comboboxInput()?.setValue(e.value);
101+
setSelected(e.value);
102+
},
103+
});
104+
const comboboxInput = useCombobox({
105+
onInputValueChange: handleChange,
106+
collection: collection(),
107+
inputBehavior: "autohighlight",
108+
value: currentReqs(),
109+
onValueChange: (e) => {
110+
tagsInput()?.setValue(e.value);
111+
tagsInput()?.clearInputValue();
112+
setSelected(e.value);
113+
},
114+
openOnClick: true,
115+
multiple: true,
116+
});
117+
118+
return (
119+
<Combobox.RootProvider value={comboboxInput}>
120+
{/* @ts-expect-error: it works i swear */}
121+
<TagsInput.RootProvider value={tagsInput}>
122+
<Combobox.Label>Your Programs</Combobox.Label>
123+
124+
<Combobox.Control
125+
asChild={(controlProps) => (
126+
<TagsInput.Control {...controlProps}>
127+
<Index each={tagsInput().value}>
128+
{(value, index) => (
129+
<TagsInput.Item index={index} value={value()}>
130+
<TagsInput.ItemPreview minHeight="fit-content">
131+
<TagsInput.ItemText>
132+
{
133+
getCourses().find((elem) => elem.value === value())
134+
?.label
135+
}
136+
</TagsInput.ItemText>
137+
<TagsInput.ItemInput />
138+
<TagsInput.ItemDeleteTrigger
139+
asChild={(triggerProps) => (
140+
<IconButton
141+
variant="link"
142+
size="xs"
143+
{...triggerProps()}
144+
>
145+
<XIcon />
146+
</IconButton>
147+
)}
148+
/>
149+
<TagsInput.ItemInput />
150+
<TagsInput.HiddenInput />
151+
</TagsInput.ItemPreview>
152+
</TagsInput.Item>
153+
)}
154+
</Index>
155+
156+
<TagsInput.Input
157+
placeholder={
158+
comboboxInput().hasSelectedItems
159+
? undefined
160+
: "Click to add a major"
161+
}
162+
asChild={(inputProps) => <Combobox.Input {...inputProps()} />}
163+
/>
164+
<Combobox.Trigger
165+
asChild={(triggerProps) => (
166+
<IconButton
167+
variant="link"
168+
aria-label="open"
169+
size="xs"
170+
{...triggerProps()}
171+
>
172+
<ChevronsUpDownIcon />
173+
</IconButton>
174+
)}
175+
/>
176+
</TagsInput.Control>
177+
)}
178+
/>
179+
180+
<Combobox.Positioner>
181+
<Combobox.Content maxH="200px" overflowY="auto">
182+
<Combobox.ItemGroup>
183+
<For
184+
each={items()}
185+
fallback={
186+
<Combobox.Item item={null}>
187+
<Combobox.ItemText>No results found</Combobox.ItemText>
188+
</Combobox.Item>
189+
}
190+
>
191+
{(item) => (
192+
<Combobox.Item item={item} minH="fit-content">
193+
<Combobox.ItemText>{item.label}</Combobox.ItemText>
194+
<Combobox.ItemIndicator>
195+
<CheckIcon />
196+
</Combobox.ItemIndicator>
197+
</Combobox.Item>
198+
)}
199+
</For>
200+
</Combobox.ItemGroup>
201+
</Combobox.Content>
202+
</Combobox.Positioner>
203+
</TagsInput.RootProvider>
204+
</Combobox.RootProvider>
205+
);
206+
};
207+
208+
const sortCourses = (c1: CourseRequirements, c2: CourseRequirements) => {
209+
const sortKey = "medium-title" as const;
210+
211+
const a = c1[sortKey].toLowerCase();
212+
const b = c2[sortKey].toLowerCase();
213+
214+
// same type of program
215+
if (
216+
(a.includes("major") && b.includes("major")) ||
217+
(a.includes("minor") && b.includes("minor"))
218+
) {
219+
// get course numbers
220+
let n1 = a.split(" ")[0].split("-")[0];
221+
let n2 = b.split(" ")[0].split("-")[0];
222+
223+
n1 =
224+
Number.isNaN(Number.parseInt(n1)) &&
225+
!Number.isNaN(Number.parseInt(n1.slice(0, -1)))
226+
? n1.slice(0, -1)
227+
: n1;
228+
n2 =
229+
Number.isNaN(Number.parseInt(n2)) &&
230+
!Number.isNaN(Number.parseInt(n2.slice(0, -1)))
231+
? n2.slice(0, -1)
232+
: n2;
233+
234+
if (n1 === n2) return a.localeCompare(b);
235+
236+
return (!Number.isNaN(Number.parseInt(n1)) &&
237+
!Number.isNaN(Number.parseInt(n2))) ||
238+
(Number.isNaN(Number.parseInt(n1)) && Number.isNaN(Number.parseInt(n2)))
239+
? // @ts-expect-error the original function returned n1 - n2 which should be NaN?? idk why this works but oh well
240+
n1 - n2
241+
: !Number.isNaN(Number.parseInt(n1))
242+
? -1
243+
: 1;
244+
}
245+
246+
if (a.includes("major") && b.includes("minor")) return -1;
247+
if (b.includes("major") && a.includes("minor")) return 1;
248+
if (a.includes("major") || a.includes("minor")) return -1;
249+
if (b.includes("major") || b.includes("minor")) return 1;
250+
return a.localeCompare(b);
5251
};
6252

7253
export default Audit;

src/components/layout/Sidebar.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,27 @@ import { Box, Flex, HStack, Stack } from "styled-system/jsx";
44

55
import { SquareCheckIcon } from "lucide-solid";
66
import About from "~/components/About";
7+
import Audit from "~/components/Audit";
78
import Settings from "~/components/Settings";
89
import ThemeToggler from "~/components/ThemeToggler";
910
import { Icon } from "~/components/ui/icon";
1011
import { Link } from "~/components/ui/link";
1112
import { Text } from "~/components/ui/text";
1213

14+
import type {
15+
CourseRequirementsWithKey,
16+
Reqs,
17+
SimplifiedSelectedSubjects,
18+
} from "~/context/types";
19+
1320
const Sidebar: Component<{
1421
changeYear: (year: number) => void;
22+
reqList: CourseRequirementsWithKey[];
1523
}> = (props) => {
1624
return (
1725
<Stack>
1826
<SidebarButtons changeYear={props.changeYear} />
27+
<Audit reqList={props.reqList} />
1928
</Stack>
2029
);
2130
};

src/components/ui/combobox.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { createListCollection } from "@ark-ui/solid/combobox";
2+
export * as Combobox from "./styled/combobox";

0 commit comments

Comments
 (0)