Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
883 changes: 873 additions & 10 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 @@ -25,6 +25,7 @@
"@vercel/analytics": "^1.4.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"firebase": "^12.1.0",
"html-to-image": "^1.11.13",
"html2canvas": "^1.4.1",
"lucide-react": "^0.474.0",
Expand Down
75 changes: 75 additions & 0 deletions src/components/SignIn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { FC } from "react";
import { CiLogin, CiLogout } from "react-icons/ci";
import { signInGoogle, signOut } from "../firebase/auth";
import { useAuth } from "../contexts/AuthContext";
import { useLayoutContext } from "./layout/Layout";
import { loadCourses, loadCoursesOnGrid, loadCoursesUsed, loadDependencies, loadLayouts, loadTheme } from "../firebase/firestore";

const SignIn: FC = () => {
const { signedIn } = useAuth();

const {
setSavedLayouts,
setCourses,
setCoursesUsed,
setCoursesOnGrid,
setDependencies
} = useLayoutContext();

const signIn = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const result = await signInGoogle();
const user = result.user;

try {
// Fetch everything in parallel
const [
remoteSavedLayouts,
remoteCourses,
remoteCoursesUsed,
remoteCoursesOnGrid,
remoteDependencies,
remoteTheme,
] = await Promise.all([
loadLayouts(user.uid),
loadCourses(user.uid),
loadCoursesUsed(user.uid),
loadCoursesOnGrid(user.uid),
loadDependencies(user.uid),
loadTheme(user.uid),
]);

// Apply loaded data only if available
if (remoteSavedLayouts?.length) setSavedLayouts(remoteSavedLayouts);
if (Object.keys(remoteCourses ?? {}).length) setCourses(remoteCourses);
if (Object.keys(remoteCoursesUsed ?? {}).length) setCoursesUsed(remoteCoursesUsed);
if (Object.keys(remoteCoursesOnGrid ?? {}).length) setCoursesOnGrid(remoteCoursesOnGrid);
if (remoteDependencies?.size) setDependencies(remoteDependencies);
if (remoteTheme) {
if (remoteTheme == 'dark') {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
}

} catch (err) {
console.error("Failed to load user data:", err);
}
};

return (
<div>
<button
onClick={signedIn ? signOut : signIn}
className="px-1 py-2 text-2xl flex items-center"
>
{signedIn ? <CiLogout /> : <CiLogin />}
</button>
</div>
);
};

export default SignIn;
6 changes: 5 additions & 1 deletion src/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { saveTheme } from "../firebase/firestore";
import { auth } from "../firebase/firebase";
import { useDarkMode } from "../utils/utilImports";
import { CiLight, CiDark } from "react-icons/ci";

Expand All @@ -11,12 +13,14 @@ const ThemeToggle = () => {

html.classList.toggle('dark');
localStorage.setItem('theme', isNowDark ? 'dark' : 'light');
const user = auth.currentUser;
if (user) saveTheme(user.uid, isNowDark ? 'dark' : 'light');
};

