export const saveSchedule = async (userID: string, rememberMe: boolean) => {
logAnalytics({
category: analyticsEnum.nav.title,
action: analyticsEnum.nav.actions.SAVE_SCHEDULE,
label: userID,
value: rememberMe ? 1 : 0,
});
if (userID != null) {
userID = userID.replace(/\s+/g, "");
if (userID.length > 0) {
if (rememberMe) {
setLocalStorageUserId(userID);
} else {
removeLocalStorageUserId();
}
const scheduleSaveState = AppStore.schedule.getScheduleAsSaveState();
if (
isEmptySchedule(scheduleSaveState.schedules) &&
!confirm(
"You are attempting to save empty schedule(s). If this is unintentional, this may overwrite your existing schedules that haven't loaded yet!"
)
) {
return;
}
try {
await trpc.users.saveUserData.mutate({
id: userID,
data: {
id: userID,
userData: scheduleSaveState,
},
});
openSnackbar(
"success",
`Schedule saved under username "${userID}". Don't forget to sign up for classes on WebReg!`
);
AppStore.saveSchedule();
} catch (e) {
if (e instanceof TRPCError) {
openSnackbar(
"error",
`Schedule could not be saved under username "${userID}`
);
} else {
openSnackbar("error", "Network error or server is down.");
}
}
}
}
};
export const loadSchedule = async (userId: string, rememberMe: boolean) => {
logAnalytics({
category: analyticsEnum.nav.title,
action: analyticsEnum.nav.actions.LOAD_SCHEDULE,
label: userId,
value: rememberMe ? 1 : 0,
});
if (
userId != null &&
(!AppStore.hasUnsavedChanges() ||
window.confirm(
`Are you sure you want to load a different schedule? You have unsaved changes!`
))
) {
userId = userId.replace(/\s+/g, "");
if (userId.length > 0) {
if (rememberMe) {
setLocalStorageUserId(userId);
} else {
removeLocalStorageUserId();
}
try {
const res = await trpc.users.getUserData.query({ userId });
const scheduleSaveState = res && "userData" in res ? res.userData : res;
if (scheduleSaveState == null) {
openSnackbar("error", `Couldn't find schedules for username "${userId}".`);
} else if (await AppStore.loadSchedule(scheduleSaveState)) {
openSnackbar("success", `Schedule for username "${userId}" loaded.`);
} else {
AppStore.loadSkeletonSchedule(scheduleSaveState);
openSnackbar(
"error",
`Network error loading course information for "${userId}".
If this continues to happen, please submit a feedback form.`
);
}
} catch (e) {
console.error(e);
openSnackbar(
"error",
`Failed to load schedules. If this continues to happen, please submit a feedback form.`
);
}
}
}
};
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
TextField,
CircularProgress,
Checkbox,
FormControlLabel,
} from "@material-ui/core";
import { CloudDownload, Save } from "@material-ui/icons";
import { LoadingButton } from "@mui/lab";
import { ChangeEvent, PureComponent, useEffect, useState } from "react";
import actionTypesStore from "$actions/ActionTypesStore";
import { loadSchedule, saveSchedule } from "$actions/AppStoreActions";
import { getLocalStorageUserId } from "$lib/localStorage";
import AppStore from "$stores/AppStore";
import { useThemeStore } from "$stores/SettingsStore";
interface LoadSaveButtonBaseProps {
action: typeof saveSchedule;
actionName: "Save" | "Load";
disabled: boolean;
loading: boolean;
colorType: "primary" | "secondary";
id?: string;
}
interface LoadSaveButtonBaseState {
isOpen: boolean;
userID: string;
rememberMe: boolean;
}
interface SaveLoadIconProps {
loading: boolean;
actionName: "Save" | "Load";
}
function SaveLoadIcon(props: SaveLoadIconProps) {
return props.loading ? (
<CircularProgress size={20} color="inherit" />
) : props.actionName === "Save" ? (
<Save />
) : (
<CloudDownload />
);
}
class LoadSaveButtonBase extends PureComponent<
LoadSaveButtonBaseProps,
LoadSaveButtonBaseState
> {
state: LoadSaveButtonBaseState = {
isOpen: false,
userID: "",
rememberMe: true,
};
handleOpen = () => {
this.setState({ isOpen: true });
if (typeof Storage !== "undefined") {
const userID = getLocalStorageUserId();
if (userID !== null) {
this.setState({ userID: userID });
}
}
};
handleClose = (wasCancelled: boolean) => {
if (wasCancelled)
this.setState({ isOpen: false }, () => {
document.removeEventListener("keydown", this.enterEvent, false);
this.setState({ userID: "" });
});
else
this.setState({ isOpen: false }, () => {
document.removeEventListener("keydown", this.enterEvent, false);
// this `void` is for eslint "no floating promises"
void this.props.action(this.state.userID, this.state.rememberMe);
this.setState({ userID: "" });
});
};
handleToggleRememberMe = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ rememberMe: event.target.checked });
};
componentDidUpdate(_prevProps: unknown, prevState: LoadSaveButtonBaseState) {
if (!prevState.isOpen && this.state.isOpen)
document.addEventListener("keydown", this.enterEvent, false);
else if (prevState.isOpen && !this.state.isOpen)
document.removeEventListener("keydown", this.enterEvent, false);
}
enterEvent = (event: KeyboardEvent) => {
const charCode = event.which ? event.which : event.keyCode;
if (charCode === 13 || charCode === 10) {
event.preventDefault();
this.handleClose(false);
return false;
}
};
render() {
return (
<>
<LoadingButton
id={this.props.id}
onClick={this.handleOpen}
color="inherit"
startIcon={
<SaveLoadIcon
loading={this.props.loading}
actionName={this.props.actionName}
/>
}
disabled={this.props.disabled}
loading={false}
>
{this.props.actionName}
</LoadingButton>
<Dialog open={this.state.isOpen} onClose={this.handleClose}>
<DialogTitle>{this.props.actionName}</DialogTitle>
<DialogContent>
<DialogContentText>
Enter your unique user ID here to {this.props.actionName.toLowerCase()}{" "}
your schedule.
</DialogContentText>
<DialogContentText style={{ color: "red" }}>
Make sure the user ID is unique and secret, or someone else can overwrite
your schedule.
</DialogContentText>
<TextField
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
margin="dense"
label="Unique User ID"
type="text"
fullWidth
placeholder="Enter here"
value={this.state.userID}
onChange={(event) => this.setState({ userID: event.target.value })}
/>
<FormControlLabel
control={
<Checkbox
checked={this.state.rememberMe}
onChange={this.handleToggleRememberMe}
color="primary"
/>
}
label="Remember Me (Uncheck on shared computers)"
/>
</DialogContent>
<DialogActions>
<Button
onClick={() => this.handleClose(true)}
color={this.props.colorType}
>
{"Cancel"}
</Button>
<Button
onClick={() => this.handleClose(false)}
color={this.props.colorType}
>
{this.props.actionName}
</Button>
</DialogActions>
</Dialog>
</>
);
}
}
const LoadSaveScheduleFunctionality = () => {
const isDark = useThemeStore((store) => store.isDark);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode());
const loadScheduleAndSetLoading = async (
userID: string,
rememberMe: boolean
) => {
setLoading(true);
await loadSchedule(userID, rememberMe);
setLoading(false);
};
const saveScheduleAndSetLoading = async (
userID: string,
rememberMe: boolean
) => {
setSaving(true);
await saveSchedule(userID, rememberMe);
setSaving(false);
};
useEffect(() => {
const handleSkeletonModeChange = () => {
setSkeletonMode(AppStore.getSkeletonMode());
};
AppStore.on("skeletonModeChange", handleSkeletonModeChange);
return () => {
AppStore.off("skeletonModeChange", handleSkeletonModeChange);
};
}, []);
useEffect(() => {
if (typeof Storage !== "undefined") {
const savedUserID = getLocalStorageUserId();
if (savedUserID != null) {
// this `void` is for eslint "no floating promises"
void loadScheduleAndSetLoading(savedUserID, true);
}
}
}, []);
useEffect(() => {
const handleAutoSaveStart = () => setSaving(true);
const handleAutoSaveEnd = () => setSaving(false);
actionTypesStore.on("autoSaveStart", handleAutoSaveStart);
actionTypesStore.on("autoSaveEnd", handleAutoSaveEnd);
return () => {
actionTypesStore.off("autoSaveStart", handleAutoSaveStart);
actionTypesStore.off("autoSaveEnd", handleAutoSaveEnd);
};
}, []);
return (
<div
id="load-save-container"
style={{ display: "flex", flexDirection: "row" }}
>
<LoadSaveButtonBase
id="save-button"
actionName={"Save"}
action={saveScheduleAndSetLoading}
disabled={loading}
loading={saving}
colorType={isDark ? "secondary" : "primary"}
/>
<LoadSaveButtonBase
id="load-button"
actionName={"Load"}
action={loadScheduleAndSetLoading}
disabled={skeletonMode}
loading={loading}
colorType={isDark ? "secondary" : "primary"}
/>
</div>
);
};
export default LoadSaveScheduleFunctionality;
static async upsertGuestUserData(db: DatabaseOrTransaction, userData: User): Promise<string> {
return db.transaction(async (tx) => {
const userId = await this.createGuestUserOptional(tx, userData.id);
if (userId === undefined) {
throw new Error(`Failed to create guest user for ${userData.id}`);
}
// Add schedules and courses
const scheduleIds = await this.upsertSchedulesAndContents(tx, userId, userData.userData.schedules);
// Update user's current schedule index
const scheduleIndex = userData.userData.scheduleIndex;
const currentScheduleId =
scheduleIndex === undefined || scheduleIndex >= scheduleIds.length ? null : scheduleIds[scheduleIndex];
if (currentScheduleId !== null) {
await tx.update(users).set({ currentScheduleId: currentScheduleId }).where(eq(users.id, userId));
}
return userId;
});
}