Skip to content
This repository has been archived by the owner on Mar 23, 2022. It is now read-only.

Commit

Permalink
Added subscription management buttons on homepage #360
Browse files Browse the repository at this point in the history
Signed-off-by: RaenonX <[email protected]>
  • Loading branch information
RaenonX committed Jan 19, 2022
1 parent 650928e commit 0a71734
Show file tree
Hide file tree
Showing 36 changed files with 741 additions and 70 deletions.
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
"@ctrl/react-adsense": "^1.3.1",
"@next-auth/mongodb-adapter": "^1.0.1",
"@reduxjs/toolkit": "^1.6.1",
"base64url": "^3.0.1",
"bootstrap": "^5.1.3",
"color": "^4.0.1",
"decimal.js": "^10.3.1",
"env-var": "^7.1.1",
"fastify": "^3.21.5",
"lodash": "^4.17.21",
"mathjs": "^9.4.4",
"mongodb": "^4.3.0",
"newrelic": "^8.3.0",
Expand Down Expand Up @@ -60,6 +62,7 @@
"@testing-library/user-event": "^13.2.1",
"@types/color": "^3.0.2",
"@types/jest": "^27.0.1",
"@types/lodash": "^4.14.178",
"@types/mongodb": "^3.6.20",
"@types/node": "^16.11.14",
"@types/node-fetch": "^2.5.12",
Expand Down
2 changes: 1 addition & 1 deletion pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const NextApp = ({
}
<React.StrictMode>
<SessionProvider session={session} refetchInterval={5 * 60}>
<AppReactContext.Provider value={{...pageProps}}>
<AppReactContext.Provider value={{session, ...pageProps}}>
<ReduxProvider>
<MainApp
isNotFound={pageProps.isNotFound}
Expand Down
28 changes: 28 additions & 0 deletions src/components/elements/common/button/subscribe/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';

import Button from 'react-bootstrap/Button';
import Spinner from 'react-bootstrap/Spinner';

import {IconNotSubscribed, IconSubscribed} from '../../icons';
import styles from './main.module.css';
import {SubscribeButtonCommonProps} from './type';


type Props = SubscribeButtonCommonProps;

export const SubscribeButtonIconOnly = ({onClick, state, disabled}: Props) => {
const {subscribed, updating} = state;

const className =
`${styles['subscribe-button']} ` +
`${subscribed ? styles['subscribe-button-as-icon-enabled'] : styles['subscribe-button-as-icon-disabled']}`;

return (
<Button variant="outline-secondary" className={className} onClick={onClick} disabled={updating || disabled}>
{updating ?
<Spinner animation="border"/> :
(subscribed ? <IconSubscribed/> : <IconNotSubscribed/>)
}
</Button>
);
};
19 changes: 19 additions & 0 deletions src/components/elements/common/button/subscribe/main.module.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions src/components/elements/common/button/subscribe/main.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import "../../../../../../styles/colors";

button {
&.subscribe-button {
border: 0;
border-radius: 50%;
font-size: 1.2rem;
padding: 0;

// 2rem is the exact size of a normal spinner
height: 2rem;
width: 2rem;

&:disabled {
background-color: $color-dark-red;
}

&.subscribe-button-as-icon-enabled {
color: white;
}

&.subscribe-button-as-icon-disabled {
color: $color-bw-170;
}
}
}
58 changes: 58 additions & 0 deletions src/components/elements/common/button/subscribe/main.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';

import {screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {ObjectId} from 'mongodb';

import {renderReact} from '../../../../../../test/render/main';
import {ApiResponseCode, SubscriptionKey} from '../../../../../api-def/api';
import {translation as translationEN} from '../../../../../i18n/translations/en/translation';
import {ApiRequestSender} from '../../../../../utils/services/api/requestSender';
import {SubscribeButton} from './main';


describe('Subscribe button', () => {
it('shows login required on click if un-authed', async () => {
renderReact(() => (
<SubscribeButton defaultSubscribed={false} subscriptionKey={{type: 'const', name: 'ALL_MISC'}}/>
));

const subButton = screen.getByText('', {selector: 'i.bi-bell-slash'});

userEvent.click(subButton);

expect(await screen.findByText(translationEN.message.error.auth.loginRequired)).toBeInTheDocument();
expect(screen.queryByText('', {selector: 'div.spinner-border'})).not.toBeInTheDocument();
});

it('sends subscription update request', async () => {
const id = new ObjectId().toHexString();
const subKey: SubscriptionKey = {type: 'const', name: 'ALL_MISC'};
const fnUpdateSubscription = jest.spyOn(ApiRequestSender, 'addSubscription').mockResolvedValue({
code: ApiResponseCode.SUCCESS,
success: true,
});

renderReact(
() => <SubscribeButton defaultSubscribed={false} subscriptionKey={subKey}/>,
{user: {id}},
);

const subButton = screen.getByText('', {selector: 'i.bi-bell-slash'});

userEvent.click(subButton);

expect(screen.getByText('', {selector: 'div.spinner-border'})).toBeInTheDocument();
expect(fnUpdateSubscription).toHaveBeenCalledWith(id, subKey);

expect(await screen.findByText('', {selector: 'i.bi-bell'})).toBeInTheDocument();
});

it('shows as text', async () => {
renderReact(() => (
<SubscribeButton defaultSubscribed={false} subscriptionKey={{type: 'const', name: 'ALL_MISC'}} asIcon={false}/>
));

expect(screen.getByText(translationEN.misc.subscription.add)).toBeInTheDocument();
});
});
80 changes: 80 additions & 0 deletions src/components/elements/common/button/subscribe/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from 'react';

import {useSession} from 'next-auth/react';

import {isFailedResponse, SubscriptionKey} from '../../../../../api-def/api';
import {useI18n} from '../../../../../i18n/hook';
import {ApiRequestSender} from '../../../../../utils/services/api/requestSender';
import {ModalFixedContent} from '../../modal/fix';
import {ModalStateFix} from '../../modal/types';
import {SubscribeButtonIconOnly} from './icon';
import {SubscribeButtonText} from './text';
import {SubscribeButtonState} from './type';


export type SubscribeButtonProps = {
subscriptionKey: SubscriptionKey,
defaultSubscribed: boolean,
asIcon?: boolean,
disabled?: boolean,
onClick?: () => void,
};

export const SubscribeButton = ({
subscriptionKey,
defaultSubscribed,
asIcon = true,
disabled,
onClick,
}: SubscribeButtonProps) => {
const {status, data} = useSession();
const {t} = useI18n();

const [state, setState] = React.useState<SubscribeButtonState>({
subscribed: defaultSubscribed,
updating: false,
});
const [modalState, setModalState] = React.useState<ModalStateFix>({
show: false,
title: '',
});
const {subscribed} = state;

const fnUpdateSubscription = subscribed ? ApiRequestSender.removeSubscription : ApiRequestSender.addSubscription;

const onClickInternal = () => {
if (status === 'unauthenticated') {
setModalState({...modalState, show: true});
return;
}

setState({...state, updating: true});

fnUpdateSubscription(data?.user.id.toString() || '', subscriptionKey)
.then((response) => {
if (isFailedResponse(response)) {
setModalState({...modalState, show: true});
return;
}

setState({subscribed: !subscribed, updating: false});

if (onClick) {
onClick();
}
});
};

return (
<>
<ModalFixedContent state={modalState} setState={setModalState}>
{t((t) => t.message.error.auth.loginRequired)}
</ModalFixedContent>
{
asIcon ?
<SubscribeButtonIconOnly onClick={onClickInternal} state={state} disabled={disabled}/> :
<SubscribeButtonText onClick={onClickInternal} state={state} disabled={disabled}/>
}
</>
);
};
27 changes: 27 additions & 0 deletions src/components/elements/common/button/subscribe/text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';

import Button from 'react-bootstrap/Button';

import {useI18n} from '../../../../../i18n/hook';
import {SubscribeButtonCommonProps} from './type';


type Props = SubscribeButtonCommonProps;

export const SubscribeButtonText = ({onClick, state, disabled}: Props) => {
const {t} = useI18n();

const {subscribed, updating} = state;

return (
<Button
variant={subscribed ? 'outline-danger' : 'outline-warning'}
onClick={onClick}
disabled={updating || disabled}
>
{subscribed ?
t((t) => t.misc.subscription.remove) :
t((t) => t.misc.subscription.add)}
</Button>
);
};
10 changes: 10 additions & 0 deletions src/components/elements/common/button/subscribe/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type SubscribeButtonState = {
subscribed: boolean,
updating: boolean,
};

export type SubscribeButtonCommonProps = {
onClick: () => void,
state: SubscribeButtonState,
disabled?: boolean,
};
4 changes: 4 additions & 0 deletions src/components/elements/common/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ export const IconStop = () => <i className="bi bi-stop-fill"/>;
export const IconCollapseToLeft = () => <i className="bi bi-arrow-bar-left"/>;

export const IconExpandToRight = () => <i className="bi bi-arrow-bar-right"/>;

export const IconSubscribed = () => <i className="bi bi-bell"/>;

export const IconNotSubscribed = () => <i className="bi bi-bell-slash"/>;
Loading

0 comments on commit 0a71734

Please sign in to comment.