return (
<button
onClick={toggleDarkMode}
className="px-2 py-2 text-black dark:text-white"
className="px-1 py-2 text-black dark:text-white text-2xl flex items-center"
>
{isDarkMode ? <CiDark /> : <CiLight />}
</button>
Expand Down
8 changes: 6 additions & 2 deletions src/components/forms/CourseForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@ import {
import Announcement from "../info/Announcement";
import TextInput from "./TextInput";
import SubmitButton from "../SubmitButton";
import { useLayoutContext } from "../layout/Layout";

const CourseForm: FC<CourseFormProps> = ({
setCourses,
setCoursesUsed,
customInfo,
setCustomInfo,
preqString,
setPreqString,
coreqString,
setCoreqString,
}) => {
const {
setCourses,
setCoursesUsed,
} = useLayoutContext();

const [errors, setErrors] = useState({
code: false,
name: false,
Expand Down
24 changes: 13 additions & 11 deletions src/components/forms/LoadLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { FC, useEffect, useRef, useState } from "react";
import { LoadLayoutProps } from "../../types/types";
import { Announcement } from "../../utils/componentImports";
import { isValidString, parseString } from "../../utils/utilImports";
import Preset from "./Preset";
import TextInput from "./TextInput";
import SubmitButton from "../SubmitButton";
import { useLayoutContext } from "../layout/Layout";

enum Load {
NONE,
SUCCESS,
ERROR,
}

const LoadLayout: FC<LoadLayoutProps> = ({
courses,
coursesUsed,
setCourses,
setCoursesOnGrid,
setCoursesUsed,
setDependencies,
savedLayouts,
}) => {
const LoadLayout: FC = () => {
const {
courses,
setCourses,
coursesUsed,
setCoursesUsed,
setCoursesOnGrid,
setDependencies,
savedLayouts,
} = useLayoutContext();

const [str, setStr] = useState("");
const [load, setLoad] = useState(Load.NONE);
const timeoutRef = useRef<NodeJS.Timeout>();
Expand Down Expand Up @@ -96,7 +98,7 @@ const LoadLayout: FC<LoadLayoutProps> = ({
<div className="lg:grid grid-cols-2 flex flex-col gap-2 mt-4">
{savedLayouts.map(
(layout, index) =>
layout && (
layout?.name && (
<Preset
key={index}
name={layout.name}
Expand Down
32 changes: 22 additions & 10 deletions src/components/forms/SaveLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { FC, useEffect, useRef, useState } from "react";
import { SaveLayoutProps } from "../../types/types";
import { Announcement } from "../../utils/componentImports";
import TextInput from "./TextInput";
import SubmitButton from "../SubmitButton";
import { useLayoutContext } from "../layout/Layout";
import { emptyGrid } from "../../utils/emptyGrid";
import { GridPositionBase } from "../../types/types";

enum Save {
NONE,
SUCCESS,
}

const SaveLayout: FC<SaveLayoutProps> = ({
courses,
coursesOnGrid,
setSavedLayouts,
}) => {
const SaveLayout: FC = () => {
const {
courses,
coursesOnGrid,
setSavedLayouts,
} = useLayoutContext();

const [str, setStr] = useState("");
const [copied, setCopied] = useState(false);

Expand All @@ -35,8 +39,9 @@ const SaveLayout: FC<SaveLayoutProps> = ({

useEffect(() => {
let newStr = "";
Object.entries(coursesOnGrid).forEach(([pos, courseCode]) => {
if (pos !== "3F.1" && pos.includes(".1")) newStr += "@@";
Object.keys(emptyGrid).map((slot) => {
const courseCode: string = coursesOnGrid[slot as GridPositionBase];
if (slot !== "3F.1" && slot.includes(".1")) newStr += "@@";
if (!courseCode) return;
const course = courses[courseCode];
newStr += courseCode + course.name + "%%";
Expand All @@ -55,7 +60,7 @@ const SaveLayout: FC<SaveLayoutProps> = ({
if (hasPreq) newStr += serializeReqs("p", course.preq);
if (hasCoreq) newStr += serializeReqs(hasPreq ? "o" : "po", course.coreq);

if (pos[3] != "5") newStr += "$$";
if (slot[3] != "5") newStr += "$$";
});
setStr(newStr);
}, [coursesOnGrid, courses]);
Expand All @@ -81,7 +86,14 @@ const SaveLayout: FC<SaveLayoutProps> = ({

setSavedLayouts((prev) => {
const newLayouts = [...prev];

// Fill any missing indices with default objects, important for firestore
for (let i = 0; i <= saveIndex; i++) {
if (!newLayouts[i]?.name) newLayouts[i] = { name: "", str: "" };
}

newLayouts[saveIndex] = newLayout;

return newLayouts;
});

Expand Down Expand Up @@ -164,7 +176,7 @@ const SaveLayout: FC<SaveLayoutProps> = ({
</form>

{save === Save.SUCCESS && (<Announcement success>
Layout saved in cache!
Layout saved!
</Announcement>)}
</section>
);
Expand Down
64 changes: 37 additions & 27 deletions src/components/grid/CourseGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
StreamRequirements,
GridPosition,
CoursesUsed,
GridPositionBase,
} from "../../types/types";
import {
Droppable,
Expand All @@ -32,6 +33,7 @@ import { getYearTerm } from "../../utils/getYearTerm";
import Announcement from "../info/Announcement";
import { addDependencies } from "../../utils/utilImports";
import { emptyGrid } from "../../utils/utilImports";
import { useLayoutContext } from "../layout/Layout";

enum DropError {
NONE = "NONE",
Expand All @@ -42,17 +44,20 @@ enum DropError {
}

const CourseGrid: FC<CourseGridProps> = ({
courses,
coursesOnGrid,
coursesUsed,
dependencies,
setCoursesOnGrid,
setCoursesUsed,
setCustomInfo,
setPreqString,
setCoreqString,
setDependencies,
}) => {
const {
courses,
coursesUsed,
setCoursesUsed,
coursesOnGrid,
setCoursesOnGrid,
dependencies,
setDependencies,
} = useLayoutContext();

const [filters, setFilters] = useState<FilterState>({
searchTerm: "",
streams: [],
Expand Down Expand Up @@ -500,27 +505,32 @@ const CourseGrid: FC<CourseGridProps> = ({
ref={screenshotRef}
data-testid="grid"
>
{Object.entries(coursesOnGrid).map(([slot, courseCode]) => (
<Droppable
key={slot}
id={slot}
valid={validYearTerms[getYearTerm(slot as GridPosition)]}
>
{courseCode ? (
<MakerCard
{Object.keys(emptyGrid).map((slot) => {
const courseCode: string = coursesOnGrid[slot as GridPositionBase];
return (
<Droppable
key={slot}
id={slot}
valid={validYearTerms[getYearTerm(slot as GridPosition)]}
id={courseCode}
code={courseCode}
setCustomInfo={setCustomInfo}
setPreqString={setPreqString}
setCoreqString={setCoreqString}
{...courses[courseCode]}
/>
) : (
slot
)}
</Droppable>
))}
>
{courseCode ? (
<MakerCard
valid={validYearTerms[getYearTerm(slot as GridPosition)]}
id={courseCode}
code={courseCode}
setCustomInfo={setCustomInfo}
setPreqString={setPreqString}
setCoreqString={setCoreqString}
{...courses[courseCode]}
/>
) : (
slot
)}
</Droppable>
)
})}


</div>
<div>
{/* Courses to choose from */}
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Footer:FC = () => {
};

const resetLocalStorage = () => {
if (window.confirm(`Are you sure you want to reset everything saved in local storage?
if (window.confirm(`Are you sure you want to reset all saved data in local storage and cloud (if signed in)?
This will remove your custom courses, undo your edits, and clear your layout. This will also allow updates to take effect.`)) {
const savedTheme = localStorage.getItem('theme');
localStorage.clear();
Expand Down
Loading