Skip to content

Commit

Permalink
Merge pull request #391 from nulib/preview/5300-expire-ai-local-storage
Browse files Browse the repository at this point in the history
Add AI `expires` value to local storage.
  • Loading branch information
mathewjordan authored Dec 5, 2024
2 parents 59350a9 + 114bc72 commit fe253fd
Show file tree
Hide file tree
Showing 9 changed files with 63 additions and 21 deletions.
14 changes: 12 additions & 2 deletions components/Header/Super.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import { NavResponsiveOnly } from "@/components/Nav/Nav.styled";
import { NorthwesternWordmark } from "@/components/Shared/SVG/Northwestern";
import React from "react";
import { UserContext } from "@/context/user-context";
import { defaultAIState } from "@/hooks/useGenerativeAISearchToggle";
import useLocalStorage from "@/hooks/useLocalStorage";
import { useRouter } from "next/router";

const nav = [
{
Expand All @@ -33,9 +35,12 @@ const nav = [
];

export default function HeaderSuper() {
const router = useRouter();
const { query } = router;

const [isLoaded, setIsLoaded] = React.useState(false);
const [isExpanded, setIsExpanded] = React.useState(false);
const [ai, setAI] = useLocalStorage("ai", "false");
const [ai, setAI] = useLocalStorage("ai", defaultAIState);

React.useEffect(() => {
setIsLoaded(true);
Expand All @@ -45,7 +50,12 @@ export default function HeaderSuper() {
const handleMenu = () => setIsExpanded(!isExpanded);

const handleLogout = () => {
if (ai === "true") setAI("false");
// reset AI state and remove query param
setAI(defaultAIState);
delete query?.ai;
router.push(router.pathname, { query });

// logout
window.location.href = `${DCAPI_ENDPOINT}/auth/logout`;
};

Expand Down
18 changes: 14 additions & 4 deletions components/Search/GenerativeAIToggle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ describe("GenerativeAIToggle", () => {

await user.click(checkbox);
expect(checkbox).toHaveAttribute("data-state", "checked");
expect(localStorage.getItem("ai")).toEqual(JSON.stringify("true"));

const ai = JSON.parse(String(localStorage.getItem("ai")));
expect(ai?.enabled).toEqual("true");
expect(typeof ai?.expires).toEqual("number");
expect(ai?.expires).toBeGreaterThan(Date.now());
});

it("renders the generative AI tooltip", () => {
Expand Down Expand Up @@ -99,7 +103,10 @@ describe("GenerativeAIToggle", () => {
...defaultSearchState,
};

localStorage.setItem("ai", JSON.stringify("true"));
localStorage.setItem(
"ai",
JSON.stringify({ enabled: "true", expires: 9733324925021 }),
);

mockRouter.setCurrentUrl("/search");
render(
Expand All @@ -117,7 +124,7 @@ describe("GenerativeAIToggle", () => {

mockRouter.setCurrentUrl("/");

localStorage.setItem("ai", JSON.stringify("false"));
localStorage.setItem("ai", JSON.stringify({ enabled: "false" }));

render(
withUserProvider(
Expand All @@ -127,6 +134,9 @@ describe("GenerativeAIToggle", () => {

await user.click(screen.getByRole("checkbox"));

expect(localStorage.getItem("ai")).toEqual(JSON.stringify("true"));
const ai = JSON.parse(String(localStorage.getItem("ai")));
expect(ai?.enabled).toEqual("true");
expect(typeof ai?.expires).toEqual("number");
expect(ai?.expires).toBeGreaterThan(Date.now());
});
});
3 changes: 2 additions & 1 deletion components/Search/GenerativeAIToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ export default function GenerativeAIToggle() {
<SharedAlertDialog
isOpen={dialog.isOpen}
cancel={{ label: "Cancel", onClick: closeDialog }}
action={{ label: "Login", onClick: handleLogin }}
action={{ label: "Sign in", onClick: handleLogin }}
title="Sign in to Digital Collections"
>
{AI_LOGIN_ALERT}
</SharedAlertDialog>
Expand Down
5 changes: 4 additions & 1 deletion components/Search/Search.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ describe("Search component", () => {
});

it("renders generative AI placeholder text when AI search is active", () => {
localStorage.setItem("ai", JSON.stringify("true"));
localStorage.setItem(
"ai",
JSON.stringify({ enabled: "true", expires: 9733324925021 }),
);

render(withUserProvider(<Search isSearchActive={mockIsSearchActive} />));

Expand Down
11 changes: 8 additions & 3 deletions components/Shared/AlertDialog.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const AlertDialogOverlay = styled(AlertDialog.Overlay, {

const AlertDialogContent = styled(AlertDialog.Content, {
backgroundColor: "white",
borderRadius: 6,
borderRadius: "6px",
boxShadow:
"hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
position: "fixed",
Expand All @@ -29,8 +29,9 @@ const AlertDialogContent = styled(AlertDialog.Content, {
width: "90vw",
maxWidth: "500px",
maxHeight: "85vh",
padding: 25,
padding: "$gr4",
zIndex: "2",
fontSize: "$gr3",

"&:focus": { outline: "none" },
});
Expand All @@ -46,7 +47,11 @@ const AlertDialogTitle = styled(AlertDialog.Title, {

const AlertDialogButtonRow = styled("div", {
display: "flex",
justifyContent: "flex-end",
justifyContent: "space-between",

"> button": {
margin: 0,
},

"& > *:not(:last-child)": {
marginRight: "$gr3",
Expand Down
4 changes: 2 additions & 2 deletions components/Shared/AlertDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ export default function SharedAlertDialog({
<AlertDialog.Description>{children}</AlertDialog.Description>
<AlertDialogButtonRow>
{cancel && (
<Button isText onClick={cancel?.onClick}>
<Button onClick={cancel?.onClick} isLowercase>
{cancelLabel}
</Button>
)}

<AlertDialog.Action asChild>
<Button isPrimary onClick={action.onClick}>
<Button isPrimary onClick={action.onClick} isLowercase>
{action.label}
</Button>
</AlertDialog.Action>
Expand Down
19 changes: 14 additions & 5 deletions hooks/useGenerativeAISearchToggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@ import { UserContext } from "@/context/user-context";
import useLocalStorage from "@/hooks/useLocalStorage";
import { useRouter } from "next/router";

const defaultModalState = {
export const defaultAIState = {
enabled: "false",
expires: undefined,
};

export const defaultModalState = {
isOpen: false,
title: "Use Generative AI",
};

export default function useGenerativeAISearchToggle() {
const router = useRouter();

const [ai, setAI] = useLocalStorage("ai", "false");
const [ai, setAI] = useLocalStorage("ai", defaultAIState);
const { user } = React.useContext(UserContext);

const [dialog, setDialog] = useState(defaultModalState);

const isAIPreference = ai === "true";
const expires = Date.now() + 1000 * 60 * 60;
const isAIPreference = ai.enabled === "true";
const isChecked = isAIPreference && user?.isLoggedIn;

const loginUrl = `${DCAPI_ENDPOINT}/auth/login?goto=${goToLocation()}`;
Expand All @@ -36,7 +42,7 @@ export default function useGenerativeAISearchToggle() {
if (router.isReady) {
const { query } = router;
if (query.ai === "true") {
setAI("true");
setAI({ enabled: "true", expires });
}
}
}, [router.asPath]);
Expand All @@ -61,7 +67,10 @@ export default function useGenerativeAISearchToggle() {
if (!user?.isLoggedIn) {
setDialog({ ...dialog, isOpen: checked });
} else {
setAI(checked ? "true" : "false");
setAI({
enabled: checked ? "true" : "false",
expires: checked ? expires : undefined,
});
}
}

Expand Down
2 changes: 1 addition & 1 deletion hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react";

function useLocalStorage(key: string, initialValue: string) {
function useLocalStorage(key: string, initialValue: any) {
// Get the initial value from localStorage or use the provided initialValue
const [storedValue, setStoredValue] = useState(() => {
if (typeof window !== "undefined") {
Expand Down
8 changes: 6 additions & 2 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import React from "react";
import { SearchProvider } from "@/context/search-context";
import { User } from "@/types/context/user";
import { UserProvider } from "@/context/user-context";
import { defaultAIState } from "@/hooks/useGenerativeAISearchToggle";
import { defaultOpenGraphData } from "@/lib/open-graph";
import { getUser } from "@/lib/user-helpers";
import globalStyles from "@/styles/global";
Expand All @@ -37,8 +38,8 @@ function MyApp({ Component, pageProps }: MyAppProps) {
const [mounted, setMounted] = React.useState(false);
const [user, setUser] = React.useState<User>();

const [ai] = useLocalStorage("ai", "false");
const isUsingAI = ai === "true";
const [ai, setAI] = useLocalStorage("ai", defaultAIState);
const isUsingAI = ai?.enabled === "true";

React.useEffect(() => {
async function getData() {
Expand All @@ -47,6 +48,9 @@ function MyApp({ Component, pageProps }: MyAppProps) {
setMounted(true);
}
getData();

// Check if AI is enabled and if it has expired
if (ai?.expires && ai.expires < Date.now()) setAI(defaultAIState);
}, []);

React.useEffect(() => {
Expand Down

0 comments on commit fe253fd

Please sign in to comment.