From 114bc721d867a625e826afbe444e5e1fe4e7783f Mon Sep 17 00:00:00 2001 From: Mat Jordan Date: Thu, 5 Dec 2024 12:12:18 -0500 Subject: [PATCH] Add AI `expires` value to local storage. --- components/Header/Super.tsx | 14 ++++++++++++-- components/Search/GenerativeAIToggle.test.tsx | 18 ++++++++++++++---- components/Search/GenerativeAIToggle.tsx | 3 ++- components/Search/Search.test.tsx | 5 ++++- components/Shared/AlertDialog.styled.ts | 11 ++++++++--- components/Shared/AlertDialog.tsx | 4 ++-- hooks/useGenerativeAISearchToggle.ts | 19 ++++++++++++++----- hooks/useLocalStorage.ts | 2 +- pages/_app.tsx | 8 ++++++-- 9 files changed, 63 insertions(+), 21 deletions(-) diff --git a/components/Header/Super.tsx b/components/Header/Super.tsx index dcafb641..9402fd3f 100644 --- a/components/Header/Super.tsx +++ b/components/Header/Super.tsx @@ -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 = [ { @@ -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); @@ -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`; }; diff --git a/components/Search/GenerativeAIToggle.test.tsx b/components/Search/GenerativeAIToggle.test.tsx index 59b311e8..67f63e4d 100644 --- a/components/Search/GenerativeAIToggle.test.tsx +++ b/components/Search/GenerativeAIToggle.test.tsx @@ -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", () => { @@ -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( @@ -117,7 +124,7 @@ describe("GenerativeAIToggle", () => { mockRouter.setCurrentUrl("/"); - localStorage.setItem("ai", JSON.stringify("false")); + localStorage.setItem("ai", JSON.stringify({ enabled: "false" })); render( withUserProvider( @@ -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()); }); }); diff --git a/components/Search/GenerativeAIToggle.tsx b/components/Search/GenerativeAIToggle.tsx index f460ff99..0774a641 100644 --- a/components/Search/GenerativeAIToggle.tsx +++ b/components/Search/GenerativeAIToggle.tsx @@ -63,7 +63,8 @@ export default function GenerativeAIToggle() { {AI_LOGIN_ALERT} diff --git a/components/Search/Search.test.tsx b/components/Search/Search.test.tsx index da91e2b5..5f747ad9 100644 --- a/components/Search/Search.test.tsx +++ b/components/Search/Search.test.tsx @@ -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()); diff --git a/components/Shared/AlertDialog.styled.ts b/components/Shared/AlertDialog.styled.ts index 941b2808..d13e7bb1 100644 --- a/components/Shared/AlertDialog.styled.ts +++ b/components/Shared/AlertDialog.styled.ts @@ -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", @@ -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" }, }); @@ -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", diff --git a/components/Shared/AlertDialog.tsx b/components/Shared/AlertDialog.tsx index a92f4e58..516419eb 100644 --- a/components/Shared/AlertDialog.tsx +++ b/components/Shared/AlertDialog.tsx @@ -43,13 +43,13 @@ export default function SharedAlertDialog({ {children} {cancel && ( - )} - diff --git a/hooks/useGenerativeAISearchToggle.ts b/hooks/useGenerativeAISearchToggle.ts index 0c4f4445..59e7b480 100644 --- a/hooks/useGenerativeAISearchToggle.ts +++ b/hooks/useGenerativeAISearchToggle.ts @@ -5,7 +5,12 @@ 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", }; @@ -13,12 +18,13 @@ const defaultModalState = { 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()}`; @@ -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]); @@ -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, + }); } } diff --git a/hooks/useLocalStorage.ts b/hooks/useLocalStorage.ts index 5488a5b8..a4f79b10 100644 --- a/hooks/useLocalStorage.ts +++ b/hooks/useLocalStorage.ts @@ -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") { diff --git a/pages/_app.tsx b/pages/_app.tsx index e2bba277..ef8f731d 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -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"; @@ -37,8 +38,8 @@ function MyApp({ Component, pageProps }: MyAppProps) { const [mounted, setMounted] = React.useState(false); const [user, setUser] = React.useState(); - 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() { @@ -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(() => {