diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 0294f93..0000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -tailwind.config.js \ No newline at end of file diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index cb7766d..0000000 --- a/.eslintrc +++ /dev/null @@ -1,60 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "react-refresh", - "@typescript-eslint", - "react-hooks", - "import", - "prettier" - ], - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:react-hooks/recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:import/typescript" - ], - "rules": { - "prettier/prettier": "error", - "no-prototype-builtins": "off", - "no-console": ["error", { "allow": ["warn", "error"] }], - "require-await": "error", - "react/jsx-uses-react": "off", - "react/react-in-jsx-scope": "off", - "@typescript-eslint/no-empty-function": "off", - "react/no-unescaped-entities": "off", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "error", - "react-refresh/only-export-components": ["warn", { "allowConstantExport": true }], - "import/order": [ - "error", - { - "groups": [["builtin", "external"], ["internal"], ["parent", "sibling", "index"]], - "newlines-between": "always" - } - ], - "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }], - "semi": ["error", "always"] - }, - "overrides": [ - { - "files": ["**/*.d.ts"], - "rules": { - "@typescript-eslint/no-explicit-any": "off" - } - } - ], - "settings": { - "import/resolver": { - "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx"] - }, - "typescript": {} - }, - "react": { - "version": "detect" - } - } -} diff --git a/.prettierrc.json b/.prettierrc.json index fd90f7a..f7bd593 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,5 +1,6 @@ { + "plugins": ["prettier-plugin-tailwindcss"], "printWidth": 105, "singleQuote": true, "endOfLine": "auto" -} \ No newline at end of file +} diff --git a/bun.lockb b/bun.lockb index 145ab49..7b61911 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..57a0145 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,116 @@ +import { fixupConfigRules, fixupPluginRules } from '@eslint/compat'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import reactHooks from 'eslint-plugin-react-hooks'; +import _import from 'eslint-plugin-import'; +import prettier from 'eslint-plugin-prettier'; +import tsParser from '@typescript-eslint/parser'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { FlatCompat } from '@eslint/eslintrc'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + files: ['**/*.ts', '**/*.tsx'], + }, + { + ignores: ['**/*tailwind.config.ts'], + }, + ...fixupConfigRules( + compat.extends( + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/typescript' + ) + ), + { + plugins: { + 'react-refresh': reactRefresh, + '@typescript-eslint': fixupPluginRules(typescriptEslint), + 'react-hooks': fixupPluginRules(reactHooks), + import: fixupPluginRules(_import), + prettier, + }, + + languageOptions: { + parser: tsParser, + }, + + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + + typescript: {}, + }, + + react: { + version: 'detect', + }, + }, + + rules: { + 'prettier/prettier': 'error', + 'no-prototype-builtins': 'off', + + 'no-console': [ + 'error', + { + allow: ['warn', 'error'], + }, + ], + + 'require-await': 'error', + 'react/jsx-uses-react': 'off', + 'react/react-in-jsx-scope': 'off', + '@typescript-eslint/no-empty-function': 'off', + 'react/no-unescaped-entities': 'off', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + + 'react-refresh/only-export-components': [ + 'warn', + { + allowConstantExport: true, + }, + ], + + 'import/order': [ + 'error', + { + groups: [['builtin', 'external'], ['internal'], ['parent', 'sibling', 'index']], + 'newlines-between': 'always', + }, + ], + + '@typescript-eslint/no-unused-vars': [ + 'error', + { + ignoreRestSiblings: true, + }, + ], + + semi: ['error', 'always'], + }, + }, + { + files: ['**/*.d.ts'], + + rules: { + '@typescript-eslint/no-explicit-any': 'off', + }, + }, +]; diff --git a/manifest.json b/manifest.json index c0b041f..e04306b 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "pile", "description": "pile", - "version": "1.10.0", + "version": "1.11.0", "icons": { "16": "src/content/icons/icon-16.png", "48": "src/content/icons/icon-48.png", diff --git a/package.json b/package.json index a0ef982..35a96f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pile", - "version": "1.10.0", + "version": "1.11.0", "author": "Emmanuel Krebs ", "license": "MIT", "type": "module", @@ -9,7 +9,7 @@ "build": "bun run clean && bunx --bun vite build", "zip": "cd dist && bestzip ../pile.zip *", "clean": "bun run clean.ts", - "lint": "eslint --ext .ts,.tsx src", + "lint": "eslint src", "type-check": "tsc --noEmit", "setup-ci": "cp ./src/env.sample.json ./src/env.json" }, @@ -23,52 +23,51 @@ "webext-prod": {} }, "dependencies": { - "@algolia/requester-fetch": "^4.23.3", - "@react-aria/checkbox": "^3.9.0", - "@react-aria/textfield": "^3.9.1", - "@react-aria/visually-hidden": "^3.8.0", - "@react-stately/toggle": "^3.5.1", - "algoliasearch": "^4.23.3", - "classnames": "^2.3.2", - "date-fns": "^2.30.0", - "extract-colors": "^3.0.0", - "radash": "^10.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "@algolia/requester-fetch": "^5.17.1", + "@react-aria/checkbox": "^3.15.0", + "@react-aria/textfield": "^3.15.0", + "@react-aria/visually-hidden": "^3.8.18", + "@react-stately/toggle": "^3.8.0", + "algoliasearch": "^5.17.1", + "classnames": "^2.5.1", + "date-fns": "^4.1.0", + "extract-colors": "^4.1.1", + "radash": "^12.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-feather": "^2.0.10", - "react-hotkeys-hook": "^4.4.0", + "react-hotkeys-hook": "^4.6.1", "react-query": "^3.39.3" }, "devDependencies": { "@crxjs/vite-plugin": "^2.0.0-beta.28", - "@e-krebs/react-library": "^0.0.23", - "@tailwindcss/typography": "^0.5.9", - "@types/bun": "^1.1.8", - "@types/chrome": "^0.0.236", - "@types/express": "^4.17.17", - "@types/node": "^20.16.1", - "@types/react": "^18.2.6", - "@types/react-dom": "^18.2.4", - "@typescript-eslint/eslint-plugin": "^5.60.1", - "@typescript-eslint/parser": "^5.60.1", - "@vitejs/plugin-react": "^4.3.1", + "@e-krebs/react-library": "^0.0.41", + "@eslint/compat": "^1.2.4", + "@tailwindcss/typography": "^0.5.15", + "@types/bun": "^1.1.14", + "@types/chrome": "^0.0.287", + "@types/node": "^20.17.10", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.2", + "@typescript-eslint/eslint-plugin": "^8.18.0", + "@typescript-eslint/parser": "^8.18.0", + "@vitejs/plugin-react": "^4.3.4", "bestzip": "^2.2.1", - "eslint": "^8.44.0", - "eslint-import-resolver-typescript": "^3.5.5", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.11", - "parcel": "^2.11.0", - "parcel-namer-rewrite": "2.10.3-rc.2", - "postcss": "^8.4.31", - "prettier": "^2.8.8", - "prettier-plugin-tailwindcss": "^0.3.0", - "tailwindcss": "^3.3.2", - "typescript": "^5.0.4", - "vite": "^5.4.2", - "vite-plugin-svgr": "^4.2.0" + "eslint": "^9.17.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.16", + "postcss": "^8.4.49", + "prettier": "^3.4.2", + "prettier-plugin-tailwindcss": "^0.6.9", + "tailwindcss": "^3.4.16", + "tailwindcss-react-aria-components": "^1.2.0", + "typescript": "^5.7.2", + "vite": "^6.0.3", + "vite-plugin-svgr": "^4.3.0" }, "resolutions": { "json5": "2.2.3", diff --git a/src/components/Action.tsx b/src/components/Action.tsx index 1fb0573..84bcbcc 100644 --- a/src/components/Action.tsx +++ b/src/components/Action.tsx @@ -21,7 +21,7 @@ export const Action: FC = ({ icon, rgb, title, onClick, loading }) className={cx( 'shrink-0 cursor-pointer', 'flex h-10 w-10 items-center justify-center', - 'rounded-full hover:bg-gray-100 hover:dark:bg-gray-800' + 'rounded-full hover:bg-gray-100 hover:dark:bg-gray-800', )} title={title} onClick={onClick} @@ -30,7 +30,7 @@ export const Action: FC = ({ icon, rgb, title, onClick, loading }) className={cx( 'h-5 w-5', loading && 'animate-spin', - !color && 'text-gray-900 dark:text-gray-100' + !color && 'text-gray-900 dark:text-gray-100', )} style={{ color }} /> diff --git a/src/components/Autocomplete.tsx b/src/components/Autocomplete.tsx index 1754a0e..74ba1cd 100644 --- a/src/components/Autocomplete.tsx +++ b/src/components/Autocomplete.tsx @@ -47,15 +47,15 @@ export const Autocomplete: FC = ({ allOptions .filter((option) => !inputOption || option.value.includes(inputOption)) .sort( - (option1, option2) => option1.value.indexOf(inputOption) - option2.value.indexOf(inputOption) + (option1, option2) => option1.value.indexOf(inputOption) - option2.value.indexOf(inputOption), ) .slice(0, 4), - [allOptions, inputOption] + [allOptions, inputOption], ); const newOption: Option = useMemo( () => (optionIndex === null ? { value: inputOption } : options[optionIndex]), - [options, inputOption, optionIndex] + [options, inputOption, optionIndex], ); const add = async (option?: string) => { @@ -104,7 +104,7 @@ export const Autocomplete: FC = ({
    = ({ key={option.value} className={cx( 'cursor-pointer truncate rounded-sm px-1 leading-5', - 'hover:bg-white hover:dark:bg-gray-900 ', - index === optionIndex && 'bg-white dark:bg-gray-900' + 'hover:bg-white hover:dark:bg-gray-900', + index === optionIndex && 'bg-white dark:bg-gray-900', )} onClick={() => add(option.value)} > diff --git a/src/components/List/Item/ItemComponent.tsx b/src/components/List/Item/ItemComponent.tsx index 41f03e9..f84b65c 100644 --- a/src/components/List/Item/ItemComponent.tsx +++ b/src/components/List/Item/ItemComponent.tsx @@ -27,7 +27,7 @@ export const ItemComponent: FC> = forwardRef
    @@ -57,5 +57,5 @@ export const ItemComponent: FC> = forwardRef
    ); - } + }, ); diff --git a/src/components/List/Item/TagAutocomplete.tsx b/src/components/List/Item/TagAutocomplete.tsx index bacf6d8..84bf136 100644 --- a/src/components/List/Item/TagAutocomplete.tsx +++ b/src/components/List/Item/TagAutocomplete.tsx @@ -18,7 +18,7 @@ export const TagAutocomplete: FC = () => { const options: Option[] = useMemo( () => allTags.filter((tag) => !tags.includes(tag)).map((value) => ({ value })), - [allTags, tags] + [allTags, tags], ); const close = useCallback(() => { @@ -35,7 +35,7 @@ export const TagAutocomplete: FC = () => { } close(); }, - [close, getQueryKey, id, queryClient, service] + [close, getQueryKey, id, queryClient, service], ); if (!service.isUpdatable) return null; diff --git a/src/components/List/Item/Tags.tsx b/src/components/List/Item/Tags.tsx index 99c79b1..ff377d4 100644 --- a/src/components/List/Item/Tags.tsx +++ b/src/components/List/Item/Tags.tsx @@ -40,14 +40,14 @@ export const Tags: FC = () => { isAddTagsOpen ? 'max-w-[calc(50%+1rem)]' : 'max-w-[25%]', 'transition-max-width hover:max-w-[calc(50%+1rem)]', !color && 'text-gray-900 dark:text-gray-100', - !borderColor && 'border-gray-900 dark:border-gray-100' + !borderColor && 'border-gray-900 dark:border-gray-100', )} style={{ color, borderColor }} >
    @@ -61,7 +61,7 @@ export const Tags: FC = () => { 'group ml-1 flex items-center pl-px', 'border-b border-transparent', !isLoading && 'border-dashed hover:border-inherit', - !isLoading && service.isUpdatable && 'cursor-pointer ' + !isLoading && service.isUpdatable && 'cursor-pointer', )} title={service.isUpdatable ? `remove tag [${tag}]` : tag} onClick={() => (isLoading || !service.isUpdatable ? {} : remove(tag))} @@ -71,7 +71,7 @@ export const Tags: FC = () => { className={cx( 'mb-[-3px] h-3', service.isUpdatable && 'transition-width group-hover:w-3', - isAddTagsOpen ? 'w-3' : 'w-0' + isAddTagsOpen ? 'w-3' : 'w-0', )} />
    @@ -88,7 +88,7 @@ export const Tags: FC = () => { diff --git a/src/components/List/SearchFilter.tsx b/src/components/List/SearchFilter.tsx index 3638c29..761086c 100644 --- a/src/components/List/SearchFilter.tsx +++ b/src/components/List/SearchFilter.tsx @@ -37,7 +37,7 @@ export const SearchFilter: FC> = ({ openSearch(false); onSearch(); }, - { enableOnFormTags: ['INPUT'] } + { enableOnFormTags: ['INPUT'] }, ); return ( diff --git a/src/components/List/TagFilter.tsx b/src/components/List/TagFilter.tsx index 4d62dbd..25a92c2 100644 --- a/src/components/List/TagFilter.tsx +++ b/src/components/List/TagFilter.tsx @@ -55,7 +55,7 @@ export const TagFilter: FC = ({ hasUntaggedItem, tagOpen, openTa e.preventDefault(); onTag(); }, - { enableOnFormTags: ['INPUT'] } + { enableOnFormTags: ['INPUT'] }, ); return ( @@ -65,14 +65,14 @@ export const TagFilter: FC = ({ hasUntaggedItem, tagOpen, openTa 'flex cursor-pointer space-x-2 py-1', hasTag ? 'px-2' : 'px-1', hasTag && 'rounded-lg border border-gray-400 text-gray-500 dark:text-gray-400', - hasTag && 'bg-gray-100 dark:bg-gray-800' + hasTag && 'bg-gray-100 dark:bg-gray-800', )} onClick={hasTag ? () => onTag() : toggleTagOpen} title={tagOpen ? 'Close tag filter (or press )' : 'Filter by tag (or press )'} > {hasTag && ( -
    +
    {tag ? tag : 'untagged'}
    diff --git a/src/components/List/index.tsx b/src/components/List/index.tsx index 4dac17f..87f7e3a 100644 --- a/src/components/List/index.tsx +++ b/src/components/List/index.tsx @@ -3,7 +3,7 @@ import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { Loader, RefreshCw, Tag } from 'react-feather'; import { useHotkeys } from 'react-hotkeys-hook'; import { useQuery, useQueryClient } from 'react-query'; -import formatDistanceToNow from 'date-fns/formatDistanceToNow'; +import { formatDistanceToNow } from 'date-fns/formatDistanceToNow'; import { clearCache } from 'utils/dataCache'; import { ListItem } from 'utils/typings'; @@ -51,7 +51,7 @@ export const List: FC = () => { const isMatching = useCallback( (url: string) => activeTab !== undefined && urlsAreMatching(url, activeTab), - [activeTab] + [activeTab], ); useEffect(() => { @@ -97,7 +97,7 @@ export const List: FC = () => { const formattedTimestamp: string | null = useMemo( () => data?.timestamp === undefined ? null : formatDistanceToNow(data.timestamp, { addSuffix: true }), - [data] + [data], ); const refresh = useCallback(async () => { @@ -163,7 +163,7 @@ export const List: FC = () => { className={cx( 'mx-2 h-4 w-4', isRefreshing && 'animate-spin', - searchOpen ? 'cursor-not-allowed' : 'cursor-pointer' + searchOpen ? 'cursor-not-allowed' : 'cursor-pointer', )} onClick={searchOpen ? () => {} : refresh} /> diff --git a/src/components/LoadingIcon.tsx b/src/components/LoadingIcon.tsx index 3519e00..a24914c 100644 --- a/src/components/LoadingIcon.tsx +++ b/src/components/LoadingIcon.tsx @@ -1,4 +1,3 @@ -import cx from 'classnames'; import { FC, PropsWithChildren, useCallback, useMemo, useState } from 'react'; import { type IconProps, Loader } from 'react-feather'; import { Button } from '@e-krebs/react-library'; @@ -6,7 +5,6 @@ import { Button } from '@e-krebs/react-library'; interface IProps { disabled?: boolean; startIcon?: FC; - className?: string; onClick: () => Promise | unknown; options?: { disableLoader?: boolean; @@ -17,14 +15,11 @@ const waitFn = (duration = 500): Promise => { return new Promise((resolve) => setTimeout(resolve, duration)); }; -const LoadingIcon: FC = ({ className, ...props }) => ( - -); +const LoadingIcon: FC = (props) => ; export const LoaderButton: FC> = ({ disabled = false, startIcon: StartIcon, - className, onClick, options, children, @@ -33,11 +28,7 @@ export const LoaderButton: FC> = ({ const [loading, setLoading] = useState(false); const Icon = useMemo(() => { if (!StartIcon) return; - const ActualIcon = loading ? LoadingIcon : StartIcon; - const ResultIcon: FC = ({ className, ...props }) => ( - - ); - return ResultIcon; + return loading ? LoadingIcon : StartIcon; }, [StartIcon, loading]); const innerDisabled = useMemo(() => disabled || loading, [disabled, loading]); @@ -51,12 +42,7 @@ export const LoaderButton: FC> = ({ }, [disableLoader, onClick]); return ( - ); diff --git a/src/components/SearchInput.tsx b/src/components/SearchInput.tsx index 7578610..eb041b4 100644 --- a/src/components/SearchInput.tsx +++ b/src/components/SearchInput.tsx @@ -28,7 +28,7 @@ export const SearchInput: FC = ({ className, onSearch }) => { className={cx( className, 'ml-4 mr-2 flex h-8 items-center px-2 text-base leading-8', - 'rounded-md border border-gray-200' + 'rounded-md border border-gray-200', )} > = ({ active = false, onClick, rounded = 't-md', ... color, rounded === 't-md' && 'rounded-t-md', rounded === 'full' && 'rounded-full', - active && 'border-b-2' + active && 'border-b-2', )} onClick={onClick} > diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx index 63da204..9a39a64 100644 --- a/src/components/Tabs/Tabs.tsx +++ b/src/components/Tabs/Tabs.tsx @@ -50,7 +50,7 @@ export const Tabs: FC> = ({ if (tabIndex === 0) tabIndex = 10; if (isNaN(tabIndex) || tabIndex > tabs.length) return; await selectedTabByIndex(tabIndex - 1); - } + }, ); return ( @@ -58,7 +58,7 @@ export const Tabs: FC> = ({
    {tabs.map(({ content, ...tab }, index) => ( diff --git a/src/content/img/smiley.svg b/src/content/img/smiley.svg index 797d25a..d210e80 100644 --- a/src/content/img/smiley.svg +++ b/src/content/img/smiley.svg @@ -1,4 +1,6 @@ - - - + + + \ No newline at end of file diff --git a/src/helpers/localstorage.ts b/src/helpers/localstorage.ts index 73031ed..9bec071 100644 --- a/src/helpers/localstorage.ts +++ b/src/helpers/localstorage.ts @@ -1,6 +1,6 @@ export const getLocalStorageValue = async ( dict: Record, - key: T + key: T, ): Promise => { const value = await chrome.storage.local.get(dict[key]); if (!value || !value[dict[key]]) return undefined; @@ -10,12 +10,12 @@ export const getLocalStorageValue = async ( export const setLocalStorageValue = async ( dict: Record, key: T, - value: string + value: string, ): Promise => await chrome.storage.local.set({ [dict[key]]: value }); export const deleteLocalStorageValue = async ( dict: Record, - key: T + key: T, ): Promise => await chrome.storage.local.remove(dict[key]); export const getFromLocalStorage = async (key: string): Promise => { diff --git a/src/pages/contentScript/Page.tsx b/src/pages/contentScript/Page.tsx index 2723128..97a93da 100644 --- a/src/pages/contentScript/Page.tsx +++ b/src/pages/contentScript/Page.tsx @@ -1,6 +1,6 @@ import { Modal, type ModalRef } from '@e-krebs/react-library'; import cx from 'classnames'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Archive, Plus, Trash2 } from 'react-feather'; import { type Service } from 'utils/services'; @@ -102,7 +102,7 @@ export const Page = () => { }; chrome.runtime.sendMessage(message, done); }, - [done, tags] + [done, tags], ); const archiveItem = useCallback( @@ -114,7 +114,7 @@ export const Page = () => { }; chrome.runtime.sendMessage(message, done); }, - [done] + [done], ); const deleteItem = useCallback( @@ -126,80 +126,65 @@ export const Page = () => { }; chrome.runtime.sendMessage(message, done); }, - [done] + [done], ); - const pageSize: number = useMemo(() => { - const fontSize = window - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .getComputedStyle(document.querySelector('html')!) - .getPropertyValue('font-size'); - return parseFloat(fontSize) ?? 16; - }, []); - return service && url ? ( - -
    - ✓ -
    - {matching ? ( - <> - archiveItem(service, matching.listItem.id)} - > - Archive from {service.name} - - deleteItem(service, matching.listItem.id)} - > - Delete from {service.name} - - addTagAsync(matching, tag)} - removeTag={(tag) => removeTagAsync(matching, tag)} - /> - - ) : ( -
    - addItem(service, url)} - > - Add to {service.name} - - + + +
    + ✓
    - )} + {matching ? ( + <> + archiveItem(service, matching.listItem.id)} + > + Archive from {service.name} + + deleteItem(service, matching.listItem.id)} + > + Delete from {service.name} + + addTagAsync(matching, tag)} + removeTag={(tag) => removeTagAsync(matching, tag)} + /> + + ) : ( +
    + addItem(service, url)} + > + Add to {service.name} + + +
    + )} +
    ) : null; }; diff --git a/src/pages/contentScript/Tags.tsx b/src/pages/contentScript/Tags.tsx index 3d4e1dc..11173fb 100644 --- a/src/pages/contentScript/Tags.tsx +++ b/src/pages/contentScript/Tags.tsx @@ -45,7 +45,7 @@ export const Tags: FC = ({ setIsAddTagsOpen(false); if (addTagSync) setIsLoading(false); }, - [addTagSync, removeTag, removeTagSync, setIsLoading] + [addTagSync, removeTag, removeTagSync, setIsLoading], ); const addValue = useCallback( @@ -54,7 +54,7 @@ export const Tags: FC = ({ await addTag?.(tag); setIsAddTagsOpen(false); }, - [addTag, setIsLoading] + [addTag, setIsLoading], ); const addValueSync = useCallback( @@ -62,7 +62,7 @@ export const Tags: FC = ({ if (addTagSync) addTagSync(tag); setIsAddTagsOpen(false); }, - [addTagSync] + [addTagSync], ); const Add = useMemo(() => (isAddTagsOpen ? XCircle : Plus), [isAddTagsOpen]); @@ -70,7 +70,7 @@ export const Tags: FC = ({ const options: Option[] = useMemo( () => allTags.filter((tag) => !tags.includes(tag)).map((value) => ({ value })), - [allTags, tags] + [allTags, tags], ); const autoCompleteProps = useMemo( @@ -83,7 +83,7 @@ export const Tags: FC = ({ setIsLoading, close: () => setIsAddTagsOpen(false), }), - [isLoading, options, setIsLoading] + [isLoading, options, setIsLoading], ); return ( @@ -95,13 +95,13 @@ export const Tags: FC = ({ isAddTagsOpen ? 'max-w-[calc(100%+1rem)]' : 'max-w-full', 'transition-max-width hover:max-w-[calc(100%+1rem)]', 'text-gray-900 dark:text-gray-100', - 'border-gray-900 dark:border-gray-100' + 'border-gray-900 dark:border-gray-100', )} >
    @@ -114,7 +114,7 @@ export const Tags: FC = ({ className={cx( 'group ml-1 flex items-center pl-px', 'border-b border-transparent', - !isLoading && 'cursor-pointer border-dashed hover:border-inherit' + !isLoading && 'cursor-pointer border-dashed hover:border-inherit', )} title={`remove tag [${tag}]`} onClick={() => (isLoading ? {} : remove(tag))} @@ -123,7 +123,7 @@ export const Tags: FC = ({
    @@ -139,7 +139,7 @@ export const Tags: FC = ({
    diff --git a/src/pages/contentScript/index.tsx b/src/pages/contentScript/index.tsx index 4f795ae..f5bddec 100644 --- a/src/pages/contentScript/index.tsx +++ b/src/pages/contentScript/index.tsx @@ -1,3 +1,4 @@ +import Icons from '@e-krebs/react-library/icons.svg'; import { createRoot } from 'react-dom/client'; import inline from 'tailwind.css?inline'; @@ -17,5 +18,10 @@ if (shadow.shadowRoot) { shadow.shadowRoot.appendChild(container); const root = createRoot(container); - root.render(); + root.render( + <> + + + , + ); } diff --git a/src/pages/oauth/index.tsx b/src/pages/oauth/index.tsx index cefa129..506ec9b 100644 --- a/src/pages/oauth/index.tsx +++ b/src/pages/oauth/index.tsx @@ -1,3 +1,4 @@ +import Icons from '@e-krebs/react-library/icons.svg'; import { createRoot } from 'react-dom/client'; import { Page } from './Page'; @@ -8,7 +9,12 @@ if (container) { const service = container.getAttribute('service'); const root = createRoot(container); - root.render(); + root.render( + <> + + + , + ); } else { console.error("couldn't find element with id 'root'"); } diff --git a/src/pages/options/OptionsWrapper.tsx b/src/pages/options/OptionsWrapper.tsx index b662866..6708172 100644 --- a/src/pages/options/OptionsWrapper.tsx +++ b/src/pages/options/OptionsWrapper.tsx @@ -37,7 +37,7 @@ export const OptionsWrapper: FC<{ service: FullService }> = ({ service }) => { if (showCount) { toggleShowCount(); } - } + }, ); return newValue; }); diff --git a/src/pages/options/SharedSettings.tsx b/src/pages/options/SharedSettings.tsx index e05c788..7030cb0 100644 --- a/src/pages/options/SharedSettings.tsx +++ b/src/pages/options/SharedSettings.tsx @@ -1,48 +1,31 @@ -import { TextInput } from '@e-krebs/react-library'; +import { NumberInput } from '@e-krebs/react-library'; import { FC, useEffect, useState } from 'react'; import { Trash2 } from 'react-feather'; -import { vars, defaultVars } from 'helpers/vars'; import { cleanIcons } from 'utils/icon'; import { LoaderButton } from 'components/LoadingIcon'; -import { getFromLocalStorage, setToLocalStorage } from 'helpers/localstorage'; - -const { refreshInterval: defaultRefreshInterval } = defaultVars; +import { getRefreshInterval, setRefreshInterval } from 'utils/refreshInterval'; export const SharedSettings: FC = () => { const [refreshIntervalValue, setRefreshIntervalValue] = useState(); - const updateRefreshInterval = async (stringValue: string) => { - const value = parseInt(stringValue); - if (typeof value === 'number' && !isNaN(value)) { - await setToLocalStorage(vars.refreshInterval, value); - } - }; - useEffect(() => { - const getRefreshInterval = async () => { - const value = - (await getFromLocalStorage(vars.refreshInterval)) ?? defaultRefreshInterval.toString(); - let periodInMinutes: number = parseInt(value); - if (isNaN(periodInMinutes)) periodInMinutes = defaultRefreshInterval; - setRefreshIntervalValue(periodInMinutes); + const getInitialRefreshInterval = async () => { + setRefreshIntervalValue(await getRefreshInterval()); }; - getRefreshInterval(); + getInitialRefreshInterval(); }, []); return refreshIntervalValue ? (
    -
    diff --git a/src/pages/options/index.tsx b/src/pages/options/index.tsx index 77f2646..4bf5993 100644 --- a/src/pages/options/index.tsx +++ b/src/pages/options/index.tsx @@ -1,3 +1,4 @@ +import Icons from '@e-krebs/react-library/icons.svg'; import { createRoot } from 'react-dom/client'; import { Page } from './Page'; @@ -6,7 +7,12 @@ import 'tailwind.css'; const container = document.getElementById('root'); if (container) { const root = createRoot(container); - root.render(); + root.render( + <> + + + , + ); } else { console.error("couldn't find element with id 'root'"); } diff --git a/src/pages/popup/Page.tsx b/src/pages/popup/Page.tsx index 147ae48..71d0800 100644 --- a/src/pages/popup/Page.tsx +++ b/src/pages/popup/Page.tsx @@ -9,12 +9,12 @@ import { Footer } from 'components/Footer'; import { Tabs, Tab, TabProps } from 'components/Tabs'; import { OptionsIcon } from 'components/OptionsIcon'; import { FullService } from 'utils/services'; -import { cacheDurationMs } from 'utils/dataCache'; import { getLastTab, setLastTab } from 'utils/lastTab'; import { getFromLocalStorage } from 'helpers/localstorage'; import { ServiceNames } from 'services'; import { serviceVars } from 'helpers/vars'; import { getFullServices } from 'utils/getFullService'; +import { getRefreshInterval } from 'utils/refreshInterval'; const services = getFullServices(); @@ -24,30 +24,32 @@ const serviceToTab = async (service: FullService): Promise => { content: connected ? List : service.hasOAuth - ? ConnectionStatus - : () => { - const Setup = service.Setup; - return ; - }, + ? ConnectionStatus + : () => { + const Setup = service.Setup; + return ; + }, service, }; }; -const queryClient = new QueryClient({ - defaultOptions: { queries: { cacheTime: cacheDurationMs } }, -}); +const getQueryClient = (cacheTime: number) => + new QueryClient({ defaultOptions: { queries: { cacheTime } } }); export const Page: FC = () => { const [initialSelected, setInitialSelected] = useState(-1); const [tabs, setTabs] = useState([]); + const [queryClient, setQueryClient] = useState(); useEffect(() => { - const getTabs = async () => { + const init = async () => { + setQueryClient(getQueryClient(await getRefreshInterval())); + const selectedService = await getLastTab(); const activeService = (await getFromLocalStorage>>(serviceVars.active)) ?? {}; const currentServices = services.filter( - (service) => !service.isTogglable || activeService[service.name] !== false + (service) => !service.isTogglable || activeService[service.name] !== false, ); const tabs = await Promise.all(currentServices.map(await serviceToTab)); @@ -56,10 +58,10 @@ export const Page: FC = () => { setInitialSelected(Math.max(0, selectedIndex)); setTabs(tabs); }; - getTabs(); + init(); }, []); - return tabs.length > 0 && initialSelected >= 0 ? ( + return queryClient && tabs.length > 0 && initialSelected >= 0 ? ( ); + root.render( + <> + + + , + ); } else { console.error("couldn't find element with id 'root'"); } diff --git a/src/pages/serviceWorker.ts b/src/pages/serviceWorker.ts index b94039e..fa503c4 100644 --- a/src/pages/serviceWorker.ts +++ b/src/pages/serviceWorker.ts @@ -1,4 +1,4 @@ -import { vars, defaultVars } from 'helpers/vars'; +import { vars } from 'helpers/vars'; import { getService, getServices } from 'utils/getService'; import { setBadge, setBadgeColor } from 'utils/badge'; import { forceGet, get } from 'utils/get'; @@ -6,21 +6,19 @@ import { currentUrlIsMatching } from 'utils/currentUrlIsMatching'; import { createContextMenus } from 'utils/createContextMenus'; import { Message } from 'utils/messages'; import { getAllTags } from 'utils/getAllTags'; -import { getFromLocalStorage } from 'helpers/localstorage'; import { getShowCountOnBadge } from 'utils/getShowCountOnBadge'; +import { getRefreshInterval } from 'utils/refreshInterval'; -const { refreshInterval: defaultRefreshInterval } = defaultVars; - -const refreshBadge = async () => { +const refreshBadge = async (force = true) => { await Promise.all( getServices().map(async (service) => { const isConnected = await service.isConnected(); if (!isConnected) return; const showCountOnBadge = await getShowCountOnBadge(); if (showCountOnBadge[service.name] === false) return; - const { data } = await forceGet(service); + const { data } = force ? await forceGet(service) : await get(service); setBadge(service.name, data.length); - }) + }), ); }; @@ -44,7 +42,7 @@ const alarmListener = async (alarm: chrome.alarms.Alarm) => { const tabsUpdatedListener = async ( _: number, __: chrome.tabs.TabChangeInfo, - { active, url }: chrome.tabs.Tab + { active, url }: chrome.tabs.Tab, ) => { if (active && url) { await refreshBadgeIfMatching(url); @@ -63,7 +61,7 @@ const onInstalledListener = async () => await createContextMenus(); const onMessageListener = async ( message: Message, sender: chrome.runtime.MessageSender, - sendMessage: () => void + sendMessage: () => void, ) => { switch (message.action) { case 'addToService': { @@ -158,7 +156,7 @@ const onMessageListener = async ( const onContextMenuClickedListener = async ( info: chrome.contextMenus.OnClickData, - tab?: chrome.tabs.Tab + tab?: chrome.tabs.Tab, ) => { if (!tab?.url) { throw Error('no tab.url on contextMenu clicked'); @@ -187,13 +185,8 @@ chrome.runtime.onMessage.addListener(onMessageListener); chrome.contextMenus.onClicked.addListener(onContextMenuClickedListener); (async () => { - const value = - (await getFromLocalStorage(vars.refreshInterval)) ?? defaultRefreshInterval.toString(); - let periodInMinutes: number = parseInt(value); - if (isNaN(periodInMinutes)) periodInMinutes = defaultRefreshInterval; - chrome.alarms.create(vars.refreshInterval, { periodInMinutes }); - + chrome.alarms.create(vars.refreshInterval, { periodInMinutes: await getRefreshInterval() }); chrome.alarms.onAlarm.addListener(alarmListener); - await refreshBadge(); + await refreshBadge(false); })(); diff --git a/src/services/algolia/Setup.tsx b/src/services/algolia/Setup.tsx index 4761d9a..6b93066 100644 --- a/src/services/algolia/Setup.tsx +++ b/src/services/algolia/Setup.tsx @@ -1,7 +1,7 @@ import { createFetchRequester } from '@algolia/requester-fetch'; import { Button, TextInput } from '@e-krebs/react-library'; import { FC, useCallback, useEffect, useState } from 'react'; -import algoliasearch from 'algoliasearch'; +import { algoliasearch } from 'algoliasearch'; import { CheckCircle, XCircle } from 'react-feather'; import { @@ -31,13 +31,14 @@ export const Setup: FC<{ context: ComponentContext }> = ({ context }) => { if (values.AppId && values.ApiKey) { try { const searchClient = algoliasearch(values.AppId, values.ApiKey, algoliaSearchOptions); - const apiKeys = await searchClient.getApiKey(values.ApiKey); + const apiKeys = await searchClient.getApiKey({ key: values.ApiKey }); if (apiKeys.acl.length === 1 && apiKeys.acl[0] === 'search') { if (values.IndexName) { try { - await searchClient.initIndex(values.IndexName).search(''); + await searchClient.search([{ indexName: values.IndexName, params: '' }]); setIsValid({ apiKey: true, indexName: true }); return; + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { /* empty */ } @@ -45,6 +46,7 @@ export const Setup: FC<{ context: ComponentContext }> = ({ context }) => { setIsValid({ apiKey: true, indexName: false }); return; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { /* empty */ } @@ -93,7 +95,6 @@ export const Setup: FC<{ context: ComponentContext }> = ({ context }) => { label="Algolia Application ID" defaultValue={values.AppId} onChange={(value) => updateValue(value, 'AppId')} - flowClassName="w-full" />
    = ({ context }) => { defaultValue={values.ApiKey} onChange={(value) => updateValue(value, 'ApiKey')} type="password" - flowClassName="w-full" /> {isValid.apiKey ? (
    @@ -120,7 +120,6 @@ export const Setup: FC<{ context: ComponentContext }> = ({ context }) => { label="Algolia Index Name" defaultValue={values.IndexName} onChange={(value) => updateValue(value, 'IndexName')} - flowClassName="w-full" /> {isValid.indexName ? (
    diff --git a/src/services/algolia/get.ts b/src/services/algolia/get.ts index afecb41..b113066 100644 --- a/src/services/algolia/get.ts +++ b/src/services/algolia/get.ts @@ -13,12 +13,11 @@ export const get = async (param: GetParams): Promise => { return []; } - const searchResponse = await algolia.search( - param.type === 'search' ? param.search : '', - param.type === 'tag' - ? { filters: param.tag ? `tags:${param.tag}` : 'NOT tags', hitsPerPage } - : { hitsPerPage } - ); + const searchResponse = await algolia.search({ + query: param.type === 'search' ? param.search : '', + hitsPerPage, + filters: param.type === 'tag' ? (param.tag ? `tags:${param.tag}` : 'NOT tags') : undefined, + }); return searchResponse.hits.map(algoliaItemToListItem); }; diff --git a/src/services/algolia/helpers.ts b/src/services/algolia/helpers.ts index 82f3ed3..61dadcb 100644 --- a/src/services/algolia/helpers.ts +++ b/src/services/algolia/helpers.ts @@ -1,10 +1,14 @@ import { createFetchRequester } from '@algolia/requester-fetch'; -import algoliasearch, { type SearchIndex } from 'algoliasearch'; +import { algoliasearch, type SearchResponse, type SearchParamsObject } from 'algoliasearch'; import { getLocalStorageValue, deleteLocalStorageValue } from 'helpers/localstorage'; import { type LocalStorageKeys, localStorageKeyCodes } from './const'; +type SearchIndex = { + search: >(params?: SearchParamsObject) => Promise>; +}; + let searchIndex: SearchIndex | undefined; export const getAlgoliaClient = async (): Promise => { @@ -14,17 +18,21 @@ export const getAlgoliaClient = async (): Promise => { const AppId = await getLocalStorageValue(localStorageKeyCodes, 'AppId'); const ApiKey = await getLocalStorageValue(localStorageKeyCodes, 'ApiKey'); - const IndexName = await getLocalStorageValue(localStorageKeyCodes, 'IndexName'); + const indexName = await getLocalStorageValue(localStorageKeyCodes, 'IndexName'); - if (!AppId || !ApiKey || !IndexName) { + if (!AppId || !ApiKey || !indexName) { return undefined; } try { const searchClient = algoliasearch(AppId, ApiKey, { requester: createFetchRequester() }); - searchIndex = searchClient.initIndex(IndexName); + searchIndex = { + search: >(params?: SearchParamsObject) => + searchClient.search([{ indexName, params }]).then((res) => res.results[0] as SearchResponse), + }; await searchIndex.search(''); return searchIndex; + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return undefined; } @@ -43,6 +51,6 @@ export const isConnected = async (): Promise => { export const deleteAllKeys = async () => await Promise.all( (Object.keys(localStorageKeyCodes) as LocalStorageKeys[]).map( - async (key) => await deleteLocalStorageValue(localStorageKeyCodes, key) - ) + async (key) => await deleteLocalStorageValue(localStorageKeyCodes, key), + ), ); diff --git a/src/services/pocket/get.ts b/src/services/pocket/get.ts index 3bb7d5e..2c9fbac 100644 --- a/src/services/pocket/get.ts +++ b/src/services/pocket/get.ts @@ -28,7 +28,7 @@ export const get = async (param: GetParams): Promise => { access_token: await getPocketToken(), sort: 'newest', search: param.type === 'search' ? param.search : undefined, - tag: param.type === 'tag' ? param.tag ?? '_untagged_' : undefined, + tag: param.type === 'tag' ? (param.tag ?? '_untagged_') : undefined, detailType: 'complete', state: 'unread', count, @@ -40,9 +40,9 @@ export const get = async (param: GetParams): Promise => { if (!response.ok) throw Error("couldn't get pocket list"); listItems.push( - ...Object.values(response.result.list) + ...Object.values(response.result.list ?? {}) .sort((a, b) => (a.time_added < b.time_added ? 1 : -1)) - .map(itemToListItem) + .map(itemToListItem), ); total = parseInt(response.result.total); diff --git a/src/services/pocket/helpers.ts b/src/services/pocket/helpers.ts index 3bcb046..1313bb3 100644 --- a/src/services/pocket/helpers.ts +++ b/src/services/pocket/helpers.ts @@ -14,7 +14,7 @@ export const isConnected = async (): Promise => { getKeys().map(async (key) => { const value = await get(localStorageKeyCodes, key); return Boolean(value); - }) + }), ); return values.reduce((a, b) => a && b); }; diff --git a/src/tailwind.css b/src/tailwind.css index 478d5d6..fc9f6d4 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -19,3 +19,127 @@ @apply outline-none; } } + +@layer utilities { + /* background-color classes */ + .bg-th { + @apply bg-gray-50 dark:bg-gray-900; + } + .bg-th-reversed { + @apply bg-gray-900 dark:bg-gray-50; + } + .bg-primary { + @apply bg-primary-500 dark:bg-primary-400; + } + .bg-primary\/5 { + @apply bg-primary-500/5 dark:bg-primary-400/5; + } + .bg-primary\/95 { + @apply bg-primary-500/95 dark:bg-primary-400/95; + } + .bg-destructive, + .bg-error { + @apply bg-red-600 dark:bg-red-500; + } + .bg-destructive\/5 { + @apply bg-red-600/5 dark:bg-red-500/5; + } + .bg-th-light { + @apply bg-gray-100 dark:bg-gray-800; + } + .bg-th-hover { + @apply bg-gray-200 dark:bg-gray-700; + } + + /* border-color classes */ + .border-th { + @apply border-gray-700 dark:border-gray-300; + } + .border-primary { + @apply border-primary-500 dark:border-primary-400; + } + .border-destructive, + .border-error { + @apply border-red-600 dark:border-red-500; + } + .border-primary.bg-primary\/95 { + @apply border-primary-500/95 dark:border-primary-400/95; + } + .border-th-light { + @apply border-gray-400 dark:border-gray-500; + } + .border-th-bg { + @apply border-gray-50 dark:border-gray-900; + } + + /* text-color classes */ + .text-th { + @apply text-gray-500 dark:text-gray-400; + } + .text-h1 { + @apply text-gray-900 dark:text-gray-50; + } + .text-primary { + @apply text-primary-500 dark:text-primary-400; + } + .text-destructive, + .text-error { + @apply text-red-600 dark:text-red-500; + } + .text-valid { + @apply text-green-600 dark:text-green-400; + } + .text-th-reversed { + @apply text-gray-50 dark:text-gray-900; + } + + /* ring-color classes */ + .ring-th { + @apply ring-gray-700 dark:ring-gray-300; + } + .ring-primary { + @apply ring-primary-500 dark:ring-primary-400; + } + .ring-destructive, + .ring-error { + @apply ring-red-600 dark:ring-red-500; + } + + /* ring-offset-color classes */ + .ring-offset-th { + @apply ring-offset-white dark:ring-offset-gray-900; + } + + /* outline-color classes */ + .outline-primary { + @apply outline-primary-500 dark:outline-primary-400; + } + .outline-destructive, + .outline-error { + @apply outline-red-600 dark:outline-red-500; + } + + /* caret-color classes */ + .caret-primary { + @apply caret-primary-500 dark:caret-primary-400; + } + .caret-destructive, + .caret-error { + @apply caret-red-600 dark:caret-red-500; + } + + /* stroke-color classes */ + .stroke-th { + @apply stroke-white dark:stroke-gray-900; + } + + /* fill-color classes */ + .fill-th { + @apply fill-gray-500 dark:fill-gray-400; + } + + /* shadow class */ + .shadow-th { + @apply shadow-md dark:shadow-inner; + } +} diff --git a/src/typings/extract-colors.d.ts b/src/typings/extract-colors.d.ts new file mode 100644 index 0000000..07afc17 --- /dev/null +++ b/src/typings/extract-colors.d.ts @@ -0,0 +1,15 @@ +import 'extract-colors'; + +declare module 'extract-colors' { + export type FinalColor = { + hex: string; + red: number; + green: number; + blue: number; + area: number; + hue: number; + saturation: number; + lightness: number; + intensity: number; + }; +} diff --git a/src/utils/__tests__/getUrl.test.ts b/src/utils/__tests__/getUrl.test.ts index a3d452a..a628ca4 100644 --- a/src/utils/__tests__/getUrl.test.ts +++ b/src/utils/__tests__/getUrl.test.ts @@ -6,7 +6,7 @@ describe('getUrl', () => { it('should work with simple urls', () => { expect(getUrl('https://www.google.fr')).toEqual(new URL('https://www.google.fr')); expect(getUrl('https://github.com/e-krebs/pile')).toEqual( - new URL('https://github.com/e-krebs/pile') + new URL('https://github.com/e-krebs/pile'), ); }); diff --git a/src/utils/badge.ts b/src/utils/badge.ts index ff13797..e4965eb 100644 --- a/src/utils/badge.ts +++ b/src/utils/badge.ts @@ -65,7 +65,7 @@ export const updateBadge = async () => { export const setBadgeColor = (currentUrlIsMatching: boolean) => { chrome.action.setIcon({ path: chrome.runtime.getURL( - `src/content/icons/icon-96${currentUrlIsMatching ? '-selected' : ''}.png` + `src/content/icons/icon-96${currentUrlIsMatching ? '-selected' : ''}.png`, ), }); chrome.action.setBadgeBackgroundColor({ color: currentUrlIsMatching ? colorSelected : color }); diff --git a/src/utils/createContextMenus.ts b/src/utils/createContextMenus.ts index ff89b20..1a52717 100644 --- a/src/utils/createContextMenus.ts +++ b/src/utils/createContextMenus.ts @@ -19,6 +19,6 @@ export const createContextMenus = async () => { type: 'normal', contexts: ['page'], }); - }) + }), ); }; diff --git a/src/utils/currentUrlIsMatching.ts b/src/utils/currentUrlIsMatching.ts index e7261a8..2758b31 100644 --- a/src/utils/currentUrlIsMatching.ts +++ b/src/utils/currentUrlIsMatching.ts @@ -6,7 +6,7 @@ import { beautifyUrl } from './url'; export const getMatchingListItem = async ( currentUrl: URL | string, - services: Service[] + services: Service[], ): Promise => { const currentURL = typeof currentUrl === 'string' ? getUrl(currentUrl) : currentUrl; const matching = await Promise.all( @@ -17,14 +17,14 @@ export const getMatchingListItem = async ( const { data } = await get(service); return data.find((item) => urlsAreMatching(item.url, currentURL)); }) - .flat() + .flat(), ); return matching.find((item) => item !== undefined); }; export const currentUrlIsMatching = async ( currentUrl: URL | string, - services: Service[] + services: Service[], ): Promise => { const currentURL = typeof currentUrl === 'string' ? getUrl(currentUrl) : currentUrl; const matches = await Promise.all( @@ -34,7 +34,7 @@ export const currentUrlIsMatching = async ( const { data } = await get(service); const matchingUrls = data.filter(({ url }) => urlsAreMatching(url, currentURL)); return matchingUrls.length > 0; - }) + }), ); const isMatching = matches.reduce((a, b) => a || b); return isMatching; @@ -45,6 +45,6 @@ export const urlsAreMatching = (url1: URL | string, url2: URL): boolean => { return ( beautifyUrl(URL1.origin) === beautifyUrl(url2.origin) && beautifyUrl(URL1.pathname) === beautifyUrl(url2.pathname) && - url1.search === url2.search + URL1.search === url2.search ); }; diff --git a/src/utils/dataCache.ts b/src/utils/dataCache.ts index 6d8b3f4..6e48d6d 100644 --- a/src/utils/dataCache.ts +++ b/src/utils/dataCache.ts @@ -1,9 +1,5 @@ import type { QueryClient } from 'react-query'; -import { deleteFromLocalStorage } from 'helpers/localstorage'; - -export const cacheDurationMs = 5 * 60 * 1000; // 5 minutes - export interface JsonArrayCache { timestamp: number; data: T[]; @@ -16,12 +12,9 @@ export interface JsonCache { export const getTimestamp = (): number => new Date().getTime(); -export const isCacheExpired = ( - cache: JsonCache | JsonArrayCache, - duration = cacheDurationMs -): boolean => getTimestamp() - cache.timestamp > duration; +export const isCacheExpired = (cache: JsonCache | JsonArrayCache, duration: number): boolean => + getTimestamp() - cache.timestamp > duration * 5 * 60 * 1000; export const clearCache = async (key: string, queryClient: QueryClient) => { - await deleteFromLocalStorage(key); await queryClient.invalidateQueries(key); }; diff --git a/src/utils/files/helpers.ts b/src/utils/files/helpers.ts index d2352aa..f3e0531 100644 --- a/src/utils/files/helpers.ts +++ b/src/utils/files/helpers.ts @@ -22,14 +22,14 @@ const getFilesystem = async (): Promise => { }, (error) => { logAndReject(error, 'getFilesystem', reject); - } + }, ); }); }; const getFolder = async ( parentDir: FileSystemDirectoryEntry, - path: string + path: string, ): Promise => { return await new Promise((resolve, reject) => { parentDir.getDirectory( @@ -38,7 +38,7 @@ const getFolder = async ( (directory) => resolve(directory), (error) => { logAndReject(error, `getFolder ${path}`, reject); - } + }, ); }); }; diff --git a/src/utils/files/index.ts b/src/utils/files/index.ts index 68f38a3..dbd1a73 100644 --- a/src/utils/files/index.ts +++ b/src/utils/files/index.ts @@ -16,7 +16,7 @@ export const readFile = async (path: Path): Promise => { }, () => { resolve(null); - } + }, ); }); }; @@ -42,7 +42,7 @@ export const readJson = async (path: Path): Promise => { }, () => { resolve(null); - } + }, ); }); }; @@ -70,13 +70,13 @@ export const writeBlob = async (blobinfo: BlobInfo): Promise => { }, (error) => { logAndReject(error, 'writeBlob.createWriter', reject); - } + }, ); } }, (error) => { logAndReject(error, 'writeBlob.getFile', reject); - } + }, ); }); }; @@ -100,12 +100,12 @@ export const deleteFile = async (url: Path): Promise => { }, (error) => { logAndReject(error, 'deleteFile', reject); - } + }, ); }, () => { resolve(true); /* file doesn't exist */ - } + }, ); }); }; @@ -119,7 +119,7 @@ export const deleteFolder = async (path: Path): Promise => { }, (error) => { logAndReject(error, 'deleteFolder', reject); - } + }, ); }); }; diff --git a/src/utils/get.ts b/src/utils/get.ts index a849d5d..dd958bb 100644 --- a/src/utils/get.ts +++ b/src/utils/get.ts @@ -3,6 +3,7 @@ import { getFromLocalStorage, setToLocalStorage } from 'helpers/localstorage'; import { getTimestamp, isCacheExpired, JsonArrayCache } from './dataCache'; import { Service } from './services'; import { ListItem } from './typings'; +import { getRefreshInterval } from './refreshInterval'; type GetType = 'default' | 'force' | 'search'; @@ -23,8 +24,9 @@ const rawGet = async (param: GetParams, service: Service): Promise | null>(service.getQueryKey); + const refreshInterval = await getRefreshInterval(); - if (param.type !== 'default' || !list || isCacheExpired(list)) { + if (param.type !== 'default' || !list || isCacheExpired(list, refreshInterval)) { const data = await service.get(param); list = { timestamp: getTimestamp(), data }; if (!['search', 'tag'].includes(param.type)) { diff --git a/src/utils/getFirstBy.ts b/src/utils/getFirstBy.ts index 4410674..2afd0b4 100644 --- a/src/utils/getFirstBy.ts +++ b/src/utils/getFirstBy.ts @@ -1,6 +1,6 @@ export const getFirstBy = >( array: T[], - property: keyof T + property: keyof T, ): T | undefined => { switch (array.length) { case 0: diff --git a/src/utils/icon.ts b/src/utils/icon.ts index f3312eb..e80a51d 100644 --- a/src/utils/icon.ts +++ b/src/utils/icon.ts @@ -1,4 +1,4 @@ -import { extractColors } from 'extract-colors'; +import { extractColors, type FinalColor } from 'extract-colors'; import { readFile, readJson, writeBlob, writeJson, deleteFolder, deleteFile } from 'utils/files'; import { getBlob } from 'utils/getBlob'; @@ -7,6 +7,7 @@ import type { BlobInfo, Path, Response } from 'utils/typings'; import { getTimestamp, isCacheExpired, JsonCache } from './dataCache'; import { RGB, getRgb } from './palette'; import { getFirstBy } from './getFirstBy'; +import { getRefreshInterval } from './refreshInterval'; const iconFolder = 'icons'; @@ -40,7 +41,7 @@ const getPalette = async (url: string, palettePath: Path): Promise(await extractColors(url), 'area')), }; }; @@ -60,7 +61,8 @@ const isExcludedIcon = async (hostname: string): Promise => { const path = getNoImageName(hostname); const excludedIcon = await readJson(path); if (!excludedIcon) return false; - if (isCacheExpired(excludedIcon)) { + const interval = await getRefreshInterval(); + if (isCacheExpired(excludedIcon, interval)) { await deleteFile(path); return false; } @@ -69,7 +71,7 @@ const isExcludedIcon = async (hostname: string): Promise => { export const getIcon = async ( hostname: string, - fallback: string | null = null + fallback: string | null = null, ): Promise => { const imageName: Path = [iconFolder, `${hostname}.png`]; const paletteName: Path = [iconFolder, `${hostname}_palette_02.json`]; diff --git a/src/utils/lastTab.ts b/src/utils/lastTab.ts index 17db159..d5092ba 100644 --- a/src/utils/lastTab.ts +++ b/src/utils/lastTab.ts @@ -7,7 +7,7 @@ export const getLastTab = async (): Promise => await getFromLocalStorage(lastTabKey); export const setLastTab = async (serviceName?: string): Promise => { - serviceName === undefined + return serviceName === undefined ? await deleteFromLocalStorage(lastTabKey) : await setToLocalStorage(lastTabKey, serviceName); }; diff --git a/src/utils/lastTag.ts b/src/utils/lastTag.ts index f4944b6..c210a75 100644 --- a/src/utils/lastTag.ts +++ b/src/utils/lastTag.ts @@ -6,7 +6,7 @@ export const getLastTag = async (serviceName: string): Promise(getLastTagKey(serviceName)); export const setLastTag = async (serviceName: string, tag: string | null | undefined): Promise => { - tag === undefined + return tag === undefined ? await deleteFromLocalStorage(getLastTagKey(serviceName)) : await setToLocalStorage(getLastTagKey(serviceName), tag); }; diff --git a/src/utils/logAndReject.ts b/src/utils/logAndReject.ts index b9246c7..9b17ea9 100644 --- a/src/utils/logAndReject.ts +++ b/src/utils/logAndReject.ts @@ -1,7 +1,7 @@ export const logAndReject = ( error: Error, identifier: string, - reject: (reason?: unknown) => void + reject: (reason?: unknown) => void, ): void => { console.warn(`[${identifier}] rejected with the following error:`, error); reject(); diff --git a/src/utils/refreshInterval.ts b/src/utils/refreshInterval.ts new file mode 100644 index 0000000..4a19b60 --- /dev/null +++ b/src/utils/refreshInterval.ts @@ -0,0 +1,8 @@ +import { getFromLocalStorage, setToLocalStorage } from 'helpers/localstorage'; +import { defaultVars, vars } from 'helpers/vars'; + +export const getRefreshInterval = async (): Promise => + (await getFromLocalStorage(vars.refreshInterval)) ?? defaultVars.refreshInterval; + +export const setRefreshInterval = async (interval: number) => + await setToLocalStorage(vars.refreshInterval, interval); diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 54053cc..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,24 +0,0 @@ -const shared = require('@e-krebs/react-library/tailwind.config.js'); - -/** @type {import('tailwindcss').Config} */ -module.exports = { - presets: [shared], - content: ["./src/**/*.{html,ts,tsx}", "./node_modules/@e-krebs/react-library/dist/**/*.js"], - theme: { - extend: { - colors: { - 'algolia': 'rgb(0, 61, 255)', - 'pocket': '#EF4056', - 'inherit': 'inherit', - }, - transitionProperty: { - 'height': 'height', - 'width': 'width', - 'max-width': 'max-width', - }, - backgroundImage: { - 'stripe-disabled': 'repeating-linear-gradient(-45deg, white, white 10px, #F3F4F6 10px, #F3F4F6 20px)', - }, - }, - }, -}; diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..18a4613 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,29 @@ +import type { Config } from 'tailwindcss'; + +const shared = require('@e-krebs/react-library/tailwind.config.ts'); + +module.exports = { + presets: [shared], + content: { + files: ['./src/**/*.{html,ts,tsx}', './node_modules/@e-krebs/react-library/dist/**/*.js'], + }, + theme: { + extend: { + colors: { + ...shared.theme.colors, + algolia: 'rgb(0, 61, 255)', + pocket: '#EF4056', + inherit: 'inherit', + }, + transitionProperty: { + height: 'height', + width: 'width', + 'max-width': 'max-width', + }, + backgroundImage: { + 'stripe-disabled': + 'repeating-linear-gradient(-45deg, white, white 10px, #F3F4F6 10px, #F3F4F6 20px)', + }, + }, + }, +} satisfies Config;