Skip to content

Commit fe253fd

Browse files
authored
Merge pull request #391 from nulib/preview/5300-expire-ai-local-storage
Add AI `expires` value to local storage.
2 parents 59350a9 + 114bc72 commit fe253fd

File tree

9 files changed

+63
-21
lines changed

9 files changed

+63
-21
lines changed

components/Header/Super.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import { NavResponsiveOnly } from "@/components/Nav/Nav.styled";
1515
import { NorthwesternWordmark } from "@/components/Shared/SVG/Northwestern";
1616
import React from "react";
1717
import { UserContext } from "@/context/user-context";
18+
import { defaultAIState } from "@/hooks/useGenerativeAISearchToggle";
1819
import useLocalStorage from "@/hooks/useLocalStorage";
20+
import { useRouter } from "next/router";
1921

2022
const nav = [
2123
{
@@ -33,9 +35,12 @@ const nav = [
3335
];
3436

3537
export default function HeaderSuper() {
38+
const router = useRouter();
39+
const { query } = router;
40+
3641
const [isLoaded, setIsLoaded] = React.useState(false);
3742
const [isExpanded, setIsExpanded] = React.useState(false);
38-
const [ai, setAI] = useLocalStorage("ai", "false");
43+
const [ai, setAI] = useLocalStorage("ai", defaultAIState);
3944

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

4752
const handleLogout = () => {
48-
if (ai === "true") setAI("false");
53+
// reset AI state and remove query param
54+
setAI(defaultAIState);
55+
delete query?.ai;
56+
router.push(router.pathname, { query });
57+
58+
// logout
4959
window.location.href = `${DCAPI_ENDPOINT}/auth/logout`;
5060
};
5161

components/Search/GenerativeAIToggle.test.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ describe("GenerativeAIToggle", () => {
5757

5858
await user.click(checkbox);
5959
expect(checkbox).toHaveAttribute("data-state", "checked");
60-
expect(localStorage.getItem("ai")).toEqual(JSON.stringify("true"));
60+
61+
const ai = JSON.parse(String(localStorage.getItem("ai")));
62+
expect(ai?.enabled).toEqual("true");
63+
expect(typeof ai?.expires).toEqual("number");
64+
expect(ai?.expires).toBeGreaterThan(Date.now());
6165
});
6266

6367
it("renders the generative AI tooltip", () => {
@@ -99,7 +103,10 @@ describe("GenerativeAIToggle", () => {
99103
...defaultSearchState,
100104
};
101105

102-
localStorage.setItem("ai", JSON.stringify("true"));
106+
localStorage.setItem(
107+
"ai",
108+
JSON.stringify({ enabled: "true", expires: 9733324925021 }),
109+
);
103110

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

118125
mockRouter.setCurrentUrl("/");
119126

120-
localStorage.setItem("ai", JSON.stringify("false"));
127+
localStorage.setItem("ai", JSON.stringify({ enabled: "false" }));
121128

122129
render(
123130
withUserProvider(
@@ -127,6 +134,9 @@ describe("GenerativeAIToggle", () => {
127134

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

130-
expect(localStorage.getItem("ai")).toEqual(JSON.stringify("true"));
137+
const ai = JSON.parse(String(localStorage.getItem("ai")));
138+
expect(ai?.enabled).toEqual("true");
139+
expect(typeof ai?.expires).toEqual("number");
140+
expect(ai?.expires).toBeGreaterThan(Date.now());
131141
});
132142
});

components/Search/GenerativeAIToggle.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ export default function GenerativeAIToggle() {
6363
<SharedAlertDialog
6464
isOpen={dialog.isOpen}
6565
cancel={{ label: "Cancel", onClick: closeDialog }}
66-
action={{ label: "Login", onClick: handleLogin }}
66+
action={{ label: "Sign in", onClick: handleLogin }}
67+
title="Sign in to Digital Collections"
6768
>
6869
{AI_LOGIN_ALERT}
6970
</SharedAlertDialog>

components/Search/Search.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ describe("Search component", () => {
106106
});
107107

108108
it("renders generative AI placeholder text when AI search is active", () => {
109-
localStorage.setItem("ai", JSON.stringify("true"));
109+
localStorage.setItem(
110+
"ai",
111+
JSON.stringify({ enabled: "true", expires: 9733324925021 }),
112+
);
110113

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

components/Shared/AlertDialog.styled.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const AlertDialogOverlay = styled(AlertDialog.Overlay, {
1919

2020
const AlertDialogContent = styled(AlertDialog.Content, {
2121
backgroundColor: "white",
22-
borderRadius: 6,
22+
borderRadius: "6px",
2323
boxShadow:
2424
"hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
2525
position: "fixed",
@@ -29,8 +29,9 @@ const AlertDialogContent = styled(AlertDialog.Content, {
2929
width: "90vw",
3030
maxWidth: "500px",
3131
maxHeight: "85vh",
32-
padding: 25,
32+
padding: "$gr4",
3333
zIndex: "2",
34+
fontSize: "$gr3",
3435

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

4748
const AlertDialogButtonRow = styled("div", {
4849
display: "flex",
49-
justifyContent: "flex-end",
50+
justifyContent: "space-between",
51+
52+
"> button": {
53+
margin: 0,
54+
},
5055

5156
"& > *:not(:last-child)": {
5257
marginRight: "$gr3",

components/Shared/AlertDialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ export default function SharedAlertDialog({
4343
<AlertDialog.Description>{children}</AlertDialog.Description>
4444
<AlertDialogButtonRow>
4545
{cancel && (
46-
<Button isText onClick={cancel?.onClick}>
46+
<Button onClick={cancel?.onClick} isLowercase>
4747
{cancelLabel}
4848
</Button>
4949
)}
5050

5151
<AlertDialog.Action asChild>
52-
<Button isPrimary onClick={action.onClick}>
52+
<Button isPrimary onClick={action.onClick} isLowercase>
5353
{action.label}
5454
</Button>
5555
</AlertDialog.Action>

hooks/useGenerativeAISearchToggle.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,26 @@ import { UserContext } from "@/context/user-context";
55
import useLocalStorage from "@/hooks/useLocalStorage";
66
import { useRouter } from "next/router";
77

8-
const defaultModalState = {
8+
export const defaultAIState = {
9+
enabled: "false",
10+
expires: undefined,
11+
};
12+
13+
export const defaultModalState = {
914
isOpen: false,
1015
title: "Use Generative AI",
1116
};
1217

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

16-
const [ai, setAI] = useLocalStorage("ai", "false");
21+
const [ai, setAI] = useLocalStorage("ai", defaultAIState);
1722
const { user } = React.useContext(UserContext);
1823

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

21-
const isAIPreference = ai === "true";
26+
const expires = Date.now() + 1000 * 60 * 60;
27+
const isAIPreference = ai.enabled === "true";
2228
const isChecked = isAIPreference && user?.isLoggedIn;
2329

2430
const loginUrl = `${DCAPI_ENDPOINT}/auth/login?goto=${goToLocation()}`;
@@ -36,7 +42,7 @@ export default function useGenerativeAISearchToggle() {
3642
if (router.isReady) {
3743
const { query } = router;
3844
if (query.ai === "true") {
39-
setAI("true");
45+
setAI({ enabled: "true", expires });
4046
}
4147
}
4248
}, [router.asPath]);
@@ -61,7 +67,10 @@ export default function useGenerativeAISearchToggle() {
6167
if (!user?.isLoggedIn) {
6268
setDialog({ ...dialog, isOpen: checked });
6369
} else {
64-
setAI(checked ? "true" : "false");
70+
setAI({
71+
enabled: checked ? "true" : "false",
72+
expires: checked ? expires : undefined,
73+
});
6574
}
6675
}
6776

hooks/useLocalStorage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useCallback, useEffect, useState } from "react";
22

3-
function useLocalStorage(key: string, initialValue: string) {
3+
function useLocalStorage(key: string, initialValue: any) {
44
// Get the initial value from localStorage or use the provided initialValue
55
const [storedValue, setStoredValue] = useState(() => {
66
if (typeof window !== "undefined") {

pages/_app.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import React from "react";
1616
import { SearchProvider } from "@/context/search-context";
1717
import { User } from "@/types/context/user";
1818
import { UserProvider } from "@/context/user-context";
19+
import { defaultAIState } from "@/hooks/useGenerativeAISearchToggle";
1920
import { defaultOpenGraphData } from "@/lib/open-graph";
2021
import { getUser } from "@/lib/user-helpers";
2122
import globalStyles from "@/styles/global";
@@ -37,8 +38,8 @@ function MyApp({ Component, pageProps }: MyAppProps) {
3738
const [mounted, setMounted] = React.useState(false);
3839
const [user, setUser] = React.useState<User>();
3940

40-
const [ai] = useLocalStorage("ai", "false");
41-
const isUsingAI = ai === "true";
41+
const [ai, setAI] = useLocalStorage("ai", defaultAIState);
42+
const isUsingAI = ai?.enabled === "true";
4243

4344
React.useEffect(() => {
4445
async function getData() {
@@ -47,6 +48,9 @@ function MyApp({ Component, pageProps }: MyAppProps) {
4748
setMounted(true);
4849
}
4950
getData();
51+
52+
// Check if AI is enabled and if it has expired
53+
if (ai?.expires && ai.expires < Date.now()) setAI(defaultAIState);
5054
}, []);
5155

5256
React.useEffect(() => {

0 commit comments

Comments
 (0)