diff --git a/package.json b/package.json index de9097bb8..1d072f51f 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@sentry/browser": "5.17.0", "@sentry/node": "5.17.0", "@sentry/webpack-plugin": "1.11.1", + "@soywod/pin-field": "^0.1.9", "@tippyjs/react": "4.2.0", "@zeit/next-source-maps": "0.0.4-canary.1", "accepts": "^1.3.4", @@ -112,6 +113,7 @@ "react-lazy-load-image-component": "1.5.0", "react-masonry-component": "^6.0.1", "react-masonry-css": "^1.0.14", + "react-pin-field": "1.0.6", "react-resize-detector": "4.2.3", "react-select": "^3.1.0", "react-sortable-hoc": "1.7.1", diff --git a/public/icons/static/shape/handshake.svg b/public/icons/static/shape/handshake.svg new file mode 100644 index 000000000..367f28dd0 --- /dev/null +++ b/public/icons/static/shape/handshake.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/containers/content/MembershipContent/Illustrations/AirBalloon.js b/src/containers/content/MembershipContent/Illustrations/AirBalloon.tsx similarity index 51% rename from src/containers/content/MembershipContent/Illustrations/AirBalloon.js rename to src/containers/content/MembershipContent/Illustrations/AirBalloon.tsx index 5d2868228..cccfaca80 100644 --- a/src/containers/content/MembershipContent/Illustrations/AirBalloon.js +++ b/src/containers/content/MembershipContent/Illustrations/AirBalloon.tsx @@ -1,10 +1,14 @@ -import React from 'react' +import React, { FC } from 'react' import { Wrapper, Balloon, Basket } from '../styles/illustrations/air_balloon' -const AirBalloon = () => { +type TProps = { + testid?: string +} + +const AirBalloon: FC = ({ testid = 'membership-airballoon' }) => { return ( - + diff --git a/src/containers/content/MembershipContent/Illustrations/Rocket.js b/src/containers/content/MembershipContent/Illustrations/Rocket.tsx similarity index 76% rename from src/containers/content/MembershipContent/Illustrations/Rocket.js rename to src/containers/content/MembershipContent/Illustrations/Rocket.tsx index 82916cd09..07234b885 100644 --- a/src/containers/content/MembershipContent/Illustrations/Rocket.js +++ b/src/containers/content/MembershipContent/Illustrations/Rocket.tsx @@ -1,8 +1,10 @@ -import React from 'react' +import React, { FC } from 'react' import { ICON } from '@/config' +import type { TPackage } from '../spec' import { PACKAGE } from '../constant' + import { Wrapper, Star1, @@ -17,9 +19,15 @@ import { GirlIcon, } from '../styles/illustrations/rocket' -const Rocket = ({ type, active }) => { +type TProps = { + testid?: string + type?: TPackage + active: boolean +} + +const Rocket: FC = ({ testid = 'membership-rocket', type, active }) => { return ( - + diff --git a/src/containers/content/MembershipContent/Illustrations/UFO.js b/src/containers/content/MembershipContent/Illustrations/UFO.tsx similarity index 75% rename from src/containers/content/MembershipContent/Illustrations/UFO.js rename to src/containers/content/MembershipContent/Illustrations/UFO.tsx index ec1834542..6cfd3fe33 100644 --- a/src/containers/content/MembershipContent/Illustrations/UFO.js +++ b/src/containers/content/MembershipContent/Illustrations/UFO.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { FC } from 'react' import { ICON } from '@/config' @@ -15,9 +15,14 @@ import { Beam, } from '../styles/illustrations/ufo' -const UFO = ({ active }) => { +type TProps = { + testid?: string + active: boolean +} + +const UFO: FC = ({ testid = 'membership-ufo', active }) => { return ( - + diff --git a/src/containers/content/MembershipContent/Illustrations/index.js b/src/containers/content/MembershipContent/Illustrations/index.tsx similarity index 66% rename from src/containers/content/MembershipContent/Illustrations/index.js rename to src/containers/content/MembershipContent/Illustrations/index.tsx index 680b7471c..c0b009d6a 100644 --- a/src/containers/content/MembershipContent/Illustrations/index.js +++ b/src/containers/content/MembershipContent/Illustrations/index.tsx @@ -1,5 +1,6 @@ -import React from 'react' +import React, { FC, ReactNode } from 'react' +import type { TPackage } from '../spec' import Rocket from './Rocket' import UFO from './UFO' import AirBalloon from './AirBalloon' @@ -7,10 +8,10 @@ import AirBalloon from './AirBalloon' import { PACKAGE } from '../constant' import { Wrapper } from '../styles/illustrations' -const renderIllustration = (type, active) => { +const renderIllustration = (type: TPackage, active: boolean): ReactNode => { switch (type) { case PACKAGE.FREE: { - return + return } case PACKAGE.GIRL: { @@ -27,7 +28,11 @@ const renderIllustration = (type, active) => { } } -const Illustrations = ({ type, active }) => { +type TProps = { + type: TPackage + active: boolean +} +const Illustrations: FC = ({ type, active }) => { return {renderIllustration(type, active)} } diff --git a/src/containers/content/MembershipContent/InviteBox/QA.tsx b/src/containers/content/MembershipContent/InviteBox/QA.tsx new file mode 100644 index 000000000..2aea527fc --- /dev/null +++ b/src/containers/content/MembershipContent/InviteBox/QA.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react' + +import { Br } from '@/components/Common' +import { Wrapper, Title, Desc } from '../styles/invite_box/qa' + +type TProps = { + testid?: string +} + +const QA: FC = ({ testid = 'membership-qa' }) => { + return ( + + 说明: + + 内侧阶段,所有新注册用户都会收到一个额外的朋友码,欢迎将它分享给身边的朋友。 + +
+ 验证通过后将自动升级为高级账户,为期一年。 +
+ 感谢你对 coderplanets 的关注和支持。 +
+ ) +} + +export default QA diff --git a/src/containers/content/MembershipContent/InviteBox/index.tsx b/src/containers/content/MembershipContent/InviteBox/index.tsx new file mode 100644 index 000000000..ce535dcd9 --- /dev/null +++ b/src/containers/content/MembershipContent/InviteBox/index.tsx @@ -0,0 +1,57 @@ +import React, { FC, useEffect, useRef } from 'react' +import ReactPinField from 'react-pin-field' + +import { ICON } from '@/config' + +import Modal from '@/components/Modal' +import QA from './QA' + +import { + Wrapper, + PinCodeWrapper, + Header, + HandIcon, + Title, +} from '../styles/invite_box' +import { closeInviteBox } from '../logic' + +type TProps = { + testid?: string + show: boolean +} + +const InviteBox: FC = ({ testid = 'membership-invite-box', show }) => { + const ref = useRef(null) + + useEffect(() => { + if (show && ref) { + ref.current[0].focus() + } + }, [show, ref]) + + return ( + closeInviteBox()} + showCloseBtn + > + +
+ + 朋友码 +
+ + console.log('v: ', v)} + /> + + +
+
+ ) +} + +export default InviteBox diff --git a/src/containers/content/MembershipContent/MonthlyWarning.js b/src/containers/content/MembershipContent/MonthlyWarning.tsx similarity index 80% rename from src/containers/content/MembershipContent/MonthlyWarning.js rename to src/containers/content/MembershipContent/MonthlyWarning.tsx index 6aa62e4cd..87416b116 100644 --- a/src/containers/content/MembershipContent/MonthlyWarning.js +++ b/src/containers/content/MembershipContent/MonthlyWarning.tsx @@ -1,9 +1,9 @@ -import React from 'react' +import React, { FC } from 'react' import { ICON } from '@/config' import { Wrapper, UpIcon, Number } from './styles/monthly_warning' -const MonthlyWarning = () => { +const MonthlyWarning: FC = () => { return ( :较年付费用 diff --git a/src/containers/content/MembershipContent/PriceTag.js b/src/containers/content/MembershipContent/PriceTag.tsx similarity index 56% rename from src/containers/content/MembershipContent/PriceTag.js rename to src/containers/content/MembershipContent/PriceTag.tsx index 8249e611d..c1e7cbf9b 100644 --- a/src/containers/content/MembershipContent/PriceTag.js +++ b/src/containers/content/MembershipContent/PriceTag.tsx @@ -1,13 +1,25 @@ -import React from 'react' +import React, { FC } from 'react' import { PAY } from './constant' import { Wrapper, Price, Slash, Unit } from './styles/price_tag' -const PriceTag = ({ active, price, unit = '月' }) => { +type TProps = { + testid?: string + active: boolean + price: string + unit?: string +} + +const PriceTag: FC = ({ + testid = 'membership-price-tag', + active, + price, + unit = '月', +}) => { const localeUnit = unit === PAY.YEARLY ? '每年' : '每月' return ( - + ¥ {price} / {localeUnit} diff --git a/src/containers/content/MembershipContent/QA.js b/src/containers/content/MembershipContent/QA.tsx similarity index 86% rename from src/containers/content/MembershipContent/QA.js rename to src/containers/content/MembershipContent/QA.tsx index c0e1d13b1..f840772ac 100644 --- a/src/containers/content/MembershipContent/QA.js +++ b/src/containers/content/MembershipContent/QA.tsx @@ -1,11 +1,15 @@ -import React from 'react' +import React, { FC } from 'react' import { ICON } from '@/config' import { Wrapper, Header, Icon, Content, QTitle, ABody } from './styles/qa' -const QA = () => { +type TProps = { + testid?: string +} + +const QA: FC = ({ testid = 'membership-qa' }) => { return ( - +
常见问题 diff --git a/src/containers/content/MembershipContent/Support.js b/src/containers/content/MembershipContent/Support.tsx similarity index 83% rename from src/containers/content/MembershipContent/Support.js rename to src/containers/content/MembershipContent/Support.tsx index 3e26eb14b..2d72c6d07 100644 --- a/src/containers/content/MembershipContent/Support.js +++ b/src/containers/content/MembershipContent/Support.tsx @@ -18,7 +18,14 @@ const MarkIcon = ({ not }) => { ) } -const Support = ({ active, items, not, pkgType }) => ( +type TProps = { + active: boolean + items: any // TODO: + not?: boolean + pkgType: string +} + +const Support: React.FC = ({ active, items, not, pkgType }) => ( {pkgType !== 'free' && ( diff --git a/src/containers/content/MembershipContent/constant.ts b/src/containers/content/MembershipContent/constant.ts index 18a43fb78..cd85a5c8c 100644 --- a/src/containers/content/MembershipContent/constant.ts +++ b/src/containers/content/MembershipContent/constant.ts @@ -1,11 +1,13 @@ +import type { TPay, TPackage } from './spec' + export const PAY = { - YEARLY: 'yearly', - MONTHLY: 'monthly', + YEARLY: 'yearly' as TPay, + MONTHLY: 'monthly' as TPay, } export const PACKAGE = { - GIRL: 'girl', - FREE: 'free', - ADVANCE: 'advance', - TEAM: 'team', + GIRL: 'girl' as TPackage, + FREE: 'free' as TPackage, + ADVANCE: 'advance' as TPackage, + TEAM: 'team' as TPackage, } diff --git a/src/containers/content/MembershipContent/index.js b/src/containers/content/MembershipContent/index.tsx similarity index 84% rename from src/containers/content/MembershipContent/index.js rename to src/containers/content/MembershipContent/index.tsx index 74bbbd29b..de2a62627 100755 --- a/src/containers/content/MembershipContent/index.js +++ b/src/containers/content/MembershipContent/index.tsx @@ -5,14 +5,13 @@ */ import React from 'react' -import T from 'prop-types' -// import { ICON_CMD, EMAIL_BUSINESS, SENIOR_AMOUNT_THRESHOLD } from '@/config' import { pluggedIn, buildLog } from '@/utils' import { OrButton, Button } from '@/components/Buttons' import Checker from '@/components/Checker' +import type { TStore } from './store' import { PAY, PACKAGE } from './constant' import Illustrations from './Illustrations' @@ -20,12 +19,14 @@ import Support from './Support' import PriceTag from './PriceTag' import MonthlyWarning from './MonthlyWarning' import QA from './QA' +import InviteBox from './InviteBox' import { Wrapper, InnerWrapper, BannerWrapper, PayButtonWrapper, + InviteCodeWrapper, Title, Desc, ContentWrapper, @@ -39,7 +40,12 @@ import { } from './styles' // import { useInit, onUpgrade } from './logic' -import { useInit, pkgTypeOnChange, payTypeOnChange } from './logic' +import { + useInit, + pkgTypeOnChange, + payTypeOnChange, + openInviteBox, +} from './logic' /* eslint-disable-next-line */ const log = buildLog('C:MembershipContent') @@ -60,14 +66,20 @@ const PayButton = ({ pkgType, payType }) => { ) } -const MembershipContentContainer = ({ +type TProps = { + membershipContent: TStore + metric: string + testid?: string +} + +const MembershipContentContainer: React.FC = ({ membershipContent: store, - testid, + testid = 'membership-content', metric, }) => { useInit(store) - const { payType, pkgType, dashboardItems } = store + const { payType, pkgType, dashboardItems, showInviteBox } = store return ( @@ -78,7 +90,6 @@ const MembershipContentContainer = ({ } + openInviteBox()}> + 使用朋友码 + + {dashboardItems.map((item) => ( @@ -144,14 +159,4 @@ const MembershipContentContainer = ({ ) } -MembershipContentContainer.propTypes = { - membershipContent: T.any.isRequired, - metric: T.string.isRequired, - testid: T.string, -} - -MembershipContentContainer.defaultProps = { - testid: 'membership-content', -} - -export default pluggedIn(MembershipContentContainer) +export default pluggedIn(MembershipContentContainer) as React.FC diff --git a/src/containers/content/MembershipContent/logic.js b/src/containers/content/MembershipContent/logic.ts similarity index 60% rename from src/containers/content/MembershipContent/logic.js rename to src/containers/content/MembershipContent/logic.ts index fbfa3e39c..083597863 100755 --- a/src/containers/content/MembershipContent/logic.js +++ b/src/containers/content/MembershipContent/logic.ts @@ -3,6 +3,9 @@ import { useEffect } from 'react' import { SENIOR_AMOUNT_THRESHOLD } from '@/config' import { PAYMENT_USAGE } from '@/constant' import { asyncSuit, buildLog } from '@/utils' + +import type { TStore } from './store' +import type { TPackage } from './spec' /* import S from './schema' */ /* eslint-disable-next-line */ @@ -12,18 +15,18 @@ const { SR71, $solver } = asyncSuit const sr71$ = new SR71() let sub$ = null -let store = null +let store: TStore | undefined -export const payTypeOnChange = (payType) => { +export const payTypeOnChange = (payType: TPackage): void => { store.mark({ payType }) } -export const pkgTypeOnChange = (pkgType) => { +export const pkgTypeOnChange = (pkgType: TPackage): void => { store.mark({ pkgType }) } -export const onUpgrade = () => { - if (!store.isLogin) return store.authWarning() +export const onUpgrade = (): void => { + if (!store.isLogin) return store.authWarning({}) store.cashierHelper({ paymentUsage: PAYMENT_USAGE.SENIOR, @@ -31,6 +34,14 @@ export const onUpgrade = () => { }) } +export const openInviteBox = (): void => { + store.mark({ showInviteBox: true }) +} + +export const closeInviteBox = (): void => { + store.mark({ showInviteBox: false }) +} + // ############################### // Data & Error handlers // ############################### @@ -38,7 +49,7 @@ export const onUpgrade = () => { const DataSolver = [] const ErrSolver = [] -export const useInit = (_store) => { +export const useInit = (_store: TStore): void => { useEffect(() => { store = _store sub$ = sr71$.data().subscribe($solver(DataSolver, ErrSolver)) diff --git a/src/containers/content/MembershipContent/spec.ts b/src/containers/content/MembershipContent/spec.ts new file mode 100644 index 000000000..44e3a087c --- /dev/null +++ b/src/containers/content/MembershipContent/spec.ts @@ -0,0 +1,2 @@ +export type TPay = 'yearly' | 'monthly' +export type TPackage = 'girl' | 'free' | 'advance' | 'team' diff --git a/src/containers/content/MembershipContent/store.js b/src/containers/content/MembershipContent/store.ts similarity index 83% rename from src/containers/content/MembershipContent/store.js rename to src/containers/content/MembershipContent/store.ts index 5e6c0c9f6..098a79a61 100755 --- a/src/containers/content/MembershipContent/store.js +++ b/src/containers/content/MembershipContent/store.ts @@ -3,9 +3,10 @@ * */ -import { types as T, getParent } from 'mobx-state-tree' +import { types as T, getParent, Instance } from 'mobx-state-tree' import { values } from 'ramda' +import type { TRootStore } from '@/spec' import { markStates, buildLog } from '@/utils' import { PACKAGE, PAY } from './constant' @@ -14,15 +15,14 @@ import { PACKAGE, PAY } from './constant' const log = buildLog('S:MembershipContent') const MembershipContent = T.model('MembershipContent', { + showInviteBox: T.optional(T.boolean, false), payType: T.optional(T.enumeration(values(PAY)), PAY.YEARLY), pkgType: T.optional(T.enumeration(values(PACKAGE)), PACKAGE.ADVANCE), }) .views((self) => ({ - get root() { - return getParent(self) - }, - get isLogin() { - return self.root.account.isLogin + get isLogin(): boolean { + const root = getParent(self) as TRootStore + return root.account.isLogin }, get dashboardItems() { return [ @@ -93,14 +93,17 @@ const MembershipContent = T.model('MembershipContent', { })) .actions((self) => ({ authWarning(options) { - self.root.authWarning(options) + const root = getParent(self) as TRootStore + root.authWarning(options) }, cashierHelper(opt) { - self.root.cashierHelper(opt) + const root = getParent(self) as TRootStore + root.cashierHelper(opt) }, - mark(sobj) { + mark(sobj: Record): void { markStates(sobj, self) }, })) +export type TStore = Instance export default MembershipContent diff --git a/src/containers/content/MembershipContent/styles/index.ts b/src/containers/content/MembershipContent/styles/index.ts index 11c5b9457..1d25e5ebb 100755 --- a/src/containers/content/MembershipContent/styles/index.ts +++ b/src/containers/content/MembershipContent/styles/index.ts @@ -37,6 +37,18 @@ export const Desc = styled.div` export const PayButtonWrapper = styled.div` position: relative; ` +export const InviteCodeWrapper = styled.div` + color: #196781; + font-size: 14px; + margin-top: 10px; + + &:hover { + color: ${theme('button.primary')}; + cursor: pointer; + } + + transition: color 0.25s; +` export const ContentWrapper = styled.div<{ metric: string }>` ${css.flex('justify-between')}; width: 100%; diff --git a/src/containers/content/MembershipContent/styles/invite_box/index.ts b/src/containers/content/MembershipContent/styles/invite_box/index.ts new file mode 100644 index 000000000..ec39442fa --- /dev/null +++ b/src/containers/content/MembershipContent/styles/invite_box/index.ts @@ -0,0 +1,68 @@ +import styled from 'styled-components' + +import Img from '@/Img' +import type { TTestable } from '@/spec' +import { theme, css } from '@/utils' + +export const Wrapper = styled.div.attrs(({ testid }: TTestable) => ({ + 'data-test-id': testid, +}))` + color: ${theme('thread.articleDigest')}; + ${css.flexColumn('align-center')}; + width: 100%; + padding: 20px; +` +export const Header = styled.div` + ${css.flex('align-center')}; + margin-bottom: 22px; +` +export const HandIcon = styled(Img)` + fill: ${theme('thread.articleTitle')}; + ${css.size(20)}; + margin-left: -10px; +` +export const Title = styled.div` + color: ${theme('thread.articleTitle')}; + font-size: 18px; + margin-left: 8px; +` +export const PinCodeWrapper = styled.div` + ${css.flex('justify-center')}; + width: 100%; + + .a-reactPinField__input { + background-color: #0b2631; + border: 1px solid; + color: #139c9e; + border-color: #0d5a7b; + border-radius: 6px; + font-size: 25px; + margin: 0.25rem; + height: 3.5rem; + outline: none; + text-align: center; + transition-duration: 250ms; + transition-property: background, color, border, box-shadow, transform; + width: 3rem; + } + + .a-reactPinField__input:focus { + border-color: #107eae; + outline: none; + } + + .a-reactPinField__input:invalid { + animation: shake 3 linear 75ms; + border-color: tomato; + box-shadow: 0 0 0.25rem rgba(220, 53, 69, 0.5); + } + + .a-reactPinField__input .-success { + border-color: rgb(40, 167, 69); + background-color: rgba(40, 167, 69, 0.25); + } + /* swd-pin-field[completed] .pin-field { + border-color: rgb(40, 167, 69); + background-color: rgba(40, 167, 69, 0.25); + } */ +` diff --git a/src/containers/content/MembershipContent/styles/invite_box/qa.ts b/src/containers/content/MembershipContent/styles/invite_box/qa.ts new file mode 100644 index 000000000..7c0ce9a9f --- /dev/null +++ b/src/containers/content/MembershipContent/styles/invite_box/qa.ts @@ -0,0 +1,23 @@ +import styled from 'styled-components' + +import type { TTestable } from '@/spec' +import { theme, css } from '@/utils' + +export const Wrapper = styled.div.attrs(({ testid }: TTestable) => ({ + 'data-test-id': testid, +}))` + color: ${theme('thread.articleDigest')}; + ${css.flexColumn('align-start')}; + width: 100%; + margin-top: 30px; + padding: 0 25px; +` +export const Title = styled.div` + color: ${theme('thread.articleTitle')}; + width: 100%; + font-size: 14px; + padding-bottom: 8px; +` +export const Desc = styled.div` + color: ${theme('thread.articleDigest')}; +` diff --git a/src/containers/content/MembershipContent/styles/monthly_warning.ts b/src/containers/content/MembershipContent/styles/monthly_warning.ts index 8ce75c415..bf794cd62 100644 --- a/src/containers/content/MembershipContent/styles/monthly_warning.ts +++ b/src/containers/content/MembershipContent/styles/monthly_warning.ts @@ -1,12 +1,9 @@ import styled from 'styled-components' -import type { TTestable } from '@/spec' import { theme, css } from '@/utils' import Img from '@/Img' -export const Wrapper = styled.div.attrs(({ testid }: TTestable) => ({ - 'data-test-id': testid, -}))` +export const Wrapper = styled.div` position: absolute; ${css.flex('align-center')}; color: ${theme('thread.articleTitle')}; diff --git a/src/pages/membership.js b/src/pages/membership.js old mode 100644 new mode 100755 diff --git a/yarn.lock b/yarn.lock index d45caef4a..3d15d60b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1095,6 +1095,10 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@soywod/pin-field@^0.1.9": + version "0.1.9" + resolved "https://registry.nlark.com/@soywod/pin-field/download/@soywod/pin-field-0.1.9.tgz#3ac81a2cfa158df99cd850e5040fe4cd6904eef2" + "@tanem/svg-injector@^8.1.0": version "8.1.0" resolved "https://registry.npm.taobao.org/@tanem/svg-injector/download/@tanem/svg-injector-8.1.0.tgz#f04c65903c4cded31d6f3c23d3986458307ad827" @@ -9433,6 +9437,12 @@ react-masonry-css@^1.0.14: version "1.0.14" resolved "https://registry.npm.taobao.org/react-masonry-css/download/react-masonry-css-1.0.14.tgz#2ac1ca7bb2c7e96826f7da3accc9e95ae12b2f65" +react-pin-field@1.0.6: + version "1.0.6" + resolved "https://registry.nlark.com/react-pin-field/download/react-pin-field-1.0.6.tgz#ef7f4b00c8261f3abd45533f586f88a9204303ec" + dependencies: + classnames "^2.2.6" + react-refresh@0.8.3: version "0.8.3" resolved "https://registry.npm.taobao.org/react-refresh/download/react-refresh-0.8.3.tgz?cache=0&sync_timestamp=1590240716203&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-refresh%2Fdownload%2Freact-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"