From 99606681616207c3fb880a941faafaed6f06a0c8 Mon Sep 17 00:00:00 2001 From: abhijithvijayan <34790378+abhijithvijayan@users.noreply.github.com> Date: Tue, 21 Jul 2020 21:57:43 +0530 Subject: [PATCH] feat: use context to store and display urls history --- source/History/History.tsx | 372 +++++++------------- source/History/Table.tsx | 256 ++++++++++++++ source/History/index.tsx | 21 +- source/Options/Form.tsx | 4 +- source/Popup/Header.tsx | 4 +- source/contexts/shortened-links-context.tsx | 96 +++++ 6 files changed, 508 insertions(+), 245 deletions(-) create mode 100644 source/History/Table.tsx create mode 100644 source/contexts/shortened-links-context.tsx diff --git a/source/History/History.tsx b/source/History/History.tsx index 1f0340b..f011dc2 100644 --- a/source/History/History.tsx +++ b/source/History/History.tsx @@ -1,251 +1,143 @@ -import React from 'react'; -import tw, {css, styled} from 'twin.macro'; +import React, {useEffect, useState} from 'react'; +import 'twin.macro'; + +import { + useShortenedLinks, + ShortenedLinksActionTypes, +} from '../contexts/shortened-links-context'; +import { + HostProperties, + useExtensionSettings, +} from '../contexts/extension-settings-context'; +import { + useRequestStatus, + RequestStatusActionTypes, +} from '../contexts/request-status-context'; +import messageUtil from '../util/mesageUtil'; +import {FETCH_URLS_HISTORY} from '../Background/constants'; +import {getExtensionSettings} from '../util/settings'; +import { + SuccessfulUrlsHistoryFetchProperties, + AuthRequestBodyProperties, + ApiErroredProperties, + ErrorStateProperties, + Kutt, +} from '../Background'; +import {isValidUrl} from '../util/tabs'; import BodyWrapper from '../components/BodyWrapper'; - -const StyledTd = styled.td` - ${tw`relative flex items-center px-0 py-4`} -`; - -const StyledButton = styled.button` - ${tw`flex items-center justify-center p-0 my-0 transition-all duration-200 ease-out border-none outline-none cursor-pointer`} - - margin-right: 2px; - margin-left: 12px; - width: 26px; - height: 26px; - box-shadow: rgba(100, 100, 100, 0.1) 0px 2px 4px; - background-color: rgb(222, 222, 222); - border-width: initial; - border-color: initial; - border-image: initial; - border-radius: 100%; - - &:hover { - transform: translateY(-3px); - } - - img { - width: 12px; - height: 12px; - } -`; +import Loader from '../components/Loader'; +import Header from '../Options/Header'; +import Table from './Table'; const History: React.FC = () => { + const shortenedLinksDispatch = useShortenedLinks()[1]; + const extensionSettingsDispatch = useExtensionSettings()[1]; + const [requestStatusState, requestStatusDispatch] = useRequestStatus(); + const [errored, setErrored] = useState({ + error: null, + message: '', + }); + + useEffect(() => { + async function getUrlsHistoryStats(): Promise { + // ********************************* // + // **** GET EXTENSIONS SETTINGS **** // + // ********************************* // + const {settings = {}} = await getExtensionSettings(); + const advancedSettings: boolean = + (settings?.advanced as boolean) || false; + + const defaultHost: HostProperties = + (advancedSettings && + (settings?.host as string) && + isValidUrl(settings.host as string) && { + hostDomain: (settings.host as string) + .replace('http://', '') + .replace('https://', '') + .replace('www.', '') + .split(/[/?#]/)[0], // extract domain + hostUrl: (settings.host as string).endsWith('/') + ? (settings.host as string).slice(0, -1) + : (settings.host as string), // slice `/` at the end + }) || + Kutt; + + // inject existing keys (if field doesn't exist, use default) + const defaultExtensionConfig = { + apikey: (settings?.apikey as string)?.trim() || '', + history: (settings?.history as boolean) || false, + advanced: + defaultHost.hostUrl.trim() !== Kutt.hostUrl && advancedSettings, // disable `advanced` if customhost is not set + host: defaultHost, + }; + + // extensionSettingsDispatch({ + // type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS, + // payload: defaultExtensionConfig, + // }); + + // ****************************************************** // + // **************** FETCH URLS HISTORY ****************** // + // ****************************************************** // + const urlsHistoryFetchRequetBody: AuthRequestBodyProperties = { + apikey: defaultExtensionConfig.apikey, + hostUrl: defaultExtensionConfig.host.hostUrl, + }; + + // call api + const response: + | SuccessfulUrlsHistoryFetchProperties + | ApiErroredProperties = await messageUtil.send( + FETCH_URLS_HISTORY, + urlsHistoryFetchRequetBody + ); + + if (!response.error) { + setErrored({error: false, message: 'Fetch successful'}); + + shortenedLinksDispatch({ + type: ShortenedLinksActionTypes.HYDRATE_SHORTENED_LINKS, + payload: { + items: response.data.data, + total: response.data.total, + }, + }); + } else { + setErrored({error: true, message: response.message}); + } + + requestStatusDispatch({ + type: RequestStatusActionTypes.SET_LOADING, + payload: false, + }); + } + + getUrlsHistoryStats(); + }, [ + extensionSettingsDispatch, + requestStatusDispatch, + shortenedLinksDispatch, + ]); + return (
-
-
-
-

- Recent shortened links. (last 15 results) -

+
+
+ + {/* eslint-disable-next-line no-nested-ternary */} + {!requestStatusState.loading ? ( + !errored.error ? ( + + ) : ( +

{errored.message}

+ ) + ) : ( +
+
-
- - - - - - - - - - - %longLink% - - - - -
- Copied to clipboard! -
- -
- - -
- - Copy - - - QR Code - -
-
- - -
- Original URL - - Short URL -
-
+ )}
diff --git a/source/History/Table.tsx b/source/History/Table.tsx new file mode 100644 index 0000000..f205a14 --- /dev/null +++ b/source/History/Table.tsx @@ -0,0 +1,256 @@ +import tw, {css, styled} from 'twin.macro'; +import React from 'react'; + +import {useShortenedLinks} from '../contexts/shortened-links-context'; +import {MAX_HISTORY_ITEMS} from '../Background/constants'; + +import Icon from '../components/Icon'; +// import Modal from './Modal'; + +const StyledTd = styled.td` + ${tw`relative flex items-center px-0 py-4`} +`; + +const StyledIcon = styled(Icon)` + ${tw`flex items-center justify-center p-0 my-0 transition-all duration-200 ease-out border-none outline-none cursor-pointer`} + + margin-right: 2px; + margin-left: 12px; + width: 26px; + height: 26px; + box-shadow: rgba(100, 100, 100, 0.1) 0px 2px 4px; + background-color: rgb(222, 222, 222); + border-radius: 100%; + + &:hover { + transform: translateY(-3px); + } + + svg { + stroke: rgb(101, 189, 137); + stroke-width: 2; + } +`; + +const Table: React.FC = () => { + const [state] = useShortenedLinks(); + + return ( + <> +
+
+
+

+ Recent shortened links. (last {MAX_HISTORY_ITEMS} results) +

+
+ + + + + + + + + {!(state.total === 0) ? ( + state.items.map((item) => { + return ( + + + + {item.target} + + + + +
+ Copied to clipboard! +
+ +
+ + +
+ + +
+ {/* */} +
+ + ); + }) + ) : ( + + + + )} + +
+ Original URL + + Short URL +
No URLs History
+
+
+ + ); +}; + +export default Table; diff --git a/source/History/index.tsx b/source/History/index.tsx index d6b68cf..285c577 100644 --- a/source/History/index.tsx +++ b/source/History/index.tsx @@ -1,3 +1,4 @@ +import {ThemeProvider} from 'styled-components'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -5,5 +6,23 @@ import ReactDOM from 'react-dom'; import '../styles/main.scss'; import History from './History'; +import {ExtensionSettingsProvider} from '../contexts/extension-settings-context'; +import {ShortenedLinksProvider} from '../contexts/shortened-links-context'; +import {RequestStatusProvider} from '../contexts/request-status-context'; -ReactDOM.render(, document.getElementById('history-root')); +// eslint-disable-next-line import/no-webpack-loader-syntax, import/no-unresolved, @typescript-eslint/no-var-requires, node/no-missing-require +const theme = require('sass-extract-loader?{"plugins": ["sass-extract-js"]}!../styles/base/_variables.scss'); +// Require sass variables using sass-extract-loader and specify the plugin + +ReactDOM.render( + + + + + + + + + , + document.getElementById('history-root') +); diff --git a/source/Options/Form.tsx b/source/Options/Form.tsx index 785e82c..0c9f239 100644 --- a/source/Options/Form.tsx +++ b/source/Options/Form.tsx @@ -9,7 +9,7 @@ import messageUtil from '../util/mesageUtil'; import {isValidUrl} from '../util/tabs'; import { SuccessfulApiKeyCheckProperties, - GetUserSettingsBodyProperties, + AuthRequestBodyProperties, ApiErroredProperties, ErrorStateProperties, Kutt, @@ -145,7 +145,7 @@ const Form: React.FC = () => { async function handleApiKeyVerification(): Promise { setSubmitting(true); // request API validation request - const apiKeyValidationBody: GetUserSettingsBodyProperties = { + const apiKeyValidationBody: AuthRequestBodyProperties = { apikey: formStateValues.apikey.trim(), hostUrl: (formStateValues.advanced && diff --git a/source/Popup/Header.tsx b/source/Popup/Header.tsx index 0ed9bd7..e82ed39 100644 --- a/source/Popup/Header.tsx +++ b/source/Popup/Header.tsx @@ -11,7 +11,7 @@ import {openExtOptionsPage} from '../util/tabs'; import messageUtil from '../util/mesageUtil'; import { SuccessfulApiKeyCheckProperties, - GetUserSettingsBodyProperties, + AuthRequestBodyProperties, ApiErroredProperties, ErrorStateProperties, } from '../Background'; @@ -39,7 +39,7 @@ const Header: React.FC = () => { // show loading spinner setLoading(true); - const apiKeyValidationBody: GetUserSettingsBodyProperties = { + const apiKeyValidationBody: AuthRequestBodyProperties = { apikey: extensionSettingsState.apikey, hostUrl: extensionSettingsState.host.hostUrl, }; diff --git a/source/contexts/shortened-links-context.tsx b/source/contexts/shortened-links-context.tsx new file mode 100644 index 0000000..7f528d2 --- /dev/null +++ b/source/contexts/shortened-links-context.tsx @@ -0,0 +1,96 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import React, {createContext, useContext, useReducer} from 'react'; + +import {UserShortenedLinkStats} from '../Background'; + +export enum ShortenedLinksActionTypes { + HYDRATE_SHORTENED_LINKS = 'hydrate-shortened-links', +} + +type HYDRATE_SHORTENED_LINKS = { + type: ShortenedLinksActionTypes.HYDRATE_SHORTENED_LINKS; + payload: { + items: UserShortenedLinkStats[]; + total: number; + }; +}; + +type Action = HYDRATE_SHORTENED_LINKS; + +type InitialValues = { + items: UserShortenedLinkStats[]; + total: number; + selected: string | null; +}; + +const initialValues: InitialValues = { + items: [], + total: 0, + selected: null, +}; + +type State = InitialValues; +type Dispatch = (action: Action) => void; + +const ShortenedLinksStateContext = createContext(undefined); +const ShortenedLinksDispatchContext = createContext( + undefined +); + +const shortenedLinksReducer = (state: State, action: Action): State => { + switch (action.type) { + case ShortenedLinksActionTypes.HYDRATE_SHORTENED_LINKS: { + return { + ...state, + ...action.payload, + }; + } + + default: + return state; + } +}; + +function useShortenedLinksContextState(): State { + const context = useContext(ShortenedLinksStateContext); + + if (context === undefined) { + throw new Error( + 'useShortenedLinksContextState must be used within a ShortenedLinksProvider' + ); + } + + return context; +} + +function useShortenedLinksContextDispatch(): Dispatch { + const context = useContext(ShortenedLinksDispatchContext); + + if (context === undefined) { + throw new Error( + 'useShortenedLinksContextDispatch must be used within a ShortenedLinksProvider' + ); + } + + return context; +} + +function useShortenedLinks(): [State, Dispatch] { + return [useShortenedLinksContextState(), useShortenedLinksContextDispatch()]; +} + +const ShortenedLinksProvider: React.FC = ({children}) => { + const [state, dispatch] = useReducer(shortenedLinksReducer, initialValues); + + return ( + <> + + + {children} + + + + ); +}; + +export {useShortenedLinks, ShortenedLinksProvider};