diff --git a/app/assets/stylesheets/article-show.scss b/app/assets/stylesheets/article-show.scss index 083e5d3b9..047011734 100644 --- a/app/assets/stylesheets/article-show.scss +++ b/app/assets/stylesheets/article-show.scss @@ -1,5 +1,6 @@ @import 'variables'; @import 'mixins'; +@import 'config/import'; .unpublished { background: $red; @@ -1308,6 +1309,7 @@ article { max-width: 98%; margin: auto; margin-top: -80px; + padding-bottom: $su-6; text-align: center; .full-discussion-button { diff --git a/app/assets/stylesheets/comments.scss b/app/assets/stylesheets/comments.scss index 332cc8b41..2823e6890 100644 --- a/app/assets/stylesheets/comments.scss +++ b/app/assets/stylesheets/comments.scss @@ -124,8 +124,6 @@ a.header-link { } .comments-container { - width: 800px; - max-width: 98%; padding-top: 10px; margin: auto; margin-bottom: 100px; diff --git a/app/javascript/CommentSubscription/CommentSubscription.jsx b/app/javascript/CommentSubscription/CommentSubscription.jsx index 90e397b41..c17d97251 100644 --- a/app/javascript/CommentSubscription/CommentSubscription.jsx +++ b/app/javascript/CommentSubscription/CommentSubscription.jsx @@ -8,28 +8,42 @@ import { RadioButton, } from '@crayons'; -const COMMENT_SUBSCRIPTION_TYPE = { +export const COMMENT_SUBSCRIPTION_TYPE = Object.freeze({ ALL: 'all_comments', TOP: 'top_level_comments', AUTHOR: 'only_author_comments', -}; + NOT_SUBSCRIBED: 'not_subscribed', +}); export class CommentSubscription extends Component { - state = { - showOptions: false, - commentSubscriptionType: COMMENT_SUBSCRIPTION_TYPE.ALL, - subscribed: false, - }; + constructor(props) { + const { subscriptionType } = props; + super(props); - componentDidMount() { - window.addEventListener('scroll', this.dropdownPlacementHandler); + const subscribed = + subscriptionType && + (subscriptionType.length > 0 && subscriptionType) !== + COMMENT_SUBSCRIPTION_TYPE.NOT_SUBSCRIBED; + + const initialState = { + subscriptionType: subscribed + ? subscriptionType + : COMMENT_SUBSCRIPTION_TYPE.ALL, + subscribed, + showOptions: false, + }; + + this.state = initialState; } componentDidUpdate() { const { showOptions } = this.state; if (showOptions) { + window.addEventListener('scroll', this.dropdownPlacementHandler); this.dropdownPlacementHandler(); + } else { + window.removeEventListener('scroll', this.dropdownPlacementHandler); } } @@ -57,13 +71,17 @@ export class CommentSubscription extends Component { commentSubscriptionClick = (event) => { this.setState({ - commentSubscriptionType: event.target.value, + subscriptionType: event.target.value, }); }; render() { - const { showOptions, commentSubscriptionType, subscribed } = this.state; - const { onSubscribe, onUnsubscribe } = this.props; + const { showOptions, subscriptionType, subscribed } = this.state; + const { + onSubscribe, + onUnsubscribe, + positionType = 'relative', + } = this.props; const CogIcon = () => ( +
{ this.buttonGroupElement = element; @@ -90,9 +108,12 @@ export class CommentSubscription extends Component { variant="outlined" onClick={(_event) => { if (subscribed) { - onUnsubscribe(); + onUnsubscribe(COMMENT_SUBSCRIPTION_TYPE.NOT_SUBSCRIBED); + this.setState({ + subscriptionType: COMMENT_SUBSCRIPTION_TYPE.ALL, + }); } else { - onSubscribe(commentSubscriptionType); + onSubscribe(subscriptionType); } this.setState({ subscribed: !subscribed }); @@ -113,7 +134,13 @@ export class CommentSubscription extends Component { {subscribed && ( { this.dropdownElement = element; }} @@ -123,10 +150,8 @@ export class CommentSubscription extends Component {
`; + +exports[` should render as subscribed with the given subscription type 1`] = ` +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+`; diff --git a/app/javascript/CommentSubscription/__tests__/commentSubscriptionUtilities.test.js b/app/javascript/CommentSubscription/__tests__/commentSubscriptionUtilities.test.js new file mode 100644 index 000000000..a3ff3f5b3 --- /dev/null +++ b/app/javascript/CommentSubscription/__tests__/commentSubscriptionUtilities.test.js @@ -0,0 +1,114 @@ +import fetch from 'jest-fetch-mock'; +import { + getCommentSubscriptionStatus, + setCommentSubscriptionStatus, +} from '../commentSubscriptionUtilities'; + +/* global globalThis */ + +describe('Comment Subscription Utilities', () => { + const csrfToken = 'this-is-a-csrf-token'; + const articleID = 26; // Just a random article ID. + + beforeAll(() => { + globalThis.fetch = fetch; + globalThis.getCsrfToken = async () => csrfToken; + }); + afterAll(() => { + delete globalThis.fetch; + delete globalThis.getCsrfToken; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('getCommentSubscriptionStatus', () => { + it('should return a subscription status', async () => { + const response = '{ "config": "all_comments" }'; + fetch.mockResponse(response); + + const subscriptionStatus = await getCommentSubscriptionStatus(articleID); + + expect(subscriptionStatus).toEqual(JSON.parse(response)); + }); + + it('should return a friendly error message when it fails', async () => { + fetch.mockResponse('...some error page'); + + const subscriptionStatus = await getCommentSubscriptionStatus(articleID); + + expect(subscriptionStatus instanceof Error).toEqual(true); + expect(subscriptionStatus.message).toEqual( + 'An error occurred, please try again', + ); + }); + }); + + describe('setCommentSubscriptionStatus', () => { + it('should unsubscribe', async () => { + fetch.mockResponse('false'); + + const subscriptionStatusMessage = await setCommentSubscriptionStatus( + articleID, + 'not_subscribed', + ); + + expect(subscriptionStatusMessage).toEqual( + 'You have been unsubscribed from comments for this post', + ); + }); + + it('should subscribe', async () => { + fetch.mockResponse(true); + + const subscriptionStatusMessage = await setCommentSubscriptionStatus( + articleID, + 'all_comments', + ); + + expect(subscriptionStatusMessage).toEqual( + 'You have been subscribed to all comments', + ); + }); + + it('should return a friendly error message if HTML is returned', async () => { + fetch.mockResponse('error'); + + const subscriptionStatusMessage = await setCommentSubscriptionStatus( + articleID, + 'all_comments', + ); + + expect(subscriptionStatusMessage).toEqual( + 'An error occurred, please try again', + ); + }); + + it('should return a friendly error message if an error occurs', async () => { + fetch.mockReject(() => new Error('oh no')); + + const subscriptionStatusMessage = await setCommentSubscriptionStatus( + articleID, + 'all_comments', + ); + + expect(subscriptionStatusMessage).toEqual( + 'An error occurred, please try again', + ); + }); + + it('should return a friendly error message if an HTTP status object is returned', async () => { + fetch.mockResponse('{"status":405,"error":"Method Not Allowed"}'); + + const subscriptionStatusMessage = await setCommentSubscriptionStatus( + articleID, + 'all_comments', + ); + + expect(subscriptionStatusMessage).toEqual( + 'An error occurred, please try again', + ); + }); + }); +}); diff --git a/app/javascript/CommentSubscription/commentSubscriptionUtilities.jsx b/app/javascript/CommentSubscription/commentSubscriptionUtilities.jsx new file mode 100644 index 000000000..7b46729f0 --- /dev/null +++ b/app/javascript/CommentSubscription/commentSubscriptionUtilities.jsx @@ -0,0 +1,69 @@ +import { request } from '../utilities/http/request'; + +/** + * Gets the comment subscription status for a given article. + * + * @param {number} articleId + * + * @returns {string} The subscription status. + */ +export async function getCommentSubscriptionStatus(articleId) { + try { + const response = await request( + `/notification_subscriptions/Article/${articleId}`, + ); + + const subscriptionStatus = await response.json(); + + return subscriptionStatus; + } catch (error) { + return new Error('An error occurred, please try again'); + } +} + +/** + * Set's the subscription status for a given article. + * + * @param {number} articleId + * @param {string} subscriptionType + * + * @returns {string} A friendly message in regards to subscription status. + */ +export async function setCommentSubscriptionStatus( + articleId, + subscriptionType, +) { + let message; + + try { + const response = await request( + `/notification_subscriptions/Article/${articleId}`, + { + method: 'POST', + body: JSON.stringify({ config: subscriptionType }), + }, + ); + + // true means you're subscribed, false means unsubscribed + const subscribed = await response.json(); + + if (typeof subscribed !== 'boolean') { + message = 'An error occurred, please try again'; + + return message; + } + + message = 'You have been unsubscribed from comments for this post'; + + if (subscribed) { + message = `You have been subscribed to ${subscriptionType.replace( + /_/g, + ' ', + )}`; + } + } catch (error) { + message = 'An error occurred, please try again'; + } + + return message; +} diff --git a/app/javascript/CommentSubscription/index.jsx b/app/javascript/CommentSubscription/index.jsx index 23a1dde2e..4eee79dc4 100644 --- a/app/javascript/CommentSubscription/index.jsx +++ b/app/javascript/CommentSubscription/index.jsx @@ -1 +1,2 @@ export * from './CommentSubscription'; +export * from './commentSubscriptionUtilities'; diff --git a/app/javascript/Snackbar/Snackbar.jsx b/app/javascript/Snackbar/Snackbar.jsx index d13aeb2f9..f2eb05609 100644 --- a/app/javascript/Snackbar/Snackbar.jsx +++ b/app/javascript/Snackbar/Snackbar.jsx @@ -1,15 +1,178 @@ -import { h } from 'preact'; +import { h, Component } from 'preact'; import PropTypes from 'prop-types'; -import { snackbarItemProps } from './SnackbarItem'; +import { SnackbarItem } from './SnackbarItem'; -export const Snackbar = ({ children = [] }) => ( -
0 ? 'crayons-snackbar' : 'hidden'}> - {children} -
-); +let snackbarItems = []; -Snackbar.displayName = 'Snackbar'; +export function addSnackbarItem(snackbarItem) { + if (!Array.isArray(snackbarItem.actions)) { + snackbarItem.actions = []; // eslint-disable-line no-param-reassign + } + + snackbarItems.push(snackbarItem); +} + +export class Snackbar extends Component { + state = { + snacks: [], + }; + + pollingId; + + paused = false; + + pauseLifespan; + + resumeLifespan; + + componentDidMount() { + this.initializePolling(); + } + + componentDidUpdate() { + if (!this.pauseLifespan) { + this.pauseLifespan = (_event) => { + this.paused = true; + }; + + this.resumeLifespan = (event) => { + event.stopPropagation(); + this.paused = false; + }; + + this.element.addEventListener('mouseover', this.pauseLifespan); + this.element.addEventListener('mouseout', this.resumeLifespan, true); + } + } + + componentWillUnmount() { + if (this.element) { + this.element.removeEventListener('mouseover', this.pauseLifespan); + this.element.addEventListener('mouseout', this.resumeLifespan); + } + } + + initializePolling() { + const { pollingTime, lifespan } = this.props; + + this.pollingId = setInterval(() => { + if (snackbarItems.length > 0) { + // Need to add the lifespan to snackbar items because each second that goes by, we + // decrease the lifespan until it is no more. + const newSnacks = snackbarItems.map((snackbarItem) => ({ + ...snackbarItem, + lifespan, + })); + + snackbarItems = []; + + this.updateSnackbarItems(newSnacks); + + // Start the lifespan countdowns for each new snackbar item. + newSnacks.forEach((snack) => { + // eslint-disable-next-line no-param-reassign + snack.lifespanTimeoutId = setTimeout(() => { + this.decreaseLifespan(snack); + }, 1000); + + if (snack.addCloseButton) { + // Adds an optional close button if addDefaultACtion is true. + snack.actions.push({ + text: 'Dismiss', + handler: () => { + this.setState((prevState) => { + return { + prevState, + snacks: prevState.snacks.filter( + (potentialSnackToFilterOut) => + potentialSnackToFilterOut !== snack, + ), + }; + }); + }, + }); + } + }); + } + }, pollingTime); + } + + updateSnackbarItems(newSnacks) { + this.setState((prevState) => { + let updatedSnacks = [...prevState.snacks, ...newSnacks]; + + if (updatedSnacks.length > 3) { + const snacksToBeDiscarded = updatedSnacks.slice( + 0, + updatedSnacks.length - 3, + ); + + snacksToBeDiscarded.forEach(({ lifespanTimeoutId }) => { + clearTimeout(lifespanTimeoutId); + }); + + updatedSnacks = updatedSnacks.slice(updatedSnacks.length - 3); + } + + return { ...prevState, snacks: updatedSnacks }; + }); + } + + decreaseLifespan(snack) { + /* eslint-disable no-param-reassign */ + if (!this.paused && snack.lifespan === 0) { + clearTimeout(snack.lifespanTimeoutId); + + this.setState((prevState) => { + const snacks = prevState.snacks.filter( + (currentSnack) => currentSnack !== snack, + ); + + return { + ...prevState, + snacks, + }; + }); + + return; + } + + if (!this.paused) { + snack.lifespan -= 1; + } + + snack.lifespanTimeoutId = setTimeout(() => { + this.decreaseLifespan(snack); + }, 1000); + /* eslint-enable no-param-reassign */ + } + + render() { + const { snacks } = this.state; + + return ( +
0 ? 'crayons-snackbar' : 'hidden'} + ref={(element) => { + this.element = element; + }} + > + {snacks.map(({ message, actions = [] }) => ( + + ))} +
+ ); + } +} + +Snackbar.defaultProps = { + lifespan: 5, + pollingTime: 300, +}; + +Snackbar.displayName = 'SnackbarPoller'; Snackbar.propTypes = { - children: PropTypes.arrayOf(snackbarItemProps).isRequired, + lifespan: PropTypes.number, + pollingTime: PropTypes.number, }; diff --git a/app/javascript/Snackbar/SnackbarPoller.jsx b/app/javascript/Snackbar/SnackbarPoller.jsx deleted file mode 100644 index 7c1237ec3..000000000 --- a/app/javascript/Snackbar/SnackbarPoller.jsx +++ /dev/null @@ -1,155 +0,0 @@ -import { h, Component } from 'preact'; -import PropTypes from 'prop-types'; -import { Snackbar } from './Snackbar'; -import { SnackbarItem } from './SnackbarItem'; - -let snackbarItems = []; - -export function addSnackbarItem(snackbarItem) { - snackbarItems.push(snackbarItem); -} - -export class SnackbarPoller extends Component { - state = { - snacks: [], - }; - - pollingId; - - paused = false; - - pauseLifespan; - - resumeLifespan; - - componentDidMount() { - this.initializePolling(); - } - - componentDidUpdate() { - if (!this.pauseLifespan) { - this.pauseLifespan = (_event) => { - this.paused = true; - }; - - this.resumeLifespan = (event) => { - event.stopPropagation(); - this.paused = false; - }; - - this.element.base.addEventListener('mouseover', this.pauseLifespan); - this.element.base.addEventListener('mouseout', this.resumeLifespan, true); - } - } - - componentWillUnmount() { - if (this.element) { - this.element.base.removeEventListener('mouseover', this.pauseLifespan); - this.element.base.addEventListener('mouseout', this.resumeLifespan); - } - } - - initializePolling() { - const { pollingTime, lifespan } = this.props; - - this.pollingId = setInterval(() => { - if (snackbarItems.length > 0) { - // Need to add the lifespan to snackbar items because each second that goes by, we - // decrease the lifespan until it is no more. - const newSnacks = snackbarItems.map((snackbarItem) => ({ - ...snackbarItem, - lifespan, - })); - - newSnacks.forEach((snack) => { - // eslint-disable-next-line no-param-reassign - snack.lifespanTimeoutId = setTimeout(() => { - this.decreaseLifespan(snack); - }, 1000); - }); - - snackbarItems = []; - - this.updateSnackbarItems(newSnacks); - } - }, pollingTime); - } - - updateSnackbarItems(newSnacks) { - this.setState((prevState) => { - let updatedSnacks = [...prevState.snacks, ...newSnacks]; - - if (updatedSnacks.length > 3) { - const snacksToBeDiscarded = updatedSnacks.slice( - 0, - updatedSnacks.length - 3, - ); - - snacksToBeDiscarded.forEach(({ lifespanTimeoutId }) => { - clearTimeout(lifespanTimeoutId); - }); - - updatedSnacks = updatedSnacks.slice(updatedSnacks.length - 3); - } - - return { ...prevState, snacks: updatedSnacks }; - }); - } - - decreaseLifespan(snack) { - /* eslint-disable no-param-reassign */ - if (!this.paused && snack.lifespan === 0) { - clearTimeout(snack.lifespanTimeoutId); - - this.setState((prevState) => { - const snacks = prevState.snacks.filter( - (currentSnack) => currentSnack !== snack, - ); - - return { - ...prevState, - snacks, - }; - }); - - return; - } - - if (!this.paused) { - snack.lifespan -= 1; - } - - snack.lifespanTimeoutId = setTimeout(() => { - this.decreaseLifespan(snack); - }, 1000); - /* eslint-enable no-param-reassign */ - } - - render() { - const { snacks } = this.state; - - return ( - { - this.element = element; - }} - > - {snacks.map(({ message, actions = [] }) => ( - - ))} - - ); - } -} - -SnackbarPoller.defaultProps = { - lifespan: 5, - pollingTime: 300, -}; - -SnackbarPoller.displayName = 'SnackbarPoller'; - -SnackbarPoller.propTypes = { - lifespan: PropTypes.number, - pollingTime: PropTypes.number, -}; diff --git a/app/javascript/Snackbar/__stories__/Snackbar.stories.jsx b/app/javascript/Snackbar/__stories__/Snackbar.stories.jsx index af2a5e996..813ea8b5c 100644 --- a/app/javascript/Snackbar/__stories__/Snackbar.stories.jsx +++ b/app/javascript/Snackbar/__stories__/Snackbar.stories.jsx @@ -1,104 +1,50 @@ import { h } from 'preact'; import { action } from '@storybook/addon-actions'; -import { Snackbar, SnackbarItem } from '..'; +import faker from 'faker'; +import { number } from '@storybook/addon-knobs'; +import { Snackbar, addSnackbarItem } from '..'; export default { - title: 'App Components/Snackbar', + title: 'App Components/Snackbar/Snackbar', }; -export const Description = () => ( -
-
-

Snackbars

-

- Snackbars inform users of a process that an app has performed. They - appear temporarily, towards the bottom of the screen. They shouldn’t - interrupt the user experience, and they don’t require user input to - disappear. -

-

- A Snackbar can contain a single action. Because they disappear - automatically, the action shouldn’t be “Dismiss” or “Cancel.” -

-

- A Snackbar disappears after 5000ms by default. Countdown will be paused - when user mouse over the snackbar. -

-

- Snackbars can be stacked on top of each other if there's more of - them. New ones show up at the bottom of snackbar. We can display maximum - 3 snackbars at a time. -

-
-
-

Usage

-

- The Snackbar component has a default lifespan of 5000ms if no - {' '} - lifespan - {' '} - prop is provided. It also has a default polling - time of 300ms to check for new Snackbar items if no - {' '} - pollingTime - {' '} - prop is provided. -

-
-        <Snackbar lifespan="3000" pollingTime="300" />
-      
-

Adding a Snackbar Item

-
-        addSnackbarItem({ text: 'Action 1', handler: event =>
-        { console.log('Action 1 clicked') } })
-      
-
-
-); +export const SimulateAddingSnackbarItems = () => { + addSnackbarItem({ + message: faker.hacker.phrase(), + actions: [ + { text: faker.lorem.word(), handler: action('action 1 clicked') }, + ], + }); -Description.story = { - name: 'description', -}; + addSnackbarItem({ + message: faker.hacker.phrase(), + actions: [ + { text: faker.lorem.word(), handler: action('action 2 clicked') }, + ], + }); -export const OneSnackbarItem = () => ( - - - -); + addSnackbarItem({ + message: faker.hacker.phrase(), + actions: [ + { text: faker.lorem.word(), handler: action('action 3 clicked') }, + ], + }); -OneSnackbarItem.story = { - name: 'one snackbar item', -}; - -export const MultipleSnackbarItems = () => { - const snackbarItems = [ - { - message: 'File uploaded successfully', - }, - - { - message: 'Unable to save file', - actions: [ - { text: 'Retry', handler: action('save file retry') }, - { text: 'Abort', handler: action('abort file save') }, - ], - }, - - { - message: 'There was a network error', - actions: [{ text: 'Retry', handler: action('retry network') }], - }, - ]; + addSnackbarItem({ + message: faker.hacker.phrase(), + actions: [ + { text: faker.lorem.word(), handler: action('action 3 clicked') }, + ], + }); return ( - - {snackbarItems.map(({ message, actions = [] }) => ( - - ))} - + ); }; -MultipleSnackbarItems.story = { - name: 'multiple snackbar items', +SimulateAddingSnackbarItems.story = { + name: 'simulating adding multiple snackbar items', }; diff --git a/app/javascript/Snackbar/__stories__/SnackbarPoller.stories.jsx b/app/javascript/Snackbar/__stories__/SnackbarPoller.stories.jsx deleted file mode 100644 index eb26fd473..000000000 --- a/app/javascript/Snackbar/__stories__/SnackbarPoller.stories.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import { h } from 'preact'; -import { action } from '@storybook/addon-actions'; -import faker from 'faker'; -import { number } from '@storybook/addon-knobs'; -import { SnackbarPoller, addSnackbarItem } from '..'; - -export default { - title: 'App Components/Snackbar/SnackbarPoller', -}; - -export const SimulateAddingSnackbarItems = () => { - addSnackbarItem({ - message: faker.hacker.phrase(), - actions: [ - { text: faker.lorem.word(), handler: action('action 1 clicked') }, - ], - }); - - addSnackbarItem({ - message: faker.hacker.phrase(), - actions: [ - { text: faker.lorem.word(), handler: action('action 2 clicked') }, - ], - }); - - addSnackbarItem({ - message: faker.hacker.phrase(), - actions: [ - { text: faker.lorem.word(), handler: action('action 3 clicked') }, - ], - }); - - addSnackbarItem({ - message: faker.hacker.phrase(), - actions: [ - { text: faker.lorem.word(), handler: action('action 3 clicked') }, - ], - }); - - return ( - - ); -}; - -SimulateAddingSnackbarItems.story = { - name: 'simulating adding multiple snackbar items', -}; diff --git a/app/javascript/Snackbar/__tests__/Snackbar.test.jsx b/app/javascript/Snackbar/__tests__/Snackbar.test.jsx deleted file mode 100644 index 1e52c7701..000000000 --- a/app/javascript/Snackbar/__tests__/Snackbar.test.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import { h } from 'preact'; -import render from 'preact-render-to-json'; -import { Snackbar, SnackbarItem } from '..'; - -describe('', () => { - it('should render with one snackbar item', () => { - const tree = render( - - - , - ); - - expect(tree).toMatchSnapshot(); - }); - - it('should render with multiple snackbar items', () => { - const snackbarItems = [ - { - message: 'File uploaded successfully', - }, - - { - message: 'Unable to save file', - actions: [ - { text: 'Retry', handler: jest.fn() }, - { text: 'Abort', handler: jest.fn() }, - ], - }, - - { - message: 'There was a network error', - actions: [{ text: 'Retry', handler: jest.fn() }], - }, - ]; - - const tree = render( - - {snackbarItems.map(({ message, actions = [] }) => ( - - ))} - , - ); - - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/app/javascript/Snackbar/__tests__/__snapshots__/Snackbar.test.jsx.snap b/app/javascript/Snackbar/__tests__/__snapshots__/Snackbar.test.jsx.snap deleted file mode 100644 index edf2d9068..000000000 --- a/app/javascript/Snackbar/__tests__/__snapshots__/Snackbar.test.jsx.snap +++ /dev/null @@ -1,89 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render with multiple snackbar items 1`] = ` -
-
-
- File uploaded successfully -
-
-
-
-
- Unable to save file -
-
- - -
-
-
-
- There was a network error -
-
- -
-
-
-`; - -exports[` should render with one snackbar item 1`] = ` -
-
-
- File uploaded successfully -
-
-
-
-`; diff --git a/app/javascript/Snackbar/index.js b/app/javascript/Snackbar/index.js index 6358c43d4..b4c8f9800 100644 --- a/app/javascript/Snackbar/index.js +++ b/app/javascript/Snackbar/index.js @@ -1,3 +1,2 @@ export * from './Snackbar'; export * from './SnackbarItem'; -export * from './SnackbarPoller'; diff --git a/app/javascript/icons/CogIcon.jsx b/app/javascript/icons/CogIcon.jsx new file mode 100644 index 000000000..ab99eafd3 --- /dev/null +++ b/app/javascript/icons/CogIcon.jsx @@ -0,0 +1,15 @@ +import { h } from 'preact'; + +export const CogIcon = () => ( + + Preferences + + +); diff --git a/app/javascript/icons/__stories__/CogIcon.stories.jsx b/app/javascript/icons/__stories__/CogIcon.stories.jsx new file mode 100644 index 000000000..5103630a0 --- /dev/null +++ b/app/javascript/icons/__stories__/CogIcon.stories.jsx @@ -0,0 +1,12 @@ +import { h } from 'preact'; +import { CogIcon } from '../CogIcon'; + +export default { + title: 'Components/Icons', +}; + +export const Default = () => ; + +Default.story = { + name: 'cog icon', +}; diff --git a/app/javascript/icons/__tests__/CogIcon.test.jsx b/app/javascript/icons/__tests__/CogIcon.test.jsx new file mode 100644 index 000000000..b0fd096fe --- /dev/null +++ b/app/javascript/icons/__tests__/CogIcon.test.jsx @@ -0,0 +1,11 @@ +import { h } from 'preact'; +import render from 'preact-render-to-json'; +import { CogIcon } from '../CogIcon'; + +describe('', () => { + it('should render', () => { + const tree = render(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/app/javascript/icons/__tests__/__snapshots__/CogIcon.test.jsx.snap b/app/javascript/icons/__tests__/__snapshots__/CogIcon.test.jsx.snap new file mode 100644 index 000000000..21ef2a976 --- /dev/null +++ b/app/javascript/icons/__tests__/__snapshots__/CogIcon.test.jsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render 1`] = ` + + + Preferences + + + +`; diff --git a/app/javascript/icons/index.js b/app/javascript/icons/index.js new file mode 100644 index 000000000..7f878f2ad --- /dev/null +++ b/app/javascript/icons/index.js @@ -0,0 +1 @@ +export * from './CogIcon'; diff --git a/app/javascript/packs/articlePage.jsx b/app/javascript/packs/articlePage.jsx new file mode 100644 index 000000000..7d22b41fc --- /dev/null +++ b/app/javascript/packs/articlePage.jsx @@ -0,0 +1,55 @@ +import { h, render } from 'preact'; +import { Snackbar, addSnackbarItem } from '../Snackbar'; + +// The Snackbar for the article page +const snackZone = document.getElementById('snack-zone'); + +render(, snackZone, snackZone.firstElementChild); + +const userDataIntervalID = setInterval(async () => { + const { user = null, userStatus } = document.body.dataset; + + if (userStatus === 'logged-out') { + // User is not logged on so nothing dynamic to add to the page. + clearInterval(userDataIntervalID); + return; + } + + if (userStatus === 'logged-in' && user !== null) { + // Load the comment subscription button for logged on users. + clearInterval(userDataIntervalID); + const root = document.querySelector('#comment-subscription'); + + try { + const { + getCommentSubscriptionStatus, + setCommentSubscriptionStatus, + CommentSubscription, + } = await import('../CommentSubscription'); + + const { articleId } = document.querySelector('#article-body').dataset; + const { config: subscriptionType } = await getCommentSubscriptionStatus( + articleId, + ); + const subscriptionRequestHandler = async (type) => { + const message = await setCommentSubscriptionStatus(articleId, type); + + addSnackbarItem({ message, addCloseButton: true }); + }; + + render( + , + root, + root.firstElementChild, + ); + } catch (e) { + document.querySelector('#comment-subscription').innerHTML = + '

Unable to load Comment Subscription component.
Try refreshing the page.

'; + } + } +}); diff --git a/app/views/articles/_actions.html.erb b/app/views/articles/_actions.html.erb index a4895f574..49bccfc24 100644 --- a/app/views/articles/_actions.html.erb +++ b/app/views/articles/_actions.html.erb @@ -41,26 +41,6 @@