From 2ecc7fa1f727b7c07377a1d52514928d67ac13f7 Mon Sep 17 00:00:00 2001 From: Aaron van den Berg Date: Mon, 19 Dec 2022 20:16:20 +0100 Subject: [PATCH] Added a feature that a user can search for experiments inside the extension --- package.json | 4 +- pnpm-lock.yaml | 50 ++++----- src/components/FloatingLabelInput.tsx | 5 +- src/components/Header.tsx | 60 ++++++----- src/components/SearchItem.tsx | 101 ++++++++++++++++++ .../settings/AccessTokenInputField.tsx | 22 ++++ .../settings/DefaultScreenField.tsx | 28 +++++ .../settings/LocalStorageInputField.tsx | 18 ++++ src/popup/index.tsx | 15 ++- src/screens/{HomeScreen.tsx => Home.tsx} | 0 src/screens/Search.tsx | 83 ++++++++++++++ src/screens/Settings.tsx | 24 +++++ src/screens/SettingsScreen.tsx | 29 ----- src/store/useStore.ts | 37 ++++--- src/types/types.ts | 1 + 15 files changed, 375 insertions(+), 102 deletions(-) create mode 100644 src/components/SearchItem.tsx create mode 100644 src/components/settings/AccessTokenInputField.tsx create mode 100644 src/components/settings/DefaultScreenField.tsx create mode 100644 src/components/settings/LocalStorageInputField.tsx rename src/screens/{HomeScreen.tsx => Home.tsx} (100%) create mode 100644 src/screens/Search.tsx create mode 100644 src/screens/Settings.tsx delete mode 100644 src/screens/SettingsScreen.tsx create mode 100644 src/types/types.ts diff --git a/package.json b/package.json index 12e2984..cbe5f1c 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "@emotion/react": "^11.10.4", - "@mantine/core": "^5.5.5", - "@mantine/hooks": "^5.5.5", + "@mantine/core": "^5.9.5", + "@mantine/hooks": "^5.9.5", "@plasmohq/storage": "^0.12.2", "@tabler/icons": "^1.104.0", "immer": "^9.0.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b01f4a..d09264e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2,8 +2,8 @@ lockfileVersion: 5.4 specifiers: '@emotion/react': ^11.10.4 - '@mantine/core': ^5.5.5 - '@mantine/hooks': ^5.5.5 + '@mantine/core': ^5.9.5 + '@mantine/hooks': ^5.9.5 '@plasmohq/prettier-plugin-sort-imports': 3.5.4 '@plasmohq/storage': ^0.12.2 '@tabler/icons': ^1.104.0 @@ -21,8 +21,8 @@ specifiers: dependencies: '@emotion/react': 11.10.4_bjroym7kxlcs2vvwnej4p3gzwu - '@mantine/core': 5.5.5_ekj4xtn5nqtot2bmww23lp2qbq - '@mantine/hooks': 5.5.5_react@18.2.0 + '@mantine/core': 5.9.5_sangcu27ug4wpw4wjq5m7ftas4 + '@mantine/hooks': 5.9.5_react@18.2.0 '@plasmohq/storage': 0.12.2_react@18.2.0 '@tabler/icons': 1.104.0_biqbaboplfbrettd7655fr4n2y immer: 9.0.15 @@ -519,18 +519,18 @@ packages: dev: false optional: true - /@mantine/core/5.5.5_ekj4xtn5nqtot2bmww23lp2qbq: - resolution: {integrity: sha512-VjNpz5mvV1hg6RbVuLkA9uY+iS3Ocpg9C0ZjnBuMKbDA9TYmpxgLJ1Eo56OMfJvQ03W8qG/prIwY8QNZddtNBg==} + /@mantine/core/5.9.5_sangcu27ug4wpw4wjq5m7ftas4: + resolution: {integrity: sha512-A3cYzGOJ9BpU6tgqTl8qzOe8mmqzvuB76N6IHsPjk+uhbQCBXuNaoxOemP0wEM4HpEAzH1FR1kGhk6tO3gogUA==} peerDependencies: - '@mantine/hooks': 5.5.5 + '@mantine/hooks': 5.9.5 react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: '@floating-ui/react-dom-interactions': 0.10.1_rj7ozvcq3uehdlnj3cbwzbi5ce - '@mantine/hooks': 5.5.5_react@18.2.0 - '@mantine/styles': 5.5.5_7xbt6cqxny5okm7nuwm4cfiesq - '@mantine/utils': 5.5.5_react@18.2.0 - '@radix-ui/react-scroll-area': 1.0.0_biqbaboplfbrettd7655fr4n2y + '@mantine/hooks': 5.9.5_react@18.2.0 + '@mantine/styles': 5.9.5_7xbt6cqxny5okm7nuwm4cfiesq + '@mantine/utils': 5.9.5_react@18.2.0 + '@radix-ui/react-scroll-area': 1.0.2_biqbaboplfbrettd7655fr4n2y react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-textarea-autosize: 8.3.4_iapumuv4e6jcjznwuxpf4tt22e @@ -539,16 +539,16 @@ packages: - '@types/react' dev: false - /@mantine/hooks/5.5.5_react@18.2.0: - resolution: {integrity: sha512-qWZAZm203s0knYk6Ut4VPvur9H5BMul02cCb9E09s60iSjnLRHDC3f73VmSzmLZNVfgE+7hXUhguR/1lC7js4Q==} + /@mantine/hooks/5.9.5_react@18.2.0: + resolution: {integrity: sha512-6u0oj5zFYAP8bY+iW5Y5HEFS6tZmvJN5KwNPH+F2Omw61hN7shehHED7Jbe5zkxcggFvqmkA/6FMk+VYfovmkA==} peerDependencies: react: '>=16.8.0' dependencies: react: 18.2.0 dev: false - /@mantine/styles/5.5.5_7xbt6cqxny5okm7nuwm4cfiesq: - resolution: {integrity: sha512-cAYLoqQbnyoJepD7zstFF+A2/rj8DJblBzEdri0V+CWQccpqfP5xOTPEvCHVwQUODg40uT9PyVwFZ4h+zyyzvg==} + /@mantine/styles/5.9.5_7xbt6cqxny5okm7nuwm4cfiesq: + resolution: {integrity: sha512-Rixu60eVS9aP8ugTM0Yoc5MpXXsfemRlu2PDZGL0fhcOyUYHi5mXvlhgxqrET3zkFrBJ+PHuPLQBgBimffGqiw==} peerDependencies: '@emotion/react': '>=11.9.0' react: '>=16.8.0' @@ -561,8 +561,8 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false - /@mantine/utils/5.5.5_react@18.2.0: - resolution: {integrity: sha512-nt8+A0N1cV4cJNqlKuXazShoEwqtLohHNKFuywEdF3B6vHWdbY799PbT2WmqGTwNQ+se8y5/nBMmamBZrUN4fg==} + /@mantine/utils/5.9.5_react@18.2.0: + resolution: {integrity: sha512-OtMOvXMyqpZ+Tz25DYRwRkvERvmF4L0RJiq+JnXk+1yKDvG+JZQuMMLnt0nZ81T6q7uzDSA29cJ45syHL2BXmQ==} peerDependencies: react: '>=16.8.0' dependencies: @@ -1835,20 +1835,20 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false - /@radix-ui/react-primitive/1.0.0_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==} + /@radix-ui/react-primitive/1.0.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==} peerDependencies: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: '@babel/runtime': 7.19.4 - '@radix-ui/react-slot': 1.0.0_react@18.2.0 + '@radix-ui/react-slot': 1.0.1_react@18.2.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 dev: false - /@radix-ui/react-scroll-area/1.0.0_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-3SNFukAjS5remgtpAVR9m3Zgo23ZojBZ8V3TCyR3A+56x2mtVqKlPV4+e8rScZUFMuvtbjIdQCmsJBFBazKZig==} + /@radix-ui/react-scroll-area/1.0.2_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-k8VseTxI26kcKJaX0HPwkvlNBPTs56JRdYzcZ/vzrNUkDlvXBy8sMc7WvCpYzZkHgb+hd72VW9MqkqecGtuNgg==} peerDependencies: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 @@ -1860,15 +1860,15 @@ packages: '@radix-ui/react-context': 1.0.0_react@18.2.0 '@radix-ui/react-direction': 1.0.0_react@18.2.0 '@radix-ui/react-presence': 1.0.0_biqbaboplfbrettd7655fr4n2y - '@radix-ui/react-primitive': 1.0.0_biqbaboplfbrettd7655fr4n2y + '@radix-ui/react-primitive': 1.0.1_biqbaboplfbrettd7655fr4n2y '@radix-ui/react-use-callback-ref': 1.0.0_react@18.2.0 '@radix-ui/react-use-layout-effect': 1.0.0_react@18.2.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 dev: false - /@radix-ui/react-slot/1.0.0_react@18.2.0: - resolution: {integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==} + /@radix-ui/react-slot/1.0.1_react@18.2.0: + resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==} peerDependencies: react: ^16.8 || ^17.0 || ^18.0 dependencies: diff --git a/src/components/FloatingLabelInput.tsx b/src/components/FloatingLabelInput.tsx index 63822fc..617d2d2 100644 --- a/src/components/FloatingLabelInput.tsx +++ b/src/components/FloatingLabelInput.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { TextInput, createStyles } from '@mantine/core'; +import { TextInput, createStyles, Loader } from "@mantine/core"; const useStyles = createStyles((theme, { floating }: { floating: boolean }) => ({ root: { @@ -38,7 +38,7 @@ const useStyles = createStyles((theme, { floating }: { floating: boolean }) => ( }, })); -const FloatingLabelInput = ({label, description= '', value, onChange}) => { +const FloatingLabelInput = ({label, description= '', value, onChange, loading = false}) => { const [focused, setFocused] = useState(false); const { classes } = useStyles({ floating: value.trim().length !== 0 || focused }); @@ -53,6 +53,7 @@ const FloatingLabelInput = ({label, description= '', value, onChange}) => { mt="md" radius="md" autoComplete="nope" + rightSection={loading && } /> ); } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 679967f..ef75734 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,5 +1,5 @@ -import { ActionIcon, Grid, Text } from "@mantine/core"; -import { IconAdjustments } from "@tabler/icons"; +import { ActionIcon, Group, Text } from "@mantine/core"; +import { IconAdjustments, IconSearch, IconX } from "@tabler/icons"; import useStore from "~store/useStore"; interface HeaderProps { @@ -7,34 +7,40 @@ interface HeaderProps { description?: string; } -const Header = ({title, description = ""}: HeaderProps) => { - const {screen, setScreen} = useStore(state => state); - - const changeScreen = () => { - if (screen === 'settings') { - setScreen('home'); - } else { - setScreen('settings'); - } - } +const Header = ({ title, description = "" }: HeaderProps) => { + const { screen, setScreen } = useStore(state => state); return ( - - - + <> + + {title} - - {description} - - - - - - - - - ) -} + + setScreen(screen === "search" ? "home" : "search")} + > + {screen === "search" ? () : ()} + + setScreen(screen === "settings" ? "home" : "settings")} + > + + + + + + {description} + + + ); +}; export default Header; diff --git a/src/components/SearchItem.tsx b/src/components/SearchItem.tsx new file mode 100644 index 0000000..ee42342 --- /dev/null +++ b/src/components/SearchItem.tsx @@ -0,0 +1,101 @@ +import { Box, Button, Card, Collapse, createStyles, Divider, Group, Loader, Text } from "@mantine/core"; +import { useEffect, useState } from "react"; +import useStore from "~store/useStore"; +import { updateLocalStorageValue } from "~handlers/localStorageHandlers"; + +const useStyles = createStyles(() => ({ + card: { + ':hover': { + transition: 'background-color 150ms ease', + backgroundColor: '#f0f0f0', + }, + }, +})); + +interface SearchItemProps { + experiment: { + id: string; + name: string; + description: string; + }; +} + +const SearchItem = ({ experiment }: SearchItemProps) => { + const { classes } = useStyles(); + const { localStorageKey, setLocalStorageValue, optimizelyAccessToken } = useStore(state => state); + const [opened, setOpened] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [experimentData, setExperimentData] = useState(null); + + useEffect(() => { + const fetchExperiment = async () => { + if (opened) { + try { + setLoading(true); + const response = await fetch(`https://api.optimizely.com/v2/experiments/${experiment.id}`, { + method: "GET", + headers: { + Authorization: `Bearer ${optimizelyAccessToken}` + } + }); + const data = await response.json(); + setExperimentData(data); + } catch (error) { + console.error(error); + setError(error.message); + } finally { + setLoading(false); + } + } + }; + fetchExperiment(); + }, [opened]); + + const saveToLocalStorage = (value) => { + setLocalStorageValue(value); + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + updateLocalStorageValue(tabs[0].id, localStorageKey, value); + }); + }; + + return ( + setOpened(true)} + className={classes.card} + style={{ cursor: "pointer" }} + > + {experiment.name} + + {experiment.description} + + + + {loading ? () : ( + + + Whitelisted users + + {experimentData?.whitelist.map((item) => ( + + ))} + + + )} + {error && ( + + {error} + + )} + + + ); +}; + +export default SearchItem; diff --git a/src/components/settings/AccessTokenInputField.tsx b/src/components/settings/AccessTokenInputField.tsx new file mode 100644 index 0000000..7a442b0 --- /dev/null +++ b/src/components/settings/AccessTokenInputField.tsx @@ -0,0 +1,22 @@ +import { PasswordInput } from "@mantine/core"; +import useStore from "~store/useStore"; + +const description = ( + Get your access token here. +); + +const AccessTokenInputField = () => { + const { setOptimizelyAccessToken, optimizelyAccessToken } = useStore(state => state); + + return ( + setOptimizelyAccessToken(e.target.value)} + /> + ); +}; + +export default AccessTokenInputField; diff --git a/src/components/settings/DefaultScreenField.tsx b/src/components/settings/DefaultScreenField.tsx new file mode 100644 index 0000000..28aa3e4 --- /dev/null +++ b/src/components/settings/DefaultScreenField.tsx @@ -0,0 +1,28 @@ +import { SegmentedControl, Text } from "@mantine/core"; +import useStore from "~store/useStore"; + +const DefaultScreenField = () => { + const { defaultScreen, setDefaultScreen } = useStore(state => state); + + return ( + <> + + Default screen + + + + ); +}; + +export default DefaultScreenField; diff --git a/src/components/settings/LocalStorageInputField.tsx b/src/components/settings/LocalStorageInputField.tsx new file mode 100644 index 0000000..e3e7402 --- /dev/null +++ b/src/components/settings/LocalStorageInputField.tsx @@ -0,0 +1,18 @@ +import { TextInput } from "@mantine/core"; +import useStore from "~store/useStore"; + +const LocalStorageField = () => { + const { localStorageKey, setLocalStorageKey } = useStore(state => state); + + return ( + setLocalStorageKey(e.target.value)} + mb="lg" + /> + ); +}; + +export default LocalStorageField; diff --git a/src/popup/index.tsx b/src/popup/index.tsx index 1779cdb..b5c06c5 100644 --- a/src/popup/index.tsx +++ b/src/popup/index.tsx @@ -1,20 +1,26 @@ import { useEffect } from "react"; import { Storage } from "@plasmohq/storage" import useStore from "~store/useStore"; -import HomeScreen from "~screens/HomeScreen"; -import SettingsScreen from "~screens/SettingsScreen"; +import HomeScreen from "~screens/Home"; +import Settings from "~screens/Settings"; +import Search from "~screens/Search"; +import type { Screen } from "~types/types"; const storage = new Storage() function IndexPopup() { - const {setLocalStorageValue, screen, setLocalStorageKey} = useStore(state => state); + const {setLocalStorageValue, screen, setLocalStorageKey, setOptimizelyAccessToken, setScreen} = useStore(state => state); useEffect(() => { const load = async () => { const key = await storage.get("localStorageKey"); const value = await storage.get("localStorageValue"); + const defaultScreen = await storage.get("defaultScreen"); + const optimizelyAccessToken = await storage.get("optimizelyAccessToken"); setLocalStorageKey(key ?? "optimizelyNonLoggedInUser"); setLocalStorageValue(value ?? ""); + setOptimizelyAccessToken(optimizelyAccessToken ?? ""); + setScreen(defaultScreen ?? "home"); } load(); }, []); @@ -22,7 +28,8 @@ function IndexPopup() { return (
{screen === "home" && } - {screen === "settings" && } + {screen === "settings" && } + {screen === "search" && }
); } diff --git a/src/screens/HomeScreen.tsx b/src/screens/Home.tsx similarity index 100% rename from src/screens/HomeScreen.tsx rename to src/screens/Home.tsx diff --git a/src/screens/Search.tsx b/src/screens/Search.tsx new file mode 100644 index 0000000..0e9948d --- /dev/null +++ b/src/screens/Search.tsx @@ -0,0 +1,83 @@ +import { useEffect, useState } from "react"; +import { Card, Text } from "@mantine/core"; +import { useDebouncedValue } from "@mantine/hooks"; +import useStore from "~store/useStore"; +import FloatingLabelInput from "~components/FloatingLabelInput"; +import Header from "~components/Header"; +import SearchItem from "~components/SearchItem"; + +function Search() { + const { optimizelyAccessToken, setScreen } = useStore(state => state); + const [value, setValue] = useState(""); + const [debounced] = useDebouncedValue(value, 300); + const [loading, setLoading] = useState(false); + const [experiments, setExperiments] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + const search = async () => { + try { + if (debounced.length > 0) { + setLoading(true); + const response = await fetch(`https://api.optimizely.com/v2/search?project_id=17209910795&per_page=100&page=1&query=${value}&type=experiment`, { + method: "GET", + headers: { + Authorization: `Bearer ${optimizelyAccessToken}` + } + }); + if (response.ok) { + const data = await response.json(); + setExperiments(data); + } else { + setError("Error searching for experiments"); + } + } + } catch (error) { + console.log(error); + setError(error); + } finally { + setLoading(false); + } + }; + search(); + }, [debounced]); + + return ( + +
+ {optimizelyAccessToken ? ( + ) : ( + + Optimizely access token not found. Please go to the setScreen("settings")} href="#">settings screen to set it. + + )} + {experiments?.length > 0 && ( + <> + + {experiments.length} experiments found + + {experiments.map((experiment) => ( + + ))} + + )} + {experiments?.length === 0 && ( + + No experiments found + + )} + {error && ( + + {error} + + )} + + ); +} + +export default Search; diff --git a/src/screens/Settings.tsx b/src/screens/Settings.tsx new file mode 100644 index 0000000..5387e48 --- /dev/null +++ b/src/screens/Settings.tsx @@ -0,0 +1,24 @@ +import { Anchor, Card, Center } from "@mantine/core"; +import Header from "~components/Header"; +import AccessTokenInputField from "~components/settings/AccessTokenInputField"; +import LocalStorageField from "~components/settings/LocalStorageInputField"; +import DefaultScreenField from "~components/settings/DefaultScreenField"; + +const Settings = () => ( + +
+ + + +
+ + GitHub + +
+ +); + +export default Settings; diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx deleted file mode 100644 index 81581f9..0000000 --- a/src/screens/SettingsScreen.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Anchor, Card, Center, TextInput } from "@mantine/core"; -import useStore from "~store/useStore"; -import Header from "~components/Header"; - -const SettingsScreen = () => { - const { localStorageKey, setLocalStorageKey } = useStore(state => state); - - return ( - -
- setLocalStorageKey(e.target.value)} - /> -
- - GitHub - -
- - ); -}; - -export default SettingsScreen; diff --git a/src/store/useStore.ts b/src/store/useStore.ts index 88fa2e9..2f6281d 100644 --- a/src/store/useStore.ts +++ b/src/store/useStore.ts @@ -1,36 +1,47 @@ import create from 'zustand'; import produce from 'immer'; import { Storage } from "@plasmohq/storage" +import type { Screen } from '~/types/types'; interface FieldState { - screen: string; - setScreen: (screen: string) => void; - + screen: Screen; + setScreen: (screen: Screen) => void; localStorageKey: string; setLocalStorageKey: (localStorageKey: string) => void; localStorageValue: string; setLocalStorageValue: (value: string) => void; + defaultScreen: Screen; + setDefaultScreen: (screen: Screen) => void; + optimizelyAccessToken: string; + setOptimizelyAccessToken: (token: string) => void; } const storage = new Storage() const useStore = create((set) => ({ screen: 'home', - setScreen: (screen: string) => set(() => ({ screen })), - - // LocalStorage Key + defaultScreen: 'home', localStorageKey: '', - setLocalStorageKey: (value: string) => set(produce((state) => { - state.localStorageKey = value; - storage.set("localStorageKey", value); - })), + localStorageValue: '', + optimizelyAccessToken: '', - // LocalStorage Value - localStorageValue: "", - setLocalStorageValue: (value: string) => set(produce((state) => { + setScreen: (screen) => set(() => ({ screen })), + setDefaultScreen: (screen) => set(produce((state) => { + state.defaultScreen = screen; + storage.set("defaultScreen", screen); + })), + setLocalStorageKey: (localStorageKey) => set(produce((state) => { + state.localStorageKey = localStorageKey; + storage.set("localStorageKey", localStorageKey); + })), + setLocalStorageValue: (value) => set(produce((state) => { state.localStorageValue = value; storage.set("localStorageValue", value); })), + setOptimizelyAccessToken: (token) => set(produce((state) => { + state.optimizelyAccessToken = token; + storage.set("optimizelyAccessToken", token); + })), })); export default useStore; diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 0000000..487738a --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1 @@ +export type Screen = 'home' | 'settings' | 'search';