diff --git a/.eslintrc.js b/.eslintrc.js index 9f4ca73..735ab2c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -21,6 +21,7 @@ module.exports = { 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:typescript-sort-keys/recommended', + 'plugin:react-hooks/recommended', ], globals: { browser: true, diff --git a/package-lock.json b/package-lock.json index d26469c..6ec6dba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "eslint-plugin-jest-dom": "^4.0.3", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.0", + "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-sort-keys-fix": "^1.1.2", "eslint-plugin-testing-library": "^5.9.1", "eslint-plugin-typescript-sort-keys": "^2.1.0", @@ -9693,10 +9694,11 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -30036,9 +30038,9 @@ } }, "eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index 251cb8a..ed7b38d 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "eslint-plugin-jest-dom": "^4.0.3", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.0", + "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-sort-keys-fix": "^1.1.2", "eslint-plugin-testing-library": "^5.9.1", "eslint-plugin-typescript-sort-keys": "^2.1.0", diff --git a/src/components/CustomProjectNameList.tsx b/src/components/CustomProjectNameList.tsx new file mode 100644 index 0000000..b502681 --- /dev/null +++ b/src/components/CustomProjectNameList.tsx @@ -0,0 +1,93 @@ +import React, { useCallback } from 'react'; +import { ProjectName } from '../utils/settings'; + +type Props = { + handleChange: (sites: ProjectName[]) => void; + helpText: string; + label: string; + projectNamePlaceholder?: string; + sites: ProjectName[]; + urlPlaceholder?: string; +}; + +export default function CustomProjectNameList({ + handleChange, + label, + urlPlaceholder, + projectNamePlaceholder, + sites, +}: Props): JSX.Element { + const handleAddNewSite = useCallback(() => { + handleChange([...sites, { projectName: '', url: '' }]); + }, [handleChange, sites]); + + const handleUrlChangeForSite = useCallback( + (event: React.ChangeEvent, index: number) => { + handleChange( + sites.map((item, i) => (i === index ? { ...item, url: event.target.value } : item)), + ); + }, + [handleChange, sites], + ); + + const handleOnProjectNameChange = useCallback( + (event: React.ChangeEvent, index: number) => { + handleChange( + sites.map((item, i) => (i === index ? { ...item, projectName: event.target.value } : item)), + ); + }, + [handleChange, sites], + ); + + const handleRemoveSite = useCallback( + (index: number) => { + handleChange(sites.filter((_, i) => i !== index)); + }, + [handleChange, sites], + ); + + return ( +
+ + + {sites.length > 0 && ( +
+ {sites.map((site, i) => ( +
+
+ handleUrlChangeForSite(e, i)} + /> +
+
+ handleOnProjectNameChange(e, i)} + /> +
+ +
+ ))} +
+ )} + + +
+ ); +} diff --git a/src/components/Options.tsx b/src/components/Options.tsx index 6dac946..33fdee2 100644 --- a/src/components/Options.tsx +++ b/src/components/Options.tsx @@ -1,9 +1,10 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import config, { SuccessOrFailType } from '../config/config'; import apiKeyInvalid from '../utils/apiKey'; import { IS_CHROME } from '../utils/operatingSystem'; -import { getSettings, saveSettings, Settings } from '../utils/settings'; +import { getSettings, ProjectName, saveSettings, Settings } from '../utils/settings'; import { logUserIn } from '../utils/user'; +import CustomProjectNameList from './CustomProjectNameList'; import SitesList from './SitesList'; interface State extends Settings { @@ -19,6 +20,7 @@ export default function Options(): JSX.Element { allowList: [], apiKey: '', apiUrl: config.apiUrl, + customProjectNames: [], denyList: [], extensionStatus: 'allGood', hostname: '', @@ -31,37 +33,42 @@ export default function Options(): JSX.Element { trackSocialMedia: config.trackSocialMedia, }); + const isApiKeyValid = useMemo(() => apiKeyInvalid(state.apiKey) === '', [state.apiKey]); + const loggingStyleRef = useRef(null); - const restoreSettings = async (): Promise => { + const restoreSettings = useCallback(async () => { const settings = await getSettings(); - setState({ - ...state, + setState((oldState) => ({ + ...oldState, ...settings, - }); - }; + })); + }, []); useEffect(() => { void restoreSettings(); - }, []); + }, [restoreSettings]); const handleSubmit = async () => { if (state.loading) return; - setState({ ...state, loading: true }); + setState((oldState) => ({ ...oldState, loading: true })); if (state.apiUrl.endsWith('/')) { state.apiUrl = state.apiUrl.slice(0, -1); } await saveSettings({ - allowList: state.allowList, + allowList: state.allowList.filter((item) => !!item.trim()), apiKey: state.apiKey, apiUrl: state.apiUrl, - denyList: state.denyList, + customProjectNames: state.customProjectNames.filter( + (item) => !!item.url.trim() && !!item.projectName.trim(), + ), + denyList: state.denyList.filter((item) => !!item.trim()), extensionStatus: state.extensionStatus, hostname: state.hostname, loggingEnabled: state.loggingEnabled, loggingStyle: state.loggingStyle, loggingType: state.loggingType, - socialMediaSites: state.socialMediaSites, + socialMediaSites: state.socialMediaSites.filter((item) => !!item.trim()), theme: state.theme, trackSocialMedia: state.trackSocialMedia, }); @@ -72,46 +79,56 @@ export default function Options(): JSX.Element { } }; - const updateDenyListState = (sites: string) => { - setState({ - ...state, - denyList: sites.trim().split('\n'), - }); - }; + const updateDenyListState = useCallback((denyList: string[]) => { + setState((oldState) => ({ + ...oldState, + denyList, + })); + }, []); - const updateAllowListState = (sites: string) => { - setState({ - ...state, - allowList: sites.trim().split('\n'), - }); - }; + const updateAllowListState = useCallback((allowList: string[]) => { + setState((oldState) => ({ + ...oldState, + allowList, + })); + }, []); - const updateLoggingStyle = (style: string) => { - setState({ - ...state, + const updateCustomProjectNamesState = useCallback((customProjectNames: ProjectName[]) => { + setState((oldState) => ({ + ...oldState, + customProjectNames, + })); + }, []); + + const updateLoggingStyle = useCallback((style: string) => { + setState((oldState) => ({ + ...oldState, loggingStyle: style === 'allow' ? 'allow' : 'deny', - }); - }; + })); + }, []); - const updateLoggingType = (type: string) => { - setState({ - ...state, + const updateLoggingType = useCallback((type: string) => { + setState((oldState) => ({ + ...oldState, loggingType: type === 'url' ? 'url' : 'domain', - }); - }; + })); + }, []); - const updateTheme = (theme: string) => { - setState({ - ...state, + const updateTheme = useCallback((theme: string) => { + setState((oldState) => ({ + ...oldState, theme: theme === 'light' ? 'light' : 'dark', - }); - }; + })); + }, []); - const toggleSocialMedia = () => { - setState({ ...state, trackSocialMedia: !state.trackSocialMedia }); - }; + const toggleSocialMedia = useCallback(() => { + setState((oldState) => ({ + ...oldState, + trackSocialMedia: !oldState.trackSocialMedia, + })); + }, []); - const loggingStyle = function () { + const loggingStyle = useCallback(() => { // TODO: rewrite SitesList to be structured inputs instead of textarea if (state.loggingStyle == 'deny') { @@ -119,7 +136,7 @@ export default function Options(): JSX.Element { ); @@ -128,14 +145,18 @@ export default function Options(): JSX.Element { ); - }; - - const isApiKeyValid = apiKeyInvalid(state.apiKey) === ''; + }, [ + state.allowList, + state.denyList, + state.loggingStyle, + updateAllowListState, + updateDenyListState, + ]); return (
@@ -168,8 +189,8 @@ export default function Options(): JSX.Element { value={state.loggingStyle} onChange={(e) => updateLoggingStyle(e.target.value)} > - - + +
@@ -221,6 +242,13 @@ export default function Options(): JSX.Element { + +
{ - setState({ - ...state, - socialMediaSites: sites.split('\n'), - }); + handleChange={(socialMediaSites) => { + setState((oldState) => ({ + ...oldState, + socialMediaSites, + })); }} label="Social" - sites={state.socialMediaSites.join('\n')} + sites={state.socialMediaSites} helpText="Sites that you don't want to show in your reports." - rows={5} />
diff --git a/src/components/SitesList.tsx b/src/components/SitesList.tsx index 86023ca..2f6ce29 100644 --- a/src/components/SitesList.tsx +++ b/src/components/SitesList.tsx @@ -1,47 +1,74 @@ -import React from 'react'; +import React, { useCallback } from 'react'; type Props = { - handleChange: (sites: string) => void; + handleChange: (sites: string[]) => void; helpText: string; label: string; - placeholder?: string; - rows?: number; - sites: string; + projectNamePlaceholder?: string; + sites: string[]; + urlPlaceholder?: string; }; export default function SitesList({ handleChange, label, - placeholder, - rows, + urlPlaceholder, sites, helpText, }: Props): JSX.Element { - const textareaChange = (event: React.ChangeEvent) => { - handleChange(event.target.value); - }; + const handleAddNewSite = useCallback(() => { + handleChange([...sites, '']); + }, [handleChange, sites]); + + const handleUrlChangeForSite = useCallback( + (event: React.ChangeEvent, index: number) => { + handleChange(sites.map((item, i) => (i === index ? event.target.value : item))); + }, + [handleChange, sites], + ); + + const handleRemoveSite = useCallback( + (index: number) => { + handleChange(sites.filter((_, i) => i !== index)); + }, + [handleChange, sites], + ); return ( -
-