diff --git a/.stylelintrc b/.stylelintrc index ff45e7849..c1d0cb9c6 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -5,6 +5,11 @@ ], "rules": { "at-rule-no-unknown": null, - "scss/at-rule-no-unknown": true + "scss/at-rule-no-unknown": null, + "scss/at-import-partial-extension": null, + "no-descending-specificity": null, + "no-duplicate-selectors": null, + "scss/operator-no-unspaced": null, + "no-invalid-position-at-import-rule": null } } diff --git a/app/assets/arrow/stylesheets/app.scss b/app/assets/arrow/stylesheets/app.scss new file mode 100644 index 000000000..c336e6e84 --- /dev/null +++ b/app/assets/arrow/stylesheets/app.scss @@ -0,0 +1,274 @@ +/* Global CSS */ + +:root { + /* Colour Palette */ + --arrow-c-brand1: #ff214b; + --arrow-c-brand2: #1bbb87; + --arrow-c-brand3: #6fe7c0; + --arrow-c-brand4: #f6f8ff; + --arrow-c-brand5: #e9e9e9; + --arrow-c-accent1: #2fd072; + --arrow-c-accent2: #f5a623; + --arrow-c-accent3: #f81010; + --arrow-c-accent4: #d71212; + --arrow-c-accent5: #f2f2f2; + --arrow-c-accent6: #e8eaed; + --arrow-c-mono1: rgba(0, 0, 0, 1); + --arrow-c-mono2: rgba(0, 0, 0, 0.9); + --arrow-c-mono3: rgba(0, 0, 0, 0.7); + --arrow-c-mono4: rgba(0, 0, 0, 0.6); + --arrow-c-mono5: rgba(0, 0, 0, 0.3); + --arrow-c-mono6: rgba(0, 0, 0, 0.2); + --arrow-c-mono7: rgba(0, 0, 0, 0.1); + --arrow-c-invert-mono1: rgba(255, 255, 255, 1); + --arrow-c-invert-mono2: rgba(255, 255, 255, 0.9); + --arrow-c-invert-mono3: rgba(255, 255, 255, 0.7); + --arrow-c-invert-mono4: rgba(255, 255, 255, 0.6); + --arrow-c-invert-mono5: rgba(255, 255, 255, 0.3); + --arrow-c-invert-mono6: rgba(255, 255, 255, 0.2); + --arrow-c-invert-mono7: rgba(255, 255, 255, 0.1); + --arrow-c-invert-mono8: rgba(222, 222, 222, 1); + --arrow-c-invert-mono9: rgba(222, 222, 222, 0.1); + --arrow-c-dark: #0d0d0d; + --arrow-c-light: #ffffff; + /* Spacing */ + --arrow-spacing-xxs: 4px; + --arrow-spacing-xs: 8px; + --arrow-spacing-s: 12px; + --arrow-spacing-m: 16px; + --arrow-spacing-20: 20px; + --arrow-spacing-l: 24px; + --arrow-spacing-xl: 32px; + --arrow-spacing-28: 28px; + --arrow-spacing-48: 48px; + --arrow-spacing-96: 96px; + /* Font Scale */ + --arrow-fs-tiny: 14px; + --arrow-fs-xs: 16px; + --arrow-fs-s: 18px; + --arrow-fs-m: 20px; + --arrow-fs-l: 24px; + --arrow-fs-xl: 26px; + --arrow-fs-xxl: 30px; + --arrow-fs-huge: 32px; + --arrow-fs-big: 40px; + --arrow-fs-jumbo: 54px; + + --arrow-fs-28: 28px; + + /* Font Family */ + --arrow-typeface-primary: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; + --arrow-typeface-secondary: Verdana, Geneva, Tahoma, sans-serif; + --arrow-sans-serif: sans-serif; + /* Line height */ + --arrow-lh-1: 1; + --arrow-lh-2: 1.2; + --arrow-lh-3: 1.3; + --arrow-lh-4: 1.4; + --arrow-lh-5: 1.5; + /* Font Weight */ + --arrow-fw-light: 300; + --arrow-fw-normal: 400; + --arrow-fw-semi-bold: 600; + --arrow-fw-bold: 700; + /* Border Color */ + --light-border: var(--arrow-c-invert-mono7); + --dark-border: var(--arrow-c-mono7); + /* Fallback Image Color */ + /* need to update this color in future */ + --fallback-img: rgb(232, 232, 232); + /* Z-Index */ + --z-index-9: 9; + --z-index-1: 1; +} + +.arrow-component { + font-family: var(--arrow-typeface-primary); + font-weight: var(--arrow-fw-normal); + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -moz-font-feature-settings: "liga" on; + h1, + h2, + h3, + h4, + h5, + h6, + ul, + ol { + font-family: var(--arrow-typeface-primary); + } + p, + li { + font-size: var(--arrow-fs-xs); + line-height: var(--arrow-lh-1); + } + + figure { + margin: 0; + padding: 0; + } + /* Typescale */ + h1 { + font-size: var(--arrow-fs-28); + font-weight: var(--arrow-fw-bold); + font-stretch: normal; + font-style: normal; + line-height: var(--arrow-lh-3); + letter-spacing: normal; + @media only screen and (min-width: 768px) { + font-size: var(--arrow-fs-big); + } + } + h2 { + font-size: var(--arrow-fs-l); + font-weight: var(--arrow-fw-bold); + font-stretch: normal; + font-style: normal; + line-height: var(--arrow-lh-3); + letter-spacing: normal; + @media only screen and (min-width: 768px) { + font-size: var(--arrow-fs-huge); + } + } + h3 { + font-size: var(--arrow-fs-m); + font-weight: var(--arrow-fw-bold); + font-stretch: normal; + font-style: normal; + line-height: var(--arrow-lh-3); + letter-spacing: normal; + @media only screen and (min-width: 768px) { + font-size: var(--arrow-fs-l); + } + } + h4 { + font-size: var(--arrow-fs-s); + font-weight: var(--arrow-fw-bold); + font-stretch: normal; + font-style: normal; + line-height: var(--arrow-lh-3); + letter-spacing: normal; + @media only screen and (min-width: 768px) { + font-size: var(--arrow-fs-m); + } + } + h5 { + font-size: var(--arrow-fs-xs); + font-weight: var(--arrow-fw-bold); + font-stretch: normal; + font-style: normal; + line-height: var(--arrow-lh-3); + letter-spacing: normal; + @media only screen and (min-width: 768px) { + font-size: var(--arrow-fs-s); + } + } + h6 { + font-size: var(--arrow-fs-xs); + font-weight: var(--arrow-fw-bold); + font-stretch: normal; + font-style: normal; + line-height: var(--arrow-lh-3); + letter-spacing: normal; + } + p { + font-size: var(--arrow-fs-m); + font-weight: normal; + font-stretch: normal; + font-style: normal; + line-height: var(--arrow-lh-5); + letter-spacing: normal; + font-family: var(--arrow-typeface-secondary); + word-break: break-word; + } +} + +.arrow-component.full-width-with-padding { + margin-left: calc(-50vw + 50%); + margin-right: calc(-50vw + 50%); + padding: var(--arrow-spacing-m) calc(50vw - 50%) var(--arrow-spacing-xs) calc(50vw - 50%); +} + +.p-alt { + font-size: var(--arrow-fs-xs); + font-weight: normal; + font-stretch: normal; + font-style: normal; + line-height: var(--arrow-lh-5); + letter-spacing: normal; +} + +.time { + font-size: var(--arrow-fs-tiny); + font-weight: normal; + font-stretch: normal; + font-style: normal; + letter-spacing: normal; +} + +.author-name { + font-size: var(--arrow-fs-tiny); + font-weight: normal; + font-stretch: normal; + font-style: normal; + letter-spacing: normal; +} + +.section-tag { + font-size: var(--arrow-fs-tiny); + font-weight: bold; + font-stretch: normal; + font-style: normal; + line-height: normal; + letter-spacing: normal; +} + +.arr-hidden-desktop { + display: none; + @media only screen and (max-width: 768px) { + display: block; + } +} + +.arr-hidden-mob { + display: block; + @media only screen and (max-width: 768px) { + display: none; + } +} + +.arr-custom-style a { + color: var(--arrow-c-mono2); + border-bottom: 1px solid var(--arrow-c-mono2); +} + +.arr-story-grid { + display: grid; + grid-column-gap: var(--arrow-spacing-l); + grid-template-columns: repeat(4, minmax(auto, 150px)); + position: relative; + margin: 0 12px; + @media only screen and (min-width: 768px) { + grid-template-columns: + [grid-start] minmax(48px, auto) [container-start] repeat(12, minmax(auto, 55px)) [container-end] minmax( + 48px, + auto + ) + [grid-end]; + margin: unset; + @media only screen and (min-width: 1025px) { + grid-template-columns: + [grid-start] minmax(48px, auto) [container-start] repeat(12, minmax(auto, 55px)) [container-end] minmax( + 48px, + auto + ) + [grid-end]; + } + } +} + +.content-style { + display: contents; +} diff --git a/app/assets/arrow/stylesheets/mixins.scss b/app/assets/arrow/stylesheets/mixins.scss new file mode 100644 index 000000000..185e32efc --- /dev/null +++ b/app/assets/arrow/stylesheets/mixins.scss @@ -0,0 +1,27 @@ +$mobile: 767px; +$tablet: 768px; +$desktop: 1025px; +$desktop-lg: 1200px; + +@mixin mobile { + @media (max-width: #{$mobile}) { + @content; + } +} + +@mixin tablet { + @media (min-width: #{$tablet}) { + @content; + } +} +@mixin desktop { + @media (min-width: #{$desktop}) { + @content; + } +} + +@mixin desktop-lg { + @media (min-width: #{$desktop-lg}) { + @content; + } +} diff --git a/app/assets/arrow/stylesheets/reset.scss b/app/assets/arrow/stylesheets/reset.scss new file mode 100644 index 000000000..fa91ff65f --- /dev/null +++ b/app/assets/arrow/stylesheets/reset.scss @@ -0,0 +1,148 @@ +/* RESET CSS */ + +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +*, +*::after, +*::before { + box-sizing: border-box; +} + +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font: inherit 100%; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} +body { + line-height: 1; +} +ol, +ul { + list-style: none; +} +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ""; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +figure, +img { + width: 100%; + margin: 0; + padding: 0; +} + +a { + text-decoration: none; + color: inherit; +} diff --git a/app/assets/arrow/stylesheets/viewports.m.css b/app/assets/arrow/stylesheets/viewports.m.css new file mode 100644 index 000000000..40b874837 --- /dev/null +++ b/app/assets/arrow/stylesheets/viewports.m.css @@ -0,0 +1,3 @@ +@value mobile: 768px; +@value tablet: 992px; +@value desktop: 1025px; diff --git a/app/assets/stylesheets/app.scss b/app/assets/stylesheets/app.scss index e61802339..a875f6e9e 100644 --- a/app/assets/stylesheets/app.scss +++ b/app/assets/stylesheets/app.scss @@ -6,7 +6,7 @@ @import "./base/colors"; @import "./base/normalize"; @import "./base/typography"; -@import "@quintype/arrow/app"; +@import "../arrow/stylesheets/app.scss"; :root { --container-width: 1280px; @@ -122,7 +122,6 @@ a { justify-content: center; .youtube-iframe-wrapper { - position: absolute; z-index: 2; } diff --git a/app/isomorphic/arrow/components/Atoms/AdPlaceholder/ads.m.css b/app/isomorphic/arrow/components/Atoms/AdPlaceholder/ads.m.css new file mode 100644 index 000000000..3f2519e7b --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AdPlaceholder/ads.m.css @@ -0,0 +1,13 @@ +.ads { + background-color: var(--arrow-c-mono5); + display: flex; + justify-content: center; + align-items: center; +} +.ad-text { + color: var(--arrow-c-mono3); +} +.ad-wrapper { + display: flex; + justify-content: center; +} diff --git a/app/isomorphic/arrow/components/Atoms/AdPlaceholder/index.js b/app/isomorphic/arrow/components/Atoms/AdPlaceholder/index.js new file mode 100644 index 000000000..a9c02c9c6 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AdPlaceholder/index.js @@ -0,0 +1,25 @@ +import React from "react"; +import PropTypes from "prop-types"; +import "./ads.m.css"; + +export const AdPlaceholder = ({ height, width }) => { + return ( +
+
+

LEADERBOARD

+
+
+ ); +}; + +AdPlaceholder.propTypes = { + // height of ad + height: PropTypes.string, + // width of ad + width: PropTypes.string, +}; + +AdPlaceholder.defaultProps = { + width: "720px", + height: "90px", +}; diff --git a/app/isomorphic/arrow/components/Atoms/Author/README.md b/app/isomorphic/arrow/components/Atoms/Author/README.md new file mode 100644 index 000000000..c3506bbd9 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Author/README.md @@ -0,0 +1,19 @@ +# Author + +Displays the author for the story card based on the first author from the Story API reponse. + +## Usage + +```jsx + +``` + +```jsx + +``` + +```jsx + +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/Author/author.m.css b/app/isomorphic/arrow/components/Atoms/Author/author.m.css new file mode 100644 index 000000000..96570aed4 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Author/author.m.css @@ -0,0 +1,65 @@ +@custom-media --viewport-medium (width >= 768px); + +.author { + display: flex; + align-items: center; + color: var(--arrow-c-mono4); + margin-bottom: var(--arrow-spacing-xs); +} + +.author-image { + border-radius: 100%; + width: var(--arrow-spacing-l); + height: var(--arrow-spacing-l); + margin-right: var(--arrow-spacing-xs); + overflow: hidden; +} + +.author-image figure, +.author-image img { + /* width & height need to be explicitly set for improving CLS */ + height: 100%; + width: 100%; +} + +.author-name { + margin: auto var(--arrow-spacing-xs) auto 0; +} + +.bottom-fix { + margin-top: auto; +} + +.prefix { + margin: auto var(--arrow-spacing-xxs) auto 0; +} + +.rtl-image { + display: none; +} + +html[dir="rtl"] { + .author-name { + margin-right: 0; + } + + .prefix { + margin-right: 0; + margin-left: var(--arrow-spacing-xxs); + } + + .ltr-image { + display: none; + } + + .rtl-image { + display: block; + } +} + +.dark { + color: var(--arrow-c-mono4); +} +.light { + color: var(--arrow-c-invert-mono4); +} diff --git a/app/isomorphic/arrow/components/Atoms/Author/index.js b/app/isomorphic/arrow/components/Atoms/Author/index.js new file mode 100644 index 000000000..cffb0a6b9 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Author/index.js @@ -0,0 +1,100 @@ +import React from "react"; +import PropTypes from "prop-types"; +import get from "lodash/get"; +import { useStateValue } from "../../SharedContext"; +import { ResponsiveImage, Link } from "@quintype/components"; +import { getTextColor } from "../../../utils/utils"; +import { connect } from "react-redux"; + +import "./author.m.css"; + +const AuthorBase = ({ story, hideAuthorImage, isBottom, prefix = "", config = {} }) => { + const configData = useStateValue() || {}; + const isAuthor = get(configData, ["showAuthor"], true); + const { + "avatar-url": avatarUrl, + "avatar-s3-key": avatarS3Key, + name: authorName, + slug, + } = get(story, ["authors", "0"], ""); + const isBottomClasses = isBottom ? "bottom-fix" : ""; + const textColor = getTextColor(configData.theme); + const mountAt = get(config, ["mountAt"], ""); + + const authorImage = (dir) => { + if (!avatarUrl && !avatarS3Key) { + return null; + } + return ( +
+
+ {avatarS3Key ? ( + + ) : ( + + )} +
+
+ ); + }; + const isPrefix = (dir) => { + if (prefix) { + return ( +
+ {prefix} +
+ ); + } + return !hideAuthorImage && authorImage(dir); + }; + return ( + <> + {isAuthor && ( + + {isPrefix("ltr")} +
+ {authorName || story["author-name"]} +
+ {!prefix && isPrefix("rtl")} + + )} + + ); +}; + +function mapStateToProps(state) { + return { + config: get(state, ["qt", "config"], {}), + }; +} + +export const Author = connect(mapStateToProps)(AuthorBase); + +AuthorBase.propTypes = { + /** The Story Object from the API response */ + story: PropTypes.object.isRequired, + /** Hide Author Avatar Image */ + hideAuthorImage: PropTypes.bool, + // fix Author bottom of the storyCard + isBottom: PropTypes.bool, + // fix prefix before the author name + prefix: PropTypes.string, + config: PropTypes.object, +}; + +AuthorBase.defaultProps = { + hideAuthorImage: false, + isBottom: false, + prefix: "", +}; diff --git a/app/isomorphic/arrow/components/Atoms/Author/stories.js b/app/isomorphic/arrow/components/Atoms/Author/stories.js new file mode 100644 index 000000000..7ff012a9d --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Author/stories.js @@ -0,0 +1,23 @@ +import React from "react"; +import { generateStory } from "../../Fixture"; +import { withStore } from "../../../../storybook"; +import { Author } from "./index"; +import Readme from "./README.md"; + +const story = generateStory(); + +withStore( + "Atoms/Author", + { + qt: { + config: { + "cdn-image": "thumbor-stg.assettype.com", + mountAt: "/sub-directory", + }, + }, + }, + Readme +) + .add("With Author Image", () => ) + .add("With Prefix", () => ) + .add("Without Author Image", () => ); diff --git a/app/isomorphic/arrow/components/Atoms/AuthorCard/README.md b/app/isomorphic/arrow/components/Atoms/AuthorCard/README.md new file mode 100644 index 000000000..6a919a5f9 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorCard/README.md @@ -0,0 +1,47 @@ +# Author Card + +Displays the author details as a card for the story page + +### Use as a component + + +#### default template + +```jsx + +``` + +#### Author card with right align template + +```jsx + +``` + +#### Author card with center align template + +```jsx + +``` + +#### Author card with default align template + +```jsx + +``` + +#### Author card with left align template + +```jsx + +``` +#### Author card with options + +```jsx + +``` + + + + + + diff --git a/app/isomorphic/arrow/components/Atoms/AuthorCard/author-card.m.css b/app/isomorphic/arrow/components/Atoms/AuthorCard/author-card.m.css new file mode 100644 index 000000000..ed2772483 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorCard/author-card.m.css @@ -0,0 +1,175 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; + +.author-card-wrapper { + display: flex; +} + +.author-card-wrapper.centerAligned { + display: block; + text-align: center; +} + +.author-image-wrapper { + display: flex; + margin-right: var(--arrow-spacing-m); + margin-bottom: var(--arrow-spacing-s); + @media (min-width: mobile) { + margin-right: var(--arrow-spacing-l); + } +} + +.default .author-image-wrapper { + margin: 0 12px 0 0; +} + +.centerAligned .author-image-wrapper { + justify-content: center; + margin-right: 0; +} + +.author-image { + margin-left: -100px; + overflow: hidden; +} + +.author-image figure { + margin: 0; +} + +.author-image img { + border-radius: 50%; + height: 72px; + width: 72px; + @media (min-width: mobile) { + height: 124px; + width: 124px; + } +} + +.multi-author-image img { + border: solid 2px var(--arrow-c-light); +} + +.default .author-image img { + height: 32px; + width: 32px; +} + +.default .multi-author-image { + margin-left: -24px; + border-width: 1px; +} + +.author-image.first-author-image { + margin-left: 0; +} + +.author-details-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + padding: 0; + margin-left: var(--arrow-spacing-m); + @media (min-width: mobile) { + padding: var(--arrow-spacing-xs) var(--arrow-spacing-xs) var(--arrow-spacing-xs) 0; + margin-left: var(--arrow-spacing-l); + } +} + +.default .author-details-wrapper { + margin-left: var(--arrow-spacing-s); +} + +.leftAligned .author-details-wrapper { + margin-left: var(--arrow-spacing-m); + @media (min-width: mobile) { + margin-left: var(--arrow-spacing-l); + } +} + +.centerAligned .author-details-wrapper { + align-items: center; + margin-left: 0; +} + +.author-name-share { + display: flex; + align-items: center; + font-size: var(--arrow-fs-xs); + line-height: var(--arrow-lh-1); + justify-content: center; + @media (min-width: mobile) { + font-size: var(--arrow-fs-s); + } +} + +html[dir="rtl"] { + .author-image-wrapper { + margin-left: var(--arrow-spacing-m); + margin-right: 0; + @media (min-width: mobile) { + margin-left: var(--arrow-spacing-l); + } + } + + .leftAligned .author-details-wrapper { + margin-right: var(--arrow-spacing-m); + margin-left: 0; + @media (min-width: mobile) { + margin-right: var(--arrow-spacing-l); + } + } + + .multi-author-image { + position: relative; + right: -100px; + } + + .default .multi-author-image { + margin-left: -16px; + right: -12px; + @media (min-width: mobile) { + margin-left: -24px; + right: -12px; + } + } + + .author-image.first-author-image { + position: static; + } + + .centerAligned .author-image-wrapper { + margin-left: 0; + } +} + +.multiple-author-style { + font-size: var(--arrow-fs-xs); + @media (min-width: mobile) { + font-size: var(--arrow-fs-s); + } +} + +.dot-wrapper { + margin: 0 var(--arrow-spacing-xxs) 0 var(--arrow-spacing-xs); +} + +.author-bio { + font-style: italic; + line-height: var(--arrow-lh-5); + color: var(--arrow-c-m); + font-size: var(--arrow-fs-tiny); + @media (min-width: mobile) { + font-size: var(--arrow-fs-xs); + } +} + +.dark { + color: var(--arrow-c-mono2); +} + +.light { + color: var(--arrow-c-invert-mono2); +} diff --git a/app/isomorphic/arrow/components/Atoms/AuthorCard/author-card.test.js b/app/isomorphic/arrow/components/Atoms/AuthorCard/author-card.test.js new file mode 100644 index 000000000..e55c20a4c --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorCard/author-card.test.js @@ -0,0 +1,96 @@ +import * as React from "react"; +import { mount } from "enzyme"; +import expect from "expect"; +import { generateStory, generateStore } from "../../Fixture/index"; +import { AuthorCard } from "./index"; +import { Provider } from "react-redux"; +import { Twitter } from "../../Svgs/SocialIcons/twitter"; + +const story = generateStory(); + +const singleAuthorStory = { + ...story, + authors: [ + { + id: 123981, + name: "Ravigopal Kesari", + slug: "ravigopal-kesari", + social: { + twitter: { + url: "https://www.twitter.com/sabqorg", + handle: "elonmusk", + }, + }, + "avatar-url": + "https://lh5.googleusercontent.com/-NhNrHEp1w4M/AAAAAAAAAAI/AAAAAAAAAAs/lzYwVY1BQdQ/photo.jpg?sz=50", + "avatar-s3-key": null, + "twitter-handle": null, + bio: "William Shakespeare was an English poet, playwright, and actor, widely regarded as the greatest writer in the English language and the world’s greatest dramatist. He is often called England’s national poet and the “Bard of Avon”", + "contributor-role": { + id: 873, + name: "Author", + }, + }, + ], +}; + +describe("default author card", () => { + it("Should render leftAligned template", () => { + const wrapper = mount( + + + + ); + expect(wrapper.find({ "data-test-id": "author-card-left-aligned" }).prop("className")).toMatch(/leftAligned/); + }); + it("Should render default template", () => { + const wrapper = mount( + + + + ); + expect(wrapper.find({ "data-test-id": "author-card-default" }).prop("className")).toMatch(/default/); + }); + it("Should render centerAligned template", () => { + const wrapper = mount( + + + + ); + expect(wrapper.find({ "data-test-id": "author-card-center-aligned" }).prop("className")).toMatch(/centerAligned/); + }); + it("Should render author card without image", () => { + const wrapper = mount( + + + + ); + expect(wrapper.find({ "data-test-id": "author-image" }).length).toEqual(0); + }); + it("Should render author card without author bio", () => { + const wrapper = mount( + + + + ); + expect(wrapper.find({ "data-test-id": "author-bio" }).length).toEqual(0); + }); + + it("Should not render twitter icon when template is centerAligned", () => { + const wrapper = mount( + + + + ); + expect(wrapper.contains()).toEqual(false); + }); + + it("Should render twitter icon when template is not centerAligned", () => { + const wrapper = mount( + + + + ); + expect(wrapper.contains()).toEqual(true); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/AuthorCard/index.js b/app/isomorphic/arrow/components/Atoms/AuthorCard/index.js new file mode 100644 index 000000000..f8e8be2ce --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorCard/index.js @@ -0,0 +1,119 @@ +import React from "react"; +import PropTypes from "prop-types"; +import kebabCase from "lodash/kebabCase"; +import { Dot } from "../Dot/dot"; +import { ResponsiveImage, Link } from "@quintype/components"; +import { Twitter } from "../../Svgs/SocialIcons/twitter"; +import "./author-card.m.css"; +import { useStateValue } from "../../SharedContext"; +import { getTextColor, getAuthorTwitterUrl } from "../../../utils/utils"; + +const authorImage = (author) => { + const { "avatar-url": avatarUrl, "avatar-s3-key": avatarS3Key, name: authorName } = author; + if (!avatarUrl && !avatarS3Key) { + return null; + } + return ( +
+ {avatarS3Key ? ( + + ) : ( + + )} +
+ ); +}; + +export const AuthorCard = ({ story = {}, template = "leftAligned", clazzName = "", opts = {}, mountAt = "" }) => { + const configData = useStateValue() || {}; + const { authors = [] } = story; + const isSingleAuthor = authors.length === 1; + const { showBio = false, showImage = true, showName = true } = opts; + const textColor = getTextColor(configData.theme); + let twitterUrl = ""; + if (isSingleAuthor) { + twitterUrl = getAuthorTwitterUrl(authors[0]); + } + + return ( +
+ {authors.map((author, index) => { + return ( + + {showImage && (author["avatar-url"] || author["avatar-s3-key"]) && ( +
+ +
+ {authorImage(author)} +
+ +
+ )} +
+ ); + })} + {showName && ( +
+ {authors.map((author, index) => { + const spaceHolder = `,\xa0`; + return ( + <> + {index !== 0 && (authors.length > 1 ? spaceHolder : "")} + +
{author.name}
+ + + ); + })} + {isSingleAuthor && template !== "centerAligned" && ( + <> + {twitterUrl && ( + <> +
+ +
+ + + + + )} + + )} +
+ )} +
+ {showBio && isSingleAuthor && template !== "default" && ( +

{authors[0].bio}

+ )} +
+
+ ); +}; + +AuthorCard.propTypes = { + story: PropTypes.shape({ + authors: PropTypes.array, + }), + template: PropTypes.string, + opts: PropTypes.shape({ + showImage: PropTypes.bool, + showBio: PropTypes.bool, + showName: PropTypes.bool, + }), + mountAt: PropTypes.string, + clazzName: PropTypes.string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/AuthorCard/stories.js b/app/isomorphic/arrow/components/Atoms/AuthorCard/stories.js new file mode 100644 index 000000000..14253b314 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorCard/stories.js @@ -0,0 +1,74 @@ +import React from "react"; +import { generateStory } from "../../Fixture"; +import { withStore, optionalSelect } from "../../../../storybook"; +import { AuthorCard } from "./index"; +import Readme from "./README.md"; +import { boolean } from "@storybook/addon-knobs"; + +const authorTemplate = { + "No value": "", + default: "default", + leftAligned: "leftAligned", + centerAligned: "centerAligned", +}; + +const story = generateStory(); + +const singleAuthorStory = { + ...story, + authors: [ + { + id: 123981, + name: "Ravigopal Kesari", + slug: "ravigopal-kesari", + social: { + twitter: { + url: "https://www.twitter.com/sabqorg", + handle: "elonmusk", + }, + }, + "avatar-url": + "https://lh5.googleusercontent.com/-NhNrHEp1w4M/AAAAAAAAAAI/AAAAAAAAAAs/lzYwVY1BQdQ/photo.jpg?sz=50", + "avatar-s3-key": null, + "twitter-handle": "quintype_inc", + bio: "William Shakespeare was an English poet, playwright, and actor, widely regarded as the greatest writer in the English language and the world’s greatest dramatist. He is often called England’s national poet and the “Bard of Avon”", + "contributor-role": { + id: 873, + name: "Author", + }, + }, + ], +}; + +withStore( + "Atoms/Author Card", + { + qt: { + config: { + "cdn-image": "thumbor-stg.assettype.com", + }, + }, + }, + Readme +) + .addDecorator((story) =>
{story()}
) + .add("Default", () => ( + + )) + .add("single author template", () => ( + + )); diff --git a/app/isomorphic/arrow/components/Atoms/AuthorImage/README.md b/app/isomorphic/arrow/components/Atoms/AuthorImage/README.md new file mode 100644 index 000000000..72ef7209a --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorImage/README.md @@ -0,0 +1,31 @@ +# Author Image + +Author image is a generic component which picks a specific author's image. + +## Usage + +Import the AuthorImage component and pass author details as props. + +```jsx +import { AuthorImage } from "@quintype/arrow"; +``` + +### Use as a component + +```jsx + + + +``` + +```jsx + + + +``` + +```jsx + + + +``` diff --git a/app/isomorphic/arrow/components/Atoms/AuthorImage/author-image.m.css b/app/isomorphic/arrow/components/Atoms/AuthorImage/author-image.m.css new file mode 100644 index 000000000..d8e34a65c --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorImage/author-image.m.css @@ -0,0 +1,53 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; +@value tablet from viewports; + +.image-container { + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; +} + +.author-image img, +.author-image svg { + border-radius: 50%; + width: 160px; + height: 160px; + @media (min-width: mobile) { + position: relative; + width: 224px; + height: 224px; + @media (min-width: tablet) { + width: 364px; + } + } +} + +.square img { + border-radius: 0; +} + +.small-circle.author-image img, +.small-circle.author-image svg { + border-radius: 133.5px; + width: 40px; + height: 40px; + @media (min-width: mobile) { + width: 88px; + height: 88px; + } + @media (min-width: tablet) { + width: 120px; + height: 120px; + } +} + +.smaller-circle.author-image img, +.smaller-circle.author-image svg { + width: 64px; + height: 64px; +} diff --git a/app/isomorphic/arrow/components/Atoms/AuthorImage/index.js b/app/isomorphic/arrow/components/Atoms/AuthorImage/index.js new file mode 100644 index 000000000..40384a6f6 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorImage/index.js @@ -0,0 +1,59 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { ResponsiveImage, Link } from "@quintype/components"; +import { UserFallbackIcon } from "../../Svgs/user-fallback-icon"; +import "./author-image.m.css"; + +const authorWithImage = (author, templateStyle) => { + const { "avatar-url": avatarUrl, "avatar-s3-key": avatarS3Key, name } = author; + return ( +
+
+ {avatarS3Key ? ( + + ) : avatarUrl ? ( + + ) : ( + + )} +
+
+ ); +}; + +const AuthorImage = ({ author = {}, template = "", config = {} }) => { + const { slug } = author; + const mountAt = config.mountAt || ""; + let templateStyle = ""; + + if (template === "smallerCircle") templateStyle = "smaller-circle"; + else if (template === "smallCircle") templateStyle = "small-circle"; + else if (template === "square") templateStyle = "square"; + + if (slug) return {authorWithImage(author, templateStyle)}; + else return authorWithImage(author, templateStyle); +}; + +AuthorImage.propTypes = { + author: PropTypes.shape({ + "avatar-url": PropTypes.string, + "avatar-s3-key": PropTypes.string, + name: PropTypes.string, + slug: PropTypes.string, + }), + template: PropTypes.string, + config: PropTypes.object, +}; + +export default AuthorImage; diff --git a/app/isomorphic/arrow/components/Atoms/AuthorImage/stories.js b/app/isomorphic/arrow/components/Atoms/AuthorImage/stories.js new file mode 100644 index 000000000..70bdc9290 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorImage/stories.js @@ -0,0 +1,20 @@ +import React from "react"; +import { withStore } from "../../../../storybook"; +import AuthorImage from "./index"; +import Readme from "./README.md"; +import { authorData } from "../../Fixture"; + +withStore( + "Atoms/AuthorImage", + { + qt: { + config: { + "cdn-image": "thumbor-stg.assettype.com", + }, + }, + }, + Readme +) + .add("Default", () => ) + .add("Square Image", () => ) + .add("Small Circle Image", () => ); diff --git a/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/README.md b/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/README.md new file mode 100644 index 000000000..02e8e2bcb --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/README.md @@ -0,0 +1,12 @@ +# AuthorWithTimestamp +A combination of the Author and Timestamp atoms. + +## Usage + +```jsx + +``` +```jsx + +``` + diff --git a/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/author-with-timestamp.m.css b/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/author-with-timestamp.m.css new file mode 100644 index 000000000..40ba194e7 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/author-with-timestamp.m.css @@ -0,0 +1,42 @@ +@custom-media --viewport-medium (width >= 768px); + +.author-time-wrapper { + display: flex; + flex-wrap: wrap; + align-items: center; + color: var(--arrow-c-mono2); +} + +.author-image { + border-radius: 100%; + width: var(--arrow-spacing-l); + height: var(--arrow-spacing-l); + margin-right: var(--arrow-spacing-xs); +} + +.dot { + position: relative; + margin-right: var(--arrow-spacing-xs); + top: -6px; +} +.bottom-fix { + margin-top: auto; +} +.dash { + position: relative; + margin-right: var(--arrow-spacing-xs); + top: -1px; + @media (--viewport-medium) { + top: -3px; + } +} + +html[dir="rtl"] { + .dot { + margin-left: var(--arrow-spacing-xs); + } + + .dash { + padding-left: var(--arrow-spacing-xs); + } +} diff --git a/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/index.js b/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/index.js new file mode 100644 index 000000000..b2501aadf --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/index.js @@ -0,0 +1,79 @@ +import React from "react"; +import get from "lodash/get"; +import { TimeStamp } from "../TimeStamp"; +import PropTypes from "prop-types"; +import { Author } from "../Author"; +import { Dot } from "../Dot/dot"; +import { Divider } from "../Divider/"; +import { useStateValue } from "../../SharedContext"; + +import "./author-with-timestamp.m.css"; +import { getTextColor } from "../../../utils/utils"; +import { ReadTime } from "../ReadTime"; + +export const AuthorWithTime = ({ story, separator, isBottom, hideAuthorImage, prefix, config = {}, isLightTheme }) => { + const configState = useStateValue() || {}; + const isAuthor = get(config, ["showAuthor"], get(configState, ["showAuthor"], true)); + const isTime = get(config, ["showTime"], get(configState, ["showTime"], true)); + const isReadTime = get(configState, ["showReadTime"], true); + const isBottomClasses = isBottom ? "bottom-fix" : ""; + const seperatorStyle = separator === "dot" ? "dot" : "dash"; + const textColor = isLightTheme ? "light" : getTextColor(configState.theme); + + const getSeparator = () => { + if (separator === "divider") { + return ; + } + return ; + }; + + const getTimeStamp = () => { + if (!isTime) return null; + return ( + <> + {isAuthor && ( + + {getSeparator()} + + )} + + {isReadTime && } + + ); + }; + + return ( +
+ {isAuthor ? ( + <> + + {getTimeStamp()} + + ) : ( + getTimeStamp() + )} +
+ ); +}; + +AuthorWithTime.propTypes = { + story: PropTypes.object, + /** Style of separator between the Author & Timestamp */ + separator: PropTypes.oneOf(["dot", "divider"]), + /** hide the author image */ + hideAuthorImage: PropTypes.bool, + // fix author and time at the end + isBottom: PropTypes.bool, + // fix prefix before the author name + prefix: PropTypes.string, + config: PropTypes.object, + isLightTheme: PropTypes.bool, +}; + +AuthorWithTime.defaultProps = { + separator: "dot", + isBottom: false, + hideAuthorImage: true, + prefix: "", + isLightTheme: false, +}; diff --git a/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/stories.js b/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/stories.js new file mode 100644 index 000000000..86743995d --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/AuthorWithTimestamp/stories.js @@ -0,0 +1,27 @@ +import React from "react"; +import { generateStory } from "../../Fixture"; +import { withStore } from "../../../../storybook"; +import { AuthorWithTime } from "./index"; +import Readme from "./README.md"; + +const story = generateStory(); +const config = { + localizationConfig: { + localizedTimeToRead: "মিনিট পড়া", + }, +}; + +withStore( + "Atoms/Author With Time Stamp", + { + qt: { + config: { + "cdn-image": "thumbor-stg.assettype.com", + }, + }, + }, + Readme +) + .add("default", () => ) + .add("Author without Divider and Time", () => ) + .add("Author with Localization Time", () => ); diff --git a/app/isomorphic/arrow/components/Atoms/BulletPoint/README.md b/app/isomorphic/arrow/components/Atoms/BulletPoint/README.md new file mode 100644 index 000000000..1408a0616 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/BulletPoint/README.md @@ -0,0 +1,9 @@ +# Bullet Point + +Component to add a bullet point to a story card. + +## Usage + +```jsx + +``` diff --git a/app/isomorphic/arrow/components/Atoms/BulletPoint/bullet-point.m.css b/app/isomorphic/arrow/components/Atoms/BulletPoint/bullet-point.m.css new file mode 100644 index 000000000..2f31a772b --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/BulletPoint/bullet-point.m.css @@ -0,0 +1,14 @@ +.bullet { + margin-right: var(--arrow-spacing-l); + font-size: var(--arrow-fs-huge); + font-weight: var(--arrow-fw-bold); + line-height: var(--arrow-lh-3); +} + +.dark { + color: var(--arrow-c-mono6); +} + +.light { + color: var(--arrow-c-invert-mono6); +} diff --git a/app/isomorphic/arrow/components/Atoms/BulletPoint/index.js b/app/isomorphic/arrow/components/Atoms/BulletPoint/index.js new file mode 100644 index 000000000..483479667 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/BulletPoint/index.js @@ -0,0 +1,19 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { useStateValue } from "../../SharedContext"; +import { getTextColor } from "../../../utils/utils"; +import "./bullet-point.m.css"; + +export const BulletPoint = ({ bulletValue = "" }) => { + const configData = useStateValue() || {}; + const textColor = getTextColor(configData.theme); + return ( +
+ {bulletValue} +
+ ); +}; + +BulletPoint.propTypes = { + bulletValue: PropTypes.string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/BulletPoint/stories.js b/app/isomorphic/arrow/components/Atoms/BulletPoint/stories.js new file mode 100644 index 000000000..e0dc8df6e --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/BulletPoint/stories.js @@ -0,0 +1,6 @@ +import React from "react"; +import { withStore } from "../../../../storybook"; +import { BulletPoint } from "./index"; +import Readme from "./README.md"; + +withStore("Atoms/Bullet Point", {}, Readme).add("Default", () => ); diff --git a/app/isomorphic/arrow/components/Atoms/CaptionAttribution/README.md b/app/isomorphic/arrow/components/Atoms/CaptionAttribution/README.md new file mode 100644 index 000000000..2f7c502da --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/CaptionAttribution/README.md @@ -0,0 +1,15 @@ +# CaptionAttribution + +Displays the Caption and Attribution of Hero Image or Image Element of the Story. + +## Usage + +```jsx + +``` + +```jsx + +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/CaptionAttribution/caption-attribution.m.css b/app/isomorphic/arrow/components/Atoms/CaptionAttribution/caption-attribution.m.css new file mode 100644 index 000000000..7a9515057 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/CaptionAttribution/caption-attribution.m.css @@ -0,0 +1,41 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; + +.caption { + font-size: 12px; + font-family: var(--arrow-typeface-secondary); + margin: var(--arrow-spacing-xs) 0; + line-height: var(--arrow-lh-5); + @media (min-width: mobile) { + font-size: var(--arrow-fs-tiny); + } +} + +.wrapper { + padding-left: var(--arrow-spacing-s); +} + +.caption.dark { + color: var(--arrow-c-mono2); +} + +.attribution.dark { + color: var(--arrow-c-mono4); +} + +.caption.light { + color: var(--arrow-c-invert-mono2); +} + +.attribution.light { + color: var(--arrow-c-invert-mono4); +} + +html[dir="rtl"] { + .wrapper { + padding-left: 0; + padding-right: var(--arrow-spacing-s); + display: inline-block; + } +} diff --git a/app/isomorphic/arrow/components/Atoms/CaptionAttribution/caption-attribution.test.js b/app/isomorphic/arrow/components/Atoms/CaptionAttribution/caption-attribution.test.js new file mode 100644 index 000000000..5aec7ae8b --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/CaptionAttribution/caption-attribution.test.js @@ -0,0 +1,29 @@ +import React from "react"; +import { mount, shallow } from "enzyme"; +import { CaptionAttribution } from "./index"; +import { generateStory, generateStoryElementData } from "../../Fixture"; + +describe("Caption Attribution Component", () => { + it("Should display the Caption and Attribution of Image element", () => { + const element = generateStoryElementData("image"); + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "caption" }).prop("className")).toMatch(/caption/); + // the below test is to check if the attribution is present + expect(wrapper.find({ "data-test-id": "attribution" }).prop("className")).toMatch(/attribution/); + // the below test is to check if the caption is present + expect(wrapper.find({ "data-test-id": "attribution" }).prop("className")).toMatch(/wrapper/); + }); + + it("Should display the Caption and Attribution of Story", () => { + const story = generateStory(); + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "caption" }).prop("className")).toMatch(/caption/); + expect(wrapper.find({ "data-test-id": "attribution" }).prop("className")).toMatch(/attribution/); + expect(wrapper.find({ "data-test-id": "attribution" }).prop("className")).toMatch(/wrapper/); + }); + + test("Should not render the Caption and Attribution if the data is null", () => { + const component = shallow(); + expect(component.contains()).toBe(false); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/CaptionAttribution/index.js b/app/isomorphic/arrow/components/Atoms/CaptionAttribution/index.js new file mode 100644 index 000000000..98ac4f5a3 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/CaptionAttribution/index.js @@ -0,0 +1,40 @@ +import React from "react"; +import PropTypes from "prop-types"; +import "./caption-attribution.m.css"; +import { getTextColor, shapeStory } from "../../../utils/utils"; +import { useStateValue } from "../../SharedContext"; + +export const CaptionAttribution = ({ story, element, config = {} }) => { + if (!(story || element)) return null; + const configData = useStateValue() || {}; + const textColor = getTextColor(configData.theme); + + const { "hero-image-caption": heroImageCaption = "", "hero-image-attribution": heroImageAttribution = "" } = + story || {}; + const { title = "", "image-attribution": imageAttribution = "" } = element || {}; + + const caption = heroImageCaption || title; + const attribution = heroImageAttribution || imageAttribution; + const updateStyle = caption ? "wrapper" : ""; + return ( +
+ + {attribution && ( + + )} +
+ ); +}; + +CaptionAttribution.propTypes = { + config: PropTypes.shape({ theme: PropTypes.string }), + story: shapeStory, + element: PropTypes.shape({ + "image-attribution": PropTypes.string, + title: PropTypes.string, + }), +}; diff --git a/app/isomorphic/arrow/components/Atoms/CaptionAttribution/stories.js b/app/isomorphic/arrow/components/Atoms/CaptionAttribution/stories.js new file mode 100644 index 000000000..0dc104969 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/CaptionAttribution/stories.js @@ -0,0 +1,40 @@ +import React from "react"; +import { CaptionAttribution } from "./index"; +import Readme from "./README.md"; +import { withStore } from "../../../../storybook"; +import { color } from "@storybook/addon-knobs"; +import { generateStory, generateStoryElementData } from "../../Fixture"; + +const story = generateStory(); +const element = generateStoryElementData("image"); +withStore("Atoms/Caption Attribution", Readme) + .add("Hero Image", () => { + const config = { + theme: color("BG Color", "#ffffff"), + }; + return ; + }) + .add("Image Element", () => { + const config = { + theme: color("BG Color", "#ffffff"), + }; + return ; + }) + .add("Caption", () => { + const config = { + theme: color("BG Color", "#ffffff"), + }; + const data = { + title: "Caption", + }; + return ; + }) + .add("Attribution", () => { + const config = { + theme: color("BG Color", "#ffffff"), + }; + const data = { + "image-attribution": "Attribution", + }; + return ; + }); diff --git a/app/isomorphic/arrow/components/Atoms/CollectionName/README.md b/app/isomorphic/arrow/components/Atoms/CollectionName/README.md new file mode 100644 index 000000000..ab9860aab --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/CollectionName/README.md @@ -0,0 +1,11 @@ +# CollectionName + +Displays the collection name, usually for a Row, and links it to a collection or section page accordingly. + +## Usage + +```jsx + +``` + + \ No newline at end of file diff --git a/app/isomorphic/arrow/components/Atoms/CollectionName/collection-name.m.css b/app/isomorphic/arrow/components/Atoms/CollectionName/collection-name.m.css new file mode 100644 index 000000000..0cadcfac7 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/CollectionName/collection-name.m.css @@ -0,0 +1,94 @@ +@custom-media --viewport-medium (width >= 768px); + +.collection { + font-size: var(--arrow-fs-m); + margin-bottom: var(--arrow-spacing-m); + font-weight: var(--arrow-fw-bold); + @media (--viewport-medium) { + font-size: var(--arrow-fs-l); + margin-bottom: var(--arrow-spacing-l); + } +} + +.collection-borderLeft { + display: flex; + align-items: baseline; + + .border-left { + margin-right: var(--arrow-spacing-xs); + min-width: 5px; + width: 5px; + overflow: hidden; + } + + .border-left::before { + content: "I"; + font-family: var(--arrow-sans-serif); + } +} + +.collection-borderBottomFull { + display: inline-block; +} + +.collection-borderBottom { + display: block; +} +.collection-crossLine { + display: flex; + align-items: center; +} +.collection-crossLine:before { + content: " "; + position: relative; + flex: 1 1 auto; + border-top: 1px solid var(--arrow-c-mono5); + margin-right: var(--arrow-spacing-l); +} +.collection-crossLine:after { + content: " "; + position: relative; + flex: 1 1 auto; + border-top: 1px solid var(--arrow-c-mono5); + margin-left: var(--arrow-spacing-l); +} + +html[dir="rtl"] { + .collection-borderLeft { + .border-left { + margin-right: 0; + margin-left: var(--arrow-spacing-xs); + } + } + + .collection-crossLine:before { + margin-right: 0; + margin-left: var(--arrow-spacing-l); + } + .collection-crossLine:after { + margin-left: 0; + margin-right: var(--arrow-spacing-l); + } +} + +.border-bottom { + width: 20px; + height: 4px; + margin-top: var(--arrow-spacing-xxs); +} + +.collection-crossLine .border-bottom { + visibility: hidden; +} +.dark a, +.dark h3, +.dark h2, +.dark h5 { + color: var(--arrow-c-mono2); +} +.light a, +.light h3, +.light h2, +.light h5 { + color: var(--arrow-c-invert-mono2); +} diff --git a/app/isomorphic/arrow/components/Atoms/CollectionName/index.js b/app/isomorphic/arrow/components/Atoms/CollectionName/index.js new file mode 100644 index 000000000..d86558e78 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/CollectionName/index.js @@ -0,0 +1,146 @@ +import React from "react"; +import { connect } from "react-redux"; +import { PropTypes } from "prop-types"; +import { Link } from "@quintype/components"; +import { useStateValue } from "../../SharedContext"; +import get from "lodash/get"; +import { getTextColor, getSlug, rgbToHex, clientWidth } from "../../../utils/utils"; + +import "./collection-name.m.css"; + +export const CollectionNameBase = ({ + collectionNameBorderColor = "", + collection, + config, + collectionNameTemplate, + headerLevel, + customCollectionName = "", + navigate = true, +}) => { + const collectionTitle = useStateValue() || {}; + const showRowTitle = get(collectionTitle, ["showRowTitle"], true); + const supportedTemplates = ["borderLeft", "borderBottom", "crossLine", "borderBottomFull"]; + const templateStyle = supportedTemplates.includes(collectionNameTemplate) + ? ` collection-${collectionNameTemplate}` + : ""; + const slug = (navigate && collection && getSlug(collection, config)) || ""; + const collectionName = customCollectionName || collection.name || ""; + const textColor = getTextColor(collectionTitle.theme); + const HeaderTag = "h" + headerLevel; + const CollectionNameBorderColor = rgbToHex(collectionNameBorderColor); + + const borderBottomFullStyle = templateStyle.includes("collection-borderBottomFull") + ? `4px solid ${CollectionNameBorderColor}` + : ""; + + const borderBottomStyle = () => { + return templateStyle.includes("collection-borderBottom") && + !templateStyle.includes("collection-borderBottomFull") ? ( +
+ ) : ( + "" + ); + }; + + const isMobile = clientWidth("mobile"); + + const getBorderHeight = () => { + if (isMobile) { + switch (headerLevel) { + case "1": + return "32px"; + case "2": + return "24px"; + case "3": + return "20px"; + case "4": + return "18px"; + case "5": + return "16px"; + case "6": + return "16px"; + default: + return "20px"; + } + } + switch (headerLevel) { + case "1": + return "40px"; + case "2": + return "32px"; + case "3": + return "24px"; + case "4": + return "20px"; + case "5": + return "18px"; + case "6": + return "16px"; + default: + return "24px"; + } + }; + return ( + <> + {showRowTitle && collectionName && ( +
+ {templateStyle.includes("collection-borderLeft") && ( + + )} + {slug && !slug.includes(undefined) ? ( + + {collectionName} + + ) : ( + {collectionName} + )} + {borderBottomStyle()} +
+ )} + + ); +}; + +CollectionNameBase.propTypes = { + /** Collection name border color */ + collectionNameBorderColor: PropTypes.string, + /** The Collection Object from the API response */ + collection: PropTypes.object, + /** The publisher config from the API response */ + config: PropTypes.object, + /** The style template for the component */ + collectionNameTemplate: PropTypes.oneOf(["", "borderLeft", "borderBottom", "crossLine", "borderBottomFull"]), + /** Header tags ranging h1-h6, where h[headerLevel] */ + headerLevel: PropTypes.string, + customCollectionName: PropTypes.string, + navigate: PropTypes.bool, +}; + +CollectionNameBase.defaultProps = { + collectionNameTemplate: "", + headerLevel: "3", +}; + +function mapStateToProps(state) { + return { + config: get(state, ["qt", "config"], {}), + }; +} + +export const CollectionName = connect(mapStateToProps)(CollectionNameBase); diff --git a/app/isomorphic/arrow/components/Atoms/CollectionName/stories.js b/app/isomorphic/arrow/components/Atoms/CollectionName/stories.js new file mode 100644 index 000000000..f486f6383 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/CollectionName/stories.js @@ -0,0 +1,52 @@ +import React from "react"; + +import { CollectionName } from "./index.js"; +import { withStore, optionalSelect } from "../../../../storybook"; + +import { generateCollection } from "../../Fixture"; +import { color } from "@storybook/addon-knobs"; + +import Readme from "./README.md"; +const collection = generateCollection(); + +const collectionTemplates = { + default: "default", + borderBottom: "borderBottom", + borderLeft: "borderLeft", + crossLine: "crossLine", + borderBottomFull: "borderBottomFull", +}; + +withStore( + "Atoms/collection Title", + { + qt: { + config: { + sections: [ + { + "domain-slug": null, + slug: "health", + name: "Health", + "section-url": "https://ace-web.qtstage.io/health", + id: 11181, + "parent-id": null, + "display-name": "Health", + collection: { + slug: "health", + name: "Health", + id: 15603, + }, + data: null, + }, + ], + }, + }, + }, + Readme +).add("default", () => ( + +)); diff --git a/app/isomorphic/arrow/components/Atoms/Divider/__snapshots__/divider.test.js.snap b/app/isomorphic/arrow/components/Atoms/Divider/__snapshots__/divider.test.js.snap new file mode 100644 index 000000000..d1871e62d --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Divider/__snapshots__/divider.test.js.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Divider should render 1`] = ` +
+ + + +
+`; diff --git a/app/isomorphic/arrow/components/Atoms/Divider/divider.m.css b/app/isomorphic/arrow/components/Atoms/Divider/divider.m.css new file mode 100644 index 000000000..467a6aaaf --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Divider/divider.m.css @@ -0,0 +1,6 @@ +.dark { + fill: var(--arrow-c-mono4); +} +.light { + fill: var(--arrow-c-invert-mono4); +} diff --git a/app/isomorphic/arrow/components/Atoms/Divider/divider.test.js b/app/isomorphic/arrow/components/Atoms/Divider/divider.test.js new file mode 100644 index 000000000..76abb2955 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Divider/divider.test.js @@ -0,0 +1,10 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { Divider } from "."; + +describe("Divider", () => { + it("should render", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/Divider/index.js b/app/isomorphic/arrow/components/Atoms/Divider/index.js new file mode 100644 index 000000000..f1872d9e0 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Divider/index.js @@ -0,0 +1,20 @@ +import React from "react"; +import PropTypes from "prop-types"; +import "./divider.m.css"; + +export const Divider = ({ width = "1", height = "10", color = "light" }) => { + return ( +
+ + + +
+ ); +}; + +Divider.propTypes = { + /** height , width and color of the dot icon */ + color: PropTypes.string, + width: PropTypes.string, + height: PropTypes.string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/Dot/dot.js b/app/isomorphic/arrow/components/Atoms/Dot/dot.js new file mode 100644 index 000000000..0a43e291c --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Dot/dot.js @@ -0,0 +1,20 @@ +import React from "react"; +import PropTypes from "prop-types"; +import "./dot.m.css"; + +export const Dot = ({ width = "3px", height = "3px", color = "light" }) => { + return ( +
+ + + +
+ ); +}; + +Dot.propTypes = { + /** height , width and color of the dot icon */ + color: PropTypes.string, + width: PropTypes.string, + height: PropTypes.string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/Dot/dot.m.css b/app/isomorphic/arrow/components/Atoms/Dot/dot.m.css new file mode 100644 index 000000000..467a6aaaf --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Dot/dot.m.css @@ -0,0 +1,6 @@ +.dark { + fill: var(--arrow-c-mono4); +} +.light { + fill: var(--arrow-c-invert-mono4); +} diff --git a/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/README.md b/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/README.md new file mode 100644 index 000000000..043642462 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/README.md @@ -0,0 +1,9 @@ +# Extended Load More + +Used for performance gains whilst to help with selective hydration + +## Usage + +```jsx +renderComponent(ExtendedLoadMore, elementId, store, { config, componentName: layout }); +``` diff --git a/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/__snapshots__/extended-load-more.test.js.snap b/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/__snapshots__/extended-load-more.test.js.snap new file mode 100644 index 000000000..1849f0d50 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/__snapshots__/extended-load-more.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Extended Load more button Should render extended load more button 1`] = ` +
+ Read More +
+`; diff --git a/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/extended-load-more.test.js b/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/extended-load-more.test.js new file mode 100644 index 000000000..346694adf --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/extended-load-more.test.js @@ -0,0 +1,19 @@ +import * as React from "react"; +import renderer from "react-test-renderer"; +import { ExtendedLoadMore } from "./index"; +import { Provider } from "react-redux"; +import { generateStore } from "../../Fixture"; + +describe("Extended Load more button", () => { + it("Should render extended load more button", () => { + /* Useless test */ + const wrapper = renderer + .create( + + + + ) + .toJSON(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/index.js b/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/index.js new file mode 100644 index 000000000..ac916d586 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ExtendedLoadMore/index.js @@ -0,0 +1,115 @@ +/* + * ************************************************************************ + * * © [2015 - 2020] Quintype Technologies India Private Limited + * * All Rights Reserved. + * ************************************************************************* + */ +import get from "lodash/get"; +import PropTypes from "prop-types"; +import React, { lazy, Suspense, useState } from "react"; +import { getTextColor } from "../../../utils/utils"; +import "./../Loadmore/load-more.m.css"; + +const getTemplate = (templateName) => { + switch (templateName) { + case "ArrowFourColGrid": + return lazy(() => import("../../Rows/FourColGrid")); + case "ArrowThreeColGrid": + return lazy(() => import("../../Rows/ThreeColGrid")); + case "ArrowOneColStoryList": + return lazy(() => import("../../Rows/OneColStoryList")); + case "ArrowFourColFiveStories": + return lazy(() => import("../../Rows/FourColFiveStories")); + case "ArrowOpinionCollection": + return lazy(() => import("../../Rows/OpinionCollection")); + case "ArrowSixColSixStories": + return lazy(() => import("../../Rows/SixColSixStories")); + case "ArrowThreeColFlexStories": + return lazy(() => import("../../Rows/ThreeColFlexStories")); + case "ArrowFourColPortraitStories": + return lazy(() => import("../../Rows/FourColPortraitStories")); + default: + return lazy(() => import("../../Rows/ThreeColGrid")); + } +}; + +export const ExtendedLoadMore = ({ config, componentName, WithArrowConfig, withSubsequentLoad }) => { + const showButton = get(config, ["showButton"], true); + const btnText = get(config, ["buttonText"], "Read More"); + const theme = get(config, ["theme"], ""); + const textColor = getTextColor(theme); + + /* Open an array to maintain load-more initiated components to re-render */ + const [components, setComponents] = useState([]); + + const addComponent = async (event) => { + event.persist(); + const collectionSlug = event.target.parentElement.getAttribute("data-collection-slug"); + const offset = event.target.parentElement.getAttribute("data-collection-offset"); + const limit = event.target.parentElement.getAttribute("data-collection-limit"); + + const Component = getTemplate(componentName); + + const { items } = await ( + await fetch(`/api/v1/collections/${collectionSlug}?item-type=story&offset=${offset}&limit=${limit}`) + ).json(); + + if (items.length) { + const updatedOffset = parseInt(offset) + parseInt(limit); + event.target.parentElement.setAttribute("data-collection-offset", updatedOffset); + } + + const updatedCompList = [ + ...components, + { + // Passing a number below the initial collection length (which we will not know here) + // as the second parameter causes the Load More button to be rendered regardless of + // the value given to `isLoadMoreVisible`. Since this paramter will be ignored anyway, + // we pass in a number that is sufficiently always larger. + Component: WithArrowConfig(withSubsequentLoad(Component, limit || 10, true)), + props: { + collection: { + items, + }, + isLoadMoreVisible: false, + config, + }, + componentName, + }, + ]; + + setComponents(updatedCompList); + }; + + if (!showButton) return null; + + /* TODO: !HACK! Remove this line post deployment */ + const componentStyles = (componentName) => (componentName === "ArrowOneColStoryList" ? { marginLeft: 0 } : {}); + + return ( + <> + {components.map(({ Component, props }, index) => ( +
+ Loading...
}> + + + + ))} +
+ {btnText} +
+ + ); +}; + +ExtendedLoadMore.propTypes = { + componentName: PropTypes.string, + config: PropTypes.object, + WithArrowConfig: PropTypes.func, + withSubsequentLoad: PropTypes.func, +}; diff --git a/app/isomorphic/arrow/components/Atoms/FallbackImage/fallback.m.css b/app/isomorphic/arrow/components/Atoms/FallbackImage/fallback.m.css new file mode 100644 index 000000000..cccc61152 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/FallbackImage/fallback.m.css @@ -0,0 +1,33 @@ +.image-position { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; +} + +.image { + composes: image-position; + background: var(--fallback-img); +} + +.image::after { + content: ""; + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + /* background: linear-gradient(0deg, rgba(0, 0, 0, 0.3), transparent 50%, transparent); */ +} + +.fallback-svg svg { + object-fit: cover; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 50%; + transform: translateX(-50%); + height: 100%; +} diff --git a/app/isomorphic/arrow/components/Atoms/FallbackImage/index.js b/app/isomorphic/arrow/components/Atoms/FallbackImage/index.js new file mode 100644 index 000000000..a8cb4be5c --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/FallbackImage/index.js @@ -0,0 +1,30 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { ImageFallbackIcon } from "../../Svgs/fallbackImage"; +import { Link } from "@quintype/components"; + +import "./fallback.m.css"; + +const imageFallback = () => ( +
+
+ +
+
+); + +export const FallbackImage = ({ slug }) => { + if (slug) { + return ( + + {imageFallback()} + + ); + } else { + return imageFallback(); + } +}; + +FallbackImage.propTypes = { + slug: PropTypes.string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/Headline/README.md b/app/isomorphic/arrow/components/Atoms/Headline/README.md new file mode 100644 index 000000000..9e40b47de --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Headline/README.md @@ -0,0 +1,7 @@ +# Headline + +## Usage +```jsx + +``` + \ No newline at end of file diff --git a/app/isomorphic/arrow/components/Atoms/Headline/headline.m.css b/app/isomorphic/arrow/components/Atoms/Headline/headline.m.css new file mode 100644 index 000000000..b2283e562 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Headline/headline.m.css @@ -0,0 +1,26 @@ +@custom-media --viewport-medium (width >= 768px); + +.wrapper { + margin-bottom: 6px; +} + +.headline { + margin-bottom: var(--arrow-spacing-xxs); + color: var(--arrow-c-mono2); + font-weight: var(--arrow-fw-semi-bold); + @media (--viewport-medium) { + margin-bottom: var(--arrow-spacing-xs); + } +} + +.wrapper .headline { + display: inline; + margin-bottom: var(--arrow-spacing-xs); +} + +.dark { + color: var(--arrow-c-mono2); +} +.light { + color: var(--arrow-c-invert-mono2); +} diff --git a/app/isomorphic/arrow/components/Atoms/Headline/index.js b/app/isomorphic/arrow/components/Atoms/Headline/index.js new file mode 100644 index 000000000..6db4712d5 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Headline/index.js @@ -0,0 +1,91 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Link } from "@quintype/components"; +import { useStateValue } from "../../SharedContext"; +import get from "lodash/get"; +import { getTextColor, isExternalStory, getStoryUrl } from "../../../utils/utils"; +import { PremiumStoryIcon } from "../PremiumStoryIcon"; +import LiveIcon from "../../Svgs/liveicon"; + +import "./headline.m.css"; + +export const Headline = ({ story, headerLevel, premiumStoryIconConfig = {}, isLink }) => { + const configData = useStateValue() || {}; + const { + iconColor = "#F7B500", + iconStyle = "star", + enablePremiumStoryIcon = false, + showLiveIcon = false, + } = premiumStoryIconConfig; + const alternateHeadline = get(story, ["alternative", "home", "default", "headline"]); + const premiumStory = enablePremiumStoryIcon && get(story, ["access"]) === "subscription"; + const headline = alternateHeadline || story.headline; + const HeaderTag = "h" + headerLevel; + + const positionHeadline = () => { + switch (parseInt(headerLevel)) { + case 1: + return "-2px"; + case 2: + return "2px"; + case 3: + return "4px"; + case 4: + return "5px"; + default: + return "6px"; + } + }; + + const textColor = getTextColor(configData.theme); + const wrapperClass = premiumStory ? "wrapper" : ""; + const iconSize = "24px"; + + const enableLiveIcon = + showLiveIcon && story["story-template"] === "live-blog" && !get(story, ["metadata", "is-closed"], false); + + return ( +
+ {premiumStory && ( + + )} + {isLink ? ( + + + {enableLiveIcon && } + {headline} + + + ) : ( + + {enableLiveIcon && } + {headline} + + )} +
+ ); +}; + +Headline.propTypes = { + /** The Story Object from the API response */ + story: PropTypes.object.isRequired, + /** Header tags ranging h1-h6, where h[headerLevel] */ + headerLevel: PropTypes.string, + premiumStoryIconConfig: PropTypes.shape({ + iconColor: PropTypes.string, + iconType: PropTypes.string, + enablePremiumStoryIcon: PropTypes.bool, + }), + isLink: PropTypes.bool, +}; + +Headline.defaultProps = { + headerLevel: "6", + isLink: true, +}; diff --git a/app/isomorphic/arrow/components/Atoms/Headline/stories.js b/app/isomorphic/arrow/components/Atoms/Headline/stories.js new file mode 100644 index 000000000..29e51c727 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Headline/stories.js @@ -0,0 +1,52 @@ +import React from "react"; +import { generateStory } from "../../Fixture"; +import { optionalSelect, withStore } from "../../../../storybook"; + +import { Headline } from "./index"; +import Readme from "./README.md"; +import { boolean, color } from "@storybook/addon-knobs"; + +const story = generateStory(); + +const headerLevelOptions = { + "No Value": "", + h1: 1, + h2: 2, + h3: 3, + h4: 4, + h5: 5, + h6: 6, +}; + +const iconType = { + Star: "star", + Crown: "crown", + Lock: "lock", + Key: "key", +}; + +withStore( + "Atoms/Headline", + { + qt: { + config: { + "cdn-image": "thumbor-stg.assettype.com", + }, + }, + }, + Readme +).add( + "default", + () => ( + + ), + Readme +); diff --git a/app/isomorphic/arrow/components/Atoms/HeroImage/README.md b/app/isomorphic/arrow/components/Atoms/HeroImage/README.md new file mode 100644 index 000000000..0d7300aa7 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/HeroImage/README.md @@ -0,0 +1,13 @@ +# HeroImage +Displays the Hero Image for a story with the help of the `ResponsiveHeroImage` component from the `@quintype/components` package. + +## Usage +```jsx + +``` + +```jsx + +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/HeroImage/hero-image.m.css b/app/isomorphic/arrow/components/Atoms/HeroImage/hero-image.m.css new file mode 100644 index 000000000..e4e163b84 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/HeroImage/hero-image.m.css @@ -0,0 +1,72 @@ +@custom-media --viewport-medium (width >= 768px); + +.hero-image { + margin-bottom: var(--arrow-spacing-xs); + padding-right: 0; + flex-grow: 1; + position: relative; +} +.right-padding { + margin-bottom: 0; + padding-right: var(--arrow-spacing-s); + flex-grow: 1; +} + +.left-padding { + margin-bottom: 0; + padding-left: var(--arrow-spacing-s); + flex-grow: 1; +} + +.right-padding-mob { + margin-bottom: 0; + padding-right: var(--arrow-spacing-s); + flex-grow: 1; + flex-basis: 30%; + @media (--viewport-medium) { + padding-right: 0; + margin-bottom: var(--arrow-spacing-xs); + } +} + +.placeholder { + background-color: lightgray; +} + +html[dir="rtl"] { + .right-padding, + .right-padding-mob { + padding-right: 0; + padding-left: var(--arrow-spacing-s); + } + + .right-padding-mob { + @media (--viewport-medium) { + padding-left: 0; + } + } +} + +.hero-image-mob { + margin-bottom: var(--arrow-spacing-xs); + padding-right: 0; + flex-grow: 1; + @media (--viewport-medium) { + /* margin-bottom: 0; */ + } +} +.with-padding { + padding: 16px 16px 0 16px; +} + +.image { + position: relative; +} +.image img { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + height: 100%; +} diff --git a/app/isomorphic/arrow/components/Atoms/HeroImage/index.js b/app/isomorphic/arrow/components/Atoms/HeroImage/index.js new file mode 100644 index 000000000..13a6e535b --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/HeroImage/index.js @@ -0,0 +1,179 @@ +import { Link, ResponsiveHeroImage } from "@quintype/components"; +import get from "lodash/get"; +import PropTypes from "prop-types"; +import React from "react"; +import { useSelector } from "react-redux"; +import { clientWidth, isExternalStory, getStoryUrl, removeHtmlTags } from "../../../utils/utils"; +import { useStateValue } from "../../SharedContext"; +import { FallbackImage } from "../FallbackImage"; +import { HyperLink } from "../Hyperlink"; +import "./hero-image.m.css"; + +export const HeroImage = ({ + story, + FullBleed, + aspectRatio, + defaultWidth, + widths, + isHorizontal, + isHorizontalMobile, + isHorizontalWithImageLast, + defaultFallback, + isStoryPageImage, + config, +}) => { + const { fallbackImageUrl = "" } = useStateValue() || {}; + const { StoryTemplateIcon = () => null } = config; + + const showImagePlaceholder = useSelector((state) => get(state, ["qt", "config", "showPlaceholder"])); + const getPlaceholderStyleName = showImagePlaceholder ? "placeholder" : ""; + + const fullBleed = FullBleed ? "" : "with-padding"; + + let heroImageClass = "hero-image"; + if (isHorizontal) heroImageClass = "right-padding"; + else if (isHorizontalWithImageLast) heroImageClass = "left-padding"; + + const heroImageMobClasses = isHorizontalMobile ? "right-padding-mob" : ""; + + const getPadding = (aspectRatio) => { + const [height, width] = aspectRatio; + if (height > 0 && width > 0) { + const padding = (width / height) * 100; + return padding; + } + return 0; + }; + + const [mobileAspectRatio, desktopAspectRatio = mobileAspectRatio] = aspectRatio; + const isMobile = clientWidth("mobile"); + + const imageAspectRatio = isMobile ? mobileAspectRatio : desktopAspectRatio; + const imagePadding = isMobile ? getPadding(mobileAspectRatio) : getPadding(desktopAspectRatio); + const slug = + get(story, ["hero-image-s3-key"]) || + get(story, ["alternative", "home", "default", "hero-image", "hero-image-s3-key"]); + const hyperLink = get(story, ["hero-image-hyperlink"], ""); + const imageCaption = get(story, ["hero-image-caption"], ""); + const imageAltText = imageCaption ? removeHtmlTags(imageCaption) : get(story, ["headline"], ""); + + const fallbackImage = () => { + if (!slug) { + if (fallbackImageUrl && !isStoryPageImage) { + return ( +
+ image-fallback +
+ ); + } + + return ( + <> + {defaultFallback ? ( +
+ + +
+ ) : null} + + ); + } + return ( + <> +
+ + +
+ {isStoryPageImage && hyperLink && } + + ); + }; + + return ( + story && + (isStoryPageImage ? ( +
+ {fallbackImage()} +
+ ) : ( + + {fallbackImage()} + + )) + ); +}; + +HeroImage.propTypes = { + /** The Story Object from the API response */ + story: PropTypes.object.isRequired, + /** Removes the Hero Image padding for the image to bleed over the margin */ + FullBleed: PropTypes.bool, + /** The default width for the image, in case the browser does not support srcset */ + defaultWidth: PropTypes.number, + /** The list of available widths */ + widths: PropTypes.array, + // fatch the correct hero image + aspectRatio: PropTypes.array, + + // Hints for the browser to choose the best size + /** This prop is responsible for adding padding-right of the image and remove padding bottom of the image */ + isHorizontal: PropTypes.bool, + /** This prop is responsible for adding padding-bottom in desktop and padding-right in mobile */ + isHorizontalMobile: PropTypes.bool, + // this prop is responsive for rendering fallback image on story page + defaultFallback: PropTypes.bool, + isHorizontalWithImageLast: PropTypes.bool, + isStoryPageImage: PropTypes.bool, + config: PropTypes.object, +}; + +HeroImage.defaultProps = { + FullBleed: true, + aspectRatio: [ + [1, 1], + [16, 9], + ], + defaultWidth: 480, + widths: [250, 480, 640], + isHorizontal: false, + isHorizontalMobile: false, + defaultFallback: true, + isStoryPageImage: false, + isHorizontalWithImageLast: false, + config: {}, +}; diff --git a/app/isomorphic/arrow/components/Atoms/HeroImage/stories.js b/app/isomorphic/arrow/components/Atoms/HeroImage/stories.js new file mode 100644 index 000000000..203fcf3f3 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/HeroImage/stories.js @@ -0,0 +1,31 @@ +import React from "react"; +import { generateStory } from "../../Fixture"; +import { HeroImage } from "./index"; +import Readme from "./README.md"; + +import { withStore } from "../../../../storybook"; + +const story = generateStory(); + +withStore( + "Atoms/Hero Image", + { + qt: { + config: { + "cdn-image": "thumbor-stg.assettype.com", + }, + }, + }, + Readme +) + .add("Full Bleed Image", () => ) + .add("With Padding Image", () => ( + + )); diff --git a/app/isomorphic/arrow/components/Atoms/Hyperlink/hyperlink.m.css b/app/isomorphic/arrow/components/Atoms/Hyperlink/hyperlink.m.css new file mode 100644 index 000000000..e9e87bec8 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Hyperlink/hyperlink.m.css @@ -0,0 +1,10 @@ +.hyperlink-button { + position: absolute; + right: var(--arrow-spacing-xxs); + bottom: var(--arrow-spacing-xxs); + z-index: 10; +} + +.hyperlink-button svg rect { + fill: var(--arrow-c-brand1); +} diff --git a/app/isomorphic/arrow/components/Atoms/Hyperlink/index.js b/app/isomorphic/arrow/components/Atoms/Hyperlink/index.js new file mode 100644 index 000000000..d519e6ae1 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Hyperlink/index.js @@ -0,0 +1,25 @@ +import { Link } from "@quintype/components"; +import React from "react"; +import PropTypes from "prop-types"; +import { HyperLinkIcon } from "../../Svgs/hyperlink"; +import "./hyperlink.m.css"; + +export const HyperLink = ({ hyperLink = "" }) => { + if (!hyperLink) return null; + return ( + + + + ); +}; + +HyperLink.propTypes = { + hyperLink: PropTypes.string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/LoadMoreTarget/index.js b/app/isomorphic/arrow/components/Atoms/LoadMoreTarget/index.js new file mode 100644 index 000000000..d2234f6a0 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/LoadMoreTarget/index.js @@ -0,0 +1,24 @@ +import React from "react"; +import PropTypes from "prop-types"; + +export const LoadMoreTarget = ({ collection, componentName, offset, limit }) => { + return ( +
+ ); +}; + +LoadMoreTarget.propTypes = { + collection: PropTypes.shape({ + id: PropTypes.string, + slug: PropTypes.string, + }), + componentName: PropTypes.string, + offset: PropTypes.number, + limit: PropTypes.number, +}; diff --git a/app/isomorphic/arrow/components/Atoms/Loadmore/README.md b/app/isomorphic/arrow/components/Atoms/Loadmore/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/app/isomorphic/arrow/components/Atoms/Loadmore/__snapshots__/load-more.test.js.snap b/app/isomorphic/arrow/components/Atoms/Loadmore/__snapshots__/load-more.test.js.snap new file mode 100644 index 000000000..5511517bd --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Loadmore/__snapshots__/load-more.test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Load more button Should render button and load items subsequently with SubsequentLoadCount as template property. 1`] = ` +
+ Read More +
+`; + +exports[`Load more button Should render button and navigate to the corresponding url with NavigateToPage as template property. 1`] = ` + +
+ Read More +
+
+`; diff --git a/app/isomorphic/arrow/components/Atoms/Loadmore/index.js b/app/isomorphic/arrow/components/Atoms/Loadmore/index.js new file mode 100644 index 000000000..af5c2e3bb --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Loadmore/index.js @@ -0,0 +1,41 @@ +import React from "react"; +import { LinkBase } from "@quintype/components"; +import PropTypes from "prop-types"; +import { getTextColor, generateNavigateSlug } from "../../../utils/utils"; +import "./load-more.m.css"; + +export const LoadmoreButton = ({ + config = {}, + collection, + template = "SubsequentLoadCount", + onClick, + navigate, + qtConfig, +}) => { + const { showButton = true, buttonText = "Read More", theme = "", customUrlPath = "" } = config; + if (!showButton) return null; + + const textColor = getTextColor(theme); + const slug = customUrlPath ? `/${customUrlPath}` : generateNavigateSlug(collection, { ...qtConfig, ...config }); + + return template === "SubsequentLoadCount" ? ( +
+ {buttonText} +
+ ) : ( + +
+ {buttonText} +
+
+ ); +}; + +LoadmoreButton.propTypes = { + collection: PropTypes.object, + template: PropTypes.string, + config: PropTypes.object, + onClick: PropTypes.func, + navigate: PropTypes.func, + qtConfig: PropTypes.object, +}; diff --git a/app/isomorphic/arrow/components/Atoms/Loadmore/load-more.m.css b/app/isomorphic/arrow/components/Atoms/Loadmore/load-more.m.css new file mode 100644 index 000000000..df3ff4808 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Loadmore/load-more.m.css @@ -0,0 +1,23 @@ +.button { + padding: var(--arrow-spacing-xs) var(--arrow-spacing-l); + border: 1px solid; + display: flex; + margin: var(--arrow-spacing-m) auto var(--arrow-spacing-xs) auto; + font-size: var(--arrow-fs-xs); + border-radius: var(--arrow-spacing-xxs); + background-color: transparent; + width: fit-content; + cursor: pointer; +} + +.default { + display: block; +} + +.dark { + color: var(--arrow-c-mono2); +} + +.light { + color: var(--arrow-c-invert-mono2); +} diff --git a/app/isomorphic/arrow/components/Atoms/Loadmore/load-more.test.js b/app/isomorphic/arrow/components/Atoms/Loadmore/load-more.test.js new file mode 100644 index 000000000..c1a7142f4 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Loadmore/load-more.test.js @@ -0,0 +1,30 @@ +import * as React from "react"; +import renderer from "react-test-renderer"; +import { LoadmoreButton } from "./index"; +import { Provider } from "react-redux"; +import { generateStore, generateCollection } from "../../Fixture"; + +const collection = generateCollection(); + +describe("Load more button", () => { + it("Should render button and load items subsequently with SubsequentLoadCount as template property.", () => { + const wrapper = renderer + .create( + + + + ) + .toJSON(); + expect(wrapper).toMatchSnapshot(); + }); + it("Should render button and navigate to the corresponding url with NavigateToPage as template property.", () => { + const wrapper = renderer + .create( + + + + ) + .toJSON(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/Loadmore/stories.js b/app/isomorphic/arrow/components/Atoms/Loadmore/stories.js new file mode 100644 index 000000000..9732db7d6 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/Loadmore/stories.js @@ -0,0 +1,17 @@ +import React from "react"; +import { withStore, optionalSelect } from "../../../../storybook"; +import { LoadmoreButton } from "./index"; +import Readme from "./README.md"; +import { generateCollection } from "../../Fixture"; + +const collection = generateCollection(); + +const buttonOptions = { + NavigateToPage: "NavigateToPage", + SubsequentLoadCount: "SubsequentLoadCount", + CustomUrlPath: "CustomUrlPath", +}; + +withStore("Atoms/Button", {}, Readme).add("Buttons Options", () => ( + +)); diff --git a/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/README.md b/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/README.md new file mode 100644 index 000000000..6c4f39bc5 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/README.md @@ -0,0 +1,21 @@ +# MagazineCoverImage + +Magazine cover image is used to represent the cover of the magazine. + +## Usage + +Import the MagazineCoverImage and pass collection, props and the config. + +```jsx +import { MagazineCoverImage } from "@quintype/arrow"; +``` + +### Use as a component + +```jsx +const contextConfig = { + theme: #333333; +}; + + +``` diff --git a/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/index.js b/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/index.js new file mode 100644 index 000000000..91f5b0e71 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/index.js @@ -0,0 +1,61 @@ +import React from "react"; +import { LazyLoadImages, ResponsiveImage, Link } from "@quintype/components"; +import PropTypes from "prop-types"; +import get from "lodash/get"; +import { getSlug } from "../../../utils/utils"; +import "./magazine-cover-image.m.css"; +import { FallbackImage } from "../FallbackImage"; + +export const MagazineCoverImageCard = ({ collection, config = {} }) => { + const imgAltText = get(collection, ["metadata", "magazine", "name"], "cover-image"); + const coverImageData = get(collection, ["metadata", "cover-image"]) || {}; + const { magazineSlug = "" } = config; + const { + "cover-image-url": coverUrl, + "cover-image-s3-key": covers3Key, + "cover-image-metadata": coverImageMetaData, + } = coverImageData; + + const slug = magazineSlug && collection && getSlug(collection, config); + + return ( +
+ {covers3Key || coverUrl ? ( +
+ {covers3Key ? ( + + + + + + ) : ( + cover-image + )} +
+ ) : ( +
+ +
+ )} +
+ ); +}; + +MagazineCoverImageCard.propTypes = { + collection: PropTypes.shape({ + "cover-image-url": PropTypes.string, + "cover-image-s3-key": PropTypes.string, + "cover-image-metadata": PropTypes.string, + slug: PropTypes.string, + }), + config: PropTypes.shape({ + magazineSlug: PropTypes.string, + }), +}; diff --git a/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/magazine-cover-image.m.css b/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/magazine-cover-image.m.css new file mode 100644 index 000000000..d671e8378 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/magazine-cover-image.m.css @@ -0,0 +1,24 @@ +.cover-image { + position: relative; +} + +.cover-image figure.image { + padding-top: 138%; +} + +.image { + position: relative; +} + +.image img { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + height: 100%; +} + +.fallback-img { + padding-top: 138%; +} diff --git a/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/stories.js b/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/stories.js new file mode 100644 index 000000000..512b293e2 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/MagazineCoverImage/stories.js @@ -0,0 +1,21 @@ +import React from "react"; +import { withStore } from "../../../../storybook"; +import { MagazineCoverImageCard } from "./index"; +import Readme from "./README.md"; +import { generateCollection } from "../../Fixture"; +import { config } from "@storybook/addon-actions"; + +const collection = generateCollection({ stories: 4 }); + +withStore( + "Atoms/Magazine CoverImage", + { + qt: { + config: { + "cdn-image": "thumbor-stg.assettype.com", + mountAt: "/sub-directory", + }, + }, + }, + Readme +).add("MagazineCoverImage", () => ); diff --git a/app/isomorphic/arrow/components/Atoms/PremiumStoryIcon/index.js b/app/isomorphic/arrow/components/Atoms/PremiumStoryIcon/index.js new file mode 100644 index 000000000..b5fd05c08 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/PremiumStoryIcon/index.js @@ -0,0 +1,27 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { CrownIcon } from "../../Svgs/crown"; +import { KeyIcon } from "../../Svgs/key"; +import { LockIcon } from "../../Svgs/lock"; +import { StarIcon } from "../../Svgs/star"; + +export const PremiumStoryIcon = ({ width, height, color, iconType, positionTop }) => { + switch (iconType) { + case "crown": + return ; + case "lock": + return ; + case "key": + return ; + default: + return ; + } +}; + +PremiumStoryIcon.propTypes = { + width: PropTypes.string, + height: PropTypes.string, + color: PropTypes.string, + iconType: PropTypes.string, + positionTop: PropTypes.string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/PublishDetail/README.md b/app/isomorphic/arrow/components/Atoms/PublishDetail/README.md new file mode 100644 index 000000000..ae9b3b927 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/PublishDetail/README.md @@ -0,0 +1,15 @@ +# Publish Details + +Component to display the publish details for the story card. + +## Usage + +```jsx + +``` + +```jsx + + + +``` diff --git a/app/isomorphic/arrow/components/Atoms/PublishDetail/index.js b/app/isomorphic/arrow/components/Atoms/PublishDetail/index.js new file mode 100644 index 000000000..47ad202a5 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/PublishDetail/index.js @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from "react"; +import PropTypes from "prop-types"; + +import { getTextColor, getTimeStamp, timestampToFormat } from "../../../utils/utils"; +import { ReadTime } from "../ReadTime"; + +import "./publish-details.m.css"; +import { useStateValue } from "../../SharedContext"; + +export const PublishDetails = ({ story, opts = {}, template = "", timezone = null }) => { + const [direction, setDirection] = useState("ltr"); + useEffect(() => { + const htmlElement = document.getElementsByTagName("HTML"); + if (htmlElement && htmlElement.length > 0 && htmlElement[0].dir.toLocaleLowerCase() === "rtl") { + setDirection("rtl"); + } + }, []); + + const config = useStateValue() || {}; + const { "first-published-at": firstPublishAt, "last-published-at": lastPublish, "updated-at": updatedAt } = story; + const { enableUpdatedTime, enablePublishedTime, showReadTime, localizedPublishedOn, localizedUpdatedOn } = opts; + const time = lastPublish || firstPublishAt; + const textColor = getTextColor(config.theme); + const timeStampProps = + direction === "rtl" + ? { ...opts, direction: direction, isTimeFirst: true, showTime: true } + : { ...opts, direction: direction, showTime: true }; + + return ( +
+
+ {enablePublishedTime && ( + <> + {localizedPublishedOn || "Published on"} :  +
{getTimeStamp(time, timestampToFormat, timeStampProps, template, timezone)}
+ {showReadTime && } + + )} +
+ {enableUpdatedTime && ( +
+ {localizedUpdatedOn || "Updated on"} :  +
{getTimeStamp(updatedAt, timestampToFormat, timeStampProps, template, timezone)}
+ {showReadTime && } +
+ )} +
+ ); +}; + +PublishDetails.propTypes = { + /** The Story Object from the API response */ + story: PropTypes.shape({ + "first-published-at": PropTypes.number, + "last-published-at": PropTypes.number, + "read-time": PropTypes.number, + "updated-at": PropTypes.number, + }), + timezone: PropTypes.string, + template: PropTypes.string, + opts: PropTypes.shape({ + showReadTime: PropTypes.bool, + enableUpdatedTime: PropTypes.bool, + enablePublishedTime: PropTypes.bool, + }), +}; diff --git a/app/isomorphic/arrow/components/Atoms/PublishDetail/publish-details.m.css b/app/isomorphic/arrow/components/Atoms/PublishDetail/publish-details.m.css new file mode 100644 index 000000000..ca4b6d85b --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/PublishDetail/publish-details.m.css @@ -0,0 +1,48 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; + +.timeStamp { + font-size: var(--arrow-fs-tiny); + @media (min-width: mobile) { + font-size: var(--arrow-fs-xs); + } +} + +.timeStamp.dark { + color: var(--arrow-c-mono4); +} + +.timeStamp.light { + color: var(--arrow-c-invert-mono4); +} + +.publish-details { + display: flex; + align-items: baseline; + flex-wrap: wrap; +} + +.update-details { + display: flex; + margin-top: var(--arrow-spacing-xs); +} +.dot-indicator { + display: flex; + align-items: center; + padding: 0 var(--arrow-spacing-xs); +} + +.timeStamp :global(.arr--read-time) { + font-size: var(--arrow-fs-tiny); + @media (min-width: mobile) { + font-size: var(--arrow-fs-xs); + } +} + +.date.dark { + color: var(--arrow-c-mono2); +} +.date.light { + color: var(--arrow-c-invert-mono2); +} diff --git a/app/isomorphic/arrow/components/Atoms/PublishDetail/publish-details.test.js b/app/isomorphic/arrow/components/Atoms/PublishDetail/publish-details.test.js new file mode 100644 index 000000000..1e1256cad --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/PublishDetail/publish-details.test.js @@ -0,0 +1,22 @@ +import * as React from "react"; +import { mount } from "enzyme"; +import expect from "expect"; +import { generateStory } from "../../Fixture/index"; +import { PublishDetails } from "./index"; + +const story = generateStory(); + +describe("default author card", () => { + it("Should render default publish details componen", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "timeStamp" }).prop("className")).toMatch(/timeStamp/); + }); + it("Should render without updated time", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "update-details" }).length).toEqual(0); + }); + it("Should render without read time", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "read-time" }).length).toEqual(0); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/PublishDetail/stories.js b/app/isomorphic/arrow/components/Atoms/PublishDetail/stories.js new file mode 100644 index 000000000..6c7234132 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/PublishDetail/stories.js @@ -0,0 +1,22 @@ +import React from "react"; +import { generateStory } from "../../Fixture"; +import { withStore } from "../../../../storybook"; +import { PublishDetails } from "./index"; +import { boolean, text } from "@storybook/addon-knobs"; +import Readme from "./README.md"; + +const story = generateStory(); + +withStore("Atoms/Publish Details", {}, Readme).add("Default", () => ( + +)); diff --git a/app/isomorphic/arrow/components/Atoms/ReadTime/README.md b/app/isomorphic/arrow/components/Atoms/ReadTime/README.md new file mode 100644 index 000000000..d84aae127 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ReadTime/README.md @@ -0,0 +1,13 @@ +# Read Time + +Component to display the read time for the story card. + +## Usage + +```jsx + +``` + +```jsx + +``` diff --git a/app/isomorphic/arrow/components/Atoms/ReadTime/index.js b/app/isomorphic/arrow/components/Atoms/ReadTime/index.js new file mode 100644 index 000000000..08526d7b8 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ReadTime/index.js @@ -0,0 +1,53 @@ +import get from "lodash/get"; +import React from "react"; +import PropTypes from "prop-types"; +import { useSelector } from "react-redux"; +import { getTextColor } from "../../../utils/utils"; +import { useStateValue } from "../../SharedContext"; +import "./read-time.m.css"; +import { Dot } from "../Dot/dot"; + +export const ReadTime = ({ story, opts = {}, isLightTheme = false }) => { + const config = useStateValue() || {}; + const showReadTime = get(config, ["showReadTime"]) || get(opts, ["showReadTime"], true); + const readTime = get(story, ["read-time"], 0); + const textColor = isLightTheme ? "light" : getTextColor(config.theme); + + const isLocalizedNumber = useSelector((state) => get(state, ["qt", "config", "isLocalizedNumber"], false)); + const languageCode = isLocalizedNumber + ? useSelector((state) => get(state, ["qt", "config", "language", "ietf-code"], "en")) + : "en"; + + if (!showReadTime || !readTime) return null; + + const timeToRead = + get(opts, ["localizedTimeToRead"]) || get(opts, ["localizationConfig", "localizedTimeToRead"]) || "min read"; + + return ( + <> +
+ + + + + {readTime.toLocaleString(`${languageCode}`)} {timeToRead} + +
+ + ); +}; + +ReadTime.propTypes = { + story: PropTypes.shape({ + "read-time": PropTypes.number, + }), + opts: PropTypes.shape({ + showReadTime: PropTypes.bool, + }), + isLightTheme: PropTypes.bool, + languageCode: PropTypes.string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/ReadTime/read-time.m.css b/app/isomorphic/arrow/components/Atoms/ReadTime/read-time.m.css new file mode 100644 index 000000000..75e51369d --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ReadTime/read-time.m.css @@ -0,0 +1,24 @@ +@custom-media --viewport-medium (width >= 768px); + +.read-time-wrapper { + display: flex; + font-size: var(--arrow-fs-tiny); +} + +.dot-indicator { + padding: 0 var(--arrow-spacing-xs) var(--arrow-spacing-xs); +} + +.dark { + color: var(--arrow-c-mono4); +} + +.light { + color: var(--arrow-c-invert-mono4); +} + +html[dir="rtl"] { + .read-time-wrapper { + margin-right: var(--arrow-spacing-xs); + } +} diff --git a/app/isomorphic/arrow/components/Atoms/ReadTime/read-time.test.js b/app/isomorphic/arrow/components/Atoms/ReadTime/read-time.test.js new file mode 100644 index 000000000..01c685ce9 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ReadTime/read-time.test.js @@ -0,0 +1,27 @@ +import * as React from "react"; +import { mount } from "enzyme"; +import expect from "expect"; +import { generateStore, generateStory } from "../../Fixture/index"; +import { ReadTime } from "./index"; +import { Provider } from "react-redux"; + +const story = generateStory(); + +describe("Read time", () => { + it("Should render read time component", () => { + const wrapper = mount( + + + + ); + expect(wrapper.find({ "data-test-id": "read-time" }).prop("className")).toMatch(/read-time-wrapper/); + }); + it("Should not render read time if showReadTime is false", () => { + const wrapper = mount( + + + + ); + expect(wrapper.find({ "data-test-id": "read-time" }).length).toEqual(0); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/ReadTime/stories.js b/app/isomorphic/arrow/components/Atoms/ReadTime/stories.js new file mode 100644 index 000000000..ff3b389a8 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ReadTime/stories.js @@ -0,0 +1,19 @@ +import React from "react"; +import { generateStory } from "../../Fixture"; +import { withStore } from "../../../../storybook"; +import { ReadTime } from "./index"; +import Readme from "./README.md"; +import { boolean } from "@storybook/addon-knobs"; + +const story = generateStory(); + +withStore("Atoms/Read Time", {}, Readme).add("default", () => { + return ( + + ); +}); diff --git a/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderArrow/index.js b/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderArrow/index.js new file mode 100644 index 000000000..e69ffee11 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderArrow/index.js @@ -0,0 +1,59 @@ +import React from "react"; +import { number, string, func } from "prop-types"; + +import { getTextColor } from "../../../../utils/utils"; +import { LeftArrow } from "../../../Svgs/left-arrow"; +import { RightArrow } from "../../../Svgs/right-arrow"; +import { useStateValue } from "../../../SharedContext"; + +import "./slider-arrow.m.css"; + +export const SliderArrow = ({ selectedIndex, previousClick, nextClick, noOfItems, perView, languageDirection }) => { + if (noOfItems < 1) { + return null; + } + + const config = useStateValue() || {}; + const textColor = getTextColor(config.theme); + + const rightArrowView = () => { + if (selectedIndex >= noOfItems - perView) return false; + return true; + }; + + const getLeftArrowClassName = languageDirection === "rtl" ? "left-arrow-rtl" : "left-arrow-ltr"; + const getRightArrowClassName = languageDirection === "rtl" ? "right-arrow-rtl" : "right-arrow-ltr"; + return ( + <> + {selectedIndex !== 0 ? ( + + ) : null} + + {selectedIndex < noOfItems - 1 && rightArrowView() ? ( + + ) : null} + + ); +}; + +SliderArrow.propTypes = { + noOfItems: number, + perView: number, + slideIndicator: string, + previousClick: func, + nextClick: func, + selectedIndex: number, + languageDirection: string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderArrow/slider-arrow.m.css b/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderArrow/slider-arrow.m.css new file mode 100644 index 000000000..4f614a8f0 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderArrow/slider-arrow.m.css @@ -0,0 +1,45 @@ +.arrow { + position: absolute; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + background-color: var(--arrow-c-light); + opacity: 0.8; + padding: var(--arrow-spacing-l) var(--arrow-spacing-s); + border: none; + border-radius: 0; + @media only screen and (max-width: 767px) { + top: calc(25% + 40px); + } +} + +.left-arrow-dark, +.right-arrow-dark { + background-color: var(--arrow-c-dark); +} +.left-arrow-light, +.right-arrow-light { + background-color: var(--arrow-c-light); +} + +.left-arrow-ltr { + left: 0; +} + +.right-arrow-ltr { + right: 0; +} + +.left-arrow-rtl { + right: 0; + left: unset; +} +.left-arrow-rtl svg, +.right-arrow-rtl svg { + transform: rotate(180deg); +} + +.right-arrow-rtl { + left: 0; + right: unset; +} diff --git a/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderIndicator/index.js b/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderIndicator/index.js new file mode 100644 index 000000000..2b04a6bd8 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderIndicator/index.js @@ -0,0 +1,43 @@ +import React from "react"; +import { array, number, string, func } from "prop-types"; + +import { getTextColor } from "../../../../utils/utils"; +import { useStateValue } from "../../../SharedContext"; + +import "./slider-indicator.m.css"; + +export const SliderIndicator = ({ indicatorItems, indicatorClick, selectedIndex, indicatorType }) => { + if (indicatorItems.length < 1) { + return null; + } + + const config = useStateValue() || {}; + const textColor = getTextColor(config.theme); + + return ( +
    + {indicatorItems.map((_, index) => ( +
  • indicatorClick(e, index)} + > + +
  • + ))} +
+ ); +}; + +SliderIndicator.propTypes = { + indicatorItems: array, + perView: number, + indicatorType: string, + indicatorClick: func, + selectedIndex: number, +}; diff --git a/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderIndicator/slider-indicator.m.css b/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderIndicator/slider-indicator.m.css new file mode 100644 index 000000000..4345aeb58 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ScrollSnap/SliderIndicator/slider-indicator.m.css @@ -0,0 +1,71 @@ +.indicators { + display: flex; + justify-content: center; + width: 100%; + list-style: none; + font-family: var(--arrow-typeface-secondary); + cursor: pointer; + line-height: 0; +} + +.indicator { + margin: var(--arrow-spacing-xs) 0; + padding: 4px; +} + +.indicator-button { + background: none; + border: none; + overflow: hidden; + padding: var(--arrow-spacing-s) var(--arrow-spacing-xxs); + cursor: pointer; +} + +.indicator-dots { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--arrow-c-dark); + border: 1px solid var(--arrow-c-dark); + padding: 0; + opacity: 0.25; +} + +.indicator-dots-dark[aria-pressed="true"] { + background-color: var(--arrow-c-dark); + opacity: 1; +} + +.indicator-dots-light { + background-color: var(--arrow-c-light); + border-color: var(--arrow-c-light); +} + +.indicator-dots-light[aria-pressed="true"] { + background-color: var(--arrow-c-light); + opacity: 1; +} + +.indicator-dashes { + display: inline-block; + width: var(--arrow-spacing-s); + height: 2px; + padding: 0; +} +.indicator-dashes-dark { + opacity: 0.25; + background-color: var(--arrow-c-dark); +} +.indicator-dashes-light { + opacity: 0.25; + background-color: var(--arrow-c-light); +} + +.indicator-dashes-dark[aria-pressed="true"] { + background-color: var(--arrow-c-dark); + opacity: 1; +} +.indicator-dashes-light[aria-pressed="true"] { + background-color: var(--arrow-c-light); + opacity: 1; +} diff --git a/app/isomorphic/arrow/components/Atoms/ScrollSnap/index.js b/app/isomorphic/arrow/components/Atoms/ScrollSnap/index.js new file mode 100644 index 000000000..813d9a7e2 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ScrollSnap/index.js @@ -0,0 +1,170 @@ +import React, { useEffect, useState, useRef } from "react"; +import { useSelector } from "react-redux"; +import { array, number, bool, string } from "prop-types"; +import get from "lodash/get"; + +import { SliderArrow } from "./SliderArrow"; +import { SliderIndicator } from "./SliderIndicator"; + +import "./scroll-snap.m.css"; + +const smoothScroll = (node, topOrLeft, horizontal) => { + node.scrollTo({ + [horizontal ? "left" : "top"]: topOrLeft, + behavior: "smooth", + }); +}; + +const updateItemSelection = (index, languageDirection, scroller, noOfItems) => { + const getScrollValue = Math.floor(scroller.current.scrollWidth * (index / noOfItems)); + const scrollValue = languageDirection === "rtl" ? getScrollValue * -1 : getScrollValue; + smoothScroll(scroller.current, scrollValue, true); +}; + +const setIndicatorValue = (func, ms) => { + let timeout; + + return () => { + clearTimeout(timeout); + timeout = setTimeout(() => { + timeout = null; + func(); + }, ms); + }; +}; + +export const ScrollSnap = ({ children, isArrow, interval, isInfinite, pauseOnHover, perView, slideIndicator }) => { + const [selectedIndex, setSelectedIndex] = useState(0); + const [autoScroll, setAutoScroll] = useState(false); + const languageDirection = useSelector((state) => get(state, ["qt", "config", "language", "direction"], "ltr")); + const noOfItems = children ? children.length : 0; + const scroller = useRef(null); + + const numberOfIndicatorsToShow = children.length - (perView - 1); + const indicatorItems = children.slice(0, numberOfIndicatorsToShow); + + useEffect(() => { + if (scroller.current) { + scroller.current.addEventListener( + "scroll", + setIndicatorValue(() => { + const value = Math.round((scroller.current.scrollLeft / scroller.current.scrollWidth) * noOfItems); + setSelectedIndex(Math.abs(value)); + }, 150) + ); + + // For Default Selection if selectedIndex is not 0 then manually select it + if (selectedIndex !== 0) { + updateItemSelection(selectedIndex); + } + } + + return () => { + scroller.current.removeEventListener("scroll", setIndicatorValue); + }; + }, []); + + // Infinite scroll + useEffect(() => { + if (isInfinite && !autoScroll) { + const timer = setInterval(() => { + infiniteScroll(); + }, interval); + + return () => { + clearInterval(timer); + }; + } + }, [isInfinite, autoScroll]); + + const infiniteScroll = () => { + setSelectedIndex((value) => { + const newIndex = value === noOfItems - perView ? 0 : value + 1; + updateItemSelection(newIndex, languageDirection, scroller, noOfItems); + return newIndex; + }); + }; + + const pauseAutoPlay = () => { + pauseOnHover && setAutoScroll(!autoScroll); + }; + + const indicatorClick = (e, newIndex) => { + e.preventDefault(); + e.stopPropagation(); + setSelectedIndex(newIndex); + updateItemSelection(newIndex, languageDirection, scroller, noOfItems); + }; + + const nextClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (selectedIndex < noOfItems) { + const newIndex = selectedIndex + 1; + setSelectedIndex(newIndex); + updateItemSelection(newIndex, languageDirection, scroller, noOfItems); + } + }; + + const previousClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + const newIndex = selectedIndex - 1; + setSelectedIndex(newIndex); + updateItemSelection(newIndex, languageDirection, scroller, noOfItems); + }; + + return ( +
+
+
+ {children} +
+ {isArrow && ( + + )} +
+ + {children.length > perView && slideIndicator !== "none" && ( + + )} +
+ ); +}; + +ScrollSnap.defaultProps = { + isArrow: true, + isInfinite: false, + interval: 4000, + pauseOnHover: true, + perView: 1, + slideIndicator: "dots", +}; + +ScrollSnap.propTypes = { + children: array.isRequired, + isArrow: bool, + isInfinite: bool, + interval: number, + pauseOnHover: bool, + perView: number, + slideIndicator: string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/ScrollSnap/scroll-snap.m.css b/app/isomorphic/arrow/components/Atoms/ScrollSnap/scroll-snap.m.css new file mode 100644 index 000000000..ed540e582 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/ScrollSnap/scroll-snap.m.css @@ -0,0 +1,26 @@ +.wrapper { + position: relative; +} + +.carousel { + display: flex; + overflow: auto; + scroll-snap-type: x mandatory; + -ms-overflow-style: none; + + > div { + scroll-snap-align: start; + flex-shrink: 0; + width: 100%; + height: 100%; + overflow: hidden; + } +} + +.carousel::-webkit-scrollbar { + display: none; +} + +.carousel::-moz-scrollbar { + overflow: -moz-scrollbars-none; +} diff --git a/app/isomorphic/arrow/components/Atoms/SectionTag/README.md b/app/isomorphic/arrow/components/Atoms/SectionTag/README.md new file mode 100644 index 000000000..04d6d8e02 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/SectionTag/README.md @@ -0,0 +1,11 @@ +# SectionTag +Component to display the section name for the story card. + +## Usage +```jsx + +``` +```jsx + +``` + \ No newline at end of file diff --git a/app/isomorphic/arrow/components/Atoms/SectionTag/index.js b/app/isomorphic/arrow/components/Atoms/SectionTag/index.js new file mode 100644 index 000000000..830c1d6ee --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/SectionTag/index.js @@ -0,0 +1,63 @@ +import React from "react"; +import get from "lodash/get"; +import { Link } from "@quintype/components"; +import { useStateValue } from "../../SharedContext"; +import PropTypes from "prop-types"; +import { getTextColor, rgbToHex } from "../../../utils/utils"; +import "./section.m.css"; + +export const SectionTag = ({ story, template = "", borderColor = "", isLightTheme }) => { + const config = useStateValue() || {}; + const isSection = get(config, ["showSection"], true); + const templateFromRowContext = get(config, ["sectionTagTemplate"]); + const section = get(story, ["sections", 0], ""); + const supportedTemplates = ["borderBottomSml", "borderLeft", "solid"]; + const getUrl = section ? section["section-url"] : "/"; + const colorToApplyContrast = template === "solid" || templateFromRowContext === "solid" ? borderColor : config.theme; + const textColor = isLightTheme ? "light" : getTextColor(colorToApplyContrast); + + const sectionTagBorderColor = rgbToHex(borderColor); + let templateStyle = supportedTemplates.includes(template) ? `section section-${template}` : "section"; + if (templateFromRowContext) { + templateStyle = supportedTemplates.includes(templateFromRowContext) + ? `section section-${templateFromRowContext}` + : "section"; + } + + if (!isSection) return null; + return ( + +
+ {templateStyle.includes("section-borderLeft") && ( + + )} + {section["display-name"] || section.name} +
+
+ + ); +}; +SectionTag.propTypes = { + /** The Story Object from the API response */ + story: PropTypes.object.isRequired, + /** The style template for section tag */ + template: PropTypes.oneOf(["", "borderBottomSml", "borderLeft", "solid"]), + borderColor: PropTypes.string, + solidBorderColor: PropTypes.string, + isLightTheme: PropTypes.bool, +}; + +SectionTag.defaultProps = { + template: "", + isLightTheme: false, +}; diff --git a/app/isomorphic/arrow/components/Atoms/SectionTag/section.m.css b/app/isomorphic/arrow/components/Atoms/SectionTag/section.m.css new file mode 100644 index 000000000..ba2c331ce --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/SectionTag/section.m.css @@ -0,0 +1,53 @@ +@custom-media --viewport-medium (width >= 768px); + +.section { + margin-bottom: var(--arrow-spacing-xxs); + @media (--viewport-medium) { + margin-bottom: var(--arrow-spacing-xs); + } +} +.section-borderBottomSml .border-bottom { + width: 20px; + height: 2px; + margin-top: var(--arrow-spacing-xxs); +} + +.section-borderLeft { + display: flex; + align-items: baseline; + + .border-left { + margin-right: var(--arrow-spacing-xs); + min-width: 3px; + max-width: 3px; + overflow: hidden; + } + + .border-left::before { + content: "I"; + font-family: var(--arrow-sans-serif); + } +} + +html[dir="rtl"] { + .border-left { + margin-left: var(--arrow-spacing-xs); + margin-right: 0; + } +} + +.section-solid { + color: var(--arrow-c-light); + align-self: self-start; + display: inline-flex; + padding: var(--arrow-spacing-xxs) var(--arrow-spacing-s); + @media (--viewport-medium) { + padding: var(--arrow-spacing-xs) var(--arrow-spacing-m); + } +} +.dark { + color: var(--arrow-c-mono2); +} +.light { + color: var(--arrow-c-invert-mono2); +} diff --git a/app/isomorphic/arrow/components/Atoms/SectionTag/stories.js b/app/isomorphic/arrow/components/Atoms/SectionTag/stories.js new file mode 100644 index 000000000..fc3bdf693 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/SectionTag/stories.js @@ -0,0 +1,26 @@ +import React from "react"; +import { generateStory } from "../../Fixture"; +import { SectionTag } from "./index"; +import { withStore, optionalSelect } from "../../../../storybook"; +import Readme from "./README.md"; +import { color } from "@storybook/addon-knobs"; + +const story = generateStory(); + +const defaultvalue = "#3a9fdd"; +const sectionTagBorderColor = "Section Tag Border Color"; + +const templateStyles = { + Default: "", + Solid: "solid", + "Border Bottom Small": "borderBottomSml", + "Border Left": "borderLeft", +}; + +withStore("Atoms/SectionTag", {}, Readme).add("Default", () => ( + +)); diff --git a/app/isomorphic/arrow/components/Atoms/SocialSharePopup/README.md b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/README.md new file mode 100644 index 000000000..4d35f398a --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/README.md @@ -0,0 +1,25 @@ +# Social Share Popup + +This is the Box which shows up with a list of social share fields + +## Usage + +```jsx +const props = { + fbUrl: "https://www.facebook.com", + fullUrl: + "https://ace-web.qtstage.io/anything/recent-stories/newsready-player-one-review-spielberg-spins-a-dizzying-vr-yarn", + iconType: "plain-svg", + linkedinUrl: "https://www.linkedin.com", + mailtoUrl: + "mailto:?subject=Ready%20Player%20One%20review%20%E2%80%93%20Spielberg%C2%A0&body=https%3A%2F%2Face-web.qtstage.io%2Fanything%2Frecent-stories%2Fnews%2Fready-player-one-review-spielberg-spins-a-dizzying-vr-yarn", + publisherUrl: undefined, + theme: "#ffffff", + title: "Ready Player One review – Spielberg ", + twitterUrl: "https://twitter.com" +}; + +; +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/SocialSharePopup/__snapshots__/social-share-popup.test.js.snap b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/__snapshots__/social-share-popup.test.js.snap new file mode 100644 index 000000000..dfdf47bd2 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/__snapshots__/social-share-popup.test.js.snap @@ -0,0 +1,653 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Social Share Template Should not render facebook icon if url not passed 1`] = ` +
+
+
+ + Share via + + + + +
+ +
+
+`; + +exports[`Social Share Template Should not render linkedin icon if url not passed 1`] = ` +
+
+
+ + Share via + + + + +
+ +
+
+`; + +exports[`Social Share Template Should not render twitter icon if url not passed 1`] = ` +
+
+
+ + Share via + + + + +
+ +
+
+`; + +exports[`Social Share Template Should not render whatsapp icon if url not passed 1`] = ` +
+
+
+ + Share via + + + + +
+ +
+
+`; + +exports[`Social Share Template Should render social share popup 1`] = ` +
+
+
+ + Share via + + + + +
+ +
+
+`; + +exports[`Social Share Template Should render template if theme and icon is not passed 1`] = ` +
+
+
+ + Share via + + + + +
+ +
+
+`; diff --git a/app/isomorphic/arrow/components/Atoms/SocialSharePopup/index.js b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/index.js new file mode 100644 index 000000000..c48150d25 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/index.js @@ -0,0 +1,61 @@ +import React from "react"; +import PropTypes from "prop-types"; +import "./social-share-popup.m.css"; +import { CloseIcon } from "../../Svgs/close-icon"; +import { socialShareData } from "../../Molecules/SocialShareTemplate/social-share-data"; +import { getTextColor } from "../../../utils/utils"; + +const popupSocialShareItem = (url, icon, text, index) => { + return url ? ( +
  • + + {icon} + {text} + +
  • + ) : null; +}; + +export const SocialSharePopup = ({ + fbUrl = "", + twitterUrl = "", + linkedinUrl = "", + whatsappUrl = "", + theme, + closePopup = "", + iconType = "plain-color-svg", +}) => { + const textColor = getTextColor(theme); + const iconColor = textColor === "dark" ? "#000000" : "#ffffff"; + const options = { fbUrl, twitterUrl, linkedinUrl, whatsappUrl, iconColor, iconType }; + const popupItemsList = socialShareData(options); + return ( +
    +
    +
    + Share via + + + +
    +
      + {popupItemsList.map(({ url, icon, text, bgColor, alt }, index) => + popupSocialShareItem(url, icon, text, bgColor, alt) + )} +
    +
    +
    + ); +}; + +SocialSharePopup.propTypes = { + fbUrl: PropTypes.string, + twitterUrl: PropTypes.string, + linkedinUrl: PropTypes.string, + whatsappUrl: PropTypes.string, + grey: PropTypes.string, + primaryColor: PropTypes.string, + closePopup: PropTypes.bool, + theme: PropTypes.string, + iconType: PropTypes.string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/SocialSharePopup/social-share-popup.m.css b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/social-share-popup.m.css new file mode 100644 index 000000000..93a9b6a06 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/social-share-popup.m.css @@ -0,0 +1,109 @@ +@import "../../../../../assets/arrow/stylesheets/mixins.scss"; +.share-popup-wrapper { + z-index: var(--zIndex-social-popup); +} + +.light.share-popup-wrapper { + background-color: var(--arrow-c-dark); +} + +.dark.share-popup-wrapper { + background-color: var(--arrow-c-light); +} + +.share-popup { + composes: share-popup-wrapper; + padding: var(--arrow-spacing-l); +} + +.social-wrapper { + display: flex; + flex-direction: row; + padding: var(--arrow-fs-tiny) 0; + align-items: center; +} + +.dark .social-icon-wrapper { + border-bottom: 1px solid var(--arrow-c-mono7); +} + +.light .social-icon-wrapper { + border-bottom: 1px solid var(--arrow-c-invert-mono7); +} + +.wrapper { + list-style: none; +} + +.dark .wrapper > :last-child, +.light .wrapper > :last-child { + border-bottom: unset; +} + +.light .share-popup { + @include tablet { + border: solid 1px var(--arrow-c-invert-mono7); + } +} + +.dark .share-popup { + @include tablet { + border: solid 1px var(--arrow-c-mono7); + } +} + +.top-bar { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.share-text { + font-size: var(--arrow-fs-xs); + font-weight: var(--arrow-fw-bold); + line-height: var(--arrow-lh-2); + margin-bottom: var(--arrow-spacing-s); +} + +.close { + position: absolute; + top: var(--arrow-spacing-m); + right: var(--arrow-spacing-m); +} + +.social-icon { + border-radius: 50%; + width: var(--arrow-fs-xl); + height: var(--arrow-fs-xl); + padding: var(--arrow-spacing-xxs); +} + +.social-text { + padding-left: var(--arrow-fs-xs); + font-size: var(--arrow-fs-s); + font-weight: var(--arrow-fw-bold); + line-height: var(--arrow-lh-2); + color: var(--arrow-c-mono2); +} + +.light .social-text, +.light .share-text { + color: var(--arrow-c-invert-mono2); +} + +.dark .social-text, +.dark .share-text { + color: var(--arrow-c-mono2); +} + +html[dir="rtl"] { + .social-text { + padding-left: 0; + padding-right: var(--arrow-fs-xs); + } + + .close { + left: var(--arrow-spacing-m); + right: initial; + } +} diff --git a/app/isomorphic/arrow/components/Atoms/SocialSharePopup/social-share-popup.test.js b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/social-share-popup.test.js new file mode 100644 index 000000000..e5b85f603 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/social-share-popup.test.js @@ -0,0 +1,93 @@ +import * as React from "react"; +import { shallow } from "enzyme"; +import { SocialSharePopup } from "./index"; + +describe("Social Share Template", () => { + it("Should render social share popup", () => { + const data = { + fbUrl: "https://www.facebook.com", + twitterUrl: "https://twitter.com/", + linkedinUrl: "https://twitter.com/", + whatsappUrl: "https://twitter.com/", + theme: "#ffff", + iconType: "plain-svg", + closePopup: "", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("Should not render facebook icon if url not passed", () => { + const data = { + fbUrl: "", + twitterUrl: "https://twitter.com/", + linkedinUrl: "https://twitter.com/", + whatsappUrl: "https://twitter.com/", + theme: "#ffff", + iconType: "plain-svg", + closePopup: "", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("Should not render twitter icon if url not passed", () => { + const data = { + fbUrl: "https://twitter.com/", + twitterUrl: "", + linkedinUrl: "https://twitter.com/", + whatsappUrl: "https://twitter.com/", + theme: "#ffff", + iconType: "plain-svg", + vertical: true, + closePopup: "", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("Should not render whatsapp icon if url not passed", () => { + const data = { + fbUrl: "https://twitter.com/", + twitterUrl: "https://twitter.com/", + linkedinUrl: "https://twitter.com/", + whatsappUrl: "", + theme: "#ffff", + iconType: "plain-svg", + vertical: true, + closePopup: "", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("Should not render linkedin icon if url not passed", () => { + const data = { + fbUrl: "https://twitter.com/", + twitterUrl: "https://twitter.com/", + linkedinUrl: "", + whatsappUrl: "https://twitter.com/", + theme: "#ffff", + iconType: "plain-svg", + vertical: true, + closePopup: "", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("Should render template if theme and icon is not passed", () => { + const data = { + fbUrl: "https://twitter.com/", + twitterUrl: "https://twitter.com/", + linkedinUrl: "https://twitter.com/", + whatsappUrl: "https://twitter.com/", + theme: "", + iconType: "plain-svg", + vertical: true, + closePopup: "", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/SocialSharePopup/stories.js b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/stories.js new file mode 100644 index 000000000..ca276b703 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/SocialSharePopup/stories.js @@ -0,0 +1,19 @@ +import React from "react"; +import { withStore } from "../../../../storybook"; +import { SocialSharePopup } from "./index"; +import Readme from "./README.md"; + +const props = { + fbUrl: "https://www.facebook.com", + fullUrl: + "https://ace-web.qtstage.io/anything/recent-stories/newsready-player-one-review-spielberg-spins-a-dizzying-vr-yarn", + iconType: "plain-svg", + linkedinUrl: "https://www.linkedin.com", + mailtoUrl: + "mailto:?subject=Ready%20Player%20One%20review%20%E2%80%93%20Spielberg%C2%A0&body=https%3A%2F%2Face-web.qtstage.io%2Fanything%2Frecent-stories%2Fnews%2Fready-player-one-review-spielberg-spins-a-dizzying-vr-yarn", + publisherUrl: undefined, + theme: "#ffffff", + title: "Ready Player One review – Spielberg ", + twitterUrl: "https://twitter.com", +}; +withStore("Atoms/Social Share Popup", {}, Readme).add("default", () => ); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/README.md new file mode 100644 index 000000000..1dc1b173b --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/README.md @@ -0,0 +1,49 @@ +# AlsoRead + +Component to display the also read element. + +## Usage + +#### AlsoRead story element with default template + +```jsx + +``` + +#### AlsoRead story element with image right align template + +```jsx + +``` + +#### AlsoRead story element with text left align template + +```jsx + +``` + +#### AlsoRead story element with custom title + +```jsx +const css = { title: "Headline" }; + +; +``` + +#### AlsoRead story element with text color + +```jsx +const css = { textcolor: "#333333" }; + +; +``` + +#### Replace default also read element template with custom template passed using render + +```jsx +const customTemplate = ({ element }) =>

    ; + +; +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/also-read.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/also-read.m.css new file mode 100644 index 000000000..0a1253b23 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/also-read.m.css @@ -0,0 +1,136 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; + +.card-image img { + position: absolute; + top: 0; + height: 100%; +} + +.default-text { + font-family: var(--arrow-typeface-secondary); +} + +.alsoread-textLeftAlign .default-text, +.alsoread-imageRightAlign .default-text { + font-size: var(--arrow-fs-xs); + font-weight: var(--arrow-fw-bold); + line-height: var(--arrow-lh-3); + @media (min-width: mobile) { + font-size: var(--arrow-fs-s); + } +} + +.headline { + line-height: var(--arrow-lh-3); + font-weight: var(--arrow-fw-bold); + font-family: var(--arrow-typeface-primary); +} + +.alsoread-default .headline { + padding-top: var(--arrow-spacing-xs); + font-size: var(--arrow-fs-s); + color: var(--arrow-c-dark); + @media (min-width: mobile) { + padding-top: 0; + font-size: var(--arrow-fs-l); + } +} + +.alsoread-textLeftAlign .headline { + margin-top: var(--arrow-spacing-xs); + font-size: var(--arrow-fs-m); + color: var(--arrow-c-brand1); + @media (min-width: mobile) { + font-size: var(--arrow-fs-l); + } +} + +.alsoread-imageRightAlign .headline { + font-size: var(--arrow-fs-m); + margin-right: var(--arrow-spacing-m); + color: var(--arrow-c-dark); + @media (min-width: mobile) { + font-size: var(--arrow-fs-l); + } +} + +.card-image-wrapper { + position: relative; +} + +.alsoread-default .card-image-wrapper { + @media (min-width: mobile) { + flex-basis: 30%; + margin-right: var(--arrow-spacing-m); + } +} + +.card-image { + display: block; + position: relative; +} + +.alsoread-imageRightAlign .card-image, +.alsoread-imageRightAlign .card-image-wrapper .image { + padding-top: 75%; + @media (min-width: mobile) { + padding-top: 56.25%; + } +} + +.alsoread-default { + border: solid 1px var(--arrow-c-mono5); + display: block; + padding: var(--arrow-spacing-m); + @media (min-width: mobile) { + display: flex; + } +} + +.alsoread-imageRightAlign .content-wrapper { + display: flex; + margin-top: var(--arrow-spacing-xs); +} + +.alsoread-default .content-wrapper { + @media (min-width: mobile) { + flex-basis: 70%; + } +} + +.alsoread-default .card-image, +.alsoread .card-image-wrapper .image, +.alsoread-default .card-image-wrapper .image { + padding-top: 56.25%; +} + +.card-image-wrapper .image div { + background: var(--arrow-c-mono6); +} + +.alsoread-imageRightAlign .card-image-wrapper .image :global(.arr--fallback-image), +.alsoread-imageRightAlign .card-image-wrapper .image, +.alsoread-imageRightAlign .card-image-wrapper .card-image { + width: 100px; + height: 75px; + @media (min-width: mobile) { + width: 160px; + height: 90px; + } +} +html[dir="rtl"] { + .content-wrapper { + margin-right: var(--arrow-spacing-m); + } + .card-image-wrapper { + margin-right: 0; + } +} +.dark { + color: var(--arrow-c-mono1); +} +.light { + color: var(--arrow-c-invert-mono1); +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/also-read.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/also-read.test.js new file mode 100644 index 000000000..178a747f6 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/also-read.test.js @@ -0,0 +1,83 @@ +import * as React from "react"; +import { shallow, mount } from "enzyme"; +import { generateStoryElementData, generateStory, generateStore } from "../../../Fixture"; +import { AlsoRead } from "./index"; + +import { Provider } from "react-redux"; +import { Link } from "@quintype/components"; + +const element = generateStoryElementData("also-read"); +const story = generateStory(); + +describe("Alsoread Story Element", () => { + it("Should render default template", () => { + const wrapper = mount( + + + + ); + expect(wrapper.find(Link).prop("className")).toMatch(/alsoread-default/); + }); + + it("Should render image right align template", () => { + const wrapper = mount( + + + + ); + expect(wrapper.find(Link).prop("className")).toMatch(/alsoread-imageRightAlign/); + }); + + it("Should render text left align template", () => { + const wrapper = mount( + + + + ); + expect(wrapper.find(Link).prop("className")).toMatch(/alsoread-textLeftAlign/); + }); + + it("Should render default template with fallback image", () => { + const wrapper = shallow( + + + + ); + expect(wrapper.html()).toBe( + '
    How is the coronavirus impacting people with disabilities?
    ' + ); + }); + + it("Should render default template with text color", () => { + const wrapper = shallow( + + + + ); + expect(wrapper.html()).toBe( + '
    hero image caption
    How is the coronavirus impacting people with disabilities?
    ' + ); + }); + + it("Should render text left align template with custom title", () => { + const wrapper = shallow( + + + + ); + expect(wrapper.html()).toBe( + '
    Also-Read
    How is the coronavirus impacting people with disabilities?
    ' + ); + }); + + it("Should render custom template", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + const wrapper = shallow( + + + + ); + expect(wrapper.html()).toBe("

    How is the coronavirus impacting people with disabilities?

    "); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/index.js new file mode 100644 index 000000000..a3be69e8c --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/index.js @@ -0,0 +1,118 @@ +import React from "react"; +import get from "lodash/get"; +import { LazyLoadImages, Link, ResponsiveImage } from "@quintype/components"; +import { withElementWrapper } from "../withElementWrapper"; +import PropTypes from "prop-types"; +import { FallbackImage } from "../../FallbackImage"; +import { clientWidth, isEmpty, shapeConfig, getTextColor } from "../../../../utils/utils"; +import { useStateValue } from "../../../SharedContext"; +import "./also-read.m.css"; + +const DisplayImage = ({ story, linkedImage, template, isRightAlign }) => { + const isLeftAlign = template === "textLeftAlign"; + const isMobile = clientWidth("mobile"); + const rightAlignAspectRatio = isRightAlign && isMobile ? [4, 3] : [16, 9]; + const imageAspectRatio = isLeftAlign ? [16, 9] : rightAlignAspectRatio; + const alternateText = story["hero-image-caption"] || story.headline; + + return ( +
    + {!isEmpty(linkedImage) ? ( +
    + + + +
    + ) : ( +
    + +
    + )} +
    + ); +}; + +DisplayImage.propTypes = { + story: PropTypes.shape({ "hero-image-caption": PropTypes.string, headline: PropTypes.string }), + linkedImage: PropTypes.string, + template: PropTypes.string, + isRightAlign: PropTypes.bool, +}; + +const AlsoReadBase = ({ + story = {}, + element, + template = "", + opts = {}, + css = {}, + config = {}, + render, + ...restProps +}) => { + const content = element.text; + if (!content) return null; + + const linkedStories = get(story, ["linked-stories"], {}); + const linkedStoryId = get(element, ["metadata", "linked-story-id"]); + const linkedStorySlug = get(linkedStories, [linkedStoryId, "url"]); + const linkedImage = get(linkedStories, [linkedStoryId, "hero-image-s3-key"], ""); + const storyUrl = linkedStorySlug && linkedStorySlug; + const configData = useStateValue() || {}; + const textInvertColor = getTextColor(configData.theme); + const { title = "" } = opts; + const { textColor } = css; + + const displayTitle = title || "Also Read"; + const isDefault = template === "" || template === "default"; + const isRightAlign = template === "imageRightAlign"; + const templateStyle = template ? `alsoread-${template}` : "alsoread-default"; + + return ( + + {!isDefault &&
    {displayTitle}
    } + {isDefault && ( + + )} +
    +
    + {isRightAlign && } +
    + + ); +}; + +AlsoReadBase.propTypes = { + /** template can be default, textLeftAlign or imageRightAlign */ + template: PropTypes.string, + story: PropTypes.shape({ "linked-stories": PropTypes.object }), + element: PropTypes.shape({ + text: PropTypes.string, + metadata: PropTypes.shape({ + linkedStoryId: PropTypes.shape({ url: PropTypes.string, "hero-image-s3-key": PropTypes.string }), + }), + }), + opts: PropTypes.shape({ title: PropTypes.string }), + config: shapeConfig, + render: PropTypes.func, + css: PropTypes.object, +}; + +export const AlsoRead = withElementWrapper(AlsoReadBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/stories.js new file mode 100644 index 000000000..0256e435d --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/AlsoRead/stories.js @@ -0,0 +1,40 @@ +import React from "react"; +import { withStore, optionalSelect } from "../../../../../storybook"; +import { color, text } from "@storybook/addon-knobs"; +import { AlsoRead } from "./index"; +import { generateStoryElementData, generateStory } from "../../../Fixture"; +import Readme from "./README.md"; + +const element = generateStoryElementData("also-read"); +const story = generateStory(); +const templateStyle = { + Default: "default", + "Image Right Align": "imageRightAlign", + "Text Left Align": "textLeftAlign", +}; + +withStore( + "Atoms/Story Elements/Also Read", + { + qt: { + config: { + "cdn-image": "thumbor-stg.assettype.com", + }, + }, + }, + Readme +) + .add("Default", () => ( + + )) + .add("Custom", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + return ; + }); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/README.md new file mode 100644 index 000000000..f3b1ee71e --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/README.md @@ -0,0 +1,21 @@ +# Attachment + +Component to display the attachment element. + +## Usage + +#### Attachment story element with default template + +```jsx + +``` + +#### Replace default attachment element template with custom template passed using render + +```jsx +const customTemplate = ({ element }) =>

    ; + +; +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/__snapshots__/attachment.test.js.snap b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/__snapshots__/attachment.test.js.snap new file mode 100644 index 000000000..b0d281500 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/__snapshots__/attachment.test.js.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Attachment Story Element Should render default template 1`] = ` + +`; + +exports[`Attachment Story Element Should render default template with default file-name if not passed 1`] = ` + +`; diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/attachment.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/attachment.m.css new file mode 100644 index 000000000..68f4499a8 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/attachment.m.css @@ -0,0 +1,126 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; +@value tablet from viewports; + +.headline { + font-size: var(--arrow-fs-s); + font-weight: var(--arrow-fw-bold); + line-height: var(--arrow-lh-3); + padding-bottom: var(--arrow-spacing-xs); + @media (min-width: mobile) { + font-size: var(--arrow-fs-l); + padding-bottom: var(--arrow-spacing-s); + @media (min-width: tablet) { + padding-bottom: var(--arrow-spacing-m); + } + } +} + +.button-wrapper { + border-radius: 3px; + font-size: var(--arrow-fs-tiny); + line-height: var(--arrow-lh-4); + text-align: center; + padding: 6px var(--arrow-spacing-m); + @media (min-width: tablet) { + margin-left: var(--arrow-spacing-m); + } +} + +.file-name { + font-size: var(--arrow-fs-s); + line-height: var(--arrow-lh-5); + margin-bottom: var(--arrow-spacing-m); + word-break: break-word; + @media (min-width: mobile) { + font-size: var(--arrow-fs-m); + line-height: var(--arrow-lh-3); + @media (min-width: tablet) { + margin-bottom: unset; + } + } +} + +.dark .headline, +.dark .file-name, +.dark .button-wrapper { + color: var(--arrow-c-mono2); +} + +.light .headline, +.light .file-name, +.light .button-wrapper { + color: var(--arrow-c-invert-mono2); +} + +.pdf { + width: 35px; + height: 35px; + font-size: var(--arrow-fs-tiny); + font-weight: var(--arrow-fw-bold); + color: var(--arrow-c-light); + border-radius: 6px; + background-color: #db5449; + display: flex; + align-items: center; + justify-content: center; + margin-right: var(--arrow-spacing-m); + @media (min-width: mobile) { + width: 45px; + height: 45px; + border-radius: var(--arrow-spacing-xs); + } +} +.doc { + composes: pdf; + background-color: #007bd8; +} + +.wrapper { + display: flex; + padding: var(--arrow-spacing-m); + border-radius: var(--arrow-spacing-xxs); + @media (min-width: tablet) { + padding: var(--arrow-spacing-l); + } +} + +.dark .wrapper { + border: solid 1px var(--arrow-c-mono7); + background-color: var(--arrow-c-mono7); +} + +.light .wrapper { + border: solid 1px var(--arrow-c-invert-mono7); + background-color: var(--arrow-c-invert-mono7); +} + +.dark .button-wrapper { + border: solid 1px var(--arrow-c-mono2); +} + +.light .button-wrapper { + border: solid 1px var(--arrow-c-invert-mono2); +} + +.content-wrapper { + @media (min-width: tablet) { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + } +} + +html[dir="rtl"] { + .doc, + .pdf, + .headline { + margin-right: 0; + } + + .content-wrapper { + margin-right: var(--arrow-spacing-m); + } +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/attachment.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/attachment.test.js new file mode 100644 index 000000000..a18863de7 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/attachment.test.js @@ -0,0 +1,39 @@ +import * as React from "react"; +import { shallow } from "enzyme"; +import { generateStoryElementData } from "../../../Fixture"; +import { Attachment } from "./index"; + +let element = generateStoryElementData("attachment"); + +describe("Attachment Story Element", () => { + it("Should render default template", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("Should not render default template if url is not passed", () => { + const wrapper = shallow(); + expect(wrapper.find({ "data-test-id": "attachment" }).length).toEqual(0); + }); + + it("Should not render default template if content-type is not passed", () => { + const wrapper = shallow(); + expect(wrapper.find({ "data-test-id": "attachment" }).length).toEqual(0); + }); + + it("Should render default template with default file-name if not passed", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("Should render PDF icon if content-type is pdf", () => { + const wrapper = shallow(); + expect(wrapper.find({ "data-test-id": "doc" }).length).toEqual(0); + }); + + it("Should render DOC icon if content-type is doc", () => { + element = { ...element, "content-type": "application/doc" }; + const wrapper = shallow(); + expect(wrapper.find({ "data-test-id": "pdf" }).length).toEqual(0); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/index.js new file mode 100644 index 000000000..b1a337d72 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/index.js @@ -0,0 +1,64 @@ +import React from "react"; +import PropTypes from "prop-types"; +import "./attachment.m.css"; +import { Link } from "@quintype/components"; +import { withElementWrapper } from "../withElementWrapper"; +import { useStateValue } from "../../../SharedContext"; +import { getTextColor } from "../../../../utils/utils"; + +const AttachmentBase = ({ element = {}, render, ...restProps }) => { + const { "file-name": fileName, url, "content-type": contentType } = element; + + if (!contentType || !url) return null; + const attachmentType = { PDF: "application/pdf" }; + const isPdf = contentType === attachmentType.PDF; + const configData = useStateValue() || {}; + const textColor = getTextColor(configData.theme); + const defaultName = isPdf ? "Attached PDF" : "Attached DOC"; + const defaultFileName = fileName || defaultName; + + return ( +
    +
    Attachment
    +
    + {isPdf ? ( +
    + PDF +
    + ) : ( +
    + DOC +
    + )} +
    +
    + + {isPdf ? "Preview" : "Download"} + +
    +
    +
    + ); +}; + +AttachmentBase.propTypes = { + element: PropTypes.shape({ + url: PropTypes.string, + fileName: PropTypes.string, + contentType: PropTypes.string, + }), + render: PropTypes.func, +}; +export const Attachment = withElementWrapper(AttachmentBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/stories.js new file mode 100644 index 000000000..0931ff77d --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Attachment/stories.js @@ -0,0 +1,48 @@ +import React from "react"; +import { withStore } from "../../../../../storybook"; +import { Attachment } from "./index"; +import { generateStoryElementData } from "../../../Fixture"; +import Readme from "./README.md"; + +const doc = { + description: "", + "page-url": "/story/7155b5c2-80a4-4922-bdf1-73f1ff04311e/element/2f648c29-ef8f-442c-903a-263f82e631dd", + type: "file", + "family-id": "95bf1052-d663-446d-ace7-d014325e0027", + title: "", + id: "2f648c29-ef8f-442c-903a-263f82e631dd", + "file-name": "document__7_ (1).docx", + url: "https://thumbor-stg.assettype.com/ace/2019-08/21f3a19b-1d98-46bc-95e3-a5bdc9045ae9/document__7___1_.docx", + "s3-key": "ace/2019-08/21f3a19b-1d98-46bc-95e3-a5bdc9045ae9/document__7___1_.docx", + "content-type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + metadata: { + "file-size": 8209, + }, + subtype: "attachment", +}; + +const pdf = { + description: "", + "page-url": "/story/a9068be5-70ce-4d55-86d0-687546f921ea/element/62393c55-50ed-4310-81c2-01cc0ef17446", + type: "file", + "family-id": "591f90c3-e98e-43dc-8509-9f11f5335af6", + title: "", + id: "62393c55-50ed-4310-81c2-01cc0ef17446", + "file-name": "resume-samples.pdf", + url: "https://thumbor-stg.assettype.com/ace/2019-07/6dcf2021-615b-43e6-85f3-21acb8953cea/resume_samples.pdf", + "s3-key": "ace/2019-07/6dcf2021-615b-43e6-85f3-21acb8953cea/resume_samples.pdf", + "content-type": "application/pdf", + metadata: { + "file-size": 301808, + }, + subtype: "attachment", +}; +const element = generateStoryElementData("attachment"); +withStore("Atoms/Story Elements/Attachment", Readme) + .add("PDF", () => ) + .add("DOC", () => ) + .add("Custom", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + return ; + }); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/README.md new file mode 100644 index 000000000..e8476e9cc --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/README.md @@ -0,0 +1,9 @@ +## BigFact + +BigFact displays the big fact content of the story element. + +# Usage + +```jsx + +``` diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/big-fact.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/big-fact.m.css new file mode 100644 index 000000000..84eb93027 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/big-fact.m.css @@ -0,0 +1,42 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; + +.bigfact-element { + border-bottom: 1px solid var(--arrow-c-mono5); + border-top: 1px solid var(--arrow-c-mono5); +} + +.bigfact-element.light { + border-bottom: 1px solid var(--arrow-c-invert-mono5); + border-top: 1px solid var(--arrow-c-invert-mono5); +} + +.content { + font-size: var(--arrow-fs-huge); + font-weight: var(--arrow-fw-bold); + color: var(--arrow-c-brand1); + padding: var(--arrow-spacing-m) 0 var(--arrow-spacing-xxs) 0; + @media (min-width: mobile) { + font-size: 48px; + padding: var(--arrow-spacing-m) 0 var(--arrow-spacing-xs) 0; + line-height: var(--arrow-lh-3); + } +} + +.attribution { + font-size: var(--arrow-fs-s); + font-style: italic; + line-height: var(--arrow-lh-5); + padding-bottom: var(--arrow-spacing-m); + @media (min-width: mobile) { + font-size: var(--arrow-fs-m); + } +} + +.dark { + color: var(--arrow-c-mono1); +} +.light { + color: var(--arrow-c-invert-mono1); +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/big-fact.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/big-fact.test.js new file mode 100644 index 000000000..25e025dbf --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/big-fact.test.js @@ -0,0 +1,21 @@ +import React from "react"; +import { shallow, mount } from "enzyme"; +import { BigFact } from "./index"; +import { generateStoryElementData } from "../../../Fixture"; +import "./big-fact.m.css"; + +const element = generateStoryElementData("bigfact"); +describe("Component: BigFact", () => { + test("It should display the big fact element content and attribution", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "bigfact" }).prop("className")).toMatch(/bigfact-element/); + }); + it("Should not render attribution value if there is no data", () => { + const wrapper = shallow(); + expect(wrapper.find({ "data-test-id": "attribution" }).length).toEqual(0); + }); + test("It should not render the BigFact component if the data is null", () => { + const component = shallow(); + expect(component.contains()).toBe(false); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/index.js new file mode 100644 index 000000000..aebed8de1 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/index.js @@ -0,0 +1,34 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { withElementWrapper } from "../withElementWrapper"; +import { useStateValue } from "../../../SharedContext"; +import { getTextColor } from "../../../../utils/utils"; +import "./big-fact.m.css"; + +const BigFactBase = ({ element, story, config, ...restProps }) => { + const configData = useStateValue() || {}; + const textInvertColor = getTextColor(configData.theme); + const { content, attribution } = element.metadata; + + return ( +
    +
    {content}
    +
    + {attribution} +
    +
    + ); +}; + +BigFactBase.propTypes = { + element: PropTypes.shape({ metadata: PropTypes.shape({ content: PropTypes.string, attribution: PropTypes.string }) }), + story: PropTypes.object, + config: PropTypes.object, +}; + +export const BigFact = withElementWrapper(BigFactBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/stories.js new file mode 100644 index 000000000..236310fe6 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BigFact/stories.js @@ -0,0 +1,9 @@ +import React from "react"; +import { BigFact } from "./index"; +import { withStore } from "../../../../../storybook"; +import Readme from "./README.md"; +import { generateStoryElementData } from "../../../Fixture"; + +const element = generateStoryElementData("bigfact"); + +withStore("Atoms/Story Elements/BigFact", {}, Readme).add("Default", () => ); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/README.md new file mode 100644 index 000000000..721cb6eae --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/README.md @@ -0,0 +1,49 @@ +# Block Quote + +Displays the Block quote story element of the story + +## Usage + +#### Block quote story element with default template + +```jsx +
    +``` + +#### Block quote story element with background template + +```jsx +
    +``` + +#### Block quote story element with border template + +```jsx +
    +``` + +#### Block quote story element with background color + +```jsx +const css = { backgroundShade: "#ff214b" }; + +
    ; +``` + +#### Block quote story element with blockquote color + +```jsx +const css = { blockQuoteColor: "#ff214b" }; + +
    ; +``` + +#### Replace default block quote element template with custom template passed using render + +```jsx +const customTemplate = ({ element }) =>

    ; + +
    ; +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/Svg/curve.js b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/Svg/curve.js new file mode 100644 index 000000000..71c042f40 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/Svg/curve.js @@ -0,0 +1,26 @@ +import React from "react"; +import PropTypes from "prop-types"; +import "./icon.m.css"; + +export const CurveIcon = ({ width, height, color, opacity }) => { + const lightColor = color === "light" && "#ffffff"; + const darkColor = color === "dark" && "#0d0d0d"; + return ( +
    + + + +
    + ); +}; + +CurveIcon.propTypes = { + width: PropTypes.string, + height: PropTypes.string, + color: PropTypes.string, + opacity: PropTypes.string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/Svg/edge.js b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/Svg/edge.js new file mode 100644 index 000000000..5e180aa22 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/Svg/edge.js @@ -0,0 +1,26 @@ +import React from "react"; +import PropTypes from "prop-types"; +import "./icon.m.css"; + +export const EdgeIcon = ({ width, height, color, opacity }) => { + const lightColor = color === "light" && "#ffffff"; + const darkColor = color === "dark" && "#0d0d0d"; + return ( +
    + + + +
    + ); +}; + +EdgeIcon.propTypes = { + width: PropTypes.string, + height: PropTypes.string, + color: PropTypes.string, + opacity: PropTypes.string, +}; diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/Svg/icon.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/Svg/icon.m.css new file mode 100644 index 000000000..567f4c748 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/Svg/icon.m.css @@ -0,0 +1,4 @@ +.edge-wrapper, +.curve-wrapper { + fill: var(--arrow-c-brand1); +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/block-quote.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/block-quote.m.css new file mode 100644 index 000000000..5e32d3091 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/block-quote.m.css @@ -0,0 +1,162 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; + +.blockquote-withBackground::before { + @media (min-width: mobile) { + content: ""; + padding-top: 56.25%; + } +} + +.icon { + width: 48px; + height: 48px; + @media (min-width: mobile) { + width: 64px; + height: 64px; + } +} + +.icon-border { + display: flex; + margin-bottom: var(--arrow-spacing-s); + @media (min-width: mobile) { + margin-bottom: var(--arrow-spacing-m); + } +} + +.blockquote .icon, +.blockquote-default .icon { + margin-bottom: var(--arrow-spacing-s); + @media (min-width: mobile) { + margin-bottom: unset; + margin-right: var(--arrow-spacing-l); + } +} + +.blockquote-withBorder .icon { + margin-right: var(--arrow-spacing-m); + @media (min-width: mobile) { + margin-right: var(--arrow-spacing-l); + } +} + +.blockquote-withBackground .icon { + margin-bottom: var(--arrow-spacing-s); + @media (min-width: mobile) { + margin-bottom: var(--arrow-spacing-m); + } +} + +.blockquote-withBorder .border { + width: 100%; + height: 2px; + background-color: var(--arrow-c-brand1); + margin-top: 36px; +} + +.blockquote .content, +.blockquote-default .content, +.blockquote-withBorder .content, +.blockquote-withBackground .content { + font-family: var(--arrow-typeface-primary); + font-size: var(--arrow-fs-l); + font-weight: var(--arrow-fw-bold); + margin-bottom: var(--arrow-spacing-s); + line-height: var(--arrow-lh-3); + @media (min-width: mobile) { + font-size: var(--arrow-fs-huge); + } +} + +.blockquote .content, +.blockquote-default .content { + @media (min-width: mobile) { + margin-bottom: var(--arrow-spacing-xs); + } +} + +.blockquote-withBorder .content { + margin-bottom: var(--arrow-spacing-xs); +} + +.blockquote-withBorder .wrapper { + border-bottom: solid 2px var(--arrow-c-brand1); +} + +.blockquote .attribution, +.blockquote-default .attribution, +.blockquote-withBorder .attribution, +.blockquote-withBackground .attribution { + font-family: var(--arrow-typeface-secondary); + font-size: var(--arrow-fs-m); + font-weight: var(--arrow-fw-bold); + font-style: italic; + line-height: var(--arrow-lh-5); + @media (min-width: mobile) { + font-size: var(--arrow-fs-l); + } +} + +.blockquote-withBorder .attribution { + margin-bottom: var(--arrow-spacing-l); +} + +.blockquote-withBackground .attribution { + display: flex; + justify-content: flex-end; +} + +.blockquote-withBackground { + background-color: var(--arrow-c-brand1); + display: flex; + justify-content: center; + align-items: center; +} + +.blockquote-withBorder { + display: block; +} + +.blockquote-withBackground .attribution::before { + content: "–"; + color: inherit; + margin-right: var(--arrow-spacing-xs); + @media (min-width: mobile) { + margin-right: var(--arrow-spacing-s); + } +} + +.blockquote-withBackground .quote-wrapper { + padding: 48px; + @media (min-width: mobile) { + padding: 96px; + } +} + +.blockquote .quote-wrapper, +.blockquote-default .quote-wrapper { + @media (min-width: mobile) { + display: flex; + } +} + +html[dir="rtl"] .wrapper { + margin-right: var(--arrow-spacing-l); +} + +.attribution.dark { + color: var(--arrow-c-mono4); +} +.attribution.light { + color: var(--arrow-c-invert-mono4); +} + +.content.dark { + color: var(--arrow-c-mono2); +} + +.content.light { + color: var(--arrow-c-invert-mono2); +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/block-quote.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/block-quote.test.js new file mode 100644 index 000000000..19ffb742d --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/block-quote.test.js @@ -0,0 +1,48 @@ +import * as React from "react"; +import { shallow, mount } from "enzyme"; +import { generateStoryElementData } from "../../../Fixture"; +import { BlockQuote } from "./index"; + +const element = generateStoryElementData("blockquote"); + +describe("Block Quote Story Element", () => { + it("Should render default template", () => { + const wrapper = mount(
    ); + expect(wrapper.find({ "data-test-id": "blockquote" }).prop("className")).toMatch(/blockquote-default/); + }); + + it("Should render with background template", () => { + const wrapper = mount(
    ); + expect(wrapper.find({ "data-test-id": "blockquote" }).prop("className")).toMatch(/blockquote-withBackground/); + }); + + it("Should render with border template", () => { + const wrapper = mount(
    ); + expect(wrapper.find({ "data-test-id": "blockquote" }).prop("className")).toMatch(/blockquote-withBorder/); + }); + + it("Should render background template with background color", () => { + const wrapper = shallow( +
    + ); + expect(wrapper.html()).toBe( + '
    After the death of James Halliday, the creator of the virtual reality world, his pre-recorded message reveals the hidden fortune, which makes Wade Watts, a teenager, embark on a quest.
    Player
    ' + ); + }); + + it("Should render default template with blockquote color", () => { + const wrapper = shallow(
    ); + expect(wrapper.html()).toBe( + '
    After the death of James Halliday, the creator of the virtual reality world, his pre-recorded message reveals the hidden fortune, which makes Wade Watts, a teenager, embark on a quest.
    Player
    ' + ); + }); + + it("Should render custom template", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + const wrapper = shallow(
    ); + expect(wrapper.html()).toBe( + "

    After the death of James Halliday, the creator of the virtual reality world, his pre-recorded message reveals the hidden fortune, which makes Wade Watts, a teenager, embark on a quest.

    " + ); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/index.js new file mode 100644 index 000000000..4c02dbadc --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/index.js @@ -0,0 +1,83 @@ +import React from "react"; +import get from "lodash/get"; +import PropTypes from "prop-types"; +import { clientWidth, getTextColor, shapeConfig, shapeStory } from "../../../../utils/utils"; +import { withElementWrapper } from "../withElementWrapper"; +import { CurveIcon } from "./Svg/curve"; +import { EdgeIcon } from "./Svg/edge"; +import "./block-quote.m.css"; +import { useStateValue } from "../../../SharedContext"; + +const SelectionOfIcon = (iconType, template, blockQuoteColor, quoteColor) => { + const isMobile = clientWidth("mobile"); + const dimension = isMobile ? "48px" : "64px"; + + if (template === "withBackground") { + if (iconType === "edgeIcon") { + return ; + } + return ; + } + + if (template !== "withBackground") { + if (iconType === "edgeIcon") { + return ; + } + return ; + } +}; + +export const BlockQuoteBase = ({ element, template, css = {}, story = {}, config = {}, render, ...restProps }) => { + const { content, attribution } = get(element, ["metadata"]); + if (!content) return null; + + const { blockQuoteColor, backgroundShade, iconType } = css; + const templateStyle = template ? `blockquote-${template}` : "blockquote"; + const isBackground = template === "withBackground"; + const textColor = isBackground ? getTextColor(backgroundShade) : ""; + const theme = isBackground && backgroundShade ? { backgroundColor: backgroundShade } : {}; + const configData = useStateValue() || {}; + const textInvertColor = getTextColor(isBackground ? theme.backgroundColor : configData.theme); + + const updateStructure = () => { + return template === "withBorder" ? ( +
    +
    {SelectionOfIcon(iconType, template, blockQuoteColor, textColor)}
    +
    +
    + ) : ( +
    {SelectionOfIcon(iconType, template, blockQuoteColor, textColor)}
    + ); + }; + + return ( +
    +
    + {updateStructure()} +
    +
    +
    +
    +
    +
    + ); +}; + +BlockQuoteBase.propTypes = { + element: PropTypes.shape({ + metadata: PropTypes.shape({ content: PropTypes.string, attribution: PropTypes.string }), + }), + template: PropTypes.string, + story: shapeStory, + config: shapeConfig, + render: PropTypes.func, + css: PropTypes.object, +}; + +export const BlockQuote = withElementWrapper(BlockQuoteBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/stories.js new file mode 100644 index 000000000..d1af667aa --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/BlockQuote/stories.js @@ -0,0 +1,35 @@ +import React from "react"; +import { generateStoryElementData } from "../../../Fixture"; +import { BlockQuote } from "./index"; +import { withStore, optionalSelect } from "../../../../../storybook"; +import Readme from "./README.md"; +import { color } from "@storybook/addon-knobs"; + +const templateStyle = { + Default: "default", + "With Background": "withBackground", + "With Border": "withBorder", +}; +const element = generateStoryElementData("blockquote"); +const iconStyle = { + "Curve Icon": "curveIcon", + "Edge Icon": "edgeIcon", +}; + +withStore("Atoms/Story Elements/Block Quote", {}, Readme) + .add("Default", () => ( +
    + )) + .add("Custom", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + return
    ; + }); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/README.md new file mode 100644 index 000000000..5a54f98a4 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/README.md @@ -0,0 +1,43 @@ +# Blurb + +Displays the blurb story element of the story + +## Usage + +#### Blurb story element with default template + +```jsx + +``` + +#### Blurb story element with border template + +```jsx + +``` + +#### Blurb story element with external links opening in new tab + +```jsx +const opts = { isExternalLink: true }; + +; +``` + +#### Blurb story element with border color + +```jsx +const css = { borderColor: "#ff214b" }; + +; +``` + +#### Replace default blurb element template with custom template passed using render + +```jsx +const customTemplate = ({ element }) =>

    ; + +; +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/blurb.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/blurb.m.css new file mode 100644 index 000000000..a037b0182 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/blurb.m.css @@ -0,0 +1,56 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; + +.blurb blockquote, +.blurb-default blockquote, +.blurb-withBorder blockquote { + font-family: var(--arrow-typeface-secondary); + font-size: var(--arrow-fs-s); + line-height: var(--arrow-lh-5); + @media (min-width: mobile) { + font-size: var(--arrow-fs-m); + } +} + +.blurb blockquote, +.blurb-default blockquote { + font-style: italic; + padding-left: var(--arrow-spacing-m); + @media (min-width: mobile) { + padding-left: var(--arrow-spacing-l); + } +} + +.blurb-withBorder blockquote { + padding: var(--arrow-spacing-s) var(--arrow-spacing-m); + @media (min-width: mobile) { + padding: var(--arrow-spacing-m) var(--arrow-spacing-l); + } +} + +html[dir="rtl"] { + .blurb blockquote, + .blurb-default blockquote { + padding-left: 0; + padding-right: var(--arrow-spacing-m); + @media (min-width: mobile) { + padding-right: var(--arrow-spacing-l); + } + } +} + +.dark { + color: var(--arrow-c-mono1); + a { + color: var(--arrow-c-mono2); + border-bottom: 1px solid var(--arrow-c-mono2); + } +} +.light { + color: var(--arrow-c-invert-mono1); + a { + color: var(--arrow-c-invert-mono2); + border-bottom: 1px solid var(--arrow-c-invert-mono2); + } +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/blurb.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/blurb.test.js new file mode 100644 index 000000000..d6be6ea1f --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/blurb.test.js @@ -0,0 +1,46 @@ +import * as React from "react"; +import { shallow, mount } from "enzyme"; +import { generateStoryElementData } from "../../../Fixture"; +import { Blurb } from "./index"; + +const element = generateStoryElementData("blurb"); + +describe("Blurb Story Element", () => { + it("Should render default template", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "blurb" }).prop("className")).toMatch(/blurb-default/); + }); + + it("Should render border template", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "blurb" }).prop("className")).toMatch(/blurb-withBorder/); + }); + + it("Should render default template with external link", () => { + const wrapper = shallow(); + expect(wrapper.html()).toBe( + '
    Although the many story changes might be hard for book purists to accept, Steven Spielberg has lovingly captured the zeitgeist of 80s nostalgia in this adventure.
    ' + ); + }); + + it("Should render default template without external link", () => { + const wrapper = shallow(); + expect(wrapper.html()).toBe( + '
    Although the many story changes might be hard for book purists to accept, Steven Spielberg has lovingly captured the zeitgeist of 80s nostalgia in this adventure.
    ' + ); + }); + + it("Should render the border template with border color", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "blurb" }).prop("style")).toEqual({ border: "solid 2px #ff214b" }); + }); + + it("Should render custom template", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + const wrapper = shallow(); + expect(wrapper.html()).toBe( + '

    Although the many story changes might be hard for book purists to accept, Steven Spielberg has lovingly captured the zeitgeist of 80s nostalgia in this adventure.

    ' + ); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/index.js new file mode 100644 index 000000000..ec9b6566f --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/index.js @@ -0,0 +1,53 @@ +import React, { useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import { withElementWrapper } from "../withElementWrapper"; +import { updateContentLinks, shapeStory, shapeConfig, getTextColor } from "../../../../utils/utils"; +import "./blurb.m.css"; +import { useStateValue } from "../../../SharedContext"; + +const BlurbBase = ({ element, template = "", opts = {}, css = {}, story = {}, config = {}, render, ...restProps }) => { + const content = element.text; + const { borderColor } = css; + const initBorderDirection = + template === "withBorder" ? { border: `solid 2px ${borderColor}` } : { borderLeft: `solid 2px ${borderColor}` }; + const [borderDirection, setBorderDirection] = useState(initBorderDirection); + if (!content) return null; + + useEffect(() => { + const htmlElement = document.getElementsByTagName("HTML"); + if (htmlElement && htmlElement.length) { + if (htmlElement[0].dir.toLowerCase() === "rtl" && template !== "withBorder") { + setBorderDirection({ borderRight: `solid 2px ${borderColor}` }); + } + } + }, []); + + const { isExternalLink = true } = opts; + const text = (isExternalLink && updateContentLinks(content)) || content; + const templateStyle = template ? `blurb-${template}` : "blurb"; + const configData = useStateValue() || {}; + const textInvertColor = getTextColor(configData.theme); + return ( +
    + ); +}; + +BlurbBase.propTypes = { + element: PropTypes.shape({ text: PropTypes.string }), + /** templates can be either default or withBorder */ + template: PropTypes.string, + opts: PropTypes.shape({ isExternalLink: PropTypes.bool }), + story: shapeStory, + config: shapeConfig, + render: PropTypes.func, + css: PropTypes.object, +}; + +export const Blurb = withElementWrapper(BlurbBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/stories.js new file mode 100644 index 000000000..bdf3b8b09 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Blurb/stories.js @@ -0,0 +1,28 @@ +import React from "react"; +import { withStore, optionalSelect } from "../../../../../storybook"; +import { generateStoryElementData } from "../../../Fixture"; +import Readme from "./README.md"; +import { Blurb } from "./index"; +import { color, boolean } from "@storybook/addon-knobs"; + +const templateStyle = { + Default: "default", + "Blurb with Border": "withBorder", +}; + +const element = generateStoryElementData("blurb"); + +withStore("Atoms/Story Elements/Blurb", {}, Readme) + .add("Default", () => ( + + )) + .add("Custom", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + return ; + }); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Image/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/Image/README.md new file mode 100644 index 000000000..eef2a2693 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Image/README.md @@ -0,0 +1,13 @@ +# Image Story Element + +Displays the image story element of the story + +## Usage + +#### Default Image story element + +```jsx + +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Image/image.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/Image/image.m.css new file mode 100644 index 000000000..bc1612675 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Image/image.m.css @@ -0,0 +1,21 @@ +.element { + position: relative; +} + +.loading { + position: fixed; + left: 0; + width: 100%; + top: 0; + height: 100%; + /* To match it with lightbox overlay, hardcoding this value */ + background: rgba(0, 0, 0, 0.85); + display: flex; + color: var(--arrow-c-light); + justify-content: center; + align-items: center; +} + +.image-template { + position: relative; +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Image/image.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Image/image.test.js new file mode 100644 index 000000000..521b0f13f --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Image/image.test.js @@ -0,0 +1,20 @@ +import React from "react"; +import { mount } from "enzyme"; +import { Provider } from "react-redux"; +import { Image } from "."; +import { generateStoryElementData, generateStore } from "../../../Fixture"; +import { FullScreenImages } from "../../../Molecules/FullScreenImages"; +const element = generateStoryElementData("image"); + +describe("Image Story Element", () => { + it("should render with correct elements", () => { + const wrapper = mount( + + + + ); + expect(wrapper.find("figure").length).toEqual(1); + expect(wrapper.find("CaptionAttribution").length).toEqual(1); + expect(wrapper.find(FullScreenImages).length).toEqual(1); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Image/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Image/index.js new file mode 100644 index 000000000..46ce40b95 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Image/index.js @@ -0,0 +1,60 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { ResponsiveImage } from "@quintype/components"; +import { withElementWrapper } from "../withElementWrapper"; + +import "./image.m.css"; +import { shapeStory, shapeConfig } from "../../../../utils/utils"; +import { FullScreenImages } from "../../../Molecules/FullScreenImages"; +import { useStateValue } from "../../../SharedContext"; +import { CaptionAttribution } from "../../CaptionAttribution"; +import { HyperLink } from "../../Hyperlink"; + +const ImageBase = ({ element, opts = {}, story = {}, config = {}, caption = true, ...restProps }) => { + if (!element) return null; + const configData = useStateValue() || {}; + + const ImageTemplate = ({ onClick }) => ( +
    +
    onClick()}> + +
    + {element.hyperlink && } +
    + ); + + ImageTemplate.propTypes = { + onClick: PropTypes.func, + }; + + return ( +
    + + {caption && } +
    + ); +}; + +ImageBase.propTypes = { + element: PropTypes.shape({ + "image-s3-key": PropTypes.string, + "image-metadata": PropTypes.object, + "image-attribution": PropTypes.string, + title: PropTypes.string, + hyperlink: PropTypes.string, + }), + caption: PropTypes.bool, + opts: PropTypes.shape({ imageWidths: PropTypes.array }), + story: shapeStory, + config: shapeConfig, +}; + +export const Image = withElementWrapper(ImageBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Image/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Image/stories.js new file mode 100644 index 000000000..a01b8a6bd --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Image/stories.js @@ -0,0 +1,25 @@ +import React from "react"; +import { withStore } from "../../../../../storybook"; +import { Image } from "./index"; +import Readme from "./README.md"; +import { generateStoryElementData } from "../../../Fixture"; + +const element = generateStoryElementData("image"); + +withStore( + "Atoms/Story Elements/Image", + { + qt: { + config: { + "cdn-image": "gumlet.assettype.com", + }, + }, + }, + Readme +) + .add("Default", () => ) + .add("Custom", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    {element.title}

    ; + return ; + }); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/README.md new file mode 100644 index 000000000..9f7832254 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/README.md @@ -0,0 +1,13 @@ +# ImageSlideshow + +Displays the image slideshow/carousel story element. + +## Usage + +#### Default ImageSlideshow element + +```jsx + ); +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/image-slideshow.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/image-slideshow.m.css new file mode 100644 index 000000000..3ed7219be --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/image-slideshow.m.css @@ -0,0 +1,49 @@ +@import "../../../../../../assets/arrow/stylesheets/mixins.scss"; + +.slide { + position: relative; +} + +.slideshow { + @include mobile { + width: 90vw; + margin: 0 auto; + } +} + +.caption { + position: absolute; + bottom: 10px; + left: 0; + right: 0; + text-align: center; + color: var(--arrow-c-light); +} + +.slideshow figure { + padding-top: 56.65%; + position: relative; + margin: 0; +} + +.image-container::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; + background-image: linear-gradient(to top, rgba(var(--arrow-c-dark), 0.5), transparent 70%, transparent); +} + +.image { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + height: 100%; + object-fit: cover; +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/index.js new file mode 100644 index 000000000..f5b50c6b1 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/index.js @@ -0,0 +1,65 @@ +import { LazyLoadImages, ResponsiveImage } from "@quintype/components"; +import get from "lodash/get"; +import PropTypes from "prop-types"; +import React from "react"; +import { shapeStory } from "../../../../utils/utils"; +import { FullScreenImages } from "../../../Molecules/FullScreenImages"; +import { withElementWrapper } from "../withElementWrapper"; +import { ScrollSnap } from "../../ScrollSnap"; +import "./image-slideshow.m.css"; +import { HyperLink } from "../../Hyperlink"; + +const ImageSlideshowBase = (props) => { + if (!props.element) return null; + + const Slide = (image, index, onClickHandler) => { + const { id, "image-s3-key": imageS3Key, metadata, title, hyperlink = "" } = image; + return ( +
    +
    onClickHandler(index)}> + + + +
    + {hyperlink && } +

    +

    + ); + }; + + const ImageSlideshowTemplate = ({ onClickHandler }) => { + const storyElements = get(props, ["element", "story-elements"], []); + + return ( +
    + + {storyElements.map((image, index) => Slide(image, index, onClickHandler))} + +
    + ); + }; + + ImageSlideshowTemplate.propTypes = { + onClickHandler: PropTypes.func, + }; + + return ; +}; + +ImageSlideshowBase.propTypes = { + element: PropTypes.shape({ + "story-elements": PropTypes.array, + }), + story: shapeStory, +}; + +export const ImageSlideshow = withElementWrapper(ImageSlideshowBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/stories.js new file mode 100644 index 000000000..a67ae294f --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/ImageSlideshow/stories.js @@ -0,0 +1,34 @@ +import React from "react"; +import { withStore } from "../../../../../storybook"; +import { generateStoryElementData } from "../../../Fixture"; +import { ImageSlideshow } from "./index"; +import Readme from "./README.md"; + +const element = generateStoryElementData("image-gallery"); + +withStore( + "Atoms/Story Elements/Image Slideshow", + { + qt: { + config: { + "cdn-image": "thumbor-stg.assettype.com", + }, + }, + }, + Readme +).add("Default", () => ); + +withStore( + "Atoms/Story Elements/Image Slideshow", + { + qt: { + config: { + "cdn-image": "thumbor-stg.assettype.com", + language: { + direction: "rtl", + }, + }, + }, + }, + Readme +).add("Support Rtl", () => ); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/README.md new file mode 100644 index 000000000..027ed52b7 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/README.md @@ -0,0 +1,56 @@ +# Question & Answer / Question / Answer + +Displays the question & answer/ question/ answer story element of the story + +## Usage + +#### Question & Answer/ Question/ Answer story element with default template + +```jsx +const opts = { type: "q-and-a / question / answer" }; + +; +``` + +#### Question & Answer/ Question/ Answer story element with author image + +```jsx +const opts = { type: "q-and-a / question / answer" }; + +; +``` + +#### Question & Answer/ Question/ Answer story element with external links opening in new tab + +```jsx +const opts = { isExternalLink: true, type: "q-and-a / question / answer" }; + +; +``` + +#### Question & Answer/ Question/ Answer story element with icon type for default template + +```jsx +const opts = { defaultIconType: "curve", type: "q-and-a / question / answer" }; + +; +``` + +#### Question & Answer/ Question/ Answer story element with icon color + +```jsx +const opts = { type: "q-and-a / question / answer" }; +const css = {iconColor: "#ffff"} + +; +``` + +#### Replace default question & answer/ question/ answer element template with custom template passed using render + +```jsx +const customTemplate = ({ element }) =>

    ; + +; +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/index.js new file mode 100644 index 000000000..45623a506 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/index.js @@ -0,0 +1,136 @@ +/* eslint-disable no-case-declarations */ +import React from "react"; +import PropTypes from "prop-types"; +import get from "lodash/get"; +import { shapeConfig, shapeStory, updateContentLinks, getTextColor } from "../../../../utils/utils"; +import { withElementWrapper } from "../withElementWrapper"; +import "./question-answer.m.css"; +import { useStateValue } from "../../../SharedContext"; + +const supportedTemplates = (type, element) => { + switch (type) { + case "q-and-a": + const { question: qaQuestion, answer: qaAnswer } = get(element, ["metadata"]); + const qaQuestionAttribution = get(element, ["metadata", "interviewer", "avatar-url"]); + const qaAnswerAttribution = get(element, ["metadata", "interviewee", "avatar-url"]); + return { + question: qaQuestion, + answer: qaAnswer, + questionAttribution: qaQuestionAttribution, + answerAttribution: qaAnswerAttribution, + }; + case "question": + const qQuestion = get(element, ["text"]); + const qAttribution = get(element, ["metadata", "interviewer", "avatar-url"]); + return { + question: qQuestion, + questionAttribution: qAttribution, + }; + case "answer": + const aAnswer = get(element, ["text"]); + const aAttribution = get(element, ["metadata", "interviewee", "avatar-url"]); + return { + answer: aAnswer, + answerAttribution: aAttribution, + }; + } +}; + +const QuestionAnswerBase = ({ + element, + template, + css = {}, + story = {}, + config = {}, + opts = {}, + render, + ...restProps +}) => { + const configData = useStateValue() || {}; + const textInvertColor = getTextColor(configData.theme); + if (!element) return null; + const { type, defaultIconType, isExternalLink = true } = opts; + const { iconColor } = css; + const { question, answer, questionAttribution, answerAttribution } = supportedTemplates(type, element); + + const theme = getTextColor(iconColor); + const questionData = question && ((isExternalLink && updateContentLinks(question)) || question); + const answerData = answer && ((isExternalLink && updateContentLinks(answer)) || answer); + const isAuthorImageTemplate = template === "withAuthorImage"; + const templateStyle = isAuthorImageTemplate ? "qa-withAuthorImage" : "qa-default"; + const iconType = defaultIconType === "curve" ? "curveIcon" : "edgeIcon"; + + const supportQuestionElement = isAuthorImageTemplate ? ( + questionAttribution && ( + <> +
    + +
    + + + ) + ) : ( +
    + Q +
    + ); + + // EMPTY SPAN FOR EXTRA SPACE + + const supportAnswerElement = isAuthorImageTemplate ? ( + answerAttribution && ( + <> +
    + +
    + + + ) + ) : ( +
    + A +
    + ); + + return ( +
    + {type !== "answer" && ( +
    + {supportQuestionElement} +
    +
    + )} + {type !== "question" && ( +
    + {supportAnswerElement} +
    +
    + )} +
    + ); +}; + +QuestionAnswerBase.propTypes = { + element: PropTypes.shape({ + metadata: PropTypes.shape({ + interviewer: PropTypes.shape({ "avatar-url": PropTypes.string }), + interviewee: PropTypes.shape({ "avatar-url": PropTypes.string }), + question: PropTypes.string, + answer: PropTypes.string, + }), + text: PropTypes.string, + }), + opts: PropTypes.shape({ type: PropTypes.string, defaultIconType: PropTypes.string, isExternalLink: PropTypes.bool }), + template: PropTypes.string, + css: PropTypes.object, + config: shapeConfig, + story: shapeStory, + render: PropTypes.func, +}; + +export const QuestionAnswer = withElementWrapper(QuestionAnswerBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/question-answer.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/question-answer.m.css new file mode 100644 index 000000000..a2d970eb2 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/question-answer.m.css @@ -0,0 +1,107 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; + +.qa-withAuthorImage:global(.arrow-component) .a-element p, +.qa-default:global(.arrow-component) .a-element p, +.qa-withAuthorImage:global(.arrow-component) .q-element p, +.qa-default:global(.arrow-component) .q-element p, +.dark { + font-size: var(--arrow-fs-s); + font-weight: var(--arrow-fw-bold); + line-height: var(--arrow-lh-5); + display: inline; + color: var(--arrow-c-mono2); + @media (min-width: mobile) { + font-size: var(--arrow-fs-m); + } +} + +.q-element { + margin-bottom: var(--arrow-spacing-m); +} + +.qa-withAuthorImage:global(.arrow-component) .a-element p, +.qa-default:global(.arrow-component) .a-element p { + font-weight: var(--arrow-fw-normal); +} + +.qa-withAuthorImage .hidden { + width: 35px; + display: inline-block; + @media (min-width: mobile) { + width: 38px; + } +} + +.qa-default .curveIcon, +.qa-default .edgeIcon { + width: 27px; + height: 27px; + color: var(--arrow-c-light); + background-color: var(--arrow-c-brand1); + font-size: var(--arrow-fs-s); + font-weight: var(--arrow-fw-bold); + display: inline-block; + line-height: var(--arrow-lh-5); + text-align: center; + margin-right: var(--arrow-spacing-xs); + @media (min-width: mobile) { + width: 30px; + height: 30px; + font-size: var(--arrow-fs-m); + } +} + +.qa-default .edgeIcon { + border-radius: 2.8px; +} + +.qa-default .curveIcon { + border-radius: 13.5px; + @media (min-width: mobile) { + border-radius: 15px; + } +} + +.qa-withAuthorImage .q-element, +.qa-withAuthorImage .a-element { + position: relative; +} + +.qa-withAuthorImage .labelAttribution img { + width: 27px; + height: 27px; + border-radius: 16px; + position: absolute; + @media (min-width: mobile) { + width: 30px; + height: 30px; + } +} + +.qa-withAuthorImage:global(.arrow-component) .a-element .light p, +.qa-default:global(.arrow-component) .a-element .light p, +.qa-withAuthorImage:global(.arrow-component) .q-element .light p, +.qa-default:global(.arrow-component) .q-element .light p { + color: var(--arrow-c-invert-mono1); + a { + color: var(--arrow-c-invert-mono2); + border-bottom: 1px solid var(--arrow-c-invert-mono2); + } +} + +.content { + display: inline; +} + +html[dir="rtl"] { + .content { + margin-right: var(--arrow-spacing-s); + } + .qa-default .curveIcon, + .qa-default .edgeIcon { + margin-right: 0; + margin-left: var(--arrow-spacing-xs); + } +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/question-answer.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/question-answer.test.js new file mode 100644 index 000000000..cf5dcb802 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/question-answer.test.js @@ -0,0 +1,74 @@ +import * as React from "react"; +import { shallow, mount } from "enzyme"; +import { generateStoryElementData } from "../../../Fixture"; +import { QuestionAnswer } from "./index"; + +const qaElement = generateStoryElementData("q-and-a"); +const qElement = generateStoryElementData("question"); +const aElement = generateStoryElementData("answer"); + +describe("Question & Answer Story Element", () => { + it("Should render default template", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "question-answer" }).prop("className")).toMatch(/qa-default/); + }); + + it("Should render template with author image", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "question-answer" }).prop("className")).toMatch(/qa-withAuthorImage/); + }); + + it("Should render default template with external link", () => { + const wrapper = shallow( + + ); + expect(wrapper.html()).toBe( + '
    Q

    Is Ready Player One book better than the movie?

    A

    Ready Player One is the latest example. I read Earnest Cline\'s book a couple months before watching Steven Spielberg\'s movie. ... The book generally fares better with reviewers, averaging 4.6 out of 5 stars on Amazon, while the movie scores 73% on Rotten Tomatoes.

    ' + ); + }); + + it("Should render default template without external link", () => { + const wrapper = shallow( + + ); + expect(wrapper.html()).toBe( + '
    Q

    Is Ready Player One book better than the movie?

    A

    Ready Player One is the latest example. I read Earnest Cline\'s book a couple months before watching Steven Spielberg\'s movie. ... The book generally fares better with reviewers, averaging 4.6 out of 5 stars on Amazon, while the movie scores 73% on Rotten Tomatoes.

    ' + ); + }); + + it("Should render the default template with icon type", () => { + const wrapper1 = mount( + + ); + expect(wrapper1.find({ "data-test-id": "curveIcon" }).length).toEqual(2); + const wrapper2 = mount( + + ); + expect(wrapper2.find({ "data-test-id": "edgeIcon" }).length).toEqual(2); + }); + + it("Should render custom template", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + const wrapper = shallow(); + expect(wrapper.html()).toBe( + '

    Is Ready Player One book better than the movie?

    Ready Player One is the latest example. I read Earnest Cline\'s book a couple months before watching Steven Spielberg\'s movie. ... The book generally fares better with reviewers, averaging 4.6 out of 5 stars on Amazon, while the movie scores 73% on Rotten Tomatoes.

    ' + ); + }); +}); + +describe("Question Story Element", () => { + it("Should render question", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "answer" }).length).toEqual(0); + expect(wrapper.find({ "data-test-id": "question" }).length).toEqual(1); + }); +}); + +describe("Answer Story Element", () => { + it("Should render answer", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "question" }).length).toEqual(0); + expect(wrapper.find({ "data-test-id": "answer" }).length).toEqual(1); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/stories.js new file mode 100644 index 000000000..27a89e288 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/QuestionAnswer/stories.js @@ -0,0 +1,79 @@ +/* eslint-disable react/prop-types */ +import React from "react"; +import { generateStoryElementData } from "../../../Fixture"; +import { QuestionAnswer } from "./index"; +import { withStore, optionalSelect } from "../../../../../storybook"; +import Readme from "./README.md"; +import { color, boolean } from "@storybook/addon-knobs"; + +const templateStyle = { + Default: "default", + "With Author Image": "withAuthorImage", +}; +const qaElement = generateStoryElementData("q-and-a"); +const qElement = generateStoryElementData("question"); +const aElement = generateStoryElementData("answer"); +const iconType = { + edge: "edge", + curve: "curve", +}; + +withStore("Atoms/Story Elements/Question Answer", {}, Readme) + .add("Default", () => ( + + )) + .add("Custom", () => { + const customTemplate = ({ element }) =>

    ; + return ; + }); + +withStore("Atoms/Story Elements/Question", {}, Readme) + .add("Default", () => ( + + )) + .add("Custom", () => { + const customTemplate = ({ element }) =>

    ; + return ; + }); + +withStore("Atoms/Story Elements/Answer", {}, Readme) + .add("Default", () => ( + + )) + .add("Custom", () => { + const customTemplate = ({ element }) =>

    ; + return ; + }); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/README.md new file mode 100644 index 000000000..f2ab34f4e --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/README.md @@ -0,0 +1,41 @@ +# Quote + +Displays the Quote story element of the story + +## Usage + +#### Quote story element with default template + +```jsx + +``` + +#### Quote story element with border left template + +```jsx + +``` + +#### Quote story element with border top template + +```jsx + +``` + +#### Quote story element with border color + +```jsx +const css = { borderColor: "#ff214b" }; + +; +``` + +#### Replace default quote element template with custom template passed using render + +```jsx +const customTemplate = ({ element }) =>

    ; + +; +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/index.js new file mode 100644 index 000000000..5f81ac627 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/index.js @@ -0,0 +1,44 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { withElementWrapper } from "../withElementWrapper"; +import "./quote.m.css"; +import { shapeStory, shapeConfig, getTextColor } from "../../../../utils/utils"; +import { useStateValue } from "../../../SharedContext"; + +const QuoteBase = ({ element, template = "", css = {}, story = {}, config = {}, render, ...restProps }) => { + const configData = useStateValue() || {}; + const textInvertColor = getTextColor(configData.theme); + const { content, attribution } = element.metadata; + if (!content) return null; + const { borderColor } = css; + const updateBorderColor = template === "borderLeft" && `4px solid ${borderColor}`; + const templateStyle = template ? `quote-${template}` : "quote"; + + return ( +
    + {template === "borderTopSmall" && ( +
    + )} +

    +

    + {attribution} +

    +
    + ); +}; + +QuoteBase.propTypes = { + element: PropTypes.shape({ metadata: PropTypes.shape({ content: PropTypes.string, attribution: PropTypes.string }) }), + /** template can be default, borderTopSmall and borderLeft */ + template: PropTypes.string, + story: shapeStory, + config: shapeConfig, + render: PropTypes.func, + css: PropTypes.object, +}; + +export const Quote = withElementWrapper(QuoteBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/quote.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/quote.m.css new file mode 100644 index 000000000..0bf41937a --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/quote.m.css @@ -0,0 +1,112 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; + +.quote .text, +.quote-borderNone .text, +.quote-borderTopSmall .text, +.quote-borderLeft .text, +.dark { + font-weight: var(--arrow-fw-semi-bold); + color: var(--arrow-c-mono2); + line-height: var(--arrow-lh-5); + font-size: var(--arrow-fs-m); + @media (min-width: mobile) { + font-size: var(--arrow-fs-l); + } +} + +.quote .text.light, +.quote-borderNone .text.light, +.quote-borderTopSmall .text.light, +.quote-borderLeft .text.light { + color: var(--arrow-c-invert-mono2); +} + +.quote .text, +.quote-borderNone .text { + font-style: italic; +} + +.quote-borderLeft .text { + border-left: 4px solid var(--arrow-c-brand1); + padding-left: var(--arrow-spacing-l); +} + +.quote .attribution, +.quote-borderNone .attribution, +.quote-borderLeft .attribution, +.quote-borderTopSmall .attribution, +.dark { + font-size: var(--arrow-fs-s); + font-weight: var(--arrow-fw-semi-bold); + font-family: var(--arrow-typeface-secondary); + color: var(--arrow-c-mono4); + font-style: italic; + display: flex; + margin-top: var(--arrow-spacing-s); + @media (min-width: mobile) { + font-size: var(--arrow-fs-m); + margin-top: var(--arrow-spacing-m); + } +} +.quote .attribution.light, +.quote-borderNone .attribution.light, +.quote-borderLeft .attribution.light, +.quote-borderTopSmall .attribution.light { + color: var(--arrow-c-invert-mono4); +} + +.quote-borderLeft .attribution { + margin-left: var(--arrow-spacing-l); +} + +.quote .attribution, +.quote-borderNone .attribution { + justify-content: flex-end; +} + +.quote-borderLeft .attribution, +.quote-borderTopSmall .attribution { + justify-content: flex-start; +} + +.quote .attribution:before, +.quote-borderNone .attribution:before { + content: " "; + width: 12px; + height: 2px; + background-color: var(--arrow-c-mono4); + margin: var(--arrow-spacing-s) var(--arrow-spacing-xs) 0 0; +} + +.quote .attribution.light:before, +.quote-borderNone .attribution.light:before { + background-color: var(--arrow-c-invert-mono4); +} + +html[dir="rtl"] { + .quote .attribution, + .quote-borderNone .attribution { + &::before { + content: none; + } + &::after { + content: " "; + width: 12px; + height: 2px; + background-color: var(--arrow-c-mono4); + margin: var(--arrow-spacing-s) var(--arrow-spacing-xs) 0 0; + } + } +} + +.quote-borderTopSmall .line { + width: 32px; + height: 4px; + background-color: var(--arrow-c-brand1); + margin-bottom: var(--arrow-spacing-xs); + @media (min-width: mobile) { + margin-bottom: var(--arrow-spacing-s); + } +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/quote.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/quote.test.js new file mode 100644 index 000000000..cde78f90c --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/quote.test.js @@ -0,0 +1,39 @@ +import * as React from "react"; +import { shallow, mount } from "enzyme"; +import { generateStoryElementData } from "../../../Fixture"; +import { Quote } from "./index"; + +const element = generateStoryElementData("quote"); + +describe("Quote Story Element", () => { + it("Should render default template", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "quote" }).prop("className")).toMatch(/quote-borderNone/); + }); + + it("Should render border left template", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "quote" }).prop("className")).toMatch(/quote-borderLeft/); + }); + + it("Should render border top template", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "quote" }).prop("className")).toMatch(/quote-borderTopSmall/); + }); + + it("Should render border left template with border color", () => { + const wrapper = shallow(); + expect(wrapper.html()).toBe( + '

    After the death of James Halliday, the creator of the virtual reality world, his pre-recorded message reveals the hidden fortune, which makes Wade Watts, a teenager, embark on a quest.

    Player

    ' + ); + }); + + it("Should render custom template", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + const wrapper = shallow(); + expect(wrapper.html()).toBe( + "

    After the death of James Halliday, the creator of the virtual reality world, his pre-recorded message reveals the hidden fortune, which makes Wade Watts, a teenager, embark on a quest.

    " + ); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/stories.js new file mode 100644 index 000000000..4807d7e05 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Quote/stories.js @@ -0,0 +1,28 @@ +import React from "react"; +import { Quote } from "./index"; +import { withStore, optionalSelect } from "../../../../../storybook"; +import Readme from "./README.md"; +import { generateStoryElementData } from "../../../Fixture"; +import { boolean, color } from "@storybook/addon-knobs"; + +const element = generateStoryElementData("quote"); +const collectionTemplates = { + default: "borderNone", + borderLeft: "borderLeft", + borderTopSml: "borderTopSmall", +}; + +withStore("Atoms/Story Elements/Quote", {}, Readme) + .add("Default", () => ( + + )) + .add("Custom", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + return ; + }); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/README.md new file mode 100644 index 000000000..690f4dfb3 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/README.md @@ -0,0 +1,9 @@ +## Reference + +Reference element displays the referenced content to the story. + +# Usage + +```jsx + +``` diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/index.js new file mode 100644 index 000000000..a7f8ab037 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/index.js @@ -0,0 +1,52 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { withElementWrapper } from "../withElementWrapper"; +import { useStateValue } from "../../../SharedContext"; +import { getTextColor, shapeConfig } from "../../../../utils/utils"; +import { Link } from "@quintype/components"; +import "./reference.m.css"; + +const ReferenceBase = ({ element, config = {}, opts = {}, ...restProps }) => { + if (!element) return null; + + const configData = useStateValue() || {}; + const textInvertColor = getTextColor(configData.theme); + + const eleArr = element["story-elements"] && element["story-elements"].map((ref) => ref.metadata); + const { showHeadline = true, headlineText } = opts; + + return ( +
    + {eleArr && + eleArr.map((metadata, index) => { + const { name = "", description = "", url } = metadata; + return ( +
    + + {showHeadline && ( +
    + {headlineText || name} +
    + )} +
    {description}
    + +
    + ); + })} +
    + ); +}; + +ReferenceBase.propTypes = { + element: PropTypes.shape({ + "story-elements": PropTypes.arrayOf( + PropTypes.shape({ + metadata: PropTypes.shape({ name: PropTypes.string, description: PropTypes.string, url: PropTypes.string }), + }) + ), + }), + opts: PropTypes.shape({ hideHeadline: PropTypes.bool }), + config: shapeConfig, +}; + +export const Reference = withElementWrapper(ReferenceBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/reference.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/reference.m.css new file mode 100644 index 000000000..57c1f1670 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/reference.m.css @@ -0,0 +1,43 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; +@value tablet from viewports; + +.wrapper { + border-radius: var(--arrow-spacing-xxs); + padding: var(--arrow-spacing-m); + margin-bottom: var(--arrow-spacing-l); + @media (min-width: tablet) { + padding: var(--arrow-spacing-l); + } +} + +.name { + font-size: var(--arrow-fs-m); + font-weight: var(--arrow-fw-bold); + line-height: var(--arrow-lh-3); + margin-bottom: var(--arrow-spacing-m); + @media (min-width: mobile) { + font-size: var(--arrow-fs-l); + } +} + +.description { + font-size: var(--arrow-fs-s); + line-height: var(--arrow-lh-5); + @media (min-width: mobile) { + font-size: var(--arrow-fs-m); + } +} + +.dark { + background-color: var(--arrow-c-mono7); + border: solid 1px var(--arrow-c-mono7); + color: var(--arrow-c-mono2); +} + +.light { + background-color: var(--arrow-c-invert-mono7); + border: solid 1px var(--arrow-c-invert-mono7); + color: var(--arrow-c-invert-mono2); +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/reference.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/reference.test.js new file mode 100644 index 000000000..ac1c72e7b --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/reference.test.js @@ -0,0 +1,28 @@ +import React from "react"; +import { mount, shallow } from "enzyme"; +import { Provider } from "react-redux"; +import { Reference } from "."; +import { generateStoryElementData, generateStore } from "../../../Fixture"; + +const element = generateStoryElementData("references"); + +describe("Reference element", () => { + it("Should render default template of the reference element", () => { + const wrapper = mount( + + ; + + ); + expect(wrapper.find(Reference)).toHaveLength(1); + }); + + it("Should render headline if it is enabled", () => { + const wrapper = shallow(); + expect(wrapper.find({ "data-test-id": "ref-headline" }).length).toEqual(0); + }); + + it("Should not render headline if it is disabled", () => { + const wrapper = shallow(); + expect(wrapper.find({ "data-test-id": "ref-headline" }).length).toEqual(0); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/stories.js new file mode 100644 index 000000000..ae244da94 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Reference/stories.js @@ -0,0 +1,18 @@ +import React from "react"; +import { Reference } from "./index"; +import { withStore } from "../../../../../storybook"; +import Readme from "./README.md"; +import { generateStoryElementData } from "../../../Fixture"; +import { boolean, text } from "@storybook/addon-knobs"; + +const element = generateStoryElementData("references"); + +withStore("Atoms/Story Elements/Reference", {}, Readme).add("Default", () => ( + +)); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/README.md new file mode 100644 index 000000000..13ed9be09 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/README.md @@ -0,0 +1,10 @@ +# Wrapper Story Element + +Story element is a a wrapper around `qt/components/StoryElement` which can be used as the fallback element handler component. +For example, we can use `qt/arrow/StoryElement` for elements like, Table, File, JSEmbed, Polltype, Soundcloud etc + +## Usage + +```jsx + +``` diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/index.js new file mode 100644 index 000000000..8c2fa8272 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/index.js @@ -0,0 +1,21 @@ +import React from "react"; +import { StoryElement as QTCStoryElement } from "@quintype/components"; +import PropTypes from "prop-types"; +import { withElementWrapper } from "../withElementWrapper"; +import { shapeStory, shapeConfig } from "../../../../utils/utils"; + +const StoryElementBase = ({ element, story = {}, config = {}, ...restProps }) => { + return ( +
    + +
    + ); +}; + +export const StoryElement = withElementWrapper(StoryElementBase); + +StoryElementBase.propTypes = { + element: PropTypes.object, + story: shapeStory, + config: shapeConfig, +}; diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/stories.js new file mode 100644 index 000000000..a4d378f70 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/stories.js @@ -0,0 +1,20 @@ +import React from "react"; +import { withStore } from "../../../../../storybook"; +import { StoryElement } from "./index"; +import Readme from "./README.md"; +import { generateStoryElementData } from "../../../Fixture"; + +const elementJsembed = generateStoryElementData("jsembed"); + +withStore( + "Atoms/Story Elements/StoryElement", + + { + qt: { + config: { + "cdn-image": "thumbor-stg.assettype.com", + }, + }, + }, + Readme +).add("should work with any jsembed ", () => ); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/storyelement.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/storyelement.test.js new file mode 100644 index 000000000..89a21bd9d --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/StoryElement/storyelement.test.js @@ -0,0 +1,13 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { StoryElement } from "."; +import { generateStoryElementData } from "../../../Fixture"; +import { StoryElement as QTCStoryElement } from "@quintype/components"; +const element = generateStoryElementData("jsembed"); + +describe("Story Element", () => { + it("should render with correct elements", () => { + const wrapper = shallow().dive(); + expect(wrapper.find(QTCStoryElement).length).toEqual(1); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/README.md new file mode 100644 index 000000000..c55e4d712 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/README.md @@ -0,0 +1,56 @@ +# Summary + +Displays the Summary element of the story + +## Usage + +#### Summary story element with default template + +```jsx + +``` + +#### Summary story element with header template + +```jsx + +``` + +#### Summary story element with border template + +```jsx + +``` + +#### Summary story element with header background color + +```jsx +const css = { headerBgColor: "#ff214b" }; + +; +``` + +#### Summary story element without headline. + +```jsx +const opts = { hideHeadline: true }; + +; +``` + +#### Summary story element with custom headline. + +```jsx +const opts = { headline: true }; +; +``` + +#### Replace default Summary element template with custom template passed using render + +```jsx +const customTemplate = ({ element }) =>

    ; + +; +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/index.js new file mode 100644 index 000000000..10d629138 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/index.js @@ -0,0 +1,69 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { withElementWrapper } from "../withElementWrapper"; +import { getTextColor, updateContentLinks, shapeStory, shapeConfig } from "../../../../utils/utils"; + +import "./summary.m.css"; +import { useStateValue } from "../../../SharedContext"; + +const SummaryBase = ({ + element, + template = "", + opts = {}, + css = {}, + story = {}, + config = {}, + render, + ...restProps +}) => { + const configData = useStateValue() || {}; + const textInvertColor = getTextColor(configData.theme); + const content = element.text; + if (!content) return null; + + const { headerBgColor } = css; + const { isExternalLink = true, headline = "Summary", hideHeadline = false } = opts; + const text = (isExternalLink && updateContentLinks(content)) || content; + + const supportedTemplates = ["header", "border"]; + const templateStyle = supportedTemplates.includes(template) ? `summary-${template}` : "summary"; + + const contentBorder = hideHeadline ? `content-border` : ""; + const renderTemplate = template === "header" || template === "border"; + const updateHeaderColor = renderTemplate ? `${headerBgColor}` : ""; + const textColor = renderTemplate ? getTextColor(headerBgColor) : ""; + const updateContentColor = template === "header" ? "" : textInvertColor; + + return ( +
    + {!hideHeadline && ( +
    +
    + {headline} +
    +
    + )} +
    +
    + ); +}; + +SummaryBase.propTypes = { + element: PropTypes.shape({ text: PropTypes.string }), + template: PropTypes.string, + opts: PropTypes.shape({ isExternalLink: PropTypes.bool, headline: PropTypes.string, hideHeadline: PropTypes.bool }), + story: shapeStory, + config: shapeConfig, + render: PropTypes.func, + css: PropTypes.shape({ headerBgColor: PropTypes.string }), +}; + +export const Summary = withElementWrapper(SummaryBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/stories.js new file mode 100644 index 000000000..da1d79319 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/stories.js @@ -0,0 +1,33 @@ +import React from "react"; +import { withStore, optionalSelect } from "../../../../../storybook"; +import { generateStoryElementData } from "../../../Fixture"; +import Readme from "./README.md"; +import { Summary } from "./index"; +import { color, boolean, text } from "@storybook/addon-knobs"; + +const element = generateStoryElementData("summary"); + +const summaryTemplate = { + default: "", + header: "header", + border: "border", +}; + +withStore("Atoms/Story Elements/Summary", {}, Readme) + .add("Default", () => ( + + )) + .add("Custom", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + return ; + }); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/summary.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/summary.m.css new file mode 100644 index 000000000..e1cfee7a2 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/summary.m.css @@ -0,0 +1,122 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; + +.headline { + font-size: var(--arrow-fs-s); + font-weight: var(--arrow-fw-bold); + @media (min-width: mobile) { + font-size: var(--arrow-fs-m); + } +} + +.summary:global(.arrow-component) .content p { + font-style: italic; +} + +.summary:global(.arrow-component) .content > p, +.summary-header:global(.arrow-component) .content > p, +.summary-border:global(.arrow-component) .content > p, +.dark { + font-size: var(--arrow-fs-s); + color: var(--arrow-c-dark); + @media (min-width: mobile) { + font-size: var(--arrow-fs-m); + } +} + +.summary:global(.arrow-component) .content.light > p, +.summary-header:global(.arrow-component) .content.light > p, +.summary-border:global(.arrow-component) .content.light > p { + color: var(--arrow-c-light); +} + +.dark { + color: var(--arrow-c-mono1); +} +.light { + color: var(--arrow-c-invert-mono1); +} + +/* default template */ +.summary .headline { + margin-bottom: var(--arrow-spacing-xs); + font-size: var(--arrow-fs-s); + @media (min-width: mobile) { + font-size: var(--arrow-fs-l); + } +} + +/* headline template */ +.summary-header .headline { + padding: var(--arrow-spacing-xs) var(--arrow-spacing-s); + font-size: var(--arrow-fs-s); + background-color: var(--arrow-c-brand1); + @media (min-width: mobile) { + font-size: var(--arrow-fs-m); + padding: var(--arrow-spacing-xs) var(--arrow-spacing-l); + } +} +.summary-header .content { + background-color: var(--arrow-c-invert-mono7); + padding: var(--arrow-spacing-s); + @media (min-width: mobile) { + padding: var(--arrow-spacing-l); + } +} + +/* border template */ +.summary-border .heading-wrapper { + display: flex; + margin-bottom: -22px; +} + +.summary-border .heading-wrapper::before { + content: " "; + position: relative; + width: var(--arrow-spacing-m); + border-top: 1px solid var(--arrow-c-dark); + align-self: center; +} + +.summary-border .heading-wrapper.light::before { + border-top: 1px solid var(--arrow-c-light); +} + +.summary-border .heading-wrapper::after { + content: " "; + position: relative; + flex: 1 1 auto; + border-top: 1px solid var(--arrow-c-dark); + align-self: center; + margin-right: 0; +} + +.summary-border .heading-wrapper.light::after { + border-top: 1px solid var(--arrow-c-light); +} + +.summary-border .content { + border: 1px solid var(--arrow-c-dark); + border-top: none; + padding: var(--arrow-spacing-l); +} + +.summary-border .content.light { + border: 1px solid var(--arrow-c-light); +} +.summary-border .headline { + background-color: var(--arrow-c-brand1); + padding: var(--arrow-spacing-xs) var(--arrow-spacing-l); + line-height: var(--arrow-lh-4); + border-radius: 2px; + margin: 0 var(--arrow-spacing-xs); +} + +.summary-border .content-border.dark { + border: 1px solid var(--arrow-c-mono5); +} + +.summary-border .content-border.light { + border: 1px solid var(--arrow-c-invert-mono5); +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/summary.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/summary.test.js new file mode 100644 index 000000000..0dfc24498 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Summary/summary.test.js @@ -0,0 +1,50 @@ +import * as React from "react"; +import { shallow, mount } from "enzyme"; +import expect from "expect"; +import { generateStoryElementData } from "../../../Fixture"; +import { Summary } from "./index"; + +const element = generateStoryElementData("summary"); + +describe("Summary Story Element", () => { + it("Should render default template", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "summary" }).prop("className")).toMatch(/summary/); + }); + + it("Should render border left template", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "summary" }).prop("className")).toMatch(/summary-header/); + }); + + it("Should render border top template", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "summary" }).prop("className")).toMatch(/summary-border/); + }); + + it("Should render template without headline", () => { + const wrapper = shallow(); + expect(wrapper.find({ "data-test-id": "summary-headline" }).length).toEqual(0); + }); + + it("Should render template with custom headline", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "summary-headline" }).text()).toEqual("in short"); + }); + + it("Should render template with header background color", () => { + const wrapper = mount(); + expect(wrapper.html()).toBe( + `
    Summary

    Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

    ` + ); + }); + + it("Should render custom template", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + const wrapper = shallow(); + expect(wrapper.html()).toBe( + "

    Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

    " + ); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Text/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/Text/README.md new file mode 100644 index 000000000..eed9bfefd --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Text/README.md @@ -0,0 +1,32 @@ +# Text Story Element + +Displays the text story element of the story + +## Usage + +#### Default text story element +```jsx + +``` + +#### Text story element with external links opening in new tab +```jsx +const opts = { isExternalLink: true }; + + +``` + +#### Text story element as promotional message +```jsx +const promotionalMessage = { ...element, metadata: { "promotional-message": true } }; + +``` + +#### Replace default text story element template with custom template passed using render +```jsx +const customTemplate = ({element}) =>

    ; + + +``` + + diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Text/index.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Text/index.js new file mode 100644 index 000000000..ee17bcb09 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Text/index.js @@ -0,0 +1,41 @@ +import React from "react"; +import PropTypes from "prop-types"; +import get from "lodash/get"; +import { withElementWrapper } from "../withElementWrapper"; +import { updateContentLinks, shapeStory, shapeConfig, getTextColor } from "../../../../utils/utils"; +import "./text.m.css"; +import { useStateValue } from "../../../SharedContext"; + +const TextBase = ({ element = {}, opts = {}, css = {}, story = {}, config = {}, ...restProps }) => { + const content = element.text; + if (!content) return null; + const configData = useStateValue() || {}; + const textInvertColor = getTextColor(configData.theme); + const { textColor } = css; + const { isExternalLink = true } = opts; + const text = (isExternalLink && updateContentLinks(content)) || content; + const isPromotionalMessage = get(element, ["metadata", "promotional-message"], false); + const updatedStyle = isPromotionalMessage ? "promotionalMessage" : "textElement"; + + return ( +
    + ); +}; + +TextBase.propTypes = { + element: PropTypes.shape({ text: PropTypes.string }), + opts: PropTypes.shape({ isExternalLink: PropTypes.bool }), + css: PropTypes.shape({ hyperlinkColor: PropTypes.string, textColor: PropTypes.string }), + story: shapeStory, + config: shapeConfig, +}; + +export const Text = withElementWrapper(TextBase); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Text/stories.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Text/stories.js new file mode 100644 index 000000000..a90e83907 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Text/stories.js @@ -0,0 +1,32 @@ +import React from "react"; +import { withStore } from "../../../../../storybook"; +import { Text } from "./index"; +import Readme from "./README.md"; +import { generateStoryElementData } from "../../../Fixture"; +import { boolean, color } from "@storybook/addon-knobs"; + +const element = generateStoryElementData("text"); + +withStore("Atoms/Story Elements/Text", {}, Readme) + .add("Default", () => ( + + )) + .add("Promotional Message", () => { + const promotionalMessage = { ...element, metadata: { "promotional-message": true } }; + return ( + + ); + }) + .add("Custom", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + return ; + }); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Text/text.m.css b/app/isomorphic/arrow/components/Atoms/StoryElements/Text/text.m.css new file mode 100644 index 000000000..1dfcd4761 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Text/text.m.css @@ -0,0 +1,79 @@ +/* eslint-disable scss/at-rule-no-unknown */ +@value viewports: "../../../../../../assets/arrow/stylesheets/viewports.m.css"; +@value mobile from viewports; + +.promotionalMessage p { + font-size: var(--arrow-fs-tiny); + padding-bottom: var(--arrow-spacing-xxs); + @media (min-width: mobile) { + font-size: var(--arrow-fs-xs); + padding-bottom: var(--arrow-spacing-xs); + } +} + +.textElement p, +.dark { + line-height: var(--arrow-lh-5); + font-size: var(--arrow-fs-s); + @media (min-width: mobile) { + font-size: var(--arrow-fs-m); + } +} + +.promotionalMessage a, +.textElement a { + color: var(--textElementHyperlinkColor); +} + +.textElement a:hover { + border-bottom: 1px solid var(--textElementHyperlinkColor); + transition: border 0.3s ease; +} + +.textElement.light p { + color: var(--arrow-c-invert-mono2); +} + +.promotionalMessage a:hover { + color: var(--arrow-c-brand1); + border-bottom: 1px solid; + padding-bottom: 3px; + transition: all 0.3s ease 0s; +} + +.promotionalMessage.dark p { + color: var(--arrow-c-mono4); +} +.promotionalMessage.light p { + color: var(--arrow-c-invert-mono4); +} + +.textElement ul, +.textElement ol { + list-style: initial; + padding: var(--arrow-spacing-m) 0 0 var(--arrow-spacing-l); + @media (min-width: mobile) { + padding: var(--arrow-spacing-m) 0 0 var(--arrow-spacing-xl); + } +} + +.textElement ol { + list-style: auto; +} + +.textElement :global .cta-anchor { + background-color: var(--arrow-c-brand1); + color: var(--arrow-c-light); + border-radius: var(--arrow-spacing-xxs); + box-shadow: 0 var(--arrow-spacing-xxs) var(-arrow-spacing-xs) 0 var(--arrow-c-mono6); + font-size: var(--arrow-fs-s); + padding: var(--arrow-spacing-s) var(--arrow-spacing-m); + display: block; + text-align: center; + + @media (min-width: mobile) { + display: inline-block; + font-size: var(--arrow-fs-m); + padding: var(--arrow-spacing-m) var(--arrow-spacing-l); + } +} diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Text/text.test.js b/app/isomorphic/arrow/components/Atoms/StoryElements/Text/text.test.js new file mode 100644 index 000000000..86b8a259d --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Text/text.test.js @@ -0,0 +1,42 @@ +import * as React from "react"; +import { shallow, mount } from "enzyme"; +import { Text } from "./index"; +import { generateStoryElementData } from "../../../Fixture"; + +const element = generateStoryElementData("text"); + +describe("Text Story Element", () => { + it("should render default template", () => { + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "text" }).prop("className")).toMatch(/text-element/); + }); + + it("should render promotional message when it is enabled", () => { + const promotionalMessage = { ...element, metadata: { "promotional-message": true } }; + const wrapper = mount(); + expect(wrapper.find({ "data-test-id": "promotional-message" }).prop("className")).toMatch(/promotionalMessage/); + }); + + it("should render default template with external link", () => { + const wrapper = shallow(); + expect(wrapper.html()).toBe( + "

    Virtual reality is the air guitar solo of modern cinema: a frenetic imagined activity in a made-up world that exists one level below the already made-up world of the story. Steven Spielberg 2019s Ready Player One.

    " + ); + }); + + it("should render default template without external link", () => { + const wrapper = shallow(); + expect(wrapper.html()).toBe( + '

    Virtual reality is the air guitar solo of modern cinema: a frenetic imagined activity in a made-up world that exists one level below the already made-up world of the story. Steven Spielberg 2019s Ready Player One.

    ' + ); + }); + + it("should render custom template", () => { + // eslint-disable-next-line react/prop-types + const customTemplate = ({ element }) =>

    ; + const wrapper = shallow(); + expect(wrapper.html()).toBe( + '

    Virtual reality is the air guitar solo of modern cinema: a frenetic imagined activity in a made-up world that exists one level below the already made-up world of the story. Steven Spielberg 2019s Ready Player One.

    ' + ); + }); +}); diff --git a/app/isomorphic/arrow/components/Atoms/StoryElements/Video/README.md b/app/isomorphic/arrow/components/Atoms/StoryElements/Video/README.md new file mode 100644 index 000000000..eb3a81883 --- /dev/null +++ b/app/isomorphic/arrow/components/Atoms/StoryElements/Video/README.md @@ -0,0 +1,9 @@ +# Video Story Element + +Video story element is a simple component used to display a video story. + +## Usage + +```jsx